内核自旋锁spinlock_t的一个不足就是没有区分读者和写着,即使多个读者之间也会进行竞争。
内核读写锁**rwlock_t**实际是读写自旋锁,从自旋锁衍生而来,是对自旋锁的改进,区分读者和写者,允许多个读者同时进入临界区,读者和写者互斥,写者和写者互斥。
如果读者占有读锁,写者申请写锁的时候自旋等待。如果写者占有写锁,读者申请读锁的时候自旋等待。
数据结构
内核读写锁定义如下:
typedef struct {
arch_rwlock_t raw_lock;
...
} rwlock_t;
各种处理器架构需要自定义数据类型 arch_rwlock_t。
操作函数
初始化
定义并且初始化静态读写自旋锁的方法如下:
DEFINE RWLOCK(x);
在运行时动态初始化读写自旋锁的方法如下:
rwlock init(lock);
读锁定
申请读锁的函数如下:
(1)read_lock(lock)
申请读锁,如果写者占有写锁,当前处理器自旋等待。
(2)read_lock_bh(lock)
申请读锁,并且禁止当前处理器的软中断。
(3)read lock irq(lock)
申请读锁,并且禁止当前处理器的硬中断
(4)read lock irqsave(lock, flags)
申请读锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。
(5)read trylock(lock)
尝试申请读锁,如果没有占有写锁的写者,那么申请读锁成功,返回1;如果写者占有写锁,那么当前处理器不等待,立即返回0。
读解锁
释放读锁的函数如下
(1)read unlock(lock)
(2)read unlock bh(lock)
释放读锁,并且开启当前处理器的软中断。
(3)read unlock irq(lock)
释放读锁,并且开启当前处理器的硬中断。
(4)read unlock irqrestore(lock, flags)
释放读锁,并且恢复当前处理器的硬中断状态
写锁定
申请写锁的函数如下
(1)write lock(lock)
申请写锁,如果写者占有写锁或者读者占有读锁,当前处理器自旋等待。(2)write lock bh(lock)
申请写锁,并且禁止当前处理器的软中断。
(3)write lock irg(lock)
申请写锁,并且禁止当前处理器的硬中断。
(4)write lock irgsave(lock, flags)
申请写锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。
(5)write trylock(lock)
尝试申请写锁,如果没有占有锁的写者和读者,那么申请写锁成功,返回1;如果写者占有写锁或者读者占有读锁,那么当前处理器不等待,立即返回 0。
写解锁
释放写锁的函数如下。
(1)write unlock(lock)
(2)write unlock bh(lock)
释放写锁,并且开启当前处理器的软中断。
(3)write unlock irq(lock)
释放写锁,并且开启当前处理器的硬中断。
(4)write unlock irgrestore(lock, flags)
释放写锁,并且恢复当前处理器的硬中断状态
ARM64实现
数据结构
对于rwlock_t,各种处理器架构需要自定义数据类型 arch_rwlock_t,ARM64 架构的定义如下:
typedef struct {
volatile unsigned int lock;
} arch_rwlock_t;
arch_rwlock_t使用一个无符号 32 位整数:
<font style="color:rgb(64, 64, 64);">lock</font>的最高位 (bit 31) 表示写锁状态:1 表示有写者持有锁- 低31位表示读者计数
读写锁算法如下:
(1)申请写锁时,如果计数值是 0,那么设置计数的最高位,进入临界区;如果计数值不是 0,说明写者占有写锁或者读者占有读锁,那么自旋等待。
(2)申请读锁时,如果计数值的最高位是0,那么把计数加1,进入临界区;如果计数的最高位不是 0,说明写者占有写锁,那么自旋等待。
(3)释放写锁时,把计数值设置为 0。
(4)释放读锁时,把计数值减1。
read_lock
read_lock() -> _raw_read_lock() -> __raw_read_lock() -> do_raw_read_trylock() -> arch_read_trylock()
↓↓
do_raw_read_lock() -> **arch_read_lock**()
跟内核自旋锁spinlock_t的spin_lock()流程类似,也是分快速路径和慢速路径。
arch_read_trylock
<font style="color:rgb(0, 0, 0);">arch_read_trylock</font>函数是用于尝试获取读锁的非阻塞式实现,是快速路径。
成功获取锁时返回1,失败时返回0。
static inline int arch_read_trylock(arch_rwlock_t *rw)
{
unsigned int tmp, tmp2;
asm volatile(ARM64_LSE_ATOMIC_INSN(
/* LL/SC */
" mov %w1, #1\n" // 初始化tmp2为1
"1: ldaxr %w0, %2\n" // 独占方式加载(带获取语义) lock值到tmp
" add %w0, %w0, #1\n" // 增加读者计数
" tbnz %w0, #31, 2f\n" // 如果结果为负(写锁被持有),跳转到2。函数返回0(失败)
" stxr %w1, %w0, %2\n" // 独占方式写回内存
" cbnz %w1, 1b\n" // 如果存储失败,重试
"2:",
/* LSE atomics */
" ldr %w0, %2\n"
" adds %w1, %w0, #1\n"
" tbnz %w1, #31, 1f\n"
" casa %w0, %w1, %2\n"
" sbc %w1, %w1, %w0\n"
__nops(1)
"1:")
: "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)
:
: "cc", "memory");
return !tmp2;
}
**arch_read_lock**
arch_read_lock函数是慢速路径,用于快速路径失败的场景。
/*
* Read lock implementation.
*
* It exclusively loads the lock value, increments it and stores the new value
* back if positive and the CPU still exclusively owns the location. If the
* value is negative, the lock is already held.
*
* During unlocking there may be multiple active read locks but no write lock.
*
* The memory barriers are implicit with the load-acquire and store-release
* instructions.
*
* Note that in UNDEFINED cases, such as unlocking a lock twice, the LL/SC
* and LSE implementations may exhibit different behaviour (although this
* will have no effect on lockdep).
*/
static inline void arch_read_lock(arch_rwlock_t *rw)
{
unsigned int tmp, tmp2;
asm volatile(
" sevl\n"
ARM64_LSE_ATOMIC_INSN(
/* LL/SC */
"1: wfe\n"
"2: ldaxr %w0, %2\n"
" add %w0, %w0, #1\n"
" tbnz %w0, #31, 1b\n" // lock最高位非0(写者占有锁),重试
" stxr %w1, %w0, %2\n"
" cbnz %w1, 2b\n"
__nops(1),
/* LSE atomics */
"1: wfe\n"
"2: ldxr %w0, %2\n"
" adds %w1, %w0, #1\n"
" tbnz %w1, #31, 1b\n"
" casa %w0, %w1, %2\n"
" sbc %w0, %w1, %w0\n"
" cbnz %w0, 2b")
: "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)
:
: "cc", "memory");
}
以下几点与arch_read_trylock实现不同:
- 使用了`sevl`和`wfe`指令,实现高效等待,降低CPU功耗。与`spinlock_t`一致。
- 检测到写着占有锁时,进行重试;而`arch_read_trylock`则直接返回。
read_unlock
read_unlock() -> _raw_read_unlock() -> __raw_read_unlock() -> do_raw_read_unlock() -> arch_read_unlock()
static inline void arch_read_unlock(arch_rwlock_t *rw)
{
unsigned int tmp, tmp2;
asm volatile(ARM64_LSE_ATOMIC_INSN(
/* LL/SC */
"1: ldxr %w0, %2\n" // 独占方式将lock值加载到寄存器w0(tmp)
" sub %w0, %w0, #1\n" // 将寄存器中lock计数减1
" stlxr %w1, %w0, %2\n" // 独占方式尝试将寄存器中的lock值写回内存,并将结果写入tmp2
" cbnz %w1, 1b", // 如果tmp2 != 0,则进行重试
/* LSE atomics */
" movn %w0, #0\n"
" staddl %w0, %2\n"
__nops(2))
: "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)
:
: "memory");
}
write_lock
write_lock() -> _raw_write_lock() -> __raw_write_lock() -> do_raw_write_trylock() -> arch_write_trylock()
↓↓
do_raw_write_lock() -> **arch_write_lock**()
arch_write_trylock和arch_write_lock的区别**,与读锁**的lock/trylock类似。这里只介绍arch_write_lock。
/*
* Write lock implementation.
*
* Write locks set bit 31. Unlocking, is done by writing 0 since the lock is
* exclusively held.
*
* The memory barriers are implicit with the load-acquire and store-release
* instructions.
*/
static inline void arch_write_lock(arch_rwlock_t *rw)
{
unsigned int tmp;
asm volatile(ARM64_LSE_ATOMIC_INSN(
/* LL/SC */
" sevl\n"
"1: wfe\n"
"2: ldaxr %w0, %1\n"
" cbnz %w0, 1b\n" // lock != 0,即锁被占用,则重试(忙等待)
" stxr %w0, %w2, %1\n" // 独占方式尝试将0x80000000写回lock内存,即将第31bit置为1,并将结果写入tmp
" cbnz %w0, 2b\n" // 如果tmp != 0,即写入内存失败,则进行重试
__nops(1),
/* LSE atomics */
"1: mov %w0, wzr\n"
"2: casa %w0, %w2, %1\n"
" cbz %w0, 3f\n"
" ldxr %w0, %1\n"
" cbz %w0, 2b\n"
" wfe\n"
" b 1b\n"
"3:")
: "=&r" (tmp), "+Q" (rw->lock)
: "r" (0x80000000)
: "memory");
}
write_unlock
write_unlock() -> _raw_write_unlock() -> __raw_write_unlock() -> do_raw_write_unlock() -> arch_write_unlock()
static inline void arch_write_unlock(arch_rwlock_t *rw)
{
asm volatile(ARM64_LSE_ATOMIC_INSN(
" stlr wzr, %0", // 将lock内存清零
" swpl wzr, wzr, %0") // LSE路径
: "=Q" (rw->lock) :: "memory");
}
读写锁的优化
读写自旋锁的缺点是:如果读者很多,写者很难获取写锁,可能饿死。假设有一个读者占有读锁,然后写者申请写锁,写者需要自旋等待,接着另一个读者申请读锁,它可以获取读锁,如果两个读者轮流占有读锁,可能造成写者饿死。
针对这个缺点,内核实现了排队读写锁,主要改进是:如果写者正在等待写锁,那么读者申请读锁时自旋等待,写者在锁被释放以后先得到写锁。排队读写锁的配置宏是CONFIG_QUEUED_RWLOCKS,源文件是kernel/locking/qrwlock.c。
读写锁与读写信号量的区别
内核读写锁与读写信号量,都区分了读者和写者。以下是它们的一些比较:
| 特性 | 读写锁 (rwlock) | 读写信号量 (rwsem) |
|---|---|---|
| 睡眠等待 | 不允许(自旋等待) | 允许(可睡眠) |
| 适用场景 | 临界区短、不可睡眠的上下文 | 临界区长、可睡眠的上下文 |
| 实现复杂度 | 较简单 | 较复杂 |
| 内存开销 | 较小 | 较大 |
| 优先级反转处理 | 无 | 有基本处理机制 |
用户空间读写锁
linux用户空间读写锁通常使用posix的rwlock,它并不是通过忙等待的方式,而是阻塞方式实现的。
操作函数
#include <pthread.h>
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
// 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 读锁定(阻塞)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 读锁定(非阻塞)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
// 写锁定(阻塞)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 写锁定(非阻塞)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
属性配置
可以通过属性对象设置特殊行为:
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
// 设置优先级策略
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock, &attr);
pthread_rwlockattr_destroy(&attr);
常见优先级策略:
<font style="color:rgb(64, 64, 64);">PTHREAD_RWLOCK_PREFER_READER_NP</font>(默认,读者优先)<font style="color:rgb(64, 64, 64);">PTHREAD_RWLOCK_PREFER_WRITER_NP</font>(写者优先)<font style="color:rgb(64, 64, 64);">PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP</font>
参考资料
- Professional Linux Kernel Architecture,Wolfgang Mauerer
- Linux内核深度解析,余华兵
- Linux设备驱动开发详解,宋宝华
- linux kernel 4.12

1610

被折叠的 条评论
为什么被折叠?



