【Linux基础系列之】同步机制介绍
来源:互联网 发布:淘宝基础版不能设背景 编辑:程序博客网 时间:2024/06/04 01:02
当多核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变量不能象普通变量那样进行访问,需要使用特定的接口函数,具体如下:
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 be272 * 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
代码分析:
TODO
(三) memory barrier
编译器可以在将c翻译成汇编的时候进行优化(例如内存访问指令的重新排序),让产出的汇编指令在CPU上运行的时候更快;然而,这种优化产出的结果未必符合程序员原始的逻辑;
程序员需通过内嵌在c代码中的memory barrier来指导编译器的优化行为(这种memory barrier又叫做优化屏障,Optimization barrier),让编译器产出即高效,又逻辑正确的代码;
Memory barrier 包括两类:
1.编译器 barrier2.CPU Memory barrier
Memory barrier 能够让 CPU 或编译器在内存访问上有序。一个 Memory barrier 之前的内存访问操作必定先于其之后的完成。
Linux 内核提供函数 barrier() 用于让编译器保证其之前的内存访问先于其之后的完成;
memory barrier相关的API列表:
13 /* Optimization barrier */ 14 /* The "volatile" is due to gcc bugs */ 15 #define barrier() __asm__ __volatile__("": : :"memory") 53 #define smp_mb() barrier() 54 #define smp_rmb() barrier() 55 #define smp_wmb() barrier()
这里的memory就是告知gcc,在汇编代码中,我修改了memory中的内容,嵌入式汇编之前的c代码块和嵌入式汇编之后的c代码块看到的memory是不一样的,对memory的访问不能依赖于嵌入式汇编之前的c代码块中寄存器的内容,需要重新加载;
而之所以会有memory barrier这个“邪恶的东西”是由于CPU的速度要快于(数量级上的差异)memory以及他们之间的互连器件;
对于memory需要更深入的理解可以查看:https://www.kernel.org/doc/Documentation/memory-barriers.txt
(四) 自旋锁spin_lock
如果共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了,这时候,可以考虑使用spin lock;
特点:
spin lock是一种死等的锁机制。当前的执行thread会不断的重新尝试直到获取锁进入临界区。
只允许一个thread进入。semaphore可以允许多个thread进入,spin lock不行,一次只能有一个thread获取锁并进入临界区,其他的thread都是在门口不断的尝试。
执行时间短。由于spin lock死等这种特性,因此它使用在那些代码不是非常复杂的临界区(当然也不能太简单,否则使用原子操作或者其他适用简单场景的同步机制就OK了),如果临界区执行时间太长,那么不断在临界区门口“死等”的那些thread是多么的浪费CPU;
可以在中断上下文执行。由于不睡眠,因此spin lock可以在中断上下文中适用。
当两个进程执行在同一个cpu,执行在进程上下文的时候,其中一个进程获取spin_lock的时候禁止本cpu上的抢占;如果两个进程在不同的cpu上的时候,某个进程会等待已经获取锁的进程释放锁;
这里为什么有raw_spin_lock:
要解答这个问题,我们要回到2004年,MontaVista Software, Inc的开发人员在邮件列表中提出来一个Real-Time Linux Kernel的模型,旨在提升Linux的实时性,之后Ingo Molnar很快在他的一个项目中实现了这个模型,并最终产生了一个Real-Time preemption的patch。
该模型允许在临界区中被抢占,而且申请临界区的操作可以导致进程休眠等待,这将导致自旋锁的机制被修改,由原来的整数原子操作变更为信号量操作。当时内核中已经有大约10000处使用了自旋锁的代码,直接修改spin_lock将会导致这个patch过于庞大,于是,他们决定只修改哪些真正不允许抢占和休眠的地方,而这些地方只有100多处,这些地方改为使用raw_spin_lock,但是,因为原来的内核中已经有raw_spin_lock这一名字空间,用于代表体系相关的原子操作的实现,于是linus本人建议:
把原来的raw_spin_lock改为arch_spin_lock; 把原来的spin_lock改为raw_spin_lock; 实现一个新的spin_lock;
对于2.6.33和之后的版本,我的理解是:
尽可能使用spin_lock;绝对不允许被抢占和休眠的地方,使用raw_spin_lock,否则使用spin_lock;如果你的临界区足够小,使用raw_spin_lock;
总结就是:spinlock,在rt linux(配置了PREEMPT_RT)的时候可能会被抢占(实际底层可能是使用支持PI(优先级翻转)的mutext)。而raw_spinlock,即便是配置了PREEMPT_RT也要顽强的spin;arch_spinlock是architecture相关的实现 ;
64 typedef struct spinlock { 65 union { 66 struct raw_spinlock rlock; 67 68 #ifdef CONFIG_DEBUG_LOCK_ALLOC 69 # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map)) 70 struct { 71 u8 __padding[LOCK_PADSIZE]; 72 struct lockdep_map dep_map; 73 }; 74 #endif 75 }; 76 } spinlock_t; 20 typedef struct raw_spinlock { 21 arch_spinlock_t raw_lock; 22 #ifdef CONFIG_GENERIC_LOCKBREAK 23 unsigned int break_lock; 24 #endif 25 #ifdef CONFIG_DEBUG_SPINLOCK 26 unsigned int magic, owner_cpu; 27 void *owner; 28 #endif 29 #ifdef CONFIG_DEBUG_LOCK_ALLOC 30 struct lockdep_map dep_map; 31 #endif 32 } raw_spinlock_t;
首先定义一个spinlock_t的数据类型,其本质上是一个整数值(对该数值的操作需要保证原子性),该数值表示spin lock是否可用。初始化的时候被设定为1。当thread想要持有锁的时候调用spin_lock函数,该函数将spin lock那个整数值减去1,然后进行判断,如果等于0,表示可以获取spin lock,如果是负数,则说明其他thread的持有该锁,本thread需要spin。
补充spin_lock,spin_lock_irq,spin_lock_irqsave的区别:
- 普通的spin_lock是preempt_disable();而spin_lock_irq和spin_lock_irqsave是preempt_disable()同时local_irq_disable();在任何情况下使用spin_lock_irq都是安全的。因为它既禁止本地中断,又禁止内核抢占;
- spin_lock比spin_lock_irq速度快,但是它并不是任何情况下都是安全的。
- spin_lock_irqsave保留当前中断状态,进入退出临界区中断状态不变;
举个例子:进程A中调用了spin_lock(&lock)然后进入临界区,此时来了一个中断(interrupt),该中断也运行在和进程A相同的CPU上,并且在该中断处理程序中恰巧也会spin_lock(&lock)试图获取同一个锁。由于是在同一个CPU上被中断,进程A会被设置为TASK_INTERRUPT状态,中断处理程序无法获得锁,会不停的忙等,由于进程A被设置为中断状态,schedule()进程调度就无法再调度进程A运行,这样就导致了死锁!
但是如果该中断处理程序运行在不同的CPU上就不会触发死锁。 因为在不同的CPU上出现中断不会导致进程A的状态被设为TASK_INTERRUPT,只是换出。当中断处理程序忙等被换出后,进程A还是有机会获得CPU,执行并退出临界区。所以在使用spin_lock时要明确知道该锁不会在中断处理程序中使用;
(五) 读/写spin_lock
spin lock严格的限制只有一个thread可以进入临界区,但是实际中,有些对共享资源的访问可以严格区分读和写的,这时候,其实多个读的thread进入临界区是OK的,使用spin lock则限制一个读thread进入,从而导致性能的下降。
读写锁工作原理:
(1)假设临界区内没有任何的thread,这时候任何read thread或者write thread可以进入,但是只能是其一。
(2)假设临界区内有一个read thread,这时候新来的read thread可以任意进入,但是write thread不可以进入
(3)假设临界区内有一个write thread,这时候任何的read thread或者write thread都不可以进入
(4)假设临界区内有一个或者多个read thread,write thread当然不可以进入临界区,但是该write thread也无法阻止后续read thread的进入,他要一直等到临界区一个read thread也没有的时候,才可以进入,多么可怜的write thread。
读/写spin_lock API:
(六) RCU(Read-Copy Update)
RCU(Read-Copy Update)是Linux内核比较成熟的新型读写锁,具有较高的读写并发性能,常常用在需要互斥的性能关键路径。
RCU允许多个读者和写者并发执行,而且RCU是不用锁的,它不使用被所有CPU共享的锁或者计数器,在这一点上与读/写自旋锁和顺序锁,由于高速缓存行窃用和失效而有很高的的开销,RCU具有更大的优势;
RCU的核心理念是读者访问的同时,写者可以更新访问对象的副本,但写者需要等待所有读者完成访问之后,才能删除老对象。这个过程实现的关键和难点就在于如何判断所有的读者已经完成访问。通常把写者开始更新,到所有读者完成访问这段时间叫做宽限期(Grace Period)。
RCU的使用场景比较受限,主要适用于下面的场景:
(1)RCU只能保护动态分配的数据结构,并且必须是通过指针访问该数据结构
(2)受RCU保护的临界区内不能sleep(SRCU不是本文的内容)
(3)读写不对称,对writer的性能没有特别要求,但是reader性能要求极高。
(4)reader端对新旧数据不敏感。
对于reader,RCU的操作包括:
(1)rcu_read_lock,用来标识RCU read side临界区的开始。
(2)rcu_dereference,该接口用来获取RCU protected pointer。reader要访问RCU保护的共享数据,当然要获取RCU protected pointer,然后通过该指针进行dereference的操作。
(3)rcu_read_unlock,用来标识reader离开RCU read side临界区
对于writer,RCU的操作包括:
(1)rcu_assign_pointer。该接口被writer用来进行removal的操作,在witer完成新版本数据分配和更新之后,调用这个接口可以让RCU protected pointer指向RCU protected data。
(2)synchronize_rcu。writer端的操作可以是同步的,也就是说,完成更新操作之后,可以调用该接口函数等待所有在旧版本数据上的reader线程离开临界区,一旦从该函数返回,说明旧的共享数据没有任何引用了,可以直接进行reclaimation的操作。
(3)call_rcu。当然,某些情况下(例如在softirq context中),writer无法阻塞,这时候可以调用call_rcu接口函数,该函数仅仅是注册了callback就直接返回了,在适当的时机会调用callback函数,完成reclaimation的操作。这样的场景其实是分开removal和reclaimation的操作在两个不同的线程中:updater和reclaimer。
//读锁; 237 static inline void __rcu_read_lock(void) 238 { 239 preempt_disable(); 240 } 241 242 static inline void __rcu_read_unlock(void) 243 { 244 preempt_enable(); 245 } //读者在读完RCU保护的数据结构之前,是不能睡眠的;
call_rcu:内核每经过一个时钟tick就周期性的检测CPU是否经过了一个静止状态;如果所有cpu都经过了静止状态;本地tasklet就执行链表当中所有的回调函数;
(七) 信号量
内核信号量类似于自旋锁,因为当锁关闭的时侯,不允许内核控制路径继续运行,当一个内核任务试图获取内核信号量所保护的临界区资源时,相应的进程被挂起到一个等待队列中,然后让其睡眠,当前cpu就可以去执行其他的代码,资源被释放的时候,进程在再次变为可运行的;因此只有可以睡眠的函数才可以获取内核信号量,中断处理程序和可延迟函数都不能使用内核信号量;
16 struct semaphore { 17 raw_spinlock_t lock; 18 unsigned int count; 19 struct list_head wait_list; 20 };
信号量不同于自旋锁,它不会禁止内核抢占,所以持有信号量的代码可以被抢占;同时自旋锁在一个时刻只允许一个任务持有它,信号量同时允许的持有数量可以在申明信号量时指定;
信号量初始化:
32 static inline void sema_init(struct semaphore *sem, int val) 33 { 34 static struct lock_class_key __key; 35 *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val); 36 lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0); 37 } 38
获取信号量:
75 int down_interruptible(struct semaphore *sem) 76 { 77 unsigned long flags; 78 int result = 0; 79 80 raw_spin_lock_irqsave(&sem->lock, flags); 81 if (likely(sem->count > 0)) 82 sem->count--; 83 else 84 result = __down_interruptible(sem); 85 raw_spin_unlock_irqrestore(&sem->lock, flags); 86 87 return result; 88 } 89 EXPORT_SYMBOL(down_interruptible);241 static noinline int __sched __down_interruptible(struct semaphore *sem)242 { 243 return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);244 } 204 static inline int __sched __down_common(struct semaphore *sem, long state,205 long timeout)206 { 207 struct task_struct *task = current;208 struct semaphore_waiter waiter;209 //先添加到wait列表;210 list_add_tail(&waiter.list, &sem->wait_list);211 waiter.task = task;212 waiter.up = false;213 214 for (;;) {215 if (signal_pending_state(state, task))216 goto interrupted;217 if (unlikely(timeout <= 0))218 goto timed_out;219 __set_task_state(task, state);//设置为可interruptible;220 raw_spin_unlock_irq(&sem->lock);221 timeout = schedule_timeout(timeout); 222 raw_spin_lock_irq(&sem->lock);223 if (waiter.up)224 return 0;225 }226 227 timed_out:228 list_del(&waiter.list); 229 return -ETIME;//timeout之后就停止等待了;230 231 interrupted:232 list_del(&waiter.list);233 return -EINTR;234 }
常用接口说明:
39 extern void down(struct semaphore *sem); //该函数用于获得信号量sem,他会导致睡眠,睡眠状态不可唤醒,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。 40 extern int __must_check down_interruptible(struct semaphore *sem);//该函数功能和down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。 42 extern int __must_check down_trylock(struct semaphore *sem);该函数试着获得信号量sem,如果能够即时获得,他就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,他不会导致调用者睡眠,能在中断上下文使用。 44 extern void up(struct semaphore *sem);//释放信号量;并唤醒等待该资源进程队列的第一个进程
释放信号量:
178 void up(struct semaphore *sem)179 { 180 unsigned long flags;181 182 raw_spin_lock_irqsave(&sem->lock, flags);183 if (likely(list_empty(&sem->wait_list)))184 sem->count++;185 else186 __up(sem);187 raw_spin_unlock_irqrestore(&sem->lock, flags);188 } 189 EXPORT_SYMBOL(up);256 static noinline void __sched __up(struct semaphore *sem)257 { 258 struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,259 struct semaphore_waiter, list);260 list_del(&waiter->list);261 waiter->up = true; 262 wake_up_process(waiter->task);263 }
首先获得sem所在的wait_list为头部的链表的第一个有效节点,然后从链表中将其删除,然后唤醒该节点上睡眠的进程。
由此可见,对于sem上的每次down_interruptible调用,都会在sem的wait_list链表尾部加入一新的节点。对于sem上的每次up调用,都会删除掉wait_list链表中的第一个有效节点,并唤醒睡眠在该节点上的进程。
(八) 互斥量
前面说过信号量可以设置进入临界去的task的数量,而互斥量mutex就是允许使用计数为1的信号量;
50 struct mutex { 51 /* 1: unlocked, 0: locked, negative: locked, possible waiters */ 52 atomic_t count; 53 spinlock_t wait_lock; 54 struct list_head wait_list; 55 #if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_MUTEX_SPIN_ON_OWNER) 56 struct task_struct *owner; 57 #endif 58 #ifdef CONFIG_MUTEX_SPIN_ON_OWNER 59 struct optimistic_spin_queue osq; /* Spinner MCS lock */ 60 #endif 61 #ifdef CONFIG_DEBUG_MUTEXES 62 const char *name; 63 void *magic; 64 #endif 65 #ifdef CONFIG_DEBUG_LOCK_ALLOC 66 struct lockdep_map dep_map; 67 #endif 68 };
常用接口如下:
void init_MUTEX (struct semaphore *sem);
该函数用于初始化一个互斥锁,即他把信号量sem的值设置为1。
void init_MUTEX_LOCKED (struct semaphore *sem);
该函数也用于初始化一个互斥锁,但他把信号量sem的值设置为0,即一开始就处在已锁状态。
DECLARE_MUTEX(name)
该宏声明一个信号量name并初始化他的值为1,即声明一个互斥锁。
DECLARE_MUTEX_LOCKED(name)
该宏声明一个互斥锁name,但把他的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得
int fastcall __sched mutex_lock_interruptible(struct mutex *lock);和mutex_lock()一样,也是获取互斥锁。在获得了互斥锁或进入睡眠直到获得互斥锁之后会返回0。如果在等待获取锁的时候进入睡眠状态收到一个信号(被信号打断睡眠),则返回_EINIR。
int fastcall __sched mutex_trylock(struct mutex *lock);试图获取互斥锁,如果成功获取则返回1,否则返回0,不等待。
void fastcall mutex_unlock(struct mutex *lock);//释放被当前进程获取的互斥锁。该函数不能用在中断上下文中,而且不允许去释放一个没有上锁的互斥锁。
互斥量和信号量:它们的标准使用方式都有简单的规范:除非mutex的某个约束妨碍你使用,负责相比信号量要优先使用mutex;
(九) 完成变量
如果内核中一个任务需要发出信号通知另一个任务发生了某个特定的事件,可以用完成变量来实现这种同步:
include/linux/completion.h:
申明和定义:
25 struct completion { 26 unsigned int done; 27 wait_queue_head_t wait; 28 }; 44 #define DECLARE_COMPLETION(work) \ 45 struct completion work = COMPLETION_INITIALIZER(work)
完成变量API:
wait_for_completion() -> do_wait_for_common()
61 do_wait_for_common(struct completion *x, 62 long (*action)(long), long timeout, int state) 63 { 64 if (!x->done) { 65 DECLARE_WAITQUEUE(wait, current); 66 67 __add_wait_queue_tail_exclusive(&x->wait, &wait); 68 do { 69 if (signal_pending_state(state, current)) { 70 timeout = -ERESTARTSYS; 71 break; 72 } 73 __set_current_state(state); 74 spin_unlock_irq(&x->wait.lock); 75 timeout = action(timeout); 76 spin_lock_irq(&x->wait.lock); 77 } while (!x->done && timeout); 78 __remove_wait_queue(&x->wait, &wait); 79 if (!x->done) 80 return timeout; 81 } 82 x->done--; 83 return timeout ?: 1; 84 }
- 【Linux基础系列之】同步机制介绍
- Android之linux基础教学之八 内核同步介绍
- Linux内核同步机制之(七):RCU基础
- linux 同步机制之complete
- linux 同步机制之complete
- linux 同步机制之complete
- Linux内核源码系列(二):探究内核基础层数据结构,同步机制的应用
- 并发基础 -- Linux 内核同步机制
- 音视频同步系列文章之-----Windows同步机制
- iOS基础之----GCD多线程同步机制
- Linux内核同步机制之completion
- Linux内核同步机制之completion
- linux 同步机制之complete wait_for_completion
- linux同步机制之wait_event和wake_up
- Linux内核同步机制之completion
- Linux内核同步机制之completion
- Linux内核同步机制之completion
- Linux内核同步机制之Memory barrier
- 我的CV路
- ActivityManager框架之简要
- 关于java中的文件读写java io【复习1】
- RecyclerView 的简单使用
- HDU4325-Flowers
- 【Linux基础系列之】同步机制介绍
- python之read和tell 和SEEK_SET、SEEK_CUR、SEEK_END
- Linux Shell脚本之利用mysqldump备份MySQL数据库(详细注解)
- java与javac版本不一致问题
- Windows操作系统下安装MAC OS系统虚拟机
- nginx+keepalived实现高可用
- 30分钟了解php依赖管理工具Composer
- 性能优化:__builtin_expect详解
- 剑指Offer——(37)数字在排序数组中出现的次数