序列计数器和顺序锁
介绍
序列计数器是一种具有无锁读取器(只读重试循环)和无写入者饥饿的读者-写者一致性机制。它们用于很少写入数据的情况(例如系统时间),其中读者希望获得一致的信息集,并且愿意在信息发生变化时重试。
当读取端临界区的序列计数在开始时是偶数,并且在临界区结束时再次读取相同的序列计数值时,数据集是一致的。数据集中的数据必须在读取端临界区内部被复制出来。如果在临界区开始和结束之间序列计数发生了变化,读者必须重试。
写者在其临界区的开始和结束时递增序列计数。在开始临界区后,序列计数是奇数,并指示读者正在进行更新。在写端临界区结束时,序列计数再次变为偶数,这样读者就可以取得进展。
序列计数器写端临界区绝不能被读端部分抢占或中断。否则,由于奇数序列计数值和被中断的写者,读者将会因为整个调度器滴答而旋转。如果该读者属于实时调度类,则它可能会永远旋转,内核将会活锁。
如果受保护的数据包含指针,则无法使用此机制,因为写者可能会使读者正在跟踪的指针失效。
序列计数器(seqcount_t)
这是原始计数机制,不保护多个写者。因此,写端临界区必须由外部锁进行串行化。
如果写串行化原语未隐式禁用抢占,则必须在进入写端部分之前显式禁用抢占。如果读取部分可以从硬中断或软中断上下文中调用,则必须在进入写部分之前分别禁用中断或底半部。
如果希望自动处理写者串行化和非可抢占性的序列计数要求,请改用顺序锁(seqlock_t)。
初始化:
/* 动态 */ seqcount_t foo_seqcount; seqcount_init(&foo_seqcount); /* 静态 */ static seqcount_t foo_seqcount = SEQCNT_ZERO(foo_seqcount); /* C99 结构初始化 */ struct { .seq = SEQCNT_ZERO(foo.seq), } foo;
写路径:
/* 禁用抢占的串行化上下文 */ write_seqcount_begin(&foo_seqcount); /* ... [[写端临界区]] ... */ write_seqcount_end(&foo_seqcount);
读路径:
do { seq = read_seqcount_begin(&foo_seqcount); /* ... [[读端临界区]] ... */ } while (read_seqcount_retry(&foo_seqcount, seq));
带关联锁的序列计数器(seqcount_LOCKNAME_t)
如序列计数器(seqcount_t)中所述,序列计数写端临界区必须进行串行化和非可抢占。此序列计数器的变体在初始化时将用于写者串行化的锁关联到序列计数器上,从而使lockdep能够验证写端临界区是否得到适当的串行化。
如果禁用了lockdep,则此锁关联是一个NOOP,既不会增加存储空间,也不会增加运行时开销。如果启用了lockdep,则锁指针将存储在结构seqcount中,并且lockdep的“锁已持有”断言将被注入到写端临界区的开始,以验证其是否得到适当的保护。
对于不隐式禁用抢占的锁类型,在写端函数中将强制执行抢占保护。
以下带关联锁的序列计数器已定义:
- seqcount_spinlock_t
- seqcount_raw_spinlock_t
- seqcount_rwlock_t
- seqcount_mutex_t
- seqcount_ww_mutex_t
序列计数器的读取和写入API可以采用普通的seqcount_t或上述任何seqcount_LOCKNAME_t变体。
初始化(用支持的锁替换“LOCKNAME”):
/* 动态 */ seqcount_LOCKNAME_t foo_seqcount; seqcount_LOCKNAME_init(&foo_seqcount, &lock); /* 静态 */ static seqcount_LOCKNAME_t foo_seqcount = SEQCNT_LOCKNAME_ZERO(foo_seqcount, &lock); /* C99 结构初始化 */ struct { .seq = SEQCNT_LOCKNAME_ZERO(foo.seq, &lock), } foo;
写路径:与序列计数器(seqcount_t)中的相同,同时从已获取关联写串行化锁的上下文中运行。
读路径:与序列计数器(seqcount_t)中的相同。
锁定序列计数器(seqcount_latch_t)
锁定序列计数器是一种多版本并发控制机制,其中嵌入的seqcount_t计数器的偶/奇值用于在受保护数据的两个副本之间进行切换。这使得序列计数器读取路径可以安全地中断自己的写端临界区。
当读端无法保护写端部分免受读者中断时,请使用seqcount_latch_t。这通常是在读端可以从NMI处理程序中调用时的情况。
有关更多信息,请查看raw_write_seqcount_latch()。
顺序锁(seqlock_t)
这包含了前面讨论的序列计数器(seqcount_t)机制,以及用于写者串行化和非可抢占性的嵌入式自旋锁。
如果读端部分可以从硬中断或软中断上下文中调用,请使用禁用中断或底半部的写端函数变体。
初始化:
/* 动态 */ seqlock_t foo_seqlock; seqlock_init(&foo_seqlock); /* 静态 */ static DEFINE_SEQLOCK(foo_seqlock); /* C99 结构初始化 */ struct { .seql = __SEQLOCK_UNLOCKED(foo.seql) } foo;
写路径:
write_seqlock(&foo_seqlock); /* ... [[写端临界区]] ... */ write_sequnlock(&foo_seqlock);
读路径,分为三类:
- 普通序列读取器永远不会阻塞写者,但如果检测到序列号的变化,它们必须重试,因为写者正在进行中。写者不会等待序列读取器:
do { seq = read_seqbegin(&foo_seqlock); /* ... [[读端临界区]] ... */ } while (read_seqretry(&foo_seqlock, seq));
- 锁定读取器会在写者或另一个锁定读取器进行中时等待。进行中的锁定读取器还会阻止写者进入其临界区。此读锁是独占的。与rwlock_t不同,只有一个锁定读取器可以获取它:
read_seqlock_excl(&foo_seqlock); /* ... [[读端临界区]] ... */ read_sequnlock_excl(&foo_seqlock);
- 根据传递的标记,条件无锁读取器(如1)或锁定读取器(如2)。这用于避免在写活动急剧增加时无锁读取器饥饿(太多重试循环)。首先尝试无锁读取(传递偶数标记)。如果该尝试失败(返回奇数序列计数,用作下一次迭代标记),则无锁读取将转换为完全锁定读取,不需要重试循环:
/* 标记;偶数初始化 */ int seq = 0; do { read_seqbegin_or_lock(&foo_seqlock, &seq); /* ... [[读端临界区]] ... */ } while (need_seqretry(&foo_seqlock, seq)); done_seqretry(&foo_seqlock, seq);
API 文档
https://www.kernel.org/doc/html/v6.6/locking/seqlock.html#api-documentation