读-写自旋锁
上一章聊到了内核的自旋锁 spinlock 相关的内容,试想这样一种场景:一个内核链表元素,很多进程(或者线程)都会对其进行读写,但是使用 spinlock 的话,多个读之间无法并发,只能被 spin,为了提高系统的整体性能,内核定义了一种锁:
1. 允许多个处理器进程(或者线程或者中断上下文)并发的进行读操作(SMP 上),这样是安全的,并且提高了 SMP 系统的性能。
2. 在写的时候,保证临界区的完全互斥
所以,当某种内核数据结构被分为:读-写,或者生产-消费,这种类型的时候,类似这种 读-写自旋锁就起到了作用。对读者是共享的,对写者完全互斥。
读/写自旋锁是在保护SMP体系下的共享数据结构而引入的,它的引入是为了增加内核的并发能力。只要内核控制路径没有对数据结构进行修改,读/写自旋锁就允许多个内核控制路径同时读同一数据结构。如果一个内核控制路径想对这个结构进行写操作,那么它必须首先获取读/写锁的写锁,写锁授权独占访问这个资源。这样设计的目的,即允许对数据结构并发读可以提高系统性能。
加锁的逻辑:
(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也没有的时候,才可以进入。
解锁的逻辑:
(1)在write thread离开临界区的时候,由于write thread是排他的,因此临界区有且只有一个write thread,这时候,如果write thread执行unlock操作,释放掉锁,那些处于spin的各个thread(read或者write)可以竞争上岗。
(2)在read thread离开临界区的时候,需要根据情况来决定是否让其他处于spin的write thread们参与竞争。如果临界区仍然有read thread,那么write thread还是需要spin(注意:这时候read thread可以进入临界区,听起来也是不公平的)直到所有的read thread释放锁(离开临界区),这时候write thread们可以参与到临界区的竞争中,如果获取到锁,那么该write thread可以进入。
读-写自旋锁的使用
与 spinlock 的使用方式几乎一致,读-写自旋锁初始化方式也分为两种:
动态的:
rwlock_t rw_lock;
rwlock_init (&rw_lock);
静态的:
DEFINE_RWLOCK(rwlock);
初始化完成后就可以使用读-写自旋锁了,内核提供了一组 APIs 来操作读写自旋锁,最简单的比如:
读临界区:
rwlock_t rw_lock;
rwlock_init (&rw_lock);
read_lock(rw_lock);
------------- 读临界区 -------------
read_unlock(rw_lock);
写临界区:
rwlock_t rw_lock;
rwlock_init (&rw_lock);
write_lock(rw_lock);
------------- 写临界区 -------------
write_unlock(rw_lock);
注意:读锁和写锁会位于完全分开的代码中,若是:
read_lock(lock);
write_lock(lock);
这样会导致死锁,因为读写锁的本质还是自旋锁。写锁不断的等待读锁的释放,导致死锁。如果读-写不能清晰的分开的话,使用一般的自旋锁,就别使用读写锁了。
注意:由于读写自旋锁的这种特性(允许多个读者),使得即便是递归的获取同一个读锁也是允许的。更比如,在中断服务程序中,如果确定对数据只有读操作的话(没有写操作),那么甚至可以使用 read_lock 而不是 read_lock_irqsave,但是对于写来说,还是需要调用 write_lock_irqsave 来保证不被中断打断,否则如果在中断中去获取了锁,就会导致死锁。
读-写锁内核 APIs
与 spinlock 一样,Read/Write spinlock 有如下的 APIs:
接口API描述 | Read/Write Spinlock API |
定义rw spin lock并初始化 | DEFINE_RWLOCK |
动态初始化rw spin lock | rwlock_init |
获取指定的rw spin lock | read_lock write_lock |
获取指定的rw spin lock同时disable本CPU中断 | read_lock_irq write_lock_irq |
保存本CPU当前的irq状态,disable本CPU中断并获取指定的rw spin lock | read_lock_irqsave write_lock_irqsave |
获取指定的rw spin lock同时disable本CPU的bottom half | read_lock_bh write_lock_bh |
释放指定的spin lock | read_unlock write_unlock |
释放指定的rw spin lock同时enable本CPU中断 | read_unlock_irq write_unlock_irq |
释放指定的rw spin lock同时恢复本CPU的中断状态 | read_unlock_irqrestore write_unlock_irqrestore |
获取指定的rw spin lock同时enable本CPU的bottom half | read_unlock_bh write_unlock_bh |
尝试去获取rw spin lock,如果失败,不会spin,而是返回非零值 | read_trylock write_trylock |
读-写锁内核实现
说明:使用读写内核锁需要包含的头文件和 spinlock 一样,只需要包含:include/linux/spinlock.h 就可以了
这里仅看 和体系架构相关的部分,在 ARM 体系架构上:
arch_rwlock_t 的定义:
typedef struct {
u32 lock;
} arch_rwlock_t;
看看 arch_write_lock 的实现:
static inline void arch_write_lock(arch_rwlock_t *rw)
{
unsigned long tmp;
prefetchw(&rw->lock);------------------------(0)
__asm__ __volatile__(
"1: ldrex %0, [%1]\n"--------------------------(1)
" teq %0, #0\n"--------------------------------(2)
WFE("ne")------------------------------------(3)
" strexeq %0, %2, [%1]\n"----------------------(4)
" teq %0, #0\n"--------------------------------(5)
" bne 1b"--------------------------------------(6)
: "=&r" (tmp)
: "r" (&rw->lock), "r" (0x80000000)
: "cc");
smp_mb();------------------------------------(7)
}
(0) : 先通知 hw 进行preloading cache
(1): 标记独占,获取 rw->lock 的值并保存在 tmp 中
(2) : 判断 tmp 是否等于 0
(3) : 如果 tmp 不等于0,那么说明有read 或者write的thread持有锁,那么还是静静的等待吧。其他thread会在unlock的时候Send Event来唤醒该CPU的
(4) : 如果 tmp 等于0,将 0x80000000 这个值赋给 rw->lock
(5) : 是否 str 成功,如果有其他 thread 在上面的过程插入进来就会失败
(6) : 如果不成功,那么需要重新来过跳转到标号为 1 的地方,即开始的地方,否则持有锁,进入临界区
(7) : 内存屏障,保证执行顺序
arch_write_unlock 的实现:
static inline void arch_write_unlock(arch_rwlock_t *rw)
{
smp_mb(); ---------------------------(0)
__asm__ __volatile__(
"str %1, [%0]\n" -----------------(1)
:
: "r" (&rw->lock), "r" (0)
: "cc");
dsb_sev(); --------------------------(2)
}
(0) : 内存屏障
(1) : rw->lock 赋值为 0
(2) :唤醒处于 WFE 的 thread
arch_read_lock 的实现:
static inline void arch_read_lock(arch_rwlock_t *rw)
{
unsigned long tmp, tmp2;
prefetchw(&rw->lock);
__asm__ __volatile__(
"1: ldrex %0, [%2]\n" ----------- (0)
" adds %0, %0, #1\n" --------- (1)
" strexpl %1, %0, [%2]\n" ------- (2)
WFE("mi") --------------------- (3)
" rsbpls %0, %1, #0\n" --------- (4)
" bmi 1b" ----------------------- (5)
: "=&r" (tmp), "=&r" (tmp2)
: "r" (&rw->lock)
: "cc");
smp_mb();
}
(0) : 标记独占,获取 rw->lock 的值并保存在 tmp 中
(1) : tmp = tmp + 1
(2) : 如果 tmp 结果非负值,那么就执行该指令,将 tmp 值存入rw->lock
(3) : 如果 tmp 是负值,说明有 write thread,那么就进入 wait for event 状态
(4) : 判断strexpl指令是否成功执行
(5) : 如果不成功,那么需要重新来过,否则持有锁,进入临界区
arch_read_unlock 的实现:
static inline void arch_read_unlock(arch_rwlock_t *rw)
{
unsigned long tmp, tmp2;
smp_mb();
prefetchw(&rw->lock);
__asm__ __volatile__(
"1: ldrex %0, [%2]\n" -----------(0)
" sub %0, %0, #1\n" -------------(1)
" strex %1, %0, [%2]\n" -------(2)
" teq %1, #0\n" -----------------(3)
" bne 1b" -----------------------(4)
: "=&r" (tmp), "=&r" (tmp2)
: "r" (&rw->lock)
: "cc");
if (tmp == 0)
dsb_sev(); ----------------(5)
}
(0) : 标记独占,获取 rw->lock 的值并保存在 tmp 中
(1) : read 退出临界区,所以,tmp = tmp + 1
(2) : 将tmp值存入 rw->lock 中
(3) :是否str成功,如果有其他thread在上面的过程插入进来就会失败
(4) : 如果不成功,那么需要重新来过,否则离开临界区
(5) : 如果read thread已经等于0,说明是最后一个离开临界区的 Reader,那么调用 sev 去唤醒 WF E的 CPU Core(配合 Writer 线程)
所以看起来,读-写锁使用了一个 32bits 的数来存储当前的状态,最高位代表着是否有写线程占用了锁,而低 31 位代表可以同时并发的读的数量,看起来现在至少是绰绰有余了。
小结
读-写锁自旋锁本质上还是属于自旋锁。只不过允许了并发的读操作,对于写和写,写和读之间,都需要互斥自旋等待。
注意读-写锁自旋锁的使用,避免死锁。
合理运用内核提供的 APIs (诸如:write_lock_irqsave 等)。