Linux 锁 (4) - seqlock

文章目录

  • [1. 前言](#1. 前言)
  • [2. seqlock 实现](#2. seqlock 实现)
  • [3. 小结](#3. 小结)
  • [4. 参考资料](#4. 参考资料)

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. seqlock 实现

seqlock 通过一个初始为 0 计数器,实现 writer 和 reader 共享数据的读写同步。

writer 一方更新共享数据的步骤如下:

  1. 计数器 +1,计数变为奇数
  2. 更新共享数据
  3. 计数器再 +1,计数变为偶数

reader 一方读取共享数据的步骤如下:

  1. 循环读取计数器的值,直到读到一个偶数值并返回,记为 S1
  2. 读取共享数据
  3. 再次读取计数的值,直到读到一个偶数值并返回,计为 S2
  4. 比较 S1 和 S2,如果 S1 == S2,则表明步骤 2. 读取的共享数据是最新的,则可用;否则跳到步骤 1. 再次读取。

来看具体的实现代码:

c 复制代码
// include/linux/seqlock.h

typedef struct seqcount {
	unsigned sequence;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	...
#endif
} seqcount_t;
  • seqlock 初始化
c 复制代码
static inline void __seqcount_init(seqcount_t *s, const char *name,
					  struct lock_class_key *key)
{
	/*
	 * Make sure we are not reinitializing a held lock:
	 */
	lockdep_init_map(&s->dep_map, name, key, 0);
	s->sequence = 0;
}

# define seqcount_init(s) __seqcount_init(s, NULL, NULL)

#define SEQCNT_ZERO(lockname) { .sequence = 0, SEQCOUNT_DEP_MAP_INIT(lockname)}

初始化将 seqcount::sequence 设为 0。

  • writer:更新共享数据

writer 更新共享数据的逻辑如下:

c 复制代码
struct seqcount seqlock;

seqcount_init(&seqlock);
write_seqcount_begin(&seqlock);
// 更新 writer/reader 贡献的数据
write_seqcount_end(&seqlock);

seqcount_init() 初始化了锁的计数器为 0,这在前面已经分析,这里不再赘述。来看 write_seqcount_begin() 和 write_seqcount_end()。

先看共享数据更新之前的 write_seqcount_begin():

c 复制代码
static inline void write_seqcount_begin(seqcount_t *s)
{
	write_seqcount_begin_nested(s, 0);
}

static inline void write_seqcount_begin_nested(seqcount_t *s, int subclass)
{
	raw_write_seqcount_begin(s);
	...
}

static inline void raw_write_seqcount_begin(seqcount_t *s)
{
	s->sequence++; /* 标记写开始: 奇数 */
	/*
	 * 确保让系统中的所有 CPU 观察到:
	 * s->sequence++ 操作, 在 锁保护的共享数据 的 更新操作 之前 完成.
	 */
	smp_wmb();
}

再看共享数据更新之后的 write_seqcount_end():

c 复制代码
static inline void write_seqcount_end(seqcount_t *s)
{
	...
	raw_write_seqcount_end(s);
}

static inline void raw_write_seqcount_end(seqcount_t *s)
{
	/*
	 * 确保让系统中的所有 CPU 观察到:
	 * s->sequence++ 操作, 在 锁保护的共享数据 的 更新操作 之后 完成.
	 */
	smp_wmb();
	s->sequence++; /* 标记写结束: 偶数 */
}
  • reader:读取(更新的)共享数据

reader 读取共享数据的逻辑如下(假设使用的 struct seqcount 名为 seqlock):

c 复制代码
unsigned long seq;

do {
	seq = read_seqcount_begin(&seqlock);
	// 读取共享数据
} while (read_seqcount_retry(&seqlock, seq));

// 本次读取数据结束

先看 read_seqcount_begin():

c 复制代码
static inline unsigned read_seqcount_begin(const seqcount_t *s)
{
	...
	return raw_read_seqcount_begin(s);
}

static inline unsigned raw_read_seqcount_begin(const seqcount_t *s)
{
	unsigned ret = __read_seqcount_begin(s);
	/*
	 * 确保系统中所有 CPU 观察到:
	 * __read_seqcount_begin() 对锁计数器 s->sequence 的读取操作, 
	 * 在接下来 对共享数据的读操作 之前 完成.
	 * 也即按 writer 先递增(第 1 次)了 锁计数器 s->sequence, 然后
	 * 更新共享数据同样的顺序去读取。
	 */
	smp_rmb();
	return ret;
}

static inline unsigned __read_seqcount_begin(const seqcount_t *s)
{
	unsigned ret;

repeat:
	ret = READ_ONCE(s->sequence);
	if (unlikely(ret & 1)) { /* 奇数: 正在写 */
		cpu_relax();
		goto repeat; /* 重复读取计数器,直至写结束 */
	}
	return ret; /* 返回偶数计数器 */
}

再看 read_seqcount_retry():

c 复制代码
static inline int read_seqcount_retry(const seqcount_t *s, unsigned start)
{
	/*
	 * 确保系统中所有 CPU 观察到:
	 * __read_seqcount_retry() 对锁计数器 s->sequence 的读取操作, 在 对共享数据的读操作
	 * 之后 完成。
	 * 也即按 writer 先更新了共享数据,然后第 2 次递增了 锁计数器 s->sequence 同样的顺序
	 * 去读取。
	 */
	smp_rmb();
	return __read_seqcount_retry(s, start);
}

static inline int __read_seqcount_retry(const seqcount_t *s, unsigned start)
{
	return unlikely(s->sequence != start);
}

当然,上面 reader 的读取逻辑,在 writer 首次更新共享数据前,reader 也能正确工作,因为初始的计数值为 0,是一个偶数。

3. 小结

seqlock 适合保护快速读写的小量数据,比单单保护单个数据 atomic 操作更进一步。类似于 rwlock,seqlock 也是针对读多写少的场景,但它改善了 rwlock 的 writer 的优先级低,甚至极端情况下被饿死的问题,seqlock 的 writer 优先级更高,它可以随时打断 reader(通过更新计数器)。seqlock 不会导致进程睡眠,但也不会像 spinlock 那样禁用抢占,一直在一个 CPU 上自旋,seqlock 持锁期间可能出现临界区代码被调度出去的情形。如果 seqlock 保护的共享数据包含指针,则不宜使用 seqlock,因为 writer 可能会使 reader 正在访问的指针无效,因为 reader 工作的同时,它无法阻止 writer 对共享数据的更新。最后,seqlock 无法串行化多个 writer,必须借助外部锁来实现这一点。

4. 参考资料

Sequence counters and sequential locks

相关推荐
0+1116 小时前
Linux --从 poll 到 epoll多路转接
linux·服务器·1024程序员节
青瓦梦滋7 小时前
WebSocket协议
linux·服务器·网络·c++·websocket·网络协议·http
2601_9498477510 小时前
bcrypt.js 跨端三方库仓颉适配全流程
linux·运维·ubuntu
m0_6399069410 小时前
Linux 操作系统入门:目录结构、服务排查常用命令、Shell 脚本基础
linux·运维·后端
水饺编程12 小时前
第2章:Linux 基础,exit 命令
linux·c语言·汇编
木子欢儿12 小时前
Debian 13 + vsftpd 搭建FTP服务
linux·服务器·前端·网络·debian
GeW13 小时前
新质生产力落地实践:企业级Linux迁移与数据库国产化
linux
雯宝13 小时前
usb_serial_generic_register
linux
疯狂的Meng14 小时前
我把第一周的 Linux 命令整理成了自己的手册(附 7 个我踩的坑)
linux
wt952714 小时前
VMware 虚拟机静态 IP 配置
linux