读写锁rwlock

读写锁引入

在前面小节分析了spin_lock的实现,可以知道spin_lock只允许一个thread进入临界区,而且对进入临界区中的操作不做细分。但是在实际中,对临界区的操作分为读和写。如果按照spin_lock的实现,当多个read thread都想进入临界区读取的时候,这时候只有一个read thread进入到临界区,这样效率和性能明显下降。所以就针对某些操作read thread占绝大多数的情况下,提出了读写锁的概念。

读写锁的基本原理

加锁操作

  • 假设当前临界区没有任何进程,这时候read进程或者write进程都可以进来,但是只能是其一
  • 如果当前临界区只有一个read进程,这时候任意的read进程都可以进入,但是write进程不能进入
  • 如果当前临界区只有一个write进程,这时候任何read/write进程都无法进入。只能自旋等待
  • 如果当前当前临界区有好多个read进程,同时read进程依然还会进入,这时候进入的write进程只能等待。直到临界区一个read进程都没有,才可进入

解锁操作

  • 如果在read进程离开临界区的时候,需要根据情况决定write进程是否需要进入。只有当临界区没有read进程了,write进程方可进入。
  • 如果在write进程离开临界区的时候,无论write进程或者read进程都可进入临界区,因为write进程是排它的。

读写锁的定义

typedef struct {
	arch_rwlock_t raw_lock;
} rwlock_t;

typedef struct {
	volatile unsigned int lock;
} arch_rwlock_t;
可以看到读写锁与spin_lock的定义最终相同,只是名字不同罢了。

读写锁API

加锁API

  1. read_lock(lock)/write_lock(lock)                                                               #获取指定的锁
  2. read_trylock(lock)/write_trylock(lock)                                                       #尝试获取锁,如果失败不spin,直接返回
  3. read_lock_irq(lock)/write_lock_irq(lock)                                                   #获取指定的锁,同时关掉本地cpu中断
  4. read_lock_irqsave(lock, flags)/write_lock_irqsave(lock, flags)                #保存本地cpu的irq标志,然后关掉cpu中断,获取指定锁
  5. read_lock_bh(lock)/read_lock_bh(lock)                                                   #获取指定的锁,同时关掉中断下半部(bottom half)

解锁API

  1. read_unlock(lock)/write_unlock(lock)                                                      #释放指定的锁
  2. read_unlock_irq(lock)/write_unlock_irq(lock)                                          #释放指定的锁,同时使能cpu中断
  3. read_unlock_irqrestore/write_unlock_irqrestore                                     #释放锁,同时使能cpu中断,恢复cpu的标识
  4. read_unlock_bh/write_unlock_bh                                                            #释放锁,同时使能cpu中断的下半部

读写锁的实现

写入者加锁操作:

/*
 * 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大大提升效率和性能。










评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值