Linux 锁 (4) - seqlock

文章目录

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

1. 前言

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

2. seqlock 实现

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

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

4. 参考资料

Sequence counters and sequential locks

相关推荐
Linux技术芯1 小时前
Refault Distance算法详解
linux
0vvv01 小时前
linux-软件安装
linux
IMPYLH1 小时前
Linux 的 nproc 命令
linux·运维·服务器·bash
九英里路2 小时前
OS学习之路——动静态库制作与原理
linux·学习·操作系统·unix·进程·编译·动静态库
kcuwu.3 小时前
从0到1:VMware搭建CentOS并通过FinalShell玩转Linux命令
linux·运维·centos
s6516654963 小时前
linux-内核结构体
linux
.柒宇.3 小时前
MySQL双主同步
linux·数据库·mysql·docker
格林威3 小时前
AI视觉检测:INT8 量化对工业视觉检测精度的影响
linux·运维·人工智能·数码相机·计算机视觉·视觉检测·工业相机
万山寒3 小时前
linux日志查询,查找某个关键词后面的内容
linux·运维·服务器
房开民4 小时前
ubuntu中安装claude code
linux·运维·ubuntu