一、RCU 概述与设计哲学
Read-Copy-Update(RCU)是 Linux 内核中最重要且最独特的同步机制之一,由 Paul E. McKenney 设计并实现。RCU 的核心思想非常巧妙:读者几乎不需要任何同步开销,写者延迟释放旧数据。这使得 RCU 在读多写少的场景下能提供近乎零成本的读取性能,对于大规模系统(尤其是多核/NUMA架构)中的链表、哈希表等高频读取数据结构,RCU 相比读写锁(rwlock)和顺序锁(seqlock)具有显著的性能优势。
二、RCU 核心数据结构
在内核源码(include/linux/rcupdate.h 与 kernel/rcu/)中,RCU 的核心数据结构围绕宽限期(Grace Period)和回调机制展开:
struct rcu_state {
struct rcu_node node[MAX_NUMNODES * 4];
struct rcu_data __percpu *rda;
unsigned long gpnum;
unsigned long completed;
struct mutex onoff_mutex;
struct mutex barrier_mutex;
};
struct rcu_data {
unsigned long gpnum;
unsigned long completed;
int qs_pending;
struct rcu_head *nxtlist;
struct rcu_head **nxttail;
unsigned long resched_ipi_count;
};
struct rcu_head {
struct rcu_head *next;
void (*func)(struct rcu_head *head);
};
RCU 采用树形层级(rcu_node 树)来高效管理所有 CPU 的静止状态(quiescent state)检测,极大降低了宽限期检测的开销,使其从 O(N) 降低到 O(log N)。
三、RCU API 体系全景
RCU 的 API 体系分为三大模块:经典 RCU、睡眠 RCU(SRCU)和可睡眠 RCU(Sleepable RCU)。
3.1 经典 RCU(Classic RCU)
rcu_read_lock(); // 标记读侧临界区开始
rcu_read_unlock(); // 标记读侧临界区结束
rcu_dereference(p); // 安全获取受 RCU 保护的指针
rcu_dereference_check(p, c); // 带断言的安全获取
rcu_dereference_protected(p, c); // 用于写者持有锁时的安全获取
rcu_assign_pointer(p, v); // 安全更新受 RCU 保护的指针(写者使用)
synchronize_rcu(); // 等待当前宽限期结束(阻塞式)
synchronize_rcu_expedited(); // 快速同步(强制缩短宽限期)
call_rcu(&head, func); // 注册回调,宽限期后异步执行
kfree_rcu(head, field); // 封装了 kfree 的 call_rcu 快捷方式
3.2 可睡眠 RCU(Sleepable RCU / RCU-tasks)
rcu_read_lock_sched(); // 禁止调度
rcu_read_unlock_sched(); // 恢复调度
synchronize_sched(); // 等待所有 CPU 经过静止状态
3.3 Sleepable SRCU(可睡眠 Sleepable SRCU)
int srcu_read_lock(struct srcu_struct *sp);
void srcu_read_unlock(struct srcu_struct *sp, int idx);
void synchronize_srcu(struct srcu_struct *sp);
void call_srcu(struct srcu_struct *sp, struct rcu_head *rhp, func);
四、Grace Period 宽限期机制深度解析
4.1 宽限期的定义与本质
宽限期(Grace Period)是指从写者更新指针开始,到所有曾经持有旧指针引用的读者都退出读侧临界区为止的时间段。RCU 保证:在宽限期结束后,没有任何读者持有对旧数据的引用,此时可以安全释放旧数据。
关键洞察:宽限期不是指"从更新时刻开始的一段时间",而是指"保证所有在更新前进入读侧临界区的读者都已退出的那个时间窗口"。这意味着:宽限期开始前进入的读者必须退出;宽限期开始后进入的读者不受影响;宽限期期间,读者和写者可以并发执行。
4.2 宽限期的检测流程
RCU 通过检测所有 CPU 都经过了一次静止状态(Quiescent State)来判断宽限期结束。所谓静止状态是指 CPU 不在任何 RCU 读侧临界区内。
enum rcu_state {
RCU_IDLE, // 空闲
RCU_PRE_OFFLINE, // 预离线
RCU_PRE_WAIT, // 等待静止状态
RCU_PRE_HOLD, // 保持
RCU_WAIT, // 等待宽限期
RCU_DONE, // 宽限期完成
RCU_CUMULATIVE, // 累积
};
static void rcu_report_qs_rdp(struct rcu_data *rdp) {
rdp->qs_pending = 0;
rcu_report_qs_rnp(t, Special(s), &rdp->grpmask, rsp);
}
static void rcu_report_qs_rnp(struct rcu_node *rnp, unsigned long mask,
unsigned long gps, struct rcu_state *rsp) {
if (!(rnp->qsmask & mask) && rnp->gpnum <= gps) {
rcu_report_qs_rnp(rnp->parent, ..., rsp);
}
}
五、RCU 内存序与编译器屏障
5.1 rcu_dereference 的屏障语义
RCU 的 rcu_dereference() 宏在弱内存序架构上至关重要,它防止编译器和 CPU 对内存访问进行危险的乱序重排:
#define rcu_dereference(p) \
({ \
typeof(*p) *_________p1; \
rcu_lockdep_assert(...); \
rcu_check_sparse(p, __rcu); \
rcu_dereference_check(p, 0); \
})
#ifdef CONFIG_MEMORY_BARRIERS_NEED_CPU_RELATIONS
#define smp_read_barrier_depends() __asm__ __volatile__("mb" ::: "memory")
#else
#define smp_read_barrier_depends() do { } while (0)
#endif
#define __rcu_dereference_check(p, c, space) \
({ \
typeof(*p) *________p1 = READ_ONCE(p); \
if (c) \
rcu_lockdep_assert(...); \
((typeof(*p) __force __kernel *)(________p1)); \
})
5.2 rcu_assign_pointer 的写入屏障
#define rcu_assign_pointer(p, v) \
({ \
smp_mb(); // 保证之前的所有写入在新指针可见前完成
WRITE_ONCE(p, RCU_INITIALIZER(v)); \
})
六、内核中 RCU 的典型应用场景
6.1 进程描述符(task_struct)
Linux 内核中进程的创建和退出通过 RCU 保护进程查找:
struct task_struct *find_task_by_pid_ns(pid_t pid, struct pid_namespace *ns) {
struct result tmp;
unsigned int state;
rcu_read_lock();
tmp = find_task_by_pid_ns_rcu(pid, ns);
state = tmp.state;
if (tmp.task)
get_task_struct(tmp.task);
rcu_read_unlock();
// ...
}
6.2 路由缓存(Route Cache)
网络层的路由缓存大量使用 RCU:
struct dst_entry *dst_cahce_get(struct net *net, struct flowi *fl) {
rcu_read_lock();
for (pos = rcu_dereference(table->bucket[hash]); pos;
pos = rcu_dereference(pos->next)) {
if (pos->input == fl->input) {
dst_hold(pos);
rcu_read_unlock();
return pos;
}
}
rcu_read_unlock();
return NULL;
}
6.3 VFS 与文件系统
VFS 中的目录项缓存(dentry cache)和 inode 缓存使用 RCU 加速路径查找:
static struct dentry * __d_lookup_rcu(const struct qstr *name,
struct dentry *parent) {
unsigned int seq;
u32 hashlen = name->hash_len;
const unsigned char *str = name->name;
struct hlist_bl_head *b = d_hash(parent, hashlen);
struct hlist_bl_node *node;
struct dentry *dentry;
hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
if (dentry->d_name.hash_len != hashlen) continue;
if (!d_same_name(dentry, str, hashlen)) continue;
return dentry;
}
return NULL;
}
6.4 内存管理(mm_struct)
内存描述符使用 RCU 保护进程的 mm 查找:
struct mm_struct *mm = rcu_dereference(current->mm);
if (mm) {
// 安全访问 mm 结构
}
七、call_rcu 与延迟释放机制
7.1 call_rcu 的批处理优化
call_rcu() 是 RCU 写者最常使用的异步释放接口。它的设计非常精巧:将多个待释放的回调批量处理,减少宽限期等待的开销。
void call_rcu(struct rcu_head *head, rcu_callback_t func) {
unsigned long flags;
struct rcu_data *rdp;
BUG_ON(func == call_rcu);
raw_spin_lock_irqsave(&rcu_cpu_lock, flags);
rdp = this_cpu_ptr(rsp->rda);
if (unlikely(rdp->qlen > rdp->qlen_last_fqs_check + qhimark ||
(rdp->qs_pending && !rdp->rcu_iw_pending))) {
rdp->qlen_last_fqs_check = rdp->qlen;
if (rcu_kthread_spawnable(rsp)) {
rdp->rcu_iw_pending = true;
raise_softirq_irqoff(RCU_SOFTIRQ);
} else {
invoke_rcu_core();
}
}
local_irq_restore(flags);
head->func = func;
head->next = NULL;
local_irq_save(flags);
rdp->qlen++;
smp_mb__before_atomic();
*rdp->nxttail = head;
rdp->nxttail = &head->next;
local_irq_restore(flags);
}
7.2 kfree_rcu:内存释放的便捷封装
#define kfree_rcu(ptr, rhf) \
do { \
typeof(ptr) __krcu_ptr = (ptr); \
BUILD_BUG_ON(!__is_kfreetype(typeof(*ptr))); \
__kfree_rcu(&((ptr)->rhf), (unsigned long)(&((typeof(ptr))0)->rhf)); \
} while (0)
static inline void __kfree_rcu(struct rcu_head *head, unsigned long offset) {
call_rcu(head, kfree_rcu);
}
static void kfree_rcu(struct rcu_head *head) {
void *ptr = (char *)head - ...;
kfree(ptr);
}
八、RCU 与内存回收:SLAB 分配器集成
Linux 内核的 SLAB 分配器与 RCU 深度集成,通过 SLAB_TYPESAFE_BY_CACHE 标志实现安全回收:
void kmem_cache_free(struct kmem_cache *cachep, void *objp) {
unsigned long flags;
cachep = cache_from_obj(cachep, objp);
local_irq_save(flags);
debug_check_no_locks_freed(objp, cachep->object_size);
if (!(cachep->flags & SLAB_TYPESAFE_BY_CACHE))
__cache_free(cachep, objp, _THIS_IP_, 0);
else
call_rcu(&((struct slab *)objp)->rcu_head,
cache_rcu_destroyer);
local_irq_restore(flags);
}
九、RCU 的常见陷阱与最佳实践
9.1 禁止在 rcu_read_lock 中阻塞/睡眠
经典 RCU(非 SRCU)的读侧临界区内绝对不允许调用可能引起调度的函数(copy_to_user、kmalloc GFP_KERNEL、mutex_lock 等)。否则会导致系统崩溃或数据损坏。
9.2 rcu_dereference 必须在 RCU 读侧临界区内使用
在 rcu_read_lock() / rcu_read_unlock() 外部直接解引用 RCU 指针是危险的,因为写者可能正在更新该指针。
9.3 正确选择 RCU 变体
| RCU 类型 | 是否可睡眠 | 适用场景 |
|---|---|---|
| rcu_read_lock() | 不可睡眠 | 中断上下文、软中断、临界区 |
| rcu_read_lock_bh() | 不可睡眠 | 底半部上下文 |
| rcu_read_lock_sched() | 不可睡眠 | 调度器上下文 |
| srcu_read_lock() | 可睡眠 | 可能睡眠的读者 |
| rcu_read_lock_tasks() | 可睡眠 | tasks-trace RCU |
9.4 写者必须使用 rcu_assign_pointer
写者更新受 RCU 保护的指针时,必须使用 rcu_assign_pointer(),确保新数据对读者完全可见。
9.5 synchronize_rcu 是阻塞调用
synchronize_rcu() 会阻塞等待当前宽限期结束,因此不能在 RCU 读侧临界区内调用,否则会导致死锁。
十、RCU 性能调优与监控
10.1 sysfs 调试接口
cat /sys/kernel/debug/rcu/rcu_preempt/gpstats # 宽限期耗时统计
cat /sys/kernel/debug/rcu/rcu_preempt/rcugp # 当前宽限期信息
for cpu in /sys/devices/system/cpu/cpu*/rcu_expedited; do
echo "$cpu: $(cat $cpu)"
done
grep rcu /proc/stat
10.2 关键内核参数
kernel.rcu_normal=1 # 使用正常宽限期
kernel.rcu_expedited=0 # 加速宽限期
kernel.rcu_normal_delay_centisecs=20 # 正常宽限期延迟
10.3 性能基准测试
// 使用 rcuscale 模块测试 RCU 性能
// modprobe rcuscale nreaders=100 nwriters=10
// 读侧开销:约 2-3 个时钟周期(接近无同步开销)
// 写侧开销:synchronize_rcu 约 10-100ms
// call_rcu 开销:约 1-5μs(仅入队操作)
十一、RCU 在现代内核中的演进
11.1 Tree RCU(自 2.6.30 引入)
现代内核默认使用 Tree RCU,将 CPU 按 NUMA 节点组织成树形结构,宽限期检测从全局扫描(O(N))优化为层级聚合(O(log N)),极大提升了百万核级系统的可扩展性。
11.2 RCU-preempt(可抢占式 RCU)
CONFIG_PREEMPT=y 时,内核使用 rcu_read_lock() 禁止内核抢占,使得 RCU 读者在持有读锁期间不会被抢占,实现了 RCU 读侧临界区的完全实时性。
11.3 Tasks RCU
Tasks RCU(CONFIG_TASKS_RCU)是一种极简的 RCU 实现,将每次上下文切换都视为静止状态。它不需要显式调用 rcu_read_lock/unlock,通过 schedule() 隐式标记,特别适用于 BPF tracing。
十二、总结
RCU 是 Linux 内核中最精妙的同步原语,其"读无锁、写延迟释放"的设计哲学为高性能并发系统提供了独特优势。掌握 RCU 不仅需要理解 API 调用顺序,更需要理解宽限期机制、内存序屏障、静止状态检测等底层原理。随着内核从单核走向百万核,RCU 的重要性只会日益增长。
开发者在面对读多写少的并发场景时,应优先考虑 RCU 方案。正确使用时,RCU 能提供无与伦比的读性能;但误用也会导致难以调试的系统性故障。建议结合 ftrace、perf 工具和内核 RCU 调试选项进行验证。
本文深入剖析了 Linux 内核 RCU 同步机制的全貌,涵盖核心数据结构、API 体系、宽限期原理、内存序屏障、call_rcu延迟释放、Tree RCU架构演进,以及进程描述符、路由缓存、VFS中的典型应用场景和常见陷阱。适用于内核开发者、系统工程师和对并发原语感兴趣的高级程序员。

发表评论 取消回复