前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >读写锁rwlock

读写锁rwlock

作者头像
DragonKingZhu
发布2020-03-24 15:31:31
1.2K0
发布2020-03-24 15:31:31
举报
文章被收录于专栏:Linux内核深入分析

在前面小节分析了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进程是排它的。

读写锁的定义

代码语言:javascript
复制
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中断的下半部

读写锁的实现

写入者加锁操作:

代码语言:javascript
复制
/*
 * 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

代码语言:javascript
复制
"	sevl\n"
"1:	wfe\n"

使cpu进入低功耗模式

代码语言:javascript
复制
2:	ldaxr	%w0, %1\n

读取锁的值,赋值给tmp变量

代码语言:javascript
复制
cbnz	%w0, 1b

如果tmp的值不为0, 跳转到标号1重新执行。不等于0说明有read/write进程正在持有锁,所以需要进入低功耗等待。

代码语言:javascript
复制
stxr	%w0, %w2, %1

将锁的bit31设置为1, 然后将设置结果放入tmp中。

代码语言:javascript
复制
cbnz	%w0, 2b

如果tmp的值不为0,说明上条指令执行失败,跳转到标号2继续执行。

可以看到,对于wirte操作,只要临界区有read/write进程存在,就需要自旋等待,直到临界区没有任何进程存在。

写入者解锁操作:

代码语言:javascript
复制
static inline void arch_write_unlock(arch_rwlock_t *rw)
{
	asm volatile(
	"	stlr	%w1, %0\n"
	: "=Q" (rw->lock) : "r" (0) : "memory");
}

写操作很简单,就是将锁的值全部清为0而已。

读取者加锁操作:

代码语言:javascript
复制
/*
 * 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进程在临界区,如果有必须自旋。如果没有,则可以进入临界区。

代码语言:javascript
复制
2:	ldaxr	%w0, %2

读取锁的值,赋值给tmp变量。

代码语言:javascript
复制
add	%w0, %w0, #1

将tmp的值加1, 然后将结果放入tmp中。

代码语言:javascript
复制
tbnz	%w0, #31, 1b

判断tmp[31]是否等于0,不等于0也就是说write进程在临界区,需要自旋等待,跳到标号1继续。

代码语言:javascript
复制
stxr	%w1, %w0, %2

将tmp的值复制给lock,然后将结果放入tmp2中。

代码语言:javascript
复制
cbnz	%w1, 2b

判断tmp2是否等于0,不等于0就跳到标号2继续。

可以看到read操作需要先判断临界区是否有write进程存在,如果有就需要自旋。

读取者解锁操作:

代码语言:javascript
复制
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即可。

代码语言:javascript
复制
1:	ldxr	%w0, %2

读取锁的值,复制给tmp

代码语言:javascript
复制
sub	%w0, %w0, #1

将tmp的值减去1,同时将结果放入到tmp中

代码语言:javascript
复制
stlxr	%w1, %w0, %2

将tmp的值复制给lock,然后将结果存放到tmp2

代码语言:javascript
复制
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大大提升效率和性能。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 读写锁的基本原理
    • 加锁操作
      • 解锁操作
      • 读写锁的定义
      • 读写锁API
        • 加锁API
          • 解锁API
          • 读写锁的实现
            • 写入者加锁操作:
              • 写入者解锁操作:
                • 读取者加锁操作:
                  • 读取者解锁操作:
                  • 小节
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档