自旋锁解决了多核系统在内核抢占模式下的数据共享问题。但是,这样的自旋锁一次只能一个内核控制路径使用,这严重影响了系统的并发性能。根据我们以往的开发经验,大部分的程序都是读取共享的数据,并不更改;只有少数时候会修改数据。为此,Linux内核提出了读/写自旋锁
的概念。也就是说,没有内核控制路径修改共享数据的时候,多个内核控制路径可以同时读取它。如果有内核控制路径想要修改这个数据结构,它就请求读/写自旋锁
的写自旋锁,独占访问这个资源。这大大提高了系统的并发性能。
读/写自旋锁
的数据结构是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;
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)
}
rw->lock
的值加载到cache中,缩短等待预取指令的时延。ldrex
标记相应的内存位置已经被独占,并将其值存储到tmp变量中。0x80000000
,然后清除独占标记。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)
}
rw->lock
的值赋值为0。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();
}
rw->lock
地址处的内容,然后标记为独占。rw->lock
地址处。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两个释放锁的过程分析,可以看出除了是否根据需要进入低功耗状态之外,其它没有区别。
rw->lock
地址处的内容,然后标记为独占。rw->lock
地址处,操作结果写入res。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();
}
通过上面的分析可以看出,读写自旋锁使用bit31表示写自旋锁,bit30-0表示读自旋锁,对于读自旋锁而言,绰绰有余了。
对于另一个成员break_lock
来说,同自旋锁数据结构中的成员一样,标志锁的状态。
rwlock_init
宏初始化读写锁的lock成员。
对于X86系统来说,处理的流程跟ARM差不多。但是,因为与ARM架构体系不同,所以具体的加锁和释放锁的实现是不一样的。在此,就不一一细分析了。
本文分享自 嵌入式ARM和Linux 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!