参考自《Linux内核设计与实现》第10章
中断上下文只能使用自旋锁,因为中断不能睡眠;
中断处理的时候,不应该发生进程切换,因为在中断context中,唯一能打断当前中断handler的只有更高优先级的中断,它不会被进程打断,如果在 中断context中休眠,则没有办法唤醒它,因为所有的wake_up_xxx都是针对某个进程而言的,而在中断context中,没有进程的概念,没 有一个task_struct(这点对于softirq和tasklet一样),因此真的休眠了,比如调用了会导致block的例程,内核几乎肯定会死。
一般信号量面向很底层的代码,互斥体和信号量首选互斥体,互斥体的count只能为1;
读写锁适合于读多写少,并且代码中读写操作能明显分开的场景;
在Linux中自旋锁是不能递归的(死锁);
在新的C++11标准中,提供了recrusion_mutex和nonrecrusion_mutex,即可递归的互斥锁和不可递归的互斥锁。
Linux下的pthread_mutex_t锁默认是非递归的。可以显示的设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t设为递归锁。
自旋锁要关中断(不可被中断打断,可能会导致死锁),信号量不用关中断;
临界区中是可以发生进程调度的!!
原子操作
原子操作是:要么执行到底,要么不执行的操作;它的执行不会被线程调度所打断,这种操作不会出现context_switch(切换到另一线程,单核情况);
在多核情况下,其实现原理是执行的指令前面加了lock,这种加lock的指令会锁住总线,让该核将指令执行完后才放开总线;
原子操作适合与简单操作,对一片指令区域实现原子操作一般用加锁机制;对于一片已经加锁了的指令里,可以使用非原子操作;
自旋锁
一定要在获取锁之前,首先禁止本地中断(禁止当前处理器的中断请求),原因是:
比如:内核进程A获得了一个spinlock,但是没有禁止本地中断,这时发生中断,中断处理程序B打断了正在持有spinlock的进程A代码; 若是这个中断处理程序中,同样有对这个spinlock的争用,那么会导致:B一直在等A释放spinlock,同时A只有在B结束后才能释放spinlock; 这就导致了所谓的–双重请求死锁
完成变量completion
完成变量是使两个任务得以同步的方法;
比如,任务A在执行任务的时候,任务B等待,当任务A执行完成的时候,将使用完成变量去唤醒在等待的任务;
方法:
completion完成变量是由结构体表示的;
init_completion(struct completion*): 初始化指定的动态创建的完成变量
wait_for_completion(struct completion*): 去等待指定的完成变量(所在的任务完成)
complete(struct completion*): 发信号去唤醒所有等待这个完成变量的任务
如:
在任务A中初始化一个完成变量,任务B,C都用wait_for_completion去等待这个A中的完成变量;一旦A完成的它的任务,那么通过complete()发送信号去唤醒等待A结束的任务B和C
顺序锁seq
是读写锁的优化,读写锁在读的时候写操作只能等待,导致写者饥饿,而顺序锁在读的时候也可以写;
它用于读写共享数据,依赖一个序列计数器sequence;当被保护的数据被写入的时候,会获得一个锁; 在读取数据前和读取数据后,都会检查这个序列号,若序列号相同,那么说明刚刚的读取操作没有被写操作给打断; 若是读取操作间出现了写操作,那么这个读取操作需要重新进行,保证了数据的同步;
锁的初始值是0,写锁会使值变为奇数,释放的时候会变为偶数,所以如果读取的值是偶数,那么就表明写操作没有发生;使用顺序锁时,读不会被写执行单元阻塞,当向一个临界资源中写入的同时,也可以从此临界资源中读取,即实现同时读写,但是同时写不被允许;
seq也是适用于多读少写的场景,但是不同的是,seq是对写者有利的:
抢占preempt
preempt_count():返回抢占计数;抢占计数存放着被持有锁的数量和preempt_disable()的调用次数,若计数是0,那么内核可以抢占;若大于等于1,那么内核就不会进行抢占;
preempt_disable():抢占计数+1,并禁止内核抢占
preempt_enable():抢占计数-1,并在计数减到0的时候,检查并执行被挂起的需要调度的任务
内存屏障barriers
某些指令我们是需要按照我们希望的顺序执行的,但是处理器常常可能会对指令自己重新排序,这是流水线的乱序执行导致的(当然是无依赖关系的指令,不会影响指令间的应果关系),这样会使问题复杂化; 因为有一些指令直接并没有明显的依赖关系,但是,在实践操作的时候却需要按指定的顺序:
如:
这是在某某设备驱动程序中,这两个变量却可能对应到设备的地址端口和数据端口。并且,这个设备规定了,当你需要读写设备上的某个寄存器时,先将寄存器编号设置到地址端口,然后就可以通过对数据端口的读写而操作到对应的寄存器。那么这么一来,对前面那两条指令的乱序执行就可能造成错误
这是一种隐式的依赖关系
我们可以给指令加barriers,来指示处理器不要给定点周围的指令序列进行重新排序;
编译器屏障barrier()
barrier()方法可以防止编译器跨屏障对载入read或存储write操作进行优化;
注意区分优化屏障(编译器屏障)和内存屏障!
优化屏障是解决由编译器优化导致的乱序;
内存屏障处理的是由处理器流水线导致的乱序;