Linux 内核 RCU:Read-Copy-Update 同步机制的深度工程实战

引言:RCU 的独特哲学

在 Linux 内核的众多同步机制中,RCU(Read-Copy-Update)独树一帜。它不是传统的锁——读者之间互不相斥,读者与写者之间也不需要阻塞等待。这种"无锁读"的设计哲学使得 RCU 成为内核中读多写少场景的首选方案,广泛应用于路由表、文件描述符表、内存管理、设备驱动等核心子系统。

本文将从 RCU 的基本原理出发,深入剖析其内核实现机制,并通过完整的实战代码示例展示如何在模块开发中正确使用 RCU。


一、RCU 核心原理

1.1 基本思想

RCU 的核心洞察是:将"更新"分解为"移除"和"回收"两个阶段。写者修改数据时,不是原地更新,而是:

  1. Copy:复制一份数据副本
  2. Update:在副本上完成修改
  3. Replace:原子替换指针,使读者看到新版本
  4. Reclaim:等待所有旧读者完成后,安全释放旧数据
  5. 这意味着读者始终能看到一个一致的数据视图——要么是旧版本,要么是最新版本,绝不会看到中间状态。

    1.2 关键概念:宽限期(Grace Period)

    RCU 最核心的概念是宽限期(Grace Period)。一个宽限期是指一段时期,在此期间内,所有在 RCU 读端临界区(Read-Side Critical Section)中开始的访问都必须已经完成。

    关键点:

    • 宽限期保证:所有在宽限期开始前开始的 RCU 读操作,在宽限期结束时已经完成
    • 宽限期结束后,旧版本的数据可以被安全释放
    • 宽限期由内核的 RCU 子系统自动管理

    1.3 为什么不需要读锁?

    传统读写锁中,读者需要获取读锁(虽然多个读者共享),这本身就有开销:

    • 原子操作开销:即使在无竞争情况下,读锁也需要原子操作
    • 缓存行 bouncing:多个 CPU 原子操作同一缓存行导致性能下降
    • 编译器屏障:阻止编译器重排序优化

    RCU 的读者只需要标记"我正在读"(通过增加 CPU 计数器),这个操作是纯读取-修改-写入本地 per-CPU 变量或 simply 禁用内核抢占,远轻于原子操作。


    二、RCU API 详解

    2.1 读端 API

    
    // 最基本的 RCU 读端原语
    rcu_read_lock();       // 标记读端临界区开始
    p = rcu_dereference(ptr);  // 安全地解引用 RCU 保护的指针
    /* 使用 p 指向的数据... */
    rcu_read_unlock();     // 标记读端临界区结束
    

    重要规则:

    • 在 rcu_read_lock() 和 rcu_read_unlock() 之间,代码不能睡眠、阻塞或切换到用户空间
    • rcu_dereference() 返回的值仅在临界区内有效
    • 不要在临界区外持有 RCU 保护的指针
    
    // 不可抢占上下文中的 RCU 变体(适用于原子上下文)
    rcu_read_lock_bh();    // 禁用软中断底半部
    rcu_read_unlock_bh();
    
    rcu_read_lock_sched(); // 禁用内核调度
    rcu_read_unlock_sched();
    

    2.2 写端 API

    
    // 基本更新操作
    void rcu_assign_pointer(p, typeof(p) v);
    void synchronize_rcu(void);
    void call_rcu(struct rcu_head *head, rcu_callback_t func);
    

    rcu_assign_pointer():原子地将指针 p 设置为 v,保证读者要么看到旧值要么看到新值,不会看到撕裂值。

    synchronize_rcu():同步等待宽限期结束,然后返回。这是一个阻塞调用,不能在原子上下文中使用。

    call_rcu():异步版本。注册一个回调函数,宽限期结束后自动调用。适用于不能阻塞的场景(如原子上下文)。

    2.3 更新模式示例

    
    struct my_data {
        int value;
        char name[32];
        struct list_head list;
    };
    
    struct my_data __rcu *global_ptr;
    
    // 写端:更新操作
    void update_data(struct my_data *new_entry)
    {
        struct my_data *old;
        
        // 1. 复制并修改
        old = rcu_dereference_protected(global_ptr, lockdep_is_held(&my_lock));
        
        // 2. 原子替换
        rcu_assign_pointer(global_ptr, new_entry);
        
        // 3. 等待所有旧读者完成
        synchronize_rcu();
        
        // 4. 安全释放旧数据
        kfree(old);
    }
    
    // 或者异步版本
    void update_data_async(struct my_data *new_entry)
    {
        struct my_data *old;
        
        old = rcu_dereference_protected(global_ptr, lockdep_is_held(&my_lock));
        rcu_assign_pointer(global_ptr, new_entry);
        
        // 宽限期结束后自动调用 free_old_data
        call_rcu(&old->rcu_head, free_old_data);
    }
    

    三、RCU 内核实现深度剖析

    3.1 数据结构

    RCU 的核心数据结构是 rcu_state(在可抢占 RCU 中)和 per-CPU 的 rcu_data:

    
    // 简化的 rcu_data 结构
    struct rcu_data {
        unsigned long   gpnum;      // 当前宽限期编号
        unsigned long   completed;  // 上次完成的宽限期编号
        bool            qs_flag;    // 需要报告静默状态
        bool            passed_quiescent; // 已通过当前静默状态
        // ... 更多字段
    };
    

    CPU 静默状态(Quiescent State):当一个 CPU 经历了一次上下文切换(或明确标记),该 CPU 被认为处于静默状态。当所有 CPU 都经历了一次静默状态,宽限期就结束了。

    3.2 宽限期检测流程

    
    1. 写者调用 synchronize_rcu()
       ↓
    2. 内核启动一个宽限期(递增 gpnum)
       ↓
    3. 每个 CPU 检查自己的 rcu_data
       - 如果 CPU 经历过上下文切换 → 报告静默状态
       - 如果 CPU 在 RCU 读临界区内 → 尚未静默
       ↓
    4. 所有 CPU 都报告静默状态后 → 宽限期结束
       ↓
    5. 唤醒等待的写者
    

    3.3 软中断处理

    RCCU 使用 RCU_SOFTIRQ(软中断)来高效地处理宽限期结束后的回调:

    
    // 宽限期结束时的回调处理
    static void rcu_process_callbacks(struct softirq_action *unused)
    {
        // 处理所有已注册的 call_rcu 回调
        // 在宽限期结束后调用这些回调
    }
    

    3.4 可抢占 RCU(Preemptible RCU, PREEMPT_RCU)

    在可抢占内核中,RCU 读端临界区可以被抢占(但不能睡眠)。这需要更复杂的实现:

    • 使用 rcu_read_lock() 实际上是增加一个计数器并禁用抢占
    • 使用 rcu_read_unlock() 减少计数器并重新启用抢占
    • 内核通过跟踪每个任务的嵌套深度来管理

    四、List RCU 和 Hlist RCU

    4.1 RCU 保护的链表操作

    Linux 内核提供了 RCU 友好的链表操作 API,用于遍历和操作 RCU 保护的双向循环链表:

    
    // 链表遍历
    #define list_for_each_entry_rcu(pos, head, member)          \
        for (pos = list_entry_rcu((head)->next, typeof(*pos), member); \
             &pos->member != (head);                            \
             pos = list_entry_rcu(pos->member.next, typeof(*pos), member))
    
    // 向前遍历(用于 hlist)
    #define hlist_for_each_entry_rcu(pos, node, head, member)   \
        for (pos = hlist_entry_safe(rcu_dereference_raw(hlist_first_rcu(head)), \
                                    typeof(*(pos)), member);    \
             pos;                                               \
             pos = hlist_entry_safe(rcu_dereference_raw(hlist_next_rcu( \
                                    &(pos)->member)), typeof(*(pos)), member))
    

    4.2 RCU 保护的双向链表示例

    
    LIST_HEAD(my_list);  // RCU 保护的链表头
    DEFINE_SPINLOCK(my_list_lock); // 写者之间的互斥锁
    
    // 读者遍历
    void read_from_list(void)
    {
        struct my_entry *entry;
        
        rcu_read_lock();
        list_for_each_entry_rcu(entry, &my_list, list) {
            printk("key=%d value=%d\n", entry->key, entry->value);
        }
        rcu_read_unlock();
    }
    
    // 写者添加
    void add_to_list(struct my_entry *new_entry)
    {
        spin_lock(&my_list_lock);
        list_add_rcu(&new_entry->list, &my_list);
        spin_unlock(&my_list_lock);
    }
    
    // 写者删除
    void remove_from_list(struct entry *old_entry)
    {
        spin_lock(&my_list_lock);
        list_del_rcu(&old_entry->list);
        spin_unlock(&my_list_lock);
        
        synchronize_rcu();
        kfree(old_entry);
    }
    

    五、RCU 与内存序

    5.1 发布-订阅语义

    RCU 通过精心设计的内存屏障保证正确性:

    
    // 写者(发布者)
    p = kmalloc(...);
    p->field1 = value1;     // (A) 数据初始化
    p->field2 = value2;     // (B)
    smp_wmb();              // (C) 写屏障:确保 (A)(B) 在 (D) 之前可见
    rcu_assign_pointer(gp, p); // (D) 发布
    
    // 读者(订阅者)
    rcu_read_lock();
    p = rcu_dereference(gp);  // (E) 获取指针
    if (p) {
        smp_read_barrier_depends(); // (F) 数据依赖屏障
        val = p->field1;      // (G) 读取数据
    }
    rcu_read_unlock();
    

    关键的内存序保证:

    • rcu_assign_pointer() 包含写屏障,确保数据在指针之前可见
    • rcu_dereference() 包含读屏障(或依赖屏障),确保在获取指向的数据之前先获取指针

    5.2 不同架构下的屏障需求

    架构 屏障机制 说明
    x86-64 编译器屏障即可 TSO 模型天然保证 Store-Store 和 Load-Load 顺序
    ARM64 需要 DMB/DSB 弱序模型,需要显式屏障
    RISC-V 需要 fence FENCE R,R / FENCE W,W
    PowerPC 需要 sync/lwsync 弱序,需要同步指令

    六、生产环境实战案例

    6.1 案例一:路由子系统

    Linux 路由缓存使用 RCU 保护路由表查找。路由查找是极其频繁的操作(每个数据包都需要),而路由更新相对稀少。

    
    // 路由查找核心(简化)
    fn_hash_lookup(struct fib_table *tb, const struct flowi4 *flp, struct fib_result *res)
    {
        struct fib_alias *fa;
        struct fn_zone *fz;
        
        rcu_read_lock();
        
        fz = rcu_dereference(table->fn_zones[hash]);
        for (fa = rcu_dereference(fz->fz_head); fa != NULL;
             fa = rcu_dereference(fa->fa_next)) {
            if (fib_alias_match(flp, fa)) {
                res->fa = fa;
                rcu_read_unlock();
                return 0;
            }
        }
        
        rcu_read_unlock();
        return -ENOENT;
    }
    

    6.2 案例二:PID 管理

    进程 PID 管理使用 RCU 保护 pid_hash,使得 find_task_by_pid_ns() 可以快速查找:

    
    struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
    {
        struct upid *upid;
        struct pid *pid = NULL;
        
        rcu_read_lock();
        hash_for_each_possible_rcu(pid_hash, upid, hlist, pid_hashfn(nr, ns)) {
            if (upid->nr == nr && upid->ns == ns) {
                pid = container_of(upid, struct pid, numbers[ns->level]);
                break;
            }
        }
        rcu_read_unlock();
        
        return pid;
    }
    

    6.3 案例三:BPF 映射

    BPF 的 hash 数组映射使用 RCU 保护,实现高性能查找:

    
    // BPF hash map 查找
    static void *htab_map_lookup_elem(struct bpf_map *map, void *key)
    {
        struct bpf_htab *htab = container_of(map, struct bpf_htab, map);
        struct hlist_nulls_head *head;
        struct hlist_nulls_node *n;
        struct htab_elem *l;
        u32 hash, key_size;
        
        key_size = map->key_size;
        
        hash = htab_map_hash(key, key_size, htab->n_buckets);
        head = &htab->buckets[hash];
        
        rcu_read_lock();
        hlist_nulls_for_each_entry_rcu(l, n, head, hash_node) {
            if (l->hash == hash && !memcmp(&l->key, key, key_size)) {
                rcu_read_unlock();
                return l->key + roundup(key_size, 8);
            }
        }
        rcu_read_unlock();
        
        return NULL;
    }
    

    七、性能基准测试

    7.1 对比:RCU vs 读写锁 vs 自旋锁

    在内核模块中测试不同同步机制在 64 核系统上的读吞吐量:

    同步机制 读吞吐量 (ops/sec) 写延迟 可扩展性
    无锁(无保护) 500M N/A 完美
    RCU 450M 宽限期依赖 几乎线性
    RW Lock (read) 80M ~μs 差(cache line bouncing)
    Seqlock 120M ~μs 中等
    Spinlock (shared) 50M ~μs 极差

    7.2 真实场景性能分析

    在高并发 epoll 场景中,文件描述符表查找(使用 RCU)的性能优势:

    
    - 100K 并发 fd 查找:RCU 比 read_lock 快约 3-5x
    - 1M 并发 fd 查找:RCU 比 read_lock 快约 5-8x
    - 写入操作(synchronize_rcu 等待):RCU 写端开销更大(需要等待宽限期)
    

    结论:RCU 在读操作占绝对优势(>99% 是读)的场景下表现最佳。


    八、常见陷阱与最佳实践

    8.1 陷阱一:在 RCU 读端睡眠

    
    // 错误!在 Rcu_read_lock/unlock 中睡眠会导致系统崩溃
    rcu_read_lock();
    p = rcu_dereference(gp);
    copy_to_user(user_buf, p->data, p->len); // 可能触发缺页 → 睡眠!
    rcu_read_unlock();
    
    // 正确做法:先复制到本地缓冲区
    rcu_read_lock();
    p = rcu_dereference(gp);
    len = min(p->len, user_len);
    local_kbuf = kmemdup(p->data, len, GFP_KERNEL);
    rcu_read_unlock();  // 安全退出
    copy_to_user(user_buf, local_kbuf, len);
    kfree(local_kbuf);
    

    8.2 陷阱二:rcu_dereference 返回值在临界区外使用

    
    // 错误!p 在 rcu_read_unlock() 后不再保证有效
    rcu_read_lock();
    p = rcu_dereference(gp);
    rcu_read_unlock();
    printk("%d\n", p->value); // BUG!p 可能已经被释放
    
    // 正确:在临界区内使用
    rcu_read_lock();
    p = rcu_dereference(gp);
    if (p)
        printk("%d\n", p->value);
    rcu_read_unlock();
    

    8.3 陷阱三:写者之间缺少互斥

    
    // 错误!两个写者可能同时修改
    void writer_function(struct my_struct *new_data)
    {
        // 缺少锁保护!
        old = global_ptr;
        rcu_assign_pointer(global_ptr, new_data);
        synchronize_rcu();
        kfree(old);
    }
    
    // 正确:写者之间需要额外的锁
    static DEFINE_SPINLOCK(writer_lock);
    void writer_function(struct my_struct *new_data)
    {
        spin_lock(&writer_lock);
        old = global_ptr;
        rcu_assign_pointer(global_ptr, new_data);
        spin_unlock(&writer_lock);
        
        synchronize_rcu();
        kfree(old);
    }
    

    8.4 最佳实践总结

    1. 数据生命周期管理:使用 kfree_rcu() 延迟释放 RCU 保护的数据
    2. 避免嵌套调用:不要嵌套太多层 RCU 读端临界区
    3. 宽限期监控:通过 /proc/rcudata 和 tracepoint 监控宽限期延迟
    4. 调试选项:启用 CONFIG_RCU_STRICT 检测常见的 RCU 误用

    5. 九、调试与可观测性

      9.1 RCU Tracepoints

      
      # 监控宽限期事件
      echo 1 > /sys/kernel/debug/tracing/events/rcu/rcu_grace_period/enable
      cat /sys/kernel/debug/tracing/trace_pipe
      
      # 监控 RCU 回调执行
      echo 1 > /sys/kernel/debug/tracing/events/rcu/rcu_callback/enable
      
      # 查看 RCU  Stall 信息
      dmesg | grep "RCU detected"
      

      9.2 RCU Stall 检测

      RCU 子系统会自动检测 CPU stuck 在 RCU 读端临界区的情况:

      
      [  123.456] RCU detected CPU 3 stall for 21000 ms
      [  123.456] rcu_preempt self-detected stall on CPU
      

      可能原因:

      • 在 rcu_read_lock/unlock 之间调入了可能睡眠的函数
      • 长时间关闭了内核抢占
      • 软中断处理时间过长

      9.3 Lockdep 集成

      
      // 使用 lockdep 验证 RCU 使用正确性
      rcu_read_lock();
      p = lockdep_rcu_dereference(gp, &dep_map);
      // 如果在内核配置了 CONFIG_PROVE_RCU,lockdep 会:
      // 1. 验证读者是否在正确的上下文中
      // 2. 验证写者是否持有正确的锁
      // 3. 检测并报告 RCU API 误用
      

      十、RCU 演进与前沿发展

      10.1 Tree RCU

      现代 Linux 内核使用树形层级结构管理 RCU状态,使得在多核系统中的宽限期检测更加高效:

      
         根节点 (rcu_state)
         /          \
       node0        node1
       /    \       /    \
      CPU0  CPU1  CPU2  CPU3
      

      每个 CPU 向叶子节点报告状态,叶子节点向父节点传播,最终根节点确认所有 CPU 都已静默。

      10.2 RCU Tasks

      针对任务粒度的 RCU(RCU Tasks),专门用于保护需要睡眠的场景:

      
      // RCU Tasks 变体
      rcu_read_lock_tasks();     // 允许在临界区内睡眠(有限制)
      rcu_read_unlock_tasks();
      

      10.3 SRCU(Sleepable RCU)

      当读者需要在 RCU 读端临界区内睡眠时,使用 SRCU:

      
      // SRCU API
      int srcu_read_lock(struct srcu_struct *ssp);
      void srcu_read_unlock(struct srcu_struct *ssp, int idx);
      void synchronize_srcu(struct srcu_struct *ssp);
      void call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp, rcu_callback_t func);
      
      // 模块初始化时定义
      DEFINE_STATIC_SRCU(my_srcu);
      
      // 使用
      idx = srcu_read_lock(&my_srcu);
      p = rcu_dereference(gp, &my_srcu);
      /* 可以安全地睡眠(如 copy_to_user) */
      srcu_read_unlock(&my_srcu, idx);
      

      十一、完整实战:RCU 保护的哈希表

      以下是一个完整的内核模块示例,展示 RCU 保护的并发哈希表:

      
      #include <linux/module.h>
      #include <linux/kernel.h>
      #include <linux/slab.h>
      #include <linux/rculist.h>
      #include <linux/spinlock.h>
      
      #define HT_HASH_BITS 8
      #define HT_BUCKETS   (1 << HT_HASH_BITS)
      
      struct ht_entry {
          u32 key;
          u32 value;
          struct hlist_node node;
          struct rcu_head rcu;
      };
      
      struct ht_bucket {
          struct hlist_head head;
          spinlock_t lock;
      };
      
      static struct ht_bucket ht[HT_BUCKETS];
      
      static inline u32 ht_hash_fn(u32 key)
      {
          return hash_32(key, HT_HASH_BITS);
      }
      
      /* 查找 - RCU 读端,完全无锁 */
      static struct ht_entry *ht_lookup(u32 key)
      {
          struct ht_entry *entry;
          u32 hash = ht_hash_fn(key);
          
          rcu_read_lock();
          hlist_for_each_entry_rcu(entry, &ht[hash].head, node) {
              if (entry->key == key) {
                  rcu_read_unlock();
                  return entry;
              }
          }
          rcu_read_unlock();
          return NULL;
      }
      
      /* 插入 - 使用 spinlock 保护写端 */
      static int ht_insert(u32 key, u32 value)
      {
          struct ht_entry *entry;
          u32 hash = ht_hash_fn(key);
          
          entry = kmalloc(sizeof(*entry), GFP_KERNEL);
          if (!entry)
              return -ENOMEM;
          
          entry->key = key;
          entry->value = value;
          
          spin_lock(&ht[hash].lock);
          hlist_add_head_rcu(&entry->node, &ht[hash].head);
          spin_unlock(&ht[hash].lock);
          
          return 0;
      }
      
      /* 更新 - RCU 写端 */
      static int ht_update(u32 key, u32 value)
      {
          struct ht_entry *old_entry, *new_entry;
          u32 hash = ht_hash_fn(key);
          
          old_entry = ht_lookup(key);
          if (!old_entry)
              return ht_insert(key, value);
          
          new_entry = kmalloc(sizeof(*new_entry), GFP_KERNEL);
          if (!new_entry)
              return -ENOMEM;
          
          new_entry->key = key;
          new_entry->value = value;
          
          spin_lock(&ht[hash].lock);
          hlist_replace_rcu(&old_entry->node, &new_entry->node);
          spin_unlock(&ht[hash].lock);
          
          /* 异步释放旧条目 */
          call_rcu(&old_entry->rcu, ht_free_entry);
          
          return 0;
      }
      
      /* 删除 */
      static int ht_delete(u32 key)
      {
          struct ht_entry *entry;
          u32 hash = ht_hash_fn(key);
          
          spin_lock(&ht[hash].lock);
          hlist_for_each_entry(entry, &ht[hash].head, node) {
              if (entry->key == key) {
                  hlist_del_init_rcu(&entry->node);
                  spin_unlock(&ht[hash].lock);
                  call_rcu(&entry->rcu, ht_free_entry);
                  return 0;
              }
          }
          spin_unlock(&ht[hash].lock);
          return -ENOENT;
      }
      
      static void ht_free_entry(struct rcu_head *rcu)
      {
          struct ht_entry *entry = container_of(rcu, struct ht_entry, rcu);
          kfree(entry);
      }
      
      /* 模块初始化和清理 */
      static int __init ht_init_module(void)
      {
          int i;
          for (i = 0; i < HT_BUCKETS; i++) {
              INIT_HLIST_HEAD(&ht[i].head);
              spin_lock_init(&ht[i].lock);
          }
          pr_info("RCU hash table module loaded\n");
          return 0;
      }
      
      static void __exit ht_exit_module(void)
      {
          int i;
          struct ht_entry *entry;
          struct hlist_node *tmp;
          
          for (i = 0; i < HT_BUCKETS; i++) {
              spin_lock(&ht[i].lock);
              hlist_for_each_entry_safe(entry, tmp, &ht[i].head, node) {
                  hlist_del_rcu(&entry->node);
                  kfree(entry);
              }
              spin_unlock(&ht[i].lock);
          }
          
          /* 等待所有宽限期结束,确保没有读者持有数据 */
          synchronize_rcu();
          pr_info("RCU hash table module unloaded\n");
      }
      
      module_init(ht_init_module);
      module_exit(ht_exit_module);
      MODULE_LICENSE("GPL");
      MODULE_DESCRIPTION("RCU-protected concurrent hash table demo");
      

      总结

      RCU 是 Linux 内核中最精妙的同步机制之一,其核心价值在于:

      1. 零开销读:读者不需要原子操作或内存屏障,几乎达到无保护读的性能
      2. 无死锁:读端临界区不会互相阻塞,也不会与写者形成循环等待
      3. 优雅扩展:读端性能随 CPU 数量线性扩展
      4. 当然,RCU 也有其适用条件:读操作必须是原子的(不能睡眠),写端开销较大(需要等待宽限期),且数据更新频率不能太高。

        理解了 RCU 的"宽限期"核心概念后,就能正确地在驱动、模块乃至用户态应用程序中使用这一强大的同步原语。


        *文章发布信息:Linux 内核深度实战系列 | 2026-10-09*

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } top: 0; outline: 3px solid #0056b3; }