Skip to content

Linux 内核 RCU 机制学习与示例

1. 什么是 RCU?

RCU(Read-Copy-Update,读-拷贝-更新) 是 Linux 内核中一种高性能的同步机制,专为 读多写少 的场景设计。

核心思想:

  • 读者几乎无锁(或极低开销),可并发读取
  • 写者不原地修改共享数据,而是 拷贝一份 → 修改副本 → 原子替换指针
  • 旧数据在所有读者退出临界区(grace period,宽限期)后再释放

RCU 不是传统意义上的“锁”,而是一套 发布-订阅 + 延迟回收 的协议。

时间线 ─────────────────────────────────────────────────────────►

读者 A:  [── rcu_read_lock ── 读旧指针 ── rcu_read_unlock ──]
读者 B:        [── rcu_read_lock ── 读旧指针 ── rcu_read_unlock ──]
写者:              [分配新对象] [rcu_assign_pointer 替换] [synchronize_rcu 等待] [kfree 旧对象]
                                                          ↑ grace period 结束

2. 为什么需要 RCU?

场景 传统读写锁 RCU
读者开销 加锁/解锁 极低开销(禁止抢占或轻量标记)
读者并发 共享锁,可能竞争 读者之间几乎无竞争
写者开销 中等 较高(需等待 grace period)
适用模式 读写均衡 读远多于写

典型应用:路由表、模块链表、文件系统 dentry、BPF map 遍历等。

3. RCU 三大规则

  1. 读侧:在 rcu_read_lock()rcu_read_unlock() 之间,读者看到的指针快照不会被写者“原地破坏”
  2. 写侧:更新通过 rcu_assign_pointer() 发布新指针,旧数据仍可能被旧读者访问
  3. 回收:写者必须等 grace period 结束后才能 kfree() 旧对象(synchronize_rcu()call_rcu()

4. 内核 RCU 核心 API

4.1 读侧临界区

rcu_read_lock();           /* 进入读侧临界区,禁止抢占(不可睡眠) */
p = rcu_dereference(head); /* 读取 RCU 保护的指针 */
/* 使用 *p ... */
rcu_read_unlock();         /* 退出读侧临界区 */
  • rcu_dereference():防止编译器重排,保证读到的是一致指针
  • 读侧 不可睡眠,不可阻塞

4.2 写侧发布

struct foo *new = kmalloc(sizeof(*new), GFP_KERNEL);
/* 初始化 new ... */
rcu_assign_pointer(head, new);  /* 原子发布新指针 */
  • rcu_assign_pointer() 等价于带内存屏障的指针写入

4.3 等待宽限期(同步回收)

synchronize_rcu();  /* 阻塞直到所有先前读者退出 */
kfree(old);
  • 阻塞当前上下文,不可在中断/原子上下文中使用

4.4 异步回收

call_rcu(&old->rcu, foo_free_rcu);  /* grace period 后回调释放 */

static void foo_free_rcu(struct rcu_head *head)
{
    struct foo *p = container_of(head, struct foo, rcu);
    kfree(p);
}
  • 适合写路径不能长时间阻塞的场景
  • Linux 5.13+ 可用 kfree_rcu(ptr, rcu) 简化

4.5 辅助宏

#define rcu_dereference(p)     READ_ONCE(p)
#define rcu_assign_pointer(p, v) (p = (typeof(p))(v))
LIST_FOR_EACH_ENTRY_RCU(pos, head, member)  /* 安全遍历 RCU 链表 */

5. RCU 变体

类型 说明
Classic RCU 不可抢占读侧,最常用
PREEMPT_RCU 可抢占内核,读侧可用 rcu_read_lock() 被抢占
SRCU Sleepable RCU,读侧可睡眠,每域独立
Tasks RCU 跟踪用户态/内核线程而非抢占

本示例项目模拟 Classic RCU 语义。

6. 完整内核风格示例(链表查找 + 更新)

/* 查找(读侧) */
struct item *find_item(int key)
{
    struct item *item;

    rcu_read_lock();
    list_for_each_entry_rcu(item, &head_list, list) {
        if (item->key == key) {
            rcu_read_unlock();
            return item;
        }
    }
    rcu_read_unlock();
    return NULL;
}

/* 删除(写侧) */
void delete_item(int key)
{
    struct item *item;

    list_for_each_entry(item, &head_list, list) {
        if (item->key != key)
            continue;
        list_del_rcu(&item->list);
        synchronize_rcu();
        kfree(item);
        return;
    }
}

/* 替换(写侧) */
void replace_item(struct item *new)
{
    struct item *old;

    list_for_each_entry(old, &head_list, list) {
        if (old->key != new->key)
            continue;
        list_replace_rcu(&old->list, &new->list);
        synchronize_rcu();
        kfree(old);
        return;
    }
}

7. 常见陷阱

  1. 在读侧临界区内睡眠 → 宽限期永不结束,内存泄漏或死锁
  2. 未等 grace period 就 free → use-after-free
  3. 写侧原地修改读者可见字段 → 数据竞争(应 copy-update-replace)
  4. 在中断上下文调用 synchronize_rcu → 可能死锁
  5. 忘记 rcu_dereference → 编译器优化导致读到撕裂指针

8. 本示例项目说明

由于 RCU 依赖内核调度器与 per-CPU 状态,本仓库提供 用户态 RCU 模拟器,在 Linux/WSL 上运行,API 与内核命名一致,便于学习。

目录结构

rcu_test/
├── README.md           # 本文档
├── Makefile
├── include/
│   └── rcu.h           # RCU API 声明
└── src/
    ├── rcu.c           # 用户态 RCU 实现(grace period 检测)
    ├── rcu_list.c      # RCU 保护链表
    ├── rcu_list.h
    └── main.c          # 多线程演示

编译与运行

# 需要 Linux 或 WSL,以及 pthread
make
./rcu_demo

演示内容

  • 多读者线程并发 rcu_read_lock + rcu_dereference 读取全局链表
  • 写者线程 rcu_assign_pointer 替换头指针,或 list_del_rcu + call_rcu 异步释放
  • 统计 grace period 等待时间与读侧并发安全性

9. 进一步阅读