Linux 内核同步(三):读-写自旋锁(rwlock)

版权声明:原创文章 && 转载请著名出处 https://blog.csdn.net/zhoutaopower/article/details/86605987

读-写自旋锁


上一章聊到了内核的自旋锁 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 等)。

猜你喜欢

转载自blog.csdn.net/zhoutaopower/article/details/86605987