当前位置:首页>Linux>Linux 内核读写锁:从 seqlock 到 RCU

Linux 内核读写锁:从 seqlock 到 RCU

  • 2026-10-11 07:13:50
Linux 内核读写锁:从 seqlock 到 RCU

上回写了 seqlock,有读者留言:能不能把 seqlock 和 RCU 放一起讲讲区别。

这俩确实都是读多写少场景的锁,但思路完全相反。seqlock 与 RCU 的设计哲学对比:Seqlock写优先(读者可能重试),RCU 读优先(写端复制+延迟回收)。——今天就聊聊这个演进。

seqlock 的问题

上篇文章提了,seqlock 每次写都要把 sequence counter 改成奇数、再改回偶数。读者要是不巧撞上写操作正在进行,得重试。

// seqlock 写端write_seqlock(&sq);jiffies_64++;    // 改数据write_sequnlock(&sq);
// seqlock 读端do {    seq = read_seqbegin(&sq);    val = jiffies_64;   // 读数据} while (read_seqretry(&sq, seq));

问题就出在「写一次、读者可能得重试」这件事上。如果写特别频繁,读者一直在重试,CPU 空转。

举个数例子:每秒写 100 次,读者每次读花 100ns,理论上 1% 的时间在重试。但真实场景中,写可能集中在某个时间段——比如路由表更新每秒来 1000 次,读者全卡死。

RCU 的解法

RCU(Read-Copy-Update)的核心思路是:写端不打扰读者。

具体做法:

  1. 读者直接读旧数据,不用加锁
  2. 写端复制一份新数据,改完了把指针指向新版本
  3. 旧数据不立即释放,等所有 CPU 都经过一次 context switch(grace period)后再回收
// RCU 写端struct data *new, *old = rcu_dereference(g_data);new = kmalloc(sizeof(*new), GFP_ATOMIC);*new = *old;new->value = compute_new_value();rcu_assign_pointer(g_data, new);// 旧数据等 grace period 结束后再释放call_rcu(&old->rcu, free_old_data);
// RCU 读端(完全不用锁)rcu_read_lock();val = rcu_dereference(g_data)->value;rcu_read_unlock();

注意:读者没有任何 sequence counter,不用判断重试。写端爱怎么改怎么改,读者读的是指针切换前的那份数据。

rcu_assign_pointer 内部是 smp_store_release(),保证指针赋值之前的所有写操作都对读者可见。

grace period 是什么

grace period 是 RCU 回收旧数据的「安全期」。内核要确认所有 CPU 都经历过一次调度(context switch),确保没有读者还在读旧数据。

call_rcu 注册的回调不是立即执行的——要等 grace period 结束,也就是所有 CPU 都经过至少一次 context switch。grace period 需要所有 CPU 都经过 user mode 或 idle——不能有任何一个 CPU 卡在读者临界区里。

RCU 的约束

RCU 不是什么场景都能用,它对读端有限制:

  • read-side 临界区不能 sleep:不能调用 wait_event、mutex_lock、schedule()
  • 临界区不能太长:否则 grace period 拖很久,旧数据一直回收不了
  • 指针操作要用 rcu_dereference / rcu_assign_pointer:编译器 barrier,不是锁

违反约束的后果:grace period 卡死,旧数据永远回收不了,内存泄漏。

真实例子

内核里哪些地方用了 RCU?

dentry 查找——文件名 hash 表查 lookup:

// fs/dcache.cstatic inline struct dentry *__d_lookup_rcu(const struct dentry *parent,const struct qstr *name){    unsigned int hash = name->hash;    struct hlist_bl_head *b = d_hash(hash);    struct hlist_bl_node *n;    hlist_bl_for_each_entry_rcu(d, n, b, d_hash) {        if (d->d_parent != parent)            continue;        if (d->d_name.hash != hash)            continue;         // 找到了,返回 dentry        return d;    }    return NULL;}

每次 open("/etc/passwd") 都要走这个路径。路径每级都用 RCU 查 dentry hashtable,读端不用任何锁。

路由缓存 dst_entry——net/core/dst.c 里每条路由都是 struct dst_entry,引用计数用 rcuref_t:

// include/net/dst.hstruct dst_entry {    // ...    rcuref_t        __rcuref;   /* RCU reference count */    struct rcu_head rcu_head;/* grace period 后释放 */    // ...};

路由删除时调用 dst_release(),真正 free 延迟到 grace period:

// net/core/dst.cvoid dst_release(struct dst_entry *dst){    if (atomic_read(&dst->__rcuref.refs) == 0)        call_rcu(&dst->rcu_head, dst_rcu_put);}

网络报文的dst查找是每包必走的路径,读端完全无锁。

这些场景的共同点:读远多于写,而且读端性能要求极高。

seqlock vs RCU 怎么选

特性
seqlock
RCU
写端代价
每次写 +2 sequence
复制数据 + call_rcu
读端代价
可能重试
极少开销
适用场景
写不太频繁
读远多于写
约束
无特殊约束
读端不能 sleep

我的选法:

  • 路由缓存、dentry hashtable、jiffies——用 RCU(读太多,写偶尔来一次)
  • 设备状态、配置信息——用 seqlock(写稍多点,读者要最新值)
  • 要是对自己有信心,直接看内核源码里怎么选——CONFIG_RCU vs CONFIG_SEQLOCK

有读者问:能不能不用 seqlock 也无锁?答案是可以用 RCU,但得满足 RCU 的约束——读端不能 sleep、临界区要短。权衡一下就知道该用哪个。

最新文章

随机文章