对于 SMP 系统中,在开启 preempt 情况下,对于公共资源,如果存在两个 task 来进行更改,这就面临临界区资源竞争问题,此时会产生意想不到的结果,这是不符合预期的,因此需要来进行解决。
对于变量的操作: a =0; a++; 汇编是如下实现:
- ldr r3, [r3, #0]
- adds r2, r3, #1
- str r2, [r3, #0]
也就是说,一个 a++ 实际上需要三条指令来完成,分别对应上图的 R,M,W。
这样如果 task1 在W之后,紧接着task2 也来W, 此时会产生不符合 task1 预想的结果,会产生问题。因此 arm 提出 atomic 来解决这种问题。
- /* arch/arm/include/asm/atomic.h */
- #undef ATOMIC_OPS
- #define ATOMIC_OPS(op, c_op, asm_op) \
- ATOMIC_OP(op, c_op, asm_op) \
- ATOMIC_FETCH_OP(op, c_op, asm_op)
-
- #define ATOMIC_OP(op, c_op, asm_op) \
- static inline void atomic_##op(int i, atomic_t *v) \
- { \
- unsigned long tmp; \
- int result; \
- \
- prefetchw(&v->counter); \
- __asm__ __volatile__("@ atomic_" #op "\n" \
- "1: ldrex %0, [%3]\n" \ ①
- " " #asm_op " %0, %0, %4\n" \ ②
- " strex %1, %0, [%3]\n" \ ③
- " teq %1, #0\n" \ ④
- " bne 1b" \ ⑤
- : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) \
- : "r" (&v->counter), "Ir" (i) \
- : "cc"); \
- } \
-
- ATOMIC_OPS(add, +=, add)
这里选取了 atomic_add 来分析,上面的 #asm_op 就是 add 了,此时代码可以解析为:
那么为什么仅仅使用了 ldrex 和 strex 就实现了 atomic 功能呢?
实际上 ldrex 和 strex 在使用过程中使用了 monitor 的功能,这里选取蜗窝科技的介绍方式介绍:
- /* arch/arm64/include/asm/atomic_lse.h */
- #define ATOMIC64_OP(op, asm_op) \
- static inline void __lse_atomic64_##op(s64 i, atomic64_t *v) \
- { \
- asm volatile( \
- __LSE_PREAMBLE \
- " " #asm_op " %[i], %[v]\n" \
- : [i] "+r" (i), [v] "+Q" (v->counter) \
- : "r" (v)); \
- }
-
- ATOMIC64_OP(andnot, stclr)
- ATOMIC64_OP(or, stset)
- ATOMIC64_OP(xor, steor)
- ATOMIC64_OP(add, stadd) // 定义了 __lse_atomic64_add 函数, asm_op 是 stadd
-
- /* arch/arm64/include/asm/lse.h */
- #define __lse_ll_sc_body(op, ...) \
- ({ \
- system_uses_lse_atomics() ? \
- __lse_##op(__VA_ARGS__) : \
- __ll_sc_##op(__VA_ARGS__); \
- })
-
- /* arch/arm64/include/asm/atomic.h */
-
- #define ATOMIC64_OP(op) \
- static __always_inline void arch_##op(long i, atomic64_t *v) \
- { \
- __lse_ll_sc_body(op, i, v); \
- }
-
- ATOMIC64_OP(atomic64_andnot)
- ATOMIC64_OP(atomic64_or)
- ATOMIC64_OP(atomic64_xor)
- ATOMIC64_OP(atomic64_add) //这里传入的 op 是 atomic64_add, 定义了 arch_atomic64_add
- ATOMIC64_OP(atomic64_and)
- ATOMIC64_OP(atomic64_sub)
对于 arch_atomic64_add ,其又调用了 __lse_ll_sc_body(atomic64_add, i, v);
这样 atomic64_add 就有了定义:
- /* lib/atomic64.c */
- #define ATOMIC64_OPS(op, c_op) \
- ATOMIC64_OP(op, c_op) \
- ATOMIC64_OP_RETURN(op, c_op) \
- ATOMIC64_FETCH_OP(op, c_op)
-
- ATOMIC64_OPS(add, +=)
- ATOMIC64_OPS(sub, -=)
这里才真正使用 宏来声明了 atomic64_add 函数,它通过 stladd 将 i 加到 atomic64_add 的变量中的 counter 上面去。stladd 是 armv8.1 提供了原子操作变量,相对于 ldrex, strex 在性能又进一步提升。
- atomic_t val;
-
- atomic_set(&val, 10);
-
- int read_val = atomic_read(&val);
对于 outer-shareable 的内存而言,由于cache MESI 机制(假设是outer shareable 的),会发生如下变化:
图中黄色的小球是 Cache 是否命中,且和 RAM 中内容一致
2.更改 CacheB 中内容
此时CacheB 内容被修改,内容发生变化。
3.Invalidate 其它cpu cache
因为MESI 机制,因为B更改了,此时会自动 invalidate outer shareable 的 cache 内容。
这样会带来性能上的损耗,因为被invalidate 的内容,之后如果用到,还要重新加载。
假入内容有这样的一块内存,属于CPU自己独有,它的加载以及Cache 操作不会影响到别的CPU,这样就解决了上述面临的问题。因此linux中提出 per_cpu 变量来操作。
- #define __PCPU_ATTRS(sec) \
- __percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
- PER_CPU_ATTRIBUTES
-
- #define DEFINE_PER_CPU_SECTION(type, name, sec) \
- __PCPU_ATTRS(sec) __typeof__(type) name
- #endif
-
- #define DEFINE_PER_CPU(type, name) \
- DEFINE_PER_CPU_SECTION(type, name, "")
定义一个位于 PER_CPU_BASE_SECTION 的一个变量,这是一个静态声明,指定了其位于的地址空间。其section 定义如下:
- #ifndef PER_CPU_BASE_SECTION
- #ifdef CONFIG_SMP
- #define PER_CPU_BASE_SECTION ".data..percpu"
- #else
- #define PER_CPU_BASE_SECTION ".data"
- #endif
- #ifdef MODULE
- #define PER_CPU_SHARED_ALIGNED_SECTION ""
- #define PER_CPU_ALIGNED_SECTION ""
- #else
- #define PER_CPU_SHARED_ALIGNED_SECTION "..shared_aligned"
- #define PER_CPU_ALIGNED_SECTION "..shared_aligned"
- #endif
-
- #define DEFINE_PER_CPU_SHARED_ALIGNED(type, name) \
- DEFINE_PER_CPU_SECTION(type, name, PER_CPU_SHARED_ALIGNED_SECTION) \
- ____cacheline_aligned_in_smp
-
- #define DEFINE_PER_CPU_PAGE_ALIGNED(type, name) \
- DEFINE_PER_CPU_SECTION(type, name, "..page_aligned") \
- __aligned(PAGE_SIZE)
分别也是定义了位于 section name 为 "..page_aligned" 和 "..shared_aligned" 的变量。
那么为什么需要特殊的 Section呢?
对于kernel中的普通变量,经过了编译和链接后,会被放置到.data或者.bss段,系统在初始化的时候会准备好一切(例如clear bss),由于per cpu变量的特殊性,内核将这些变量放置到了其他的section,位于kernel address space中__per_cpu_start和__per_cpu_end之间,我们称之Per-CPU变量的原始变量。(参考蜗窝科技).
- DEFINE_PER_CPU(int, state);
- int cpu = 0;
- per_cpu(state, cpu) = 1;
- int got_state = per_cpu(state, cpu);