临界区:临界区的代码不能被打断,原子的执行。进入临界区不能发生进程切换,不能发生中断。
造成并发的原因:
1、中断——中断可能随时打断当前正在执行的代码
2、软中断和tasklet——内核可以在任何时刻唤醒软中断,打断正在执行的代码
3、内核抢占——内核中的任务可能被另一个任务抢占
4、睡眠和用户空间的同步——内核代替进程执行,可能会被阻塞,导致进程切换,不同的进程访问共享资源导致竞争
5、对称多处理器——多个处理器可以同时访问一个共享资源,导致竞争
哪些资源需要加锁保护:大部分内核数据结构需要加锁,给数据加锁,而不是代码。
死锁的原因:资源的竞争、进程推进顺序的不合理。
死锁产生的必要条件:互斥(一个资源只能被一个线程占用)、请求与保持(一个进程已经占有一种资源还想获取另一种资源)、不可剥夺、循环等待。
同步方法:
0、每CPU变量:只有本地CPU可以访问相应的变量,但是访问的时候也需要进行保护,内核抢占可能带来竞争。
1、原子操作
2、自旋锁:短时间内轻量级加锁。(多处理器环境的特殊锁)
自旋锁的特点:1、自旋锁保护的临界区不能进入休眠;2、自旋锁保护的临界区能被中断中断;3、自旋锁保护的临界区代码执行时不内核不能被抢占;4、可以在中断上下文中使用(需要禁止本地中断)。【不能因为任何原因让出处理器】
自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。
自旋锁三种状态
自旋锁保持期间是抢占失效的(内核不允许被抢占)。
1、单CPU且内核不可抢占:
自旋锁的所有操作都是空。不会引起死锁,内核进程间不存在并发操作进程,进程与中断仍然可能共享数据,存在并发操作,此时内核自旋锁已经失去效果。
2 、单CPU且内核可抢占:
当获得自旋锁的时候,禁止内核抢占直到释放锁为止。此时可能存在死锁的情况是参考自旋锁可能死锁的一般情况。
禁止内核抢占并不代表不会进行内核调度,如果在获得自旋锁后阻塞或者主动调度,内核会调度其他进程运行,被调度的内核进程返回用户空间时,会进行用户抢占,此时调用的进程再次申请上次未释放的自旋锁时,会一直自旋。但是内核被禁止抢占,从而造成死锁。
内核被禁止抢占,但此时中断并没被禁止,内核进程可能因为中断申请自旋锁而死锁。
3 、多CPU且内核可抢占:
这才是是真正的SMP的情况。当获得自旋锁的时候,禁止内核抢占直到释放锁为止
3、互斥锁:一个比信号量更加简单方便的睡眠锁
特点:必须在同一个上下文中上锁和解锁,不能在一个上下文中上锁,另一个上下文中解锁;不能递归的上锁和解锁;不能在中断或者下半部使用;不能在中断上下文使用。原子性:互斥锁的操作是原子操作;唯一性:只有一个线程持有锁;非繁忙等待。
对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
4、信号量:一种通用复杂的睡眠锁
只能在进程上下文获取信号量锁,不能在中断上下文获取信号量锁,因为可能导致睡眠。可以在持有信号量的时候睡眠,因为其他信号量可以睡眠等待。
二值信号量 / 互斥信号量:任意时刻只允许有一个持有者
计数信号量:任意时刻可以有多个持有者。
关于选择:
除非mutex的某个约束导致不能使用互斥锁,才选择信号量,否则一般情况下选择互斥锁。中断上下文只能使用自旋锁。
死锁:
在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。
因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。
1、试图递归地获得自旋锁必然会引起死锁:递归程序的持有实例在第二个实例循环,以试图获得相同自旋锁时,不会释放此自旋锁。
2、进程得到自旋锁后阻塞,睡眠:
在获得自旋锁之后调用copy_from_user()、copy_to_ser()、和kmalloc()等有可能引起阻塞的函数。
3、中断中没有关中断,或着因为申请未释放的自旋锁
在中断中使用自旋锁是可以的,应该在进入中断的时候关闭中断,不然中断再次进入的时候,中断处理函数会自旋等待自旋锁可以再次使用。或者在进程中申请了自旋锁,释放前进入中断处理函数,中断处理函数又申请同样的自旋锁,这将导致死锁。
5、读写自旋锁 和 读写信号量
6、completion变量
7、大内核锁 (已经不使用)
8、顺序锁即seq锁
9、优化屏障和内存屏障:事实上,所有的同步原语都起到了优化和内存屏障的作用。
优化屏障:保证编译程序不会混淆放在原语操作之前的程序和原语之后的程序,避免指令重新排序。
内存屏障:确保原语之前的操作都已经完成。
10、禁止内核抢占:一般对每CPU变量访问需要关闭内核抢占。