当多核CPU同时执行一段代码的时候,就容易发生抢占,这段代码可以叫做临界区,其他内核控制路径能够进入临界区前,进入临界区前的内核控制路径必须全部执行完这段代码,为了避免这种共享数据发生竞争,就需要采用同步技术,本文就简单介绍linux内核当中的一些同步原语;
(一) per-cpu变量
最简单的同步技术就是把内核变量申明为per-cpu变量,这个变量只会在本地CPU操作时调用,就不用考虑其他CPU抢占的情况;
将一个共享memory变成Per-CPU memory本质上是一个耗费更多memory来解决performance的方法。当一个在多个CPU之间共享的变量变成每个CPU都有属于自己的一个私有的变量的时候,我们就不必考虑来自多个CPU上的并发,仅仅考虑本CPU上的并发就可以了;
注意:
per-cpu变量为来自不同的CPU的并发访问提供保护,但对来自异步函数(中断函数和可延迟函数)的访问不提供保护;
内核抢占可能使CPU变量产生竞争条件,内核控制路径应该在禁用抢占的情况下访问per-cpu变量;
per-cpu API:
声明和定义Per-CPU变量的API | 描述 |
---|---|
DECLARE_PER_CPU(type, name) DEFINE_PER_CPU(type, name) | 普通的、没有特殊要求的per cpu变量定义接口函数 |
DECLARE_PER_CPU_FIRST(type, name) DEFINE_PER_CPU_FIRST(type, name) | 通过该API定义的per cpu变量位于整个per cpu相关section的最前面 |
DECLARE_PER_CPU_SHARED_ALIGNED(type, name) DEFINE_PER_CPU_SHARED_ALIGNED(type, name) | 通过该API定义的per cpu变量在SMP的情况下会对齐到L1 cache line ,对于UP,不需要对齐到cachine line |
DECLARE_PER_CPU_ALIGNED(type, name) DEFINE_PER_CPU_ALIGNED(type, name) | 无论SMP或者UP,都是需要对齐到L1 cache line |
DECLARE_PER_CPU_PAGE_ALIGNED(type, name) DEFINE_PER_CPU_PAGE_ALIGNED(type, name) | 为定义page aligned per cpu变量而设定的API接口 |
DECLARE_PER_CPU_READ_MOSTLY(type, name) DEFINE_PER_CPU_READ_MOSTLY(type, name) | 通过该API定义的per cpu变量是read mostly的 |
静态定义的per cpu变量不能象普通变量那样进行访问,需要使用特定的接口函数,具体如下:
233 #define this_cpu_ptr(ptr) \
234 ({ \
235 __verify_pcpu_ptr(ptr); \
236 SHIFT_PERCPU_PTR(ptr, my_cpu_offset); \
//SHIFT_PERCPU_PTR: 原始的per cpu变量的地址,经过shift转成实际的percpu 副本的地址;
237 })
260 /*
261 * Must be an lvalue. Since @var must be a simple identifier,
262 * we force a syntax error here if it isn't.
263 */
264 #define get_cpu_var(var) \
265 (*({ \
266 preempt_disable(); \
267 this_cpu_ptr(&var); \
268 }))
269
270 /*
271 * The weird & is necessary because sparse considers (void)(var) to be
272 * a direct dereference of percpu variable (var).
273 */
274 #define put_cpu_var(var) \
275 do { \
276 (void)&(var); \
277 preempt_enable(); \
278 } while (0)
上面这两个接口函数已经内嵌了锁的机制(preempt disable),用户可以直接调用该接口进行本CPU上该变量副本的访问。如果用户确认当前的执行环境已经是preempt disable(或者是更厉害的锁,例如关闭了CPU中断),那么可以使用lock-free版本的Per-CPU变量的API:__get_cpu_var :
258 #define __get_cpu_var(var) (*this_cpu_ptr(&(var)))
只有Per-CPU变量的原始变量还是不够的,必须为每一个CPU建立一个副本,怎么建?直接静态定义一个NR_CPUS的数组?NR_CPUS定义了系统支持的最大的processor的个数,并不是实际中系统processor的数目,这样的定义非常浪费内存对于NUMA系统,每个CPU上的Per-CPU变量的副本应该位于它访问最快的那段memory上,也就是说Per-CPU变量的各个CPU副本可能是散布在整个内存地址空间的,而这些空间之间是有空洞的。
(二)原子操作
那些有多个内核控制路径进行read-modify-write的变量,内核提供了一个特殊的类型atomic_t来避免竟态,申明的变量就叫原子变量,这样的行为我们可以叫做原子操作;
定义:atomic_t val_name = ATOMIC_INIT(val);
typedef struct {
int counter;
} atomic_t;
原子操作API
接口函数 | 描述 |
---|---|
static inline void atomic_add(int i, atomic_t *v) | 给一个原子变量v增加i |
static inline int atomic_add_return(int i, atomic_t *v) | 同上,只不过将变量v的最新值返回 |
static inline void atomic_sub(int i, atomic_t *v) | 给一个原子变量v减去i |
static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new) | 比较old和原子变量ptr中的值,如果相等,那么就把new值赋给原子变量。返回旧的原子变量ptr中的值 |
atomic_read | 获取原子变量的值 |
atomic_set | 设定原子变量的值 |
atomic_inc(v) | 原子变量的值加1 |
atomic_dec(v) | 原子变量的值减去1 |
atomic_sub_and_test(i, v) | 给一个原子变量v减去i,并判断变量v的最新值是否等于0 |
atomic_add_negative(i,v) | 给一个原子变量v增加i,并判断变量v的最新值是否是负数 |
static inline int atomic_add_unless(atomic_t *v, int a, int u) | 只要原子变量v不等于u,那么就执行原子变量v加a的操作,如果v不等于u,返回非0值,否则返回0值 |
代码分析:
TODO
(三) memory barrier
编译器可以在将c翻译成汇编的时候进行优化(例如内存访问指令的重新排序),让产出的汇编指令在CPU上运行的时候更快;然而,这种优化产出的结果未必符合程序员原始的逻辑;
程序员需通过内嵌在c代码中的memory barrier来指导编译器的优化行为(这种memory barrier又叫做优化屏障,Optimization barrier),让编译器产出即高效,又逻辑正确的代码;
Memory barrier 包括两类:
1.编译器 barrier
2.CPU Memory barrier
Memory barrier 能够让 CPU 或编译器在内存访问上有序。一个 Memory barrier 之前的内存访问操作必定先于其之后的完成。
Linux 内核提供函数 barrier() 用于让编译器保证其之前的内存访问先于其之后的完成;
memory barrier相关的API列表:
接口名称 | 描述 |
---|---|
barrier() | 优化屏障,阻止编译器为了进行性能优化而进行的memory access smp_wmb()reorder; |
mb() | 内存屏障(包括读和写),用于SMP和UP; |
rmb() | 读内存屏障,用于SMP和UP; |
wmb() |