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>
This commit is contained in:
Executable
+127
@@ -0,0 +1,127 @@
|
||||
# Phase 0 · Day 1: PCIe 总线拓扑与 Config Space
|
||||
|
||||
---
|
||||
|
||||
## 一、BDF 地址:每个设备的唯一身份
|
||||
|
||||
```
|
||||
0000 : 02 : 01 . 0
|
||||
│ │ │ └── Function (0~7) — 一个物理芯片可以有多个功能
|
||||
│ │ └────── Device (0~31) — 同一总线上最多 32 个设备
|
||||
│ └─────────── Bus (0~255) — 256 条总线
|
||||
└───────────────── Domain (0~65535) — 多 CPU socket 系统有多个 domain
|
||||
|
||||
单 CPU 系统 domain=0000。多路服务器每个 socket 一个 domain。
|
||||
```
|
||||
|
||||
DPDK 通过 BDF 地址接管网卡:`dpdk-devbind.py -b vfio-pci 02:01.0`
|
||||
|
||||
### 为什么需要三级?
|
||||
|
||||
PCI 是树形拓扑。每个 Bridge 引入一条新 bus。256 条 bus × 32 设备 × 8 功能 = 65536 个可寻址功能。如果没有 bus 层级,只靠 5-bit device 最多 32 个设备——一张显卡+一张网卡就满了。
|
||||
|
||||
## 二、拓扑中的四种角色
|
||||
|
||||
| 角色 | 你的实例 | 职责 |
|
||||
|------|---------|------|
|
||||
| **Root Complex** | `00:00.0` | CPU 和 PCIe 世界的桥梁,把 load/store 翻译成 TLP |
|
||||
| **PCI Bridge** | `00:11.0` → Bus 02 | 传统 PCI 桥,用 subtractive decode 转发未认领的访问 |
|
||||
| **PCIe Root Port** | `00:15.0` → Bus 03 | PCIe 原生桥,点对点链路,支持更高带宽 |
|
||||
| **Endpoint** | `02:01.0` `03:00.0` | 叶子节点,实际的网卡设备 |
|
||||
|
||||
关键差异:`02:01.0` (e1000) 是挂在传统 PCI Bridge 后面的老式设备,多条设备共享 Bus 02。`03:00.0` (VMXNET3) 是 PCIe Endpoint,独享 Bus 03 的整条链路(5GT/s × x32)。
|
||||
|
||||
## 二、PCIe 拓扑
|
||||
|
||||
```
|
||||
[0000:00] ← Root Complex (Host Bridge)
|
||||
│ CPU 访问 PCIe 设备的唯一入口
|
||||
│
|
||||
├── 11.0 → [02] ← PCI Bridge (老式总线)
|
||||
│ ├── 01.0 e1000 (Endpoint)
|
||||
│ └── 04.0 e1000 (Endpoint)
|
||||
│
|
||||
└── 15.0 → [03] ← PCIe Root Port (真正的 PCI Express)
|
||||
└── 00.0 VMXNET3 (Endpoint)
|
||||
```
|
||||
|
||||
数据流:CPU → Root Complex → TLP 封装 → PCIe 链路 → Endpoint。
|
||||
|
||||
## 三、Config Space 完整解码
|
||||
|
||||
e1000 (02:01.0) 的 256 字节 Config Space,逐字段解码:
|
||||
|
||||
| Offset | 字段 | Hex 值 | 含义 |
|
||||
|--------|------|--------|------|
|
||||
| 0x00-01 | Vendor ID | 0x8086 | Intel |
|
||||
| 0x02-03 | Device ID | 0x100f | 82545EM |
|
||||
| 0x04-05 | Command | 0x0113 | IO+Mem enable, BusMaster OFF |
|
||||
| 0x0a-0b | Class Code | 0x020000 | Network Controller |
|
||||
| 0x0e | Header Type | 0x00 | Endpoint |
|
||||
| 0x10-13 | BAR0 | 0xfd5c0004 | Memory@0xfd5c0000, 64bit, 128KB, Non-Prefetchable |
|
||||
| 0x18-1b | BAR2 | 0xfdff0004 | Memory@0xfdff0000, 64KB |
|
||||
| 0x20-23 | BAR4 | 0x00002001 | IO Port@0x2000 |
|
||||
| 0x34 | Capability Ptr | 0xdc | 链表起点 |
|
||||
| 0x3c | Interrupt Line | 0x07 | IRQ 7 |
|
||||
| 0x3d | Interrupt Pin | 0x01 | INTA# |
|
||||
|
||||
BAR flag bit 解码: bit0=Memory/IO, bit[2:1]=32b/64b, bit3=Prefetchable。`0xfd5c0004` 中的 `4` = 0100₂ → 64-bit Memory, Non-Prefetchable。真实基址 = 0xfd5c0004 & ~0xF = 0xfd5c0000。
|
||||
|
||||
Capability 链表: 0xdc→PM(ID=1)→0xe4→PCI-X(ID=7)→0x00(End)。
|
||||
|
||||
## 四、VMXNET3 对比 (真正的 PCIe 设备)
|
||||
|
||||
| | e1000 | VMXNET3 |
|
||||
|---|---|---|
|
||||
| 总线 | PCI-X (老) | PCIe v2.0 |
|
||||
| Capability | PM + PCI-X | PM + PCIe Express + MSI + MSI-X + Serial# |
|
||||
| MSI-X | 无 | 25 向量, BAR2 offset 0, PBA offset 0x1000 |
|
||||
|
||||
## 五、实战踩坑
|
||||
|
||||
### 坑 1: Bus Master 没开
|
||||
- **现象**: DPDK 启动后一个包都收不到
|
||||
- **根因**: Command bit2=0 → 网卡不能 DMA
|
||||
- **定位**: `lspci -vvv | grep "BusMaster"`
|
||||
- **修复**: `pci_set_master(pdev)` 或 `setpci -s XX:XX.X COMMAND=0x117`
|
||||
|
||||
### 坑 2: 64-bit BAR 误读
|
||||
- **现象**: 物理地址 >4GB 时 MMIO 地址错误
|
||||
- **根因**: 只用 uint32_t 读 BAR0 低 32 位
|
||||
- **修复**: 用 `resource_size_t pci_resource_start(pdev, 0)`
|
||||
|
||||
### 坑 3: MSI-X 向量不够
|
||||
- **现象**: 多队列网卡性能不增长
|
||||
- **根因**: pci_alloc_irq_vectors 返回数 < RX 队列数
|
||||
- **定位**: `cat /proc/interrupts | grep PCI-MSI | wc -l`
|
||||
- **修复**: 增大内核 MSI-X 限制或 BIOS 调整
|
||||
|
||||
### 坑 4: PCIe 链路降速
|
||||
- **现象**: 100Gbps 网卡只能跑 25Gbps
|
||||
- **定位**: `lspci -vvv | grep -E "LnkCap:|LnkSta:"` → Speed 2.5GT/s
|
||||
- **修复**: 检查物理槽位宽度、PCIe 延长线、BIOS 设置
|
||||
|
||||
### 坑 5: AER 静默数据损坏
|
||||
- **现象**: 偶尔出现 mbuf 数据乱码但不 crash
|
||||
- **定位**: `dmesg | grep AER` → Uncorrectable Error
|
||||
- **修复**: 必须换硬件,链路已不可靠
|
||||
|
||||
## 六、核心认知
|
||||
|
||||
CPU 操作网卡的全路径:
|
||||
|
||||
```
|
||||
读 Config Space → 匹配 Vendor/Device ID → 确定驱动
|
||||
→ pci_enable_device → ioremap(BAR0) → 拿到虚拟地址
|
||||
→ ioread32/iowrite32 操作寄存器 → 网卡开始工作
|
||||
```
|
||||
|
||||
BAR 是 MMIO 的入口,Config Space 是身份证,Capability 链表是扩展机制。
|
||||
|
||||
## 七、检验标准
|
||||
- [ ] 能解释 BDF 地址含义
|
||||
- [ ] 能手写 Config Space 前 16 字节字段
|
||||
- [ ] 能从 hex 值解码 BAR 的基址和大小
|
||||
- [ ] 能画 Capability 链表结构
|
||||
- [ ] 能对比 e1000(PCI-X) vs VMXNET3(PCIe v2)
|
||||
- [ ] 能解释为什么 Bus Master bit 不置位就没 DMA
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
# Phase 0 · Day 2: 内核中断 + 网络收包路径
|
||||
|
||||
---
|
||||
|
||||
## 一、你的系统数据
|
||||
|
||||
```
|
||||
IRQ 56 (ens160-rxtx-0): CPU0:1329万 CPU1:191万 CPU2:112万 CPU3:676万
|
||||
IRQ 57 (ens160-rxtx-1): CPU0:11万 CPU1:33万 CPU2:36万 CPU3:38万
|
||||
IRQ 58 (ens160-rxtx-2): CPU0:19万 CPU1:37万 CPU2:43万 CPU3:42万
|
||||
IRQ 59 (ens160-rxtx-3): CPU0:30万 CPU1:39万 CPU2:52万 CPU3:52万
|
||||
IRQ 60 (ens160-event): 全 0 (事件队列, 未使用)
|
||||
|
||||
NET_RX 软中断: CPU0:1339万 CPU1:436万 CPU2:381万 CPU3:791万
|
||||
ksoftirqd: 每个 CPU 一个 (PID 16/24/30/36)
|
||||
```
|
||||
|
||||
- 4 个 MSI-X 向量 → 4 个 RX 队列 → 4 个 CPU 并行收包
|
||||
- IRQ 56 在 CPU0 上触发了 1329 万次(主流量),CPU1-3 只触发了少量——IRQ affinity 配置不均
|
||||
|
||||
## 二、收包 6 道关卡
|
||||
|
||||
```
|
||||
关卡1: 硬件中断
|
||||
网卡 DMA 写 RX ring → MSI-X 中断 → APIC → CPU → IDT → irq_handler
|
||||
此时在硬中断上下文,所有中断被屏蔽
|
||||
|
||||
关卡2: NAPI 调度
|
||||
irq_handler → napi_schedule → 关网卡中断 + 触发 NET_RX_SOFTIRQ → 返回
|
||||
源码: net/core/dev.c ____napi_schedule (line 4551)
|
||||
|
||||
关卡3: ksoftirqd
|
||||
ksoftirqd 被唤醒 → __do_softirq → net_rx_action → 遍历 poll_list
|
||||
源码: kernel/softirq.c + net/core/dev.c
|
||||
|
||||
关卡4: 驱动 poll → netif_receive_skb
|
||||
从 RX descriptor ring 取包 → alloc_skb → memcpy → netif_receive_skb(skb)
|
||||
源码: net/core/dev.c netif_receive_skb (line 5899)
|
||||
|
||||
关卡5: 协议分发 (L2→L3)
|
||||
__netif_receive_skb_core → skb->protocol → 查 ptype_base 哈希表 → ip_rcv()
|
||||
源码: net/core/dev.c __netif_receive_skb_core (line 5434)
|
||||
net/ipv4/ip_input.c ip_rcv (line 560)
|
||||
协议注册: net/ipv4/af_inet.c dev_add_pack (line 2065)
|
||||
|
||||
关卡6: TCP 收包 (L3→L4→socket)
|
||||
ip_rcv → NF_HOOK → ip_local_deliver → tcp_v4_rcv
|
||||
→ __inet_lookup_established → tcp_rcv_established (fast path!)
|
||||
→ tcp_queue_rcv → sk_data_ready → wake_up → epoll 返回
|
||||
源码: net/ipv4/tcp_ipv4.c (line 1999)
|
||||
net/ipv4/tcp_input.c tcp_rcv_established
|
||||
```
|
||||
|
||||
## 三、DPDK 为什么快
|
||||
|
||||
| 内核步骤 | 耗时 | DPDK |
|
||||
|---------|------|------|
|
||||
| MSI-X → IDT → handler | ~3us | PMD 轮询, 无中断 |
|
||||
| napi_schedule + ksoftirqd 调度 | ~5us | PMD 线程永不睡眠 |
|
||||
| alloc_skb (slab 锁) | ~1us | mbuf 从 per-lcore cache 弹出 |
|
||||
| memcpy DMA→skb | ~2us | DMA 直接写 mbuf, 零拷贝 |
|
||||
| ptype_base 查找 | ~0.3us | 应用自己解析协议 |
|
||||
| Netfilter HOOK | ~5us | 不需要 |
|
||||
| ehash 查 socket | ~1us | rte_hash_lookup |
|
||||
| copy_to_user | ~3us | 零拷贝 |
|
||||
| **总计** | **~20us** | **~1us** |
|
||||
|
||||
## 四、实战踩坑
|
||||
|
||||
### 坑 1: 中断全绑 CPU0
|
||||
- **现象**: top 看 CPU0 si=99%, 其他 CPU 空闲
|
||||
- **根因**: smp_affinity 默认只开 CPU0
|
||||
- **定位**: `cat /proc/irq/56/smp_affinity`
|
||||
- **修复**: `echo ff > /proc/irq/56/smp_affinity`
|
||||
|
||||
### 坑 2: NAPI budget 耗尽
|
||||
- **现象**: 100G 线速达不到,`ethtool -S` 看 rx_missed 增长
|
||||
- **根因**: net_rx_action 默认一次最多 300 包,收不够就返回
|
||||
- **定位**: `cat /proc/net/softnet_stat`, 第三列 = "times queue full"
|
||||
- **修复**: 增大 net.core.netdev_budget,或换 DPDK
|
||||
|
||||
### 坑 3: ksoftirqd 被抢占
|
||||
- **现象**: 收包延迟 P99 抖动严重
|
||||
- **根因**: ksoftirqd 不是实时线程,被磁盘 I/O 抢占
|
||||
- **定位**: `perf sched latency`
|
||||
- **修复**: `chrt -f 1 $(pidof ksoftirqd/0)` 或换 DPDK PMD
|
||||
|
||||
### 坑 4: Netfilter 静默丢包
|
||||
- **现象**: tcpdump 能看到包,应用收不到
|
||||
- **根因**: Docker/k8s 自动注入 iptables drop 规则
|
||||
- **定位**: `iptables -L -v -n`, 看每条规则的计数器
|
||||
|
||||
### 坑 5: TCP backlog 满
|
||||
- **现象**: 客户端 connect 超时
|
||||
- **根因**: accept queue=128, 第 129 个 SYN 被内核静默丢弃
|
||||
- **定位**: `netstat -s | grep overflow`
|
||||
- **修复**: `echo 4096 > /proc/sys/net/core/somaxconn`
|
||||
|
||||
## 五、检验标准
|
||||
- [ ] 能画中断→NAPI→ksoftirqd→poll→netif_receive_skb→ip_rcv→tcp_v4_rcv 完整调用链
|
||||
- [ ] 能解释 NAPI 为什么要关中断、用软中断
|
||||
- [ ] 能说 ptype_base 的作用
|
||||
- [ ] 能列 DPDK 比内核快 ≥5 个原因
|
||||
- [ ] 能在 /proc/interrupts 和 /proc/softirqs 找到你的网卡
|
||||
Executable
+147
@@ -0,0 +1,147 @@
|
||||
# Phase 0 · Day 3: 内核内存管理 — buddy → slab → slub
|
||||
|
||||
---
|
||||
|
||||
## 一、你的系统数据
|
||||
|
||||
```
|
||||
Buddy (/proc/buddyinfo, Node 0 DMA32):
|
||||
order 0(4KB)=2275 1(8KB)=2107 2(16KB)=2287 3(32KB)=2270
|
||||
4(64KB)=1540 5(128KB)=992 6(256KB)=558 7(512KB)=215
|
||||
8(1MB)=35 9(2MB)=7 10(4MB)=0 ← 连续大块已严重碎片化
|
||||
|
||||
Slab (/proc/slabinfo):
|
||||
skbuff_head_cache: 4411 active 256B/obj 16 objs/slab
|
||||
skbuff_small_head: 345 active 704B/obj 23 objs/slab
|
||||
sock_inode_cache: 896 active 832B/obj
|
||||
kmalloc-cg-2k: 627 active 2048B/obj
|
||||
kmalloc-cg-4k: 222 active 4096B/obj
|
||||
```
|
||||
|
||||
## 二、三层架构
|
||||
|
||||
```
|
||||
kmalloc(1536)
|
||||
↓
|
||||
┌─ Slub (对象分配器) ─────────────────────┐
|
||||
│ per-CPU cache (kmem_cache_cpu) │
|
||||
│ freelist: obj0→obj3→obj7→∅ │
|
||||
│ 分配 = 弹 head, O(1), 无锁 (fastpath) │
|
||||
│ 释放 = 插 head, O(1), 无锁 │
|
||||
│ ↓ freelist 空 │
|
||||
│ partial slab → 切对象 → 填 freelist │
|
||||
│ ↓ partial 也空 │
|
||||
└────────────┬────────────────────────────┘
|
||||
↓ 需要新 slab page
|
||||
┌─ Buddy System (页分配器) ────────────────┐
|
||||
│ order 0(4KB) → order 10(4MB) │
|
||||
│ 分配: 找 order n → 没有? 分裂 n+1 │
|
||||
│ 释放: 合井相邻 buddy │
|
||||
│ 碎片: 长期运行 order 高位全 0 │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 三、Buddy System
|
||||
|
||||
两个页块互为 buddy: 大小相同 + 物理相邻 + 来自同一父块分裂。
|
||||
|
||||
```
|
||||
16KB 块 (order 2, page 0-3):
|
||||
[p0][p1][p2][p3]
|
||||
└buddy┘ └buddy┘
|
||||
|
||||
释放 p0 时: p1 空闲? → 合并成 8KB (order 1)
|
||||
[p0,p1] 和 [p2,p3] 都空闲? → 再合并成 16KB (order 2)
|
||||
```
|
||||
|
||||
/proc/buddyinfo 就是各级 order 的空闲块计数。你的系统 order 9 只剩 7 个 2MB 块 → 碎片严重。
|
||||
|
||||
## 四、Slub: per-CPU 无锁分配
|
||||
|
||||
```c
|
||||
// include/linux/slub_def.h:50
|
||||
struct kmem_cache_cpu {
|
||||
void **freelist; // 本 CPU 空闲对象链表
|
||||
unsigned long tid; // 事务 ID
|
||||
struct slab *slab; // 当前 slab
|
||||
struct slab *partial; // 部分使用的 slabs
|
||||
};
|
||||
```
|
||||
|
||||
### 分配路径
|
||||
|
||||
```
|
||||
slab_alloc(s, gfp, node)
|
||||
→ c = per_cpu_ptr(s->cpu_slab, cpu) ← 本 CPU 的 cache
|
||||
→ object = c->freelist ← 取 freelist head
|
||||
→ if (object) { ← FAST PATH (>99%)
|
||||
c->freelist = *(void**)object; ← 更新 freelist
|
||||
return object; ← done, 无锁!
|
||||
}
|
||||
→ ___slab_alloc(s, gfp, node, c) ← SLOW PATH
|
||||
→ 1. 取 partial slab → 切对象
|
||||
→ 2. partial 空 → buddy 申请新页 → 切 16 个对象
|
||||
→ 3. 1 个返回, 15 个填 freelist
|
||||
```
|
||||
|
||||
### freelist 嵌入空闲对象内部
|
||||
|
||||
```
|
||||
一个 slab page (4KB):
|
||||
┌──────┬──────┬──────┬...┬──────┐
|
||||
│ obj0 │ obj1 │ obj2 │ │obj15 │ 16个 256B 对象
|
||||
└──────┴──────┴──────┴...┴──────┘
|
||||
|
||||
空闲对象 obj0 的前 8 字节 = &obj3
|
||||
空闲对象 obj3 的前 8 字节 = &obj7
|
||||
obj7 的前 8 字节 = NULL
|
||||
|
||||
分配: head = freelist; freelist = *(void**)head; return head;
|
||||
释放: *(void**)obj = freelist; freelist = obj;
|
||||
```
|
||||
|
||||
## 五、内核 slub vs DPDK mempool
|
||||
|
||||
| | 内核 slub | DPDK mempool |
|
||||
|---|---|---|
|
||||
| **L1 cache** | per-CPU freelist | per-lcore cache |
|
||||
| **L1 操作** | 弹 freelist head, O(1) | 弹 cache->objs[--len], O(1) |
|
||||
| **L2 补货** | partial slab → buddy 新页 | rte_ring → driver dequeue |
|
||||
| **L3 兜底** | buddy alloc → OOM | 驱动层 |
|
||||
| **锁** | 无锁 (per-CPU) | 无锁 (per-lcore + ring CAS) |
|
||||
| **预分配** | 按需从 buddy 取 | hugepage 一次性分配 |
|
||||
| **碎片** | 长期运行 buddy 碎片 | 不会,固定大小,一次分配 |
|
||||
|
||||
## 六、实战踩坑
|
||||
|
||||
### 坑 1: 跨 NUMA 分配延迟 3x
|
||||
- **现象**: 双 socket 服务器上性能波动严重
|
||||
- **根因**: CPU0 分配了 socket1 的内存 → 远程访问
|
||||
- **修复**: `rte_mempool_create(socket_id=rte_socket_id())`
|
||||
|
||||
### 坑 2: buddy 碎片 → 大块分配失败
|
||||
- **现象**: dmesg "page allocation failure: order:4"
|
||||
- **根因**: 频繁 alloc/free skb 拆散所有连续大块
|
||||
- **修复**: DPDK 用 hugepage 预分配
|
||||
|
||||
### 坑 3: skb 泄漏
|
||||
- **现象**: /proc/slabinfo skb active_objs 只增不减
|
||||
- **根因**: netfilter drop 未调 kfree_skb
|
||||
- **定位**: `bpftrace -e 'k:alloc_skb { @a=kstack } k:kfree_skb { @f=kstack }'`
|
||||
|
||||
### 坑 4: GFP_ATOMIC 在中断上下文耗光
|
||||
- **现象**: 高吞吐下 alloc_skb 返回 NULL
|
||||
- **根因**: GFP_ATOMIC 不触发页面回收,分配池浅
|
||||
- **修复**: 预分配内存池,或在进程上下文用 GFP_KERNEL
|
||||
|
||||
### 坑 5: per-CPU cache false sharing
|
||||
- **现象**: 多核并发分配性能不线性增长
|
||||
- **根因**: s->cpu_slab[0] 和 s->cpu_slab[1] 在同一 cache line
|
||||
- **修复**: `____cacheline_aligned_in_smp` 或 DPDK `__rte_cache_aligned`
|
||||
|
||||
## 七、检验标准
|
||||
- [ ] 能解释 buddy 分裂/合并 + 会读 /proc/buddyinfo
|
||||
- [ ] 能画 slub 三级分配: freelist → partial slab → buddy
|
||||
- [ ] 能讲 kmem_cache_cpu 如何无锁 + 和 DPDK mempool 对照
|
||||
- [ ] 5 个坑都知道怎么定位
|
||||
- [ ] 能在 /proc/slabinfo 找到 skb cache 并解读字段
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
# 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 个数据无丢失
|
||||
Reference in New Issue
Block a user