- VitePress static site for rendering all markdown documents - Auto-discovers .md files from dpdk-roadmap-v3 and dpdk-study-kb - Features: full-text search, dark mode, responsive design - Pages: roadmap, notes (Phase 0-3), product docs, knowledge base Co-Authored-By: Claude <noreply@anthropic.com>
4.8 KiB
Executable File
4.8 KiB
Executable File
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
三个陷阱
- ABA: head 只增不减 (靠32-bit溢出) → 不会有 ABA, 但如果你让 head 回退就会
- 内存屏障位置: 数据写入必须在 CAS 之前 (CAS release 保证可见)
- False sharing: head 和 tail 放不同 cache line (
alignas(64))
代码框架
文件: code/Phase0/mpsc-queue/mpsc_queue.c
检验
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 个数据无丢失