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

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

三个陷阱

  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

检验

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 个数据无丢失