# Phase 0 · Day 4: RCU 无锁机制 + 手写 MPSC 队列 --- ## 一、RCU 核心思想 ### 1.1 rwlock 为什么不行 在多核系统中,传统 rwlock 的问题是:即使 100% 都是读操作,每个 CPU 的 `read_lock` 都要原子修改同一个 cache line → 32 核时该 cache line 在 32 个核之间乒乓(cache line bouncing)→ 性能随核数退化。 ### 1.2 RCU 的做法 ``` 读者 (数据面): 写者 (控制面): rcu_read_lock() copy data → modify copy → preempt_disable() rcu_assign_pointer(ptr, new) (非 PREEMPT_RCU 内核) synchronize_rcu() ← 等所有老读者退出 ptr = rcu_dereference(gp) kfree(old) → READ_ONCE + lockdep use(ptr) ← 无锁! 纯读! rcu_read_unlock() → preempt_enable() ``` 核心权衡: **用延迟释放换取读者零开销**。 ### 1.3 核心 API | API | 作用 | 场景 | |-----|------|------| | `rcu_read_lock/unlock` | 标记读临界区 (非PREEMPT下=preempt_disable) | 读者遍历 | | `rcu_dereference(p)` | READ_ONCE + lockdep检查 | **必须用!** 裸读=bug | | `rcu_assign_pointer(p,v)` | smp_store_release 发布新指针 | 写者更新 | | `synchronize_rcu()` | 阻塞等 grace period 结束 | 模块卸载 (不能中断上下文) | | `call_rcu(head, func)` | 异步等 GP → 调回调 | 热路径释放 (中断安全) | ### 1.4 Grace Period 机制 每个 CPU 经历一次 quiescent state (上下文切换或退出 rcu_read_lock) → 所有 CPU 都经历 → GP 结束 → 旧数据安全释放。 --- ## 二、RCU 在 DPDK 中的体现 | RCU 概念 | DPDK rte_ring 对应 | |----------|-------------------| | 读者不阻塞 | consumer 读 `prod.tail` 纯读, 无锁 | | rcu_dereference (READ_ONCE) | `*old_head = r->prod.head` + smp_rmb() | | rcu_assign_pointer (release) | CAS release/acquire 语义 | | Grace Period | head/tail 追赶: tail 更新前不覆盖旧数据 | | 延迟回收 | per-lcore cache flush → bulk 操作分摊开销 | --- ## 三、实战踩坑 (RCU) ### 坑 1: Grace Period 过长 → OOM - **现象**: kmalloc + free 循环中 call_rcu 积压, 内存耗尽 - **根因**: 长期 rcu_read_lock / NO_HZ idle CPU / CONFIG_RCU_LAZY - **定位**: `cat /sys/kernel/debug/rcu/rcu_preempt | grep cblist` - **修复**: `call_rcu_hurry()` 或 `echo 1 > /sys/kernel/rcu_expedited` ### 坑 2: 漏掉 rcu_dereference → use-after-free - **现象**: 偶发性 crash, KASAN 报告 use-after-free - **根因**: 编译器优化导致指针被多次读取, 第二次读时已被释放 - **定位**: sparse: `make C=2`, KASAN: `dmesg | grep KASAN` - **修复**: 永远用 `rcu_dereference(p)` 而不是裸读 `p` ### 坑 3: rcu_read_lock 内睡眠 - **现象**: 内核 splat: "scheduling while atomic" - **根因**: 非 PREEMPT_RCU 下 rcu_read_lock 禁止抢占, 不能睡眠 - **修复**: 用 SRCU (sleepable RCU) 或把睡眠移到临界区外 ### 坑 4: synchronize_rcu 在中断上下文调用 - **现象**: 内核 panic - **根因**: synchronize_rcu 会睡眠, 中断上下文不可 - **修复**: 用 call_rcu 异步 ### 坑 5: list_for_each_entry_rcu 遍历时写者并发删除 - **现象**: 读者遍历到已删除节点 → crash - **根因**: 写者 list_del_rcu 后立即 kfree (没等 GP) - **修复**: 写者必须在 synchronize_rcu/call_rcu 之后才能释放 --- ## 四、编码题: MPSC 无锁队列 ### 需求 ``` 题目: 用 C11 atomic 实现 MPSC 无锁队列 硬性要求: 1. 固定大小环形 buffer (大小 2 的幂) 2. enqueue: 多线程安全 (CAS 抢 head) 3. dequeue: 单线程 (不需要原子操作) 4. 2 producer + 1 consumer, 各入队 1000000 次, 总数 = 2000000 核心算法 (两阶段提交): enqueue: ① CAS 抢 head slot ② 写入数据 ③ 通知 consumer (方案A: spin等tail→更新; 方案B: consumer自己检查head) 推荐方案B (consumer更新tail): producer只CAS head+写数据→结束, 无spin 参考源码: - rte_ring CAS: source/dpdk-stable-24.11.6/lib/ring/rte_ring_c11_pvt.h:60-128 - rte_ring 结构体: source/dpdk-stable-24.11.6/lib/ring/rte_ring_core.h:116-145 ``` ### 三个陷阱 1. **ABA**: head 只增不减 (靠32-bit溢出) → 不会有 ABA, 但如果你让 head 回退就会 2. **内存屏障位置**: 数据写入必须在 CAS 之前 (CAS release 保证可见) 3. **False sharing**: head 和 tail 放不同 cache line (`alignas(64)`) ### 代码框架 文件: `code/Phase0/mpsc-queue/mpsc_queue.c` ### 检验 ```bash gcc -std=c11 -pthread -O2 -Wall -Wextra -o mpsc_test mpsc_queue.c ./mpsc_test # PASS: consumed = 2000000 (expected 2000000) ``` ## 五、检验标准 - [ ] 能解释 RCU 的核心思想 (读者零开销) - [ ] 能说出 rcu_dereference 为什么必须用 (READ_ONCE + lockdep) - [ ] 能解释 grace period 机制 - [ ] 能对比 RCU 和 DPDK rte_ring 的设计共同点 - [ ] MPSC 队列: 2 producer + 1 consumer 跑通, 2000000 个数据无丢失