一、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中的典型应用场景和常见陷阱。适用于内核开发者、系统工程师和对并发原语感兴趣的高级程序员。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部