主要内容
- 原子操作
- 自旋锁
- 读写自旋锁
- 信号量
- 读写信号量
- 互斥锁
- 完成变量
- 大内核锁
- 顺序锁
- 禁止抢占
- 顺序和屏障
1. 原子操作
原子操作可以保证指令以原子的方式执行,不会被打断。
内核提供了对整数,对位的原子操作接口
特殊的atomic_t类型,32位int的低8位嵌入了一个锁
2. 自旋锁
原子操作只能用于临界区只有一个变量的情况,实际应用中,临界区的情况要复杂的多。
对于复杂的临界区,linux内核中也提供了多种同步方法,自旋锁就是其中一种。
自旋锁的特点就是当一个线程获取了锁之后,其他试图获取这个锁的线程一直在循环等待获取这个锁,直至锁重新可用。
由于线程实在一直循环的获取这个锁,所以会造成CPU处理时间的浪费,因此最好将自旋锁用于能很快处理完的临界区。
注意:
- 自旋锁是不可递归的,递归的请求同一个自旋锁会自己锁死自己。
- 线程获取自旋锁之前,要禁止当前处理器上的中断。(防止获取锁的线程和中断形成竞争条件,导致死锁)
比如:当前线程获取自旋锁后,在临界区中被中断处理程序打断,中断处理程序正好也要获取这个锁,
于是中断处理程序会等待当前线程释放锁,而当前线程也在等待中断执行完后再执行临界区和释放锁的代码。
中断处理下半部:
- 下半部处理和进程上下文共享数据时,由于下半部的处理可以抢占进程上下文的代码,
所以进程上下文在对共享数据加锁前要禁止下半部的执行,解锁时再允许下半部的执行。 - 中断处理程序(上半部)和下半部处理共享数据时,由于中断处理(上半部)可以抢占下半部的执行,
所以下半部在对共享数据加锁前要禁止中断处理(上半部),解锁时再允许中断的执行。 - 同一种tasklet不能同时运行,所以同类tasklet中的共享数据不需要保护。
- 不同类tasklet中共享数据时,其中一个tasklet获得锁后,不用禁止其他tasklet的执行,因为同一个处理器上不会有tasklet相互抢占的情况
- 同类型或者非同类型的软中断在共享数据时,也不用禁止下半部,因为同一个处理器上不会有软中断互相抢占的情况
3. 读写自旋锁
用于生产者消费者类型的读写自旋锁
读锁之间共享,写锁之间互斥
4.信号量
信号量也是一种锁,和自旋锁不同的是,线程获取不到信号量的时候,不会像自旋锁一样循环的去试图获取锁,而是进入睡眠,直至有信号量释放出来时,才会唤醒睡眠的线程,进入临界区执行。
由于使用信号量时,线程会睡眠,所以等待的过程不会占用CPU时间。所以信号量适用于等待时间较长的临界区。
信号量消耗的CPU时间的地方在于使线程睡眠和唤醒线程,如果 (使线程睡眠 + 唤醒线程)的CPU时间 > 线程自旋等待的CPU时间,那么可以考虑使用自旋锁。
信号量有二值信号量和计数信号量2种,其中二值信号量比较常用。
- 二值信号量表示信号量只有2个值,即0和1。信号量为1时,表示临界区可用,信号量为0时,表示临界区不可访问。
二值信号量表面看和自旋锁很相似,区别在于争用自旋锁的线程会一直循环尝试获取自旋锁,
而争用信号量的线程在信号量为0时,会进入睡眠,信号量可用时再被唤醒。 - 计数信号量有个计数值,比如计数值为5,表示同时可以有5个线程访问临界区。
5. 读写信号量
读写信号量和信号量之间的关系 与 读写自旋锁和普通自旋锁之间的关系 差不多。
6. 互斥锁
互斥体也是一种可以睡眠的锁,相当于二值信号量,只是提供的API更加简单,使用的场景也更严格一些,如下所示:
- mutex的计数值只能为1,也就是最多只允许一个线程访问临界区
- 在同一个上下文中上锁和解锁
- 不能递归的上锁和解锁
- 持有个mutex时,进程不能退出
- mutex不能在中断或者下半部中使用,也就是mutex只能在进程上下文中使用
- mutex只能通过官方API来管理,不能自己写代码操作它
如何选择?
在面对互斥体和信号量的选择时,只要满足互斥体的使用场景就尽量优先使用互斥体。
在面对互斥体和自旋锁的选择时,参见下表:
需求 | 建议的加锁方法 |
---|---|
低开销加锁 | 优先使用自旋锁 |
短期锁定 | 优先使用自旋锁 |
长期加锁 | 优先使用互斥体 |
中断上下文中加锁 | 使用自旋锁 |
持有锁需要睡眠 | 使用互斥体 |
7. 完成变量
如果一个任务要执行一些工作时,另一个任务就会在完成变量上等待。当这个任务完成工作后,会使用完成变量去唤醒在等待的任务。
实现机制类似于信号量。
8. 大内核锁
不再使用,只存在于一些遗留代码
9. 顺序锁
为读写共享数据提供了一种简单的实现机制。
前提到的读写自旋锁和读写信号量,在读锁被获取之后,写锁是不能再被获取的,也就是说,必须等所有的读锁释放后,才能对临界区进行写入操作。
顺序锁则与之不同,读锁被获取的情况下,写锁仍然可以被获取。
使用顺序锁的读操作在读之前和读之后都会检查顺序锁的序列值,如果前后值不符,则说明在读的过程中有写的操作发生,那么读操作会重新执行一次,直至读前后的序列值是一样的。
10. 禁止抢占
其实使用自旋锁已经可以防止内核抢占了,但是有时候仅仅需要禁止内核抢占,不需要像自旋锁那样连中断都屏蔽掉。
这时候就需要使用禁止内核抢占的方法了:
方法 | 描述 |
---|---|
preempt_disable() | 增加抢占计数值,从而禁止内核抢占 |
preempt_enable() | 减少抢占计算,并当该值降为0时检查和执行被挂起的需调度的任务 |
preempt_enable_no_resched() | 激活内核抢占但不再检查任何被挂起的需调度的任务 |
preempt_count() | 返回抢占计数 |
11. 顺序和屏障
一种同步机制(又称栅栏,关卡),用于对一组线程进行协调,所有线程到达一个汇合点后再一起向前推进