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

相关推荐
磊 子9 分钟前
详细讲解一下epoll
linux·io·epoll·io多路复用
printfLILEI1 小时前
php中的类与对象以及反序列化
linux·开发语言·php
叠叠乐2 小时前
redmi k90 pro max 强解BL,刷海外rom, 并刷入sukisu ultra
linux
xiaoye-duck3 小时前
《Linux系统编程》Linux 进程间通信之管道基础解析:从匿名管道原理到基于管道的进程池实现
linux
z200509303 小时前
【Linux学习】Linux中的进程程序替换
linux·服务器·学习
bush43 小时前
嵌入式linux学习记录四
linux·运维·学习
lihao lihao4 小时前
软硬链接
linux·运维·服务器
YY&DS4 小时前
Qt 嵌入 CEF 在 Linux 下必须设置 `QT_XCB_GL_INTEGRATION=xcb_egl才能加载网页
linux·开发语言·qt
辰风沐阳4 小时前
ThinkPHP8.1 + think-swoole 4.1 使用指南(保姆级教程)
linux·后端·swoole