上回写了 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)的核心思路是:写端不打扰读者。
具体做法:
- 旧数据不立即释放,等所有 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 怎么选
我的选法:
- 路由缓存、dentry hashtable、jiffies——用 RCU(读太多,写偶尔来一次)
- 设备状态、配置信息——用 seqlock(写稍多点,读者要最新值)
- 要是对自己有信心,直接看内核源码里怎么选——
CONFIG_RCU vs CONFIG_SEQLOCK
有读者问:能不能不用 seqlock 也无锁?答案是可以用 RCU,但得满足 RCU 的约束——读端不能 sleep、临界区要短。权衡一下就知道该用哪个。