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

相关推荐
xlp666hub3 小时前
如果操作GPIO可能导致休眠,那么同步机制绝不能采用spinlock
linux·面试
RisunJan4 小时前
Linux命令-mkbootdisk(可建立目前系统的启动盘)
linux·运维·服务器
朽棘不雕5 小时前
Linux工具(上)
linux·运维·服务器
BestOrNothing_20155 小时前
Ubuntu 22.04 下调整 VS Code 界面及字体教程
linux·vscode·ubuntu22.04·界面调整
桌面运维家5 小时前
Windows/Linux云桌面:高校VDisk方案部署指南
linux·运维·windows
mzhan0176 小时前
Linux:intel:Cache Allocation tech
linux·cpu
学机械的鱼鱼6 小时前
【踩坑记录】Linux环境下FreeCAD打开后一新建就崩
linux
小璐资源网6 小时前
UPS电源管理:应对突发断电的应急方案
linux·运维·服务器
grrrr_16 小时前
【工具类】虚拟机 + Ubuntu 安全部署 OpenClaw,联动 Ollama 零成本解锁云端大模型
linux·运维·ubuntu·#openclaw·#小龙虾