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

相关推荐
Championship.23.247 小时前
Linux 3.0 锁机制与故障排查详解
linux·运维·服务器
天疆说7 小时前
Zotero Connector 在 Edge 里找不到桌面 Zotero(Linux / Zotero 9.0.6 / Edge 150)
linux·前端·edge
风123456789~9 小时前
【Linux专栏】ls 排除某个文件
linux·运维·服务器
IT曙光9 小时前
ubuntu apt-get离线源制作
linux·ubuntu
其实防守也摸鱼10 小时前
运维--学习阶段问题解答(1)(自测)
linux·运维·服务器·数据库·学习·自动化·命令模式
懒鸟一枚10 小时前
深入理解 Linux 内存、Swap 交换分区与分页机制的关系
java·linux·数据库
Darkwanderor11 小时前
对Linux的进程控制的研究
linux·运维·c++
bksczm14 小时前
linux之线程概念和控制
linux·开发语言·c++
爱网络爱Linux14 小时前
Linux proc 目录安全加固
linux·运维·rhce·rhca·红帽认证·proc 目录安全加固
hehelm14 小时前
Linux网络编程—TCP字典翻译系统
linux·开发语言·网络·c++·tcp/ip