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实现
- 请求写自旋锁
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()
内存屏障保证加锁成功。
- 写自旋锁的释放过程,
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指令的内核控制路径。
- 读自旋锁的释放过程(低功耗版)由
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)的操作。失败的可能就是,独占标记被其它加锁操作破坏。
- 读自旋锁的释放过程(不断尝试版)由
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; } }
- 根据4和5两个释放锁的过程分析,可以看出除了是否根据需要进入低功耗状态之外,其它没有区别。
- (0)读取
rw->lock
地址处的内容,然后标记为独占。 - (1)res = 0。
- (2)contended = contended + 1。
- (3)将contended的值写入
rw->lock
地址处,操作结果写入res。 - (4)如果res等于0,操作成功;否则重新前面的操作。
- (5)如果此时处于write状态,则释放锁失败,返回1;否则,成功返回0。
- 读自旋锁的释放过程由
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表示读自旋锁,对于读自旋锁而言,绰绰有余了。
- 成员break_lock
对于另一个成员break_lock
来说,同自旋锁数据结构中的成员一样,标志锁的状态。
rwlock_init
宏初始化读写锁的lock成员。
对于X86系统来说,处理的流程跟ARM差不多。但是,因为与ARM架构体系不同,所以具体的加锁和释放锁的实现是不一样的。在此,就不一一细分析了。