Files
dpdk-blog/notes/phase0/day04-rcu-lockfree.md
T
newbieandClaude 6d8c994dde init: DPDK → DPU learning blog (VitePress)
- 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>
2026-08-09 15:42:47 +00:00

136 lines
4.8 KiB
Markdown
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
```
### 三个陷阱
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 个数据无丢失