2020-04-09-Linux内核30-读写自旋锁

layouttitlesubtitledateauthorheader-imgcatalogtags
post
Linux内核30-读写自旋锁
Linux读写自旋锁工作原理以及应用场合
2020-04-09
Tupelo Shen
img/post-bg-unix-linux.jpg
true
Linux
Linux内核
原子操作

1 读/写自旋锁概念

自旋锁解决了多核系统在内核抢占模式下的数据共享问题。但是,这样的自旋锁一次只能一个内核控制路径使用,这严重影响了系统的并发性能。根据我们以往的开发经验,大部分的程序都是读取共享的数据,并不更改;只有少数时候会修改数据。为此,Linux内核提出了读/写自旋锁的概念。也就是说,没有内核控制路径修改共享数据的时候,多个内核控制路径可以同时读取它。如果有内核控制路径想要修改这个数据结构,它就释放读/写自旋锁的写自旋锁,独占访问这个资源。这大大提高了系统的并发性能。

2 读写自旋锁的数据结构

读/写自旋锁的数据结构是rwlock_t,其定义如下:

typedef struct {
    arch_rwlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
    unsigned int break_lock;
#endif
    ......
} rwlock_t;

从上面的代码可以看出,读/写自旋锁的实现还是依赖于具体的架构体系。下面我们先以ARM体系解析一遍:

arch_rwlock_t的定义:

typedef struct { 
    u32 lock; 
} arch_rwlock_t;

3 读写自旋锁API实现

  1. 请求写自旋锁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)通知硬件提前将rw->lock的值加载到cache中,缩短等待预取指令的时延。
    • (1)使用独占指令ldrex标记相应的内存位置已经被独占,并将其值存储到tmp变量中。
    • (2)判断tmp是否等于0。
    • (3)如果tmp不等于0,则说明rw->lock正在被占用,所以进入低功耗待机模式。
    • (4)如果tmp等于0,则向rw->lock的内存地址处写入0x80000000,然后清除独占标记。
    • (5)测试tmp是否等于0,相当于验证第4步是否成功。
    • (6)如果加锁失败,则重新(0)->(5)的过程。
    • (7)现在只是把指令写入到数据总线上,还没有完全成功。所以smp_mb()内存屏障保证加锁成功。
  2. 写自旋锁的释放过程,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)调用sev指令,唤醒正在执行WFE指令的内核控制路径。
  3. 读自旋锁的释放过程(低功耗版)由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地址处的内容,然后标记为独占。
    • (1)tmp=tmp+1。
    • (2)将这条指令的执行结果写入到tmp2变量中,将tmp的值写入到rw->lock地址处。
    • (3)如果tmp是负值,说明锁已经被占有,则执行wfe指令,进入低功耗待机模式。
    • (4)执行0减去tmp2,将结果写入tmp。因为tmp2的值有2个:0-更新成功;1-更新失败。所以正常情况,此时tmp的结果应该为0,也就是释放加锁成功。
    • (5)如果加锁失败,则重新进行(0)->(4)的操作。失败的可能就是,独占标记被其它加锁操作破坏。
  4. 读自旋锁的释放过程(不断尝试版)由arch_read_trylock函数实现,代码如下:

     static inline int arch_read_trylock(arch_rwlock_t *rw)
     {
         unsigned long contended, res;
    
         prefetchw(&rw->lock);
         do {
             __asm__ __volatile__(
             "   ldrex   %0, [%2]\n"     // ----------(0)
             "   mov %1, #0\n"           // ----------(1)
             "   adds    %0, %0, #1\n"   // ----------(2)
             "   strexpl %1, %0, [%2]"   // ----------(3)
             : "=&r" (contended), "=&r" (res)
             : "r" (&rw->lock)
             : "cc");
         } while (res);                  // ----------(4)
    
         /* 如果lock为负,则已经处于write状态 */
         if (contended < 0x80000000) {   // ----------(5)
             smp_mb();
             return 1;
         } else {
             return 0;
         }
     }
    
    • (0)读取rw->lock地址处的内容,然后标记为独占。
    • (1)res = 0。
    • (2)contended = contended + 1。
    • (3)将contended的值写入rw->lock地址处,操作结果写入res。
    • (4)如果res等于0,操作成功;否则重新前面的操作。
    • (5)如果此时处于write状态,则释放锁失败,返回1;否则,成功返回0。

    根据4和5两个释放锁的过程分析,可以看出除了是否根据需要进入低功耗状态之外,其它没有区别。

  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();
     }
    
    • (0)读取rw->lock地址处的内容,然后标记为独占。
    • (1)要退出临界区,所以,tmp = tmp - 1。
    • (2)tmp写入到rw->lock地址处,操作结果写入tmp2。
    • (3)判断tmp2是否等于0。
    • (4)等于0成功,不等于0,则跳转到标签1处继续执行。

通过上面的分析可以看出,读写自旋锁使用bit31表示写自旋锁,bit30-0表示读自旋锁,对于读自旋锁而言,绰绰有余了。

  1. 成员break_lock

对于另一个成员break_lock来说,同自旋锁数据结构中的成员一样,标志锁的状态。

rwlock_init宏初始化读写锁的lock成员。

对于X86系统来说,处理的流程跟ARM差不多。但是,因为与ARM架构体系不同,所以具体的加锁和释放锁的实现是不一样的。在此,就不一一细分析了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值