linux内核互斥机制之自旋锁
在linux内核中,使用最多的互斥原语非自旋锁莫属。从概念上来讲,自旋锁很简单,自旋锁只有两种状态:“锁”和“解锁”,这点和互斥量相同。
当进程进入临界区时,要获取自旋锁:
如果当前锁处于“解锁”状态,那么就设置锁为“锁”状态,然后该进程就持有锁并继续运行;
如果当前锁处于“锁”状态,那么该进程就会一直循环检测该锁,直到该锁状态变为“解锁”然后获取锁后继续运行,这个循环过程就是所谓的自旋,自旋时是不会释放CPU的。
当进程要离开临界区时,要释放自旋锁。
在进程自旋过程中,进程会一直占用CPU,并不会睡眠,这就是自旋锁与信号量最大的不同,也是自旋锁能够应用于非进程上下文的原因。自旋锁的概念就这么简单,但是其实现却并不简单,因为上面的检测锁状态并设置锁的过程必须是原子的,这在实现中就要考虑诸多因素,这部分内容与体系结构相关,目前并未做研究,所幸在使用自旋锁时,也无需纠结于其实现,只需理解其原理即可。
1. 初始化
//spinlock_init()是用宏来实现的,所以参数name就是变量名字,而不是指针
void spinlock_init(name)//初始化后读写自旋锁为解锁状态
1
2
2. 加锁
linux内核提供了一系列的加锁宏,使得自旋锁能够用于各种场合。
void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);
void spin_trylock(spinlock_t *lock);
void spin_trylock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_trylock_irq(spinlock_t *lock);
void spin_trylock_bh(spinlock_t *lock);
1
2
3
4
5
6
7
8
spin_lock()宏会禁止内核态抢占,然后获取自旋锁;
spin_lock_irqsave()宏会将先前的中断状态保存在flags中并禁止本地处理器中断,然后禁止内核态抢占,最后获取自旋锁;
spin_lock_irq()宏会直接禁止本地处理器中断而不保存先前的中断状态,然后禁止内核态抢占,最后获取自旋锁;
spin_lock_bh()宏会禁止中断下半部,然后禁止内核态抢占,最后获取自旋锁。
trylock系列是相应的非自旋版本,这组宏会检测自旋锁的状态,如果处于“解锁”状态,则获取锁并返回,否则直接返回,由于无法确定其返回后是否成功获取了锁,所以这组宏在实际中使用的较少。
3. 解锁
对应上面的各种加锁函数,都有相应的解锁函数,含义不再赘述:
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
1
2
3
4
4. 如何合理使用自旋锁
要想弄明白应该如何合理的使用自旋锁相关的接口,需要理解一些概念。
4.1 内核态抢占和用户态抢占
直接对这两个概念进行定义不好理解,换个角度理解可能更加合适。进程A在内核态运行,此时有一个更好优先级的进程B就绪,如果此时内核可以将进程A挂起,然后调度进程B执行,那么我们说该内核是支持内核态抢占的,否则就是不支持。
用户态抢占是指内核在进程由内核态转变为用户态时进行的进程调度切换,这种转变发生在系统调用返回和中断处理程序返回时。
linux内核在2.6之前的版本只支持用户态抢占,不支持内核态抢占,但是从2.6版本开始,内核态抢占特性可以再编译时进行配置。
下面,我们分别分几种情况来讨论自旋锁的应用场景。
4.2 单CPU,不支持内核态抢占
这种情况最简单,内核代码在运行时唯一的并发源就是中断处理程序,所以在互斥时,只需要开启和关闭CPU的中断处理能力即可。
4.3 单CPU,支持内核态抢占
这种情况稍微复杂些,可能的并发源除了中断处理程序,还有其它进程,因为支持内核态抢占,所以在代码执行过程中,可能随时会被另一个高优先级的进程打断,造成并发。这种场景下的互斥操作需要考虑:
关闭和开启CPU中断处理能力
开启和关闭内核态抢占
4.4 多CPU,不支持内核态抢占
这种情况下的并发源有中断处理程序(本地CPU和其它CPU)、运行在其它CPU上的进程。这种场景下互斥操作需要考虑:
关闭和开启本地CPU的中断处理能力
防止其它CPU上的进程造成并发
4.5 多CPU,支持内核态抢占
这种情况下的并发源有中断处理程序(本地CPU和其它CPU)、本地CPU上可能的进程、运行在其它CPU上的进程。这种场景下互斥操作需要考虑:
关闭和开启本地CPU的中断处理能力
防止其它CPU上的进程造成并发
开启和关闭内核态抢占
如上面所述,不同情况需要做的事情不同,但幸运的是,linux内核在这方面做了绝大多数工作,对外只是提供了上面介绍的统一的接口。为了更好的理解自旋锁,下面梳理下自旋锁在各种情况下的加锁、解锁行为。
A: preempt_enable(): 开启内核态抢占;
B: preempt_disable(): 关闭内核态抢占;
C: do_raw_spin_lock(): 持锁,避免其它CPU再持锁,包括其他CPU上的中断处理程序;
D: do_raw_spin_unlock(): 解锁,允许其它CPU再持锁;
E: local_irq_enable(): 开启本地CPU的硬中断响应;
F: local_irq_disable(): 关闭本地CPU的硬中断响应;
G: local_irq_save(): 关闭本地CPU的硬中断响应并保存之前的中断标记;
H: local_irq_restore(): 打开本地CPU的硬中断响应并恢复之前的中断标记;
I: local_bh_enable(): 开启本地CPU响应下半部的能力;
J: local_bh_disable():关闭本地CPU响应下半部的能力;
如果编译时选择了禁止内核态抢占,则preempt_enable/preempt_disable这两函数为空,什么都不做。