在前面小节分析了spin_lock的实现,可以知道spin_lock只允许一个thread进入临界区,而且对进入临界区中的操作不做细分。但是在实际中,对临界区的操作分为读和写。如果按照spin_lock的实现,当多个read thread都想进入临界区读取的时候,这时候只有一个read thread进入到临界区,这样效率和性能明显下降。所以就针对某些操作read thread占绝大多数的情况下,提出了读写锁的概念。
typedef struct {
arch_rwlock_t raw_lock;
} rwlock_t;
typedef struct {
volatile unsigned int lock;
} arch_rwlock_t;
可以看到读写锁与spin_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(
" sevl\n"
"1: wfe\n"
"2: ldaxr %w0, %1\n"
" cbnz %w0, 1b\n"
" stxr %w0, %w2, %1\n"
" cbnz %w0, 2b\n"
: "=&r" (tmp), "+Q" (rw->lock)
: "r" (0x80000000)
: "memory");
}
通过注释: write操作的上锁操作是给bit31写1, 解锁操作就是给bit31写0
" sevl\n"
"1: wfe\n"
使cpu进入低功耗模式
2: ldaxr %w0, %1\n
读取锁的值,赋值给tmp变量
cbnz %w0, 1b
如果tmp的值不为0, 跳转到标号1重新执行。不等于0说明有read/write进程正在持有锁,所以需要进入低功耗等待。
stxr %w0, %w2, %1
将锁的bit31设置为1, 然后将设置结果放入tmp中。
cbnz %w0, 2b
如果tmp的值不为0,说明上条指令执行失败,跳转到标号2继续执行。
可以看到,对于wirte操作,只要临界区有read/write进程存在,就需要自旋等待,直到临界区没有任何进程存在。
static inline void arch_write_unlock(arch_rwlock_t *rw)
{
asm volatile(
" stlr %w1, %0\n"
: "=Q" (rw->lock) : "r" (0) : "memory");
}
写操作很简单,就是将锁的值全部清为0而已。
/*
* 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.
*/
static inline void arch_read_lock(arch_rwlock_t *rw)
{
unsigned int tmp, tmp2;
asm volatile(
" sevl\n"
"1: wfe\n"
"2: ldaxr %w0, %2\n"
" add %w0, %w0, #1\n"
" tbnz %w0, #31, 1b\n"
" stxr %w1, %w0, %2\n"
" cbnz %w1, 2b\n"
: "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)
:
: "memory");
}
读取者进入临界区先要判断是否有write进程在临界区,如果有必须自旋。如果没有,则可以进入临界区。
2: ldaxr %w0, %2
读取锁的值,赋值给tmp变量。
add %w0, %w0, #1
将tmp的值加1, 然后将结果放入tmp中。
tbnz %w0, #31, 1b
判断tmp[31]是否等于0,不等于0也就是说write进程在临界区,需要自旋等待,跳到标号1继续。
stxr %w1, %w0, %2
将tmp的值复制给lock,然后将结果放入tmp2中。
cbnz %w1, 2b
判断tmp2是否等于0,不等于0就跳到标号2继续。
可以看到read操作需要先判断临界区是否有write进程存在,如果有就需要自旋。
static inline void arch_read_unlock(arch_rwlock_t *rw)
{
unsigned int tmp, tmp2;
asm volatile(
"1: ldxr %w0, %2\n"
" sub %w0, %w0, #1\n"
" stlxr %w1, %w0, %2\n"
" cbnz %w1, 1b\n"
: "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)
:
: "memory");
}
读取者退出临界区只需要将锁的值减1即可。
1: ldxr %w0, %2
读取锁的值,复制给tmp
sub %w0, %w0, #1
将tmp的值减去1,同时将结果放入到tmp中
stlxr %w1, %w0, %2
将tmp的值复制给lock,然后将结果存放到tmp2
cbnz %w1, 1b
如果tmp2的值不为0,就跳转到标号1继续执行。
从上面的定义可知,lock的是一个unsigned int的32位数。 0-32bit用来表示read thread counter, 31bit用来表示write therad counter。 这样设计是因为write进程每次进入临界区只能有一个,所以一个bit就可以。剩余的31bit位全部给read therad使用。
从概率上将,当一个进程试图去写时,成功获得锁的几率要远小于读进程概率。所以在一个读写相互依赖的系统中,这种设计会导致读取者饥饿,也就是没有数据可读。所以读写锁使用的系统就是读操作占用绝大多数,这样读写操作就比以前的spin lock大大提升效率和性能。