Linux 内核 RCU 免锁并发读取机制:从数据结构更新语义到驱动层宽限期 API 的完整推导

📅 2026/7/15 23:00:54
Linux 内核 RCU 免锁并发读取机制:从数据结构更新语义到驱动层宽限期 API 的完整推导
Linux 内核 RCU 免锁并发读取机制从数据结构更新语义到驱动层宽限期 API 的完整推导一、驱动中的并发读取之痛——spinlock 保护链表时 ISR 延迟已经破标在 Linux 设备驱动中一个常见的数据共享模式是中断服务程序ISR高频读取一个链表以查找设备状态而工作队列workqueue在后台更新这个链表。初版实现通常使用spin_lock_irqsave保护整个读操作。但当链表节点数超过 200 且 ISR 频率达到 50kHz 时spinlock 持有时间可能超过 3μs——超过了系统 ISR 延迟要求2μs导致后续中断丢失。问题的本质是读操作远多于写操作但读锁的开销与写锁无差异。RCURead-Copy-Update针对这一场景设计了一种非对称同步原语读路径为零开销无锁、无原子操作写路径承担更新成本。在 RCU 保护的链表中读者可以并发地遍历链表即使写者正在更新其中的部分节点——RCU 通过先复制再更新策略和宽限期Grace Period机制保证了数据一致性。二、RCU 的底层原理免锁读取的三阶段模型2.1 核心语义订阅-发布机制与宽限期RCU 将共享数据的生命周期分为三个阶段移除Removal写者创建数据副本原子切换指针旧数据仍在内存中宽限期Grace Period等待所有可能看到旧数据的读者完成其临界区回收Reclamation确认无读者持有旧数据引用后安全释放旧数据sequenceDiagram participant Writer as 写者 (workqueue) participant Memory as 共享数据 participant Reader1 as 读者1 (ISR) participant Reader2 as 读者2 (ISR) participant GP as 宽限期检测 Writer-Memory: 创建节点 C (数据副本) Writer-Memory: rcu_assign_pointer(head-next, C) Note over Memory: 链表: A→B→C→D → A→B→C→D Writer-GP: synchronize_rcu() 等待宽限期 Reader1-Memory: rcu_dereference(head-next) Note over Reader1: 可能读取到 C 或 C (取决于时机) GP--GP: 检测到 Reader1 退出 RCU 读临界区 GP--GP: 检测到 Reader2 退出 RCU 读临界区 Note over GP: 所有 CPU 都经历过一次静止状态(Quiescent State) GP--Writer: 宽限期结束 Writer-Memory: kfree(C) 安全释放旧节点2.2 静止状态Quiescent State的检测机制宽限期的边界定义为所有 CPU 在 RCU 读临界区之外至少执行过一次上下文切换。内核通过以下机制检测静止状态Tick 中断检测每次 tick 中断检查当前 CPU 是否在 RCU 读临界区rcu_read_lock()/rcu_read_unlock()原子计数仅在抢占式内核中真正执行原子操作非抢占内核中为无操作synchronize_rcu()阻塞等待所有 CPU 都报告过静止状态后才返回graph TD A[写者调用 synchronize_rcu] -- B{RCU 状态机: 等待 GP} B -- C[CPU0: 静止状态?] B -- D[CPU1: 静止状态?] B -- E[CPU2: 静止状态?] B -- F[CPU3: 静止状态?] C --|rcu_read_unlock 调用| G[标记 CPU0 静止] D --|tick 中断检测| H[标记 CPU1 静止] E --|上下文切换| I[标记 CPU2 静止] F --|调度器进入| J[标记 CPU3 静止] G -- K{所有 CPU 都已静止?} H -- K I -- K J -- K K --|是| L[宽限期结束, 唤醒写者] K --|否| B三、驱动层代码实现3.1 使用 RCU 保护设备链表#include linux/slab.h #include linux/rcupdate.h #include linux/interrupt.h /* 设备节点: 通过 RCU 链表管理 */ struct device_node { int dev_id; unsigned long state; struct list_head list; /* RCU 保护链表 */ struct rcu_head rcu; /* RCU 回调: 延迟释放 */ void *data; }; static LIST_HEAD(device_list); /* 链表头 */ static DEFINE_SPINLOCK(device_list_lock); /* 写路径锁: 仅保护修改操作 */ /* ISR 中的读路径: 完全无锁 */ irqreturn_t sensor_isr(int irq, void *dev_id) { struct device_node *node; /* RCU 读临界区: 禁止抢占, 零开销 */ rcu_read_lock(); list_for_each_entry_rcu(node, device_list, list) { if (node-state DEVICE_ACTIVE) { /* 处理活跃设备的数据 */ process_device_data(node); } } rcu_read_unlock(); return IRQ_HANDLED; } /* 工作队列中的写路径: 添加设备 */ int add_device(struct device_node *new_dev) { struct device_node *dev; /* 分配新节点并拷贝数据 */ dev kmalloc(sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; memcpy(dev, new_dev, sizeof(*dev)); INIT_LIST_HEAD(dev-list); /* 写路径: 使用 spinlock 串行化多个写者 */ spin_lock(device_list_lock); /* rcu_assign_pointer 确保其他 CPU 看到一致的指针更新 */ list_add_tail_rcu(dev-list, device_list); spin_unlock(device_list_lock); return 0; } /* 工作队列中的写路径: 移除设备 (RCU 延迟释放) */ int remove_device(int dev_id) { struct device_node *node, *found NULL; spin_lock(device_list_lock); list_for_each_entry(node, device_list, list) { if (node-dev_id dev_id) { list_del_rcu(node-list); /* 从链表移除, 但内存暂不释放 */ found node; break; } } spin_unlock(device_list_lock); if (!found) return -ENOENT; /* 延迟释放: 等待所有读者退出 RCU 临界区后, 回调中 kfree */ call_rcu(found-rcu, device_free_rcu_callback); return 0; } /* RCU 回调: 在宽限期后安全释放内存 */ static void device_free_rcu_callback(struct rcu_head *rp) { struct device_node *node container_of(rp, struct device_node, rcu); if (node-data) kfree(node-data); kfree(node); } /* 模块卸载时: 同步等待所有 RCU 回调完成 */ static void __exit sensor_driver_exit(void) { /* 确保所有 call_rcu 的回调已执行 */ rcu_barrier(); }3.2 使用synchronize_rcu替代回调的批量更新场景在配置更新这类低频操作中直接使用synchronize_rcu比call_rcu更简洁/* 批量更新设备配置: 阻塞等待直到所有读者看到新配置 */ int update_all_devices_config(const struct config *new_cfg) { struct device_node *node, *tmp; struct config *old_cfg; /* 分配新配置 (可能很大, 使用 vmalloc) */ struct config *cfg_copy vmalloc(sizeof(*new_cfg)); if (!cfg_copy) return -ENOMEM; memcpy(cfg_copy, new_cfg, sizeof(*new_cfg)); spin_lock(device_list_lock); list_for_each_entry_safe(node, tmp, device_list, list) { /* 原子替换配置指针 */ old_cfg node-data; rcu_assign_pointer(node-data, cfg_copy); } spin_unlock(device_list_lock); /* 同步等待: 确保所有 ISR 中的读者已释放旧的 old_cfg 引用 */ synchronize_rcu(); /* 安全释放: 此时已无读者持有 old_cfg */ vfree(old_cfg); return 0; }四、边界分析与架构权衡4.1synchronize_rcu的延迟上限synchronize_rcu()的返回时间取决于所有 CPU 都经历静止状态的最长时间。在没有CONFIG_PREEMPT的内核中单个 RCU 宽限期通常为 10-30ms但在CONFIG_PREEMPT_RT实时内核中如果某高优先级实时任务长期持有 CPU 且不调用schedule()宽限期可能延长至数秒。在驱动中调用synchronize_rcu时必须评估最大的阻塞延迟是否可接受。4.2 内存积压与call_rcu回调积压如果写路径使用call_rcu的高频场景如每秒更新 10000 次每个延迟释放的节点在宽限期期间占用内存。当宽限期延迟时如高优先级任务占用 CPU内存积压可能消耗大量 RAM。解决方案使用kfree_rcu的单参数版本Linux 5.10 内置批量回收或结合rcu_barrier在上层做限流。4.3 RCU 保护的复杂数据结构RCU 最适用于查询远多于更新的只读数据结构。对于 B 树、哈希表这类需要多步指针更新的复杂结构必须确保所有指针更新都通过rcu_assign_pointer且所有解引用都通过rcu_dereference。漏掉任何一处都会导致内存排序问题——在 ARM 的弱内存模型下尤为危险。4.4 适用与禁用场景适用ISR 高频读取链表/哈希表的驱动、路由表查找、文件系统的目录项缓存dcache。禁用读-写比例接近 1:1 的场景此时 spinlock 开销低于 RCU 的复制延迟释放成本、需要用synchronize_rcu但无法容忍 10ms 阻塞的实时路径。五、总结本文从 ISR 延迟超标这一驱动开发中的典型问题出发详细分析了 RCU 免锁读取的三阶段语义及其在 Linux 驱动层的最佳实践。RCU 通过移除→等待宽限期→回收三阶段实现免锁读取读者完全不受锁影响写者承担更新和延迟释放的成本。宽限期由每个 CPU 的静止状态决定tick 中断、上下文切换或显式rcu_read_unlock均在标记静止状态。call_rcu适用于高频更新的异步回收synchronize_rcu适用于批量更新的同步等待——选择取决于写路径的延迟容忍度。ARM 弱内存模型要求rcu_assign_pointer和rcu_dereference成对出现两者隐含完整的内存屏障防止编译器和 CPU 内存重排序。在高频更新的场景中kfree_rcu结合内核批量回收机制能有效控制内存积压同时通过CONFIG_RCU_CPU_STALL_TIMEOUT监控宽限期延迟。