Linux内核原语(六)——读-写自旋锁(rwlock)

 

 

Linux内核原语(六)——读-写自旋锁(rwlock)

小狼@http://blog.csdn.net/xiaolangyangyang

 


上一章聊到了内核的自旋锁 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 threadwrite thread当然不可以进入临界区,但是该write thread也无法阻止后续read thread的进入,他要一直等到临界区一个read thread也没有的时候,才可以进入。

解锁的逻辑:

1)在write thread离开临界区的时候,由于write thread是排他的,因此临界区有且只有一个write thread,这时候,如果write thread执行unlock操作,释放掉锁,那些处于spin的各个threadread或者write)可以竞争上岗。

2)在read thread离开临界区的时候,需要根据情况来决定是否让其他处于spinwrite 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 lockrwlock_init
获取指定的rw spin lockread_lock
write_lock
获取指定的rw spin lock同时disable本CPU中断read_lock_irq
write_lock_irq
保存本CPU当前的irq状态,disable本CPU中断并获取指定的rw spin lockread_lock_irqsave
write_lock_irqsave
获取指定的rw spin lock同时disable本CPU的bottom halfread_lock_bh
write_lock_bh
释放指定的spin lockread_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 halfread_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 或者writethread持有锁,那么还是静静的等待吧。其他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 赋值为

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 等)。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值