首页
关于
Search
1
PacketForge 博客部署文档
16 阅读
2
深入 SONiC 与 SAI:从命令行到芯片的完整数据之旅
14 阅读
3
DPU 与 CPO:AI 时代的网络基础设施革命
11 阅读
4
Broadcom芯片
10 阅读
5
DPDK高性能网络
10 阅读
默认分类
交换芯片
高性能网络
AI算力网络
WebSocket高性能项目
DPDK 成长路线
· ①性能工程基础
①性能工程基础
②DPDK 源码深读
③用户态协议栈
④软件交换与 SONiC
⑤PCIe、虚拟化与 DPU
⑥面试与复盘
登录
Search
标签搜索
DPDK→DPU 学习路线
P0
P1
P2
P3
P5
PCIe
P4
P6
libmtcp
NUMA
mTCP
OVS
SONiC
SAI
DPU
perf
gdb
EAL
mempool
PacketForge
累计撰写
53
篇文章
累计收到
3
条评论
首页
栏目
页面
关于
DPDK 成长路线 /
①性能工程基础
共
6
篇
最新文章
热门文章
评论最多
点赞最多
2026-08-18
P0-01 · 从 cache line 说起:CPU 缓存怎么坑你
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:无(系列第 1 篇)| 下一篇:P0-02 NUMA一句话定位CPU 缓存以 64 字节的 cache line 为最小单位在核间保持一致。两个线程各写一个"互不相干"的变量,只要它们落在同一行,就会互相把对方踢出缓存——false sharing。DPDK 所有数据结构(mbuf、ring、per-lcore 变量)的布局纪律,都从这条开始。真实源码精读先看实验的结构体定义(source/dpdk-labs/01-cache/false_sharing.c):/* 两个计数器在同一 cache line */ struct normal { volatile uint64_t counter_a; volatile uint64_t counter_b; } __attribute__((aligned(64))); // false_sharing.c:20-23 /* 两个计数器被填充隔开,在不同 cache line */ struct padded { volatile uint64_t counter_a; uint8_t padding[CACHE_LINE_SIZE - sizeof(uint64_t)]; // 56 字节填充 volatile uint64_t counter_b; } __attribute__((aligned(128))); // false_sharing.c:26-30两个 worker 各写各的计数器(false_sharing.c:41-53):void *worker_a_normal(void *arg) { (void)arg; for (int i = 0; i < ITERATIONS; i++) normal_data.counter_a++; return NULL; } void *worker_b_normal(void *arg) { (void)arg; for (int i = 0; i < ITERATIONS; i++) normal_data.counter_b++; return NULL; }写 counter_a 会让整行进入 Modified 状态,B 核再写 counter_b 就必须先使 A 的行失效、回内存重取——两个线程在 MESI 协议层乒乓。padded 版本各占一行,互不干扰。源码开头的注释写得很直白(false_sharing.c:5):单线程版 cache_line.c 只是热身,"两个变量在同一线程访问无竞争",多线程版才是真测量。内核的缓存对齐工具箱(include/linux/cache.h):#ifndef SMP_CACHE_BYTES #define SMP_CACHE_BYTES L1_CACHE_BYTES // cache.h:13 #endif #ifndef ____cacheline_aligned #define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES))) // cache.h:66-68 #endif #ifndef __cacheline_aligned #define __cacheline_aligned \ __attribute__((__aligned__(SMP_CACHE_BYTES), \ __section__(".data..cacheline_aligned"))) // cache.h:78-82 #endif内核不仅对齐,还把这些变量放进独立 section(.data..cacheline_aligned),让链接器把它们聚在一起——同样是对齐,内核的做法比实验里手写 aligned() 多一层工程考量。SLUB 的 per-CPU 设计(mm/slub.c)——内核分配器为什么快: * SLUB assigns one slab for allocation to each processor. * Allocations only occur from these slabs called cpu slabs. // slub.c:135-136每个 CPU 有一个"当前 slab",从上面取对象不需要任何锁(slub.c:117-122 的 lockless fastpath 注释);cpu partial 列表的注释说得更直接(slub.c:4362-4368):局部 partial slab 让再分配"no locking will be required"。这就是 DPDK mempool per-lcore cache 的内核原型。DPDK 的应用:lib/eal/include/rte_common.h:627 定义 __rte_cache_aligned;lib/mbuf/rte_mbuf_core.h:467 的 struct __rte_cache_aligned rte_mbuf 整个结构体按 cache line 对齐,且第一个 cache line 只放热路径字段(收包时由 descriptor 一次性写入,rte_mbuf_core.h:488 注释 "next 8 bytes are initialised on RX descriptor rearm"),慢路径字段放第二行(rte_mbuf_core.h:606 注释 "second cache line - fields only used in slow path or on TX")。实验怎么跑cd source/dpdk-labs/01-cache gcc -O2 -o false_sharing false_sharing.c -lpthread ./false_sharing # 对比 [Same CacheLine] 与 [Padded CacheLine] 的 cycles/op perf stat -e cache-misses,cache-references,L1-dcache-load-misses ./false_sharing pahole -C normal false_sharing # 看结构体真实排布(README 01-cache:29-30)预期:padded 版本明显更快,perf 里 padded 的 L1-dcache-load-misses 显著低于 normal。注意 README 01-cache 的提醒:单 socket 环境差异可能小,双 socket 差异巨大——这也埋下了 P0-02 NUMA 的伏笔。🐧 内核对照DPDK内核关键差异__rte_cache_aligned(rte_common.h:627)__cacheline_aligned(cache.h:78-82)内核额外放进独立 sectionmbuf 双 cache line(rte_mbuf_core.h:467/606)sk_buff 无此纪律DPDK 是手调热路径,内核靠通用性mempool per-lcore cacheSLUB per-CPU slab + cpu_partial(slub.c:135-136)同一套"先吃本地、再上共享"哲学RTE_CACHE_GUARD(ring 里隔开 prod/cons)____cacheline_aligned_in_smp(cache.h:70-76)内核宏在 UP 下退化为空面试 3 问false sharing 怎么发生、怎么证明? → 两变量同行(false_sharing.c:20-23),MESI 下互相 invalidate;用多线程版对比 cycles/op 和 perf stat 的 cache-misses 证明(false_sharing.c:5 注释解释了为什么单线程版测不出)。内核怎么防止结构体成员互相拖累? → ____cacheline_aligned(cache.h:66-68)按 SMP_CACHE_BYTES 对齐;__cacheline_aligned 还放进 .data..cacheline_aligned section(cache.h:78-82)。为什么 mbuf 要拆两个 cache line? → 第一个 cache line 由 RX descriptor 一次写入、收包只碰它(rte_mbuf_core.h:488 的 rearm 注释);慢路径字段放第二行(rte_mbuf_core.h:606 注释),不污染热行。数据落点perf-data/P0/01-cache/:false_sharing.txt(两次 cycles/op 对比)、perf.stat.txt(cache-misses 实测)。
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P0-02 · NUMA 拓扑与跨 socket 的代价
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-01 cache line | 下一篇:P0-03 PCIe一句话定位多路服务器上内存是分片的:访问本 socket 内存约 80ns,跨 socket 走 QPI/UPI 约 130ns。DPDK 犯一次"内存分错了 node"的错,性能直接掉 40%+——所以 EAL 有 --socket-mem、mempool 创建要传 rte_socket_id()。真实源码精读实验(source/dpdk-labs/02-numa/numa_test.c)把数组钉在 node 0 上,然后用两种访存模式测延迟:#define ARRAY_SIZE (32 * 1024 * 1024) // 32M 元素 // numa_test.c:16 #define ITERATIONS 10000000 // numa_test.c:17 // ... if (numa_available() < 0) { printf("NUMA not available (VM/单socket环境正常)\n"); // numa_test.c:27-31 return 0; } // 在 node 0 上分配内存 long *array = numa_alloc_onnode(sizeof(long) * ARRAY_SIZE, 0); // numa_test.c:37顺序访问(cache friendly,numa_test.c:44-51)大部分命中缓存;随机访问(numa_test.c:53-62)步长 4096(一整页),每次访问必 cache miss,直接暴露 DRAM 真实延迟。这个设计很聪明:同一次实验里同时测到了"缓存的收益"和"内存的底价"。内核对照(mm/mempolicy.c)——用户态内存策略的三个决策点:static int policy_node(gfp_t gfp, struct mempolicy *policy, int nd) // mempolicy.c:1868 { if (policy->mode == MPOL_PREFERRED) { nd = first_node(policy->nodes); // mempolicy.c:1870-1871 } // ... 省略 BIND / PREFERRED_MANY 的 home_node 检查 (mempolicy.c:1881-1884) return nd; } /* Do dynamic interleaving for a process */ static unsigned interleave_nodes(struct mempolicy *policy) // mempolicy.c:1889 { unsigned next; struct task_struct *me = current; next = next_node_in(me->il_prev, policy->nodes); // mempolicy.c:1895-1897 if (next < MAX_NUMNODES) me->il_prev = next; return next; }缺页时 vma_alloc_folio() 查 VMA 的策略(mempolicy.c:2166-2186):MPOL_INTERLEAVE 分支用 interleave_nid() 轮转选 node 后调 alloc_page_interleave()(mempolicy.c:2113),最终落到 __alloc_pages(gfp, order, nid, NULL)(mempolicy.c:2118)。"进程级轮流分配"(interleave)与 DPDK 的"一次分配、永久钉死"是两种相反的哲学:内核追求均衡,DPDK 追求可预测。实验怎么跑lstopo --no-io # 可视化拓扑(README 02-numa:12) numactl --hardware # node 距离矩阵(README 02-numa:15) cd source/dpdk-labs/02-numa gcc -O2 -o numa_test numa_test.c -lnuma ./numa_test # Sequential vs Random 的 cycles/iter perf stat -e node-loads,node-load-misses,node-stores,node-store-misses ./numa_test预期:Random 访问的 cycles/iter 比 Sequential 高一个数量级(cache miss → DRAM 延迟);双 socket 机器上 node-load-misses 直接给出跨 node 访问比例。单 socket 环境 numa_available() 会 fallback(numa_test.c:27-31),实验照跑,只是测不出远程延迟——生产调优一定要上真双路机。🐧 内核对照DPDK内核关键差异rte_socket_id() 传给 mempool/ring 创建(README 02-numa:49-57)policy_node() 按 mempolicy 选 node(mempolicy.c:1868)DPDK 显式指定,内核按进程策略--socket-mem 1024,0(README 02-numa:60)alloc_page_interleave(mempolicy.c:2113)内核交错求均衡,DPDK 钉死求局部性mempool per-lcore cache=256(README 02-numa:65-71)SLUB per-CPU slab同一"先本地后共享"思路绑定后无迁移内核自动 page migrationDPDK 牺牲弹性换确定性面试 3 问为什么跨 socket 访问慢 60%? → 走 QPI/UPI 互连而非本地内存控制器:带宽低、延迟高、还要跨 socket 做缓存一致性协议。实验用随机访问直接测出这个差距(numa_test.c:53-62)。内核怎么决定把页分到哪个 node? → VMA 的 mempolicy:MPOL_PREFERRED 时 policy_node 直接取 first_node(mempolicy.c:1870-1871);MPOL_INTERLEAVE 时 interleave_nodes 按 il_prev 轮转(mempolicy.c:1895-1897),缺页入口在 vma_alloc_folio(mempolicy.c:2166)。DPDK 怎么保证内存落在本地 socket? → 创建时传 rte_socket_id()(README 02-numa:49-51),启动时 --socket-mem 按 node 配额分配 hugepage(README 02-numa:60)。数据落点perf-data/P0/02-numa/:numa_test.txt(两种访存 cycles/iter)、perf.node-stat.txt(node-load-misses)、numactl-hardware.txt(拓扑存档)。
2026年08月18日
2 阅读
0 评论
0 点赞
2026-08-18
P0-04 · 无锁编程入门:从 CAS 到 MPSC 队列
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-03 PCIe | 下一篇:P0-05 perf一句话定位无锁编程 = CAS 抢位置 + 内存屏障保证可见性。多生产者竞争时,CAS 保证只有一个线程抢到 slot;抢到之后数据怎么"安全地"被消费者看到,靠的是 release/acquire 语义。这套范式放大 100 倍,就是 DPDK rte_ring。真实源码精读:有锁 vs 无锁先看实验室的定量对比(source/dpdk-labs/04-lockfree/lockfree_demo.c):void *locked_worker(void *arg) { (void)arg; for (int i = 0; i < ITERATIONS; i++) { pthread_mutex_lock(&lock); locked_counter++; pthread_mutex_unlock(&lock); } return NULL; } // lockfree_demo.c:19-28 void *lockfree_worker(void *arg) { for (int i = 0; i < ITERATIONS; i++) { uint64_t old, new_val; do { old = atomic_load(&atomic_counter); new_val = old + 1; } while (!atomic_compare_exchange_weak(&atomic_counter, &old, new_val)); } return NULL; } // lockfree_demo.c:33-44CAS 循环是 lockfree_demo.c:41 那一行:compare_exchange_weak 失败时会把最新的值写回 old,循环自然重试。这里有个关键认知(README 04-lockfree:76-77):低竞争下无锁更快,高竞争下 CAS 疯狂重试可能反而不如锁——所以 DPDK 热路径的终极答案永远是"每核私有数据结构",无锁只是兜底。MPSC 队列:我的 Day 4 编码题code/Phase0/mpsc-queue/mpsc_queue.c 是路线图规划的编码题模板——结构体和算法注释已给全,mpsc_queue_create/mpsc_enqueue/mpsc_dequeue 三个函数是 TODO(mpsc_queue.c:54-57, 81-84, 114-117),要自己填。先把模板里"已经替你设计好"的部分读透:typedef struct { // ===== producer 写的区域 (cache line 0) ===== alignas(CACHE_LINE) _Atomic uint32_t head; // 所有 producer CAS 抢这个 char _pad1[CACHE_LINE - sizeof(_Atomic uint32_t)]; // ===== consumer 读写的区域 (cache line 1) ===== alignas(CACHE_LINE) _Atomic uint32_t tail; // consumer 更新, producer 只读 char _pad2[CACHE_LINE - sizeof(_Atomic uint32_t)]; // ===== 共享数据 ===== uint32_t mask; // size - 1, 用于取模 void *ring[]; // FAM (Flexible Array Member): 环形数组 } mpsc_queue_t; // mpsc_queue.c:30-42head/tail 分居两个 cache line——这正是 P0-01 false sharing 的工程应用。enqueue 的算法注释是整道题的核心(mpsc_queue.c:60-80): * 1. atomic_load head (acquire) → 拿到当前队尾位置 * 2. atomic_load tail (acquire) → 看队列满没满 * 3. if (head - tail == size) → 队列满,返回 false * 4. new_head = head + 1 * 5. CAS(&q->head, head, new_head) → 抢 slot * - 成功: 跳出循环 * - 失败: head 被更新为当前最新值 → 回第 1 步重试 * 6. ring[head & mask] = data → 写入数据 (此时只有我能写这个位置!)注释里的自问自答值得抄进笔记(mpsc_queue.c:75-80):数据写入必须在 CAS 之后——先 CAS 抢 slot 保证自己是唯一写 ring[head] 的人,再写数据就无竞争;"CAS 的 release 语义保证 consumer 看到 head 更新时数据已就绪"。dequeue 的方案 B 简化实现也有讲究(mpsc_queue.c:107-112):consumer 发现 ring[tail & mask] == NULL 说明 producer 抢到了 head 但还没写完,返回空即可,不需要锁也不需要等待。测试验收标准是硬性的:2 个 producer 各入队 100 万,consumed == 2000000 才算 PASS(mpsc_queue.c:134-135, 193-196)。🐧 内核对照:RCU 是另一种"免锁"内核最著名的免锁机制是 RCU。include/linux/rculist.h:static inline void __list_add_rcu(struct list_head *new, struct list_head *prev, struct list_head *next) // rculist.h:76 { if (!__list_add_valid(new, prev, next)) return; new->next = next; new->prev = prev; rcu_assign_pointer(list_next_rcu(prev), new); // rculist.h:84 next->prev = new; } static inline void list_add_tail_rcu(struct list_head *new, struct list_head *head) // rculist.h:125 { __list_add_rcu(new, head->prev, head); }先把自己的前后指针接好,最后用 rcu_assign_pointer(含 release 屏障)发布——读者任何时刻看到的都是完整节点。写者延迟回收:void call_rcu(struct rcu_head *head, rcu_callback_t func)(kernel/rcu/tree.c:2835)注册回调,等所有读者度过宽限期后再释放。队列无锁是"生产者-消费者互不阻塞",RCU 无锁是"发布-订阅、读者零开销"——面试能讲出这个区别是加分项。实验怎么跑cd source/dpdk-labs/04-lockfree gcc -O2 -o lockfree_demo lockfree_demo.c -lpthread ./lockfree_demo # 对比 [locked] 与 [lockfree] 的 cycles/op cd ../../code/Phase0/mpsc-queue gcc -std=c11 -pthread -O2 -Wall -Wextra -o mpsc_test mpsc_queue.c ./mpsc_test # 验收:consumed == 2000000 且 PASS变化 N_THREADS(lockfree_demo.c:13)观察竞争强度与两方案胜负关系,验证"低竞争无锁赢、高竞争锁赢"的边界(README 04-lockfree:73-77)。面试 3 问MPSC 入队为什么先 CAS 后写数据? → 先 CAS 抢 slot 保证唯一写者,后写数据无竞争;且 CAS 的 release 语义让数据先于 head 发布可见(mpsc_queue.c:75-80 注释原文)。MPSC 和 SPSC 的实现差异在哪? → SPSC 的 head 只有一个人写,普通 store 即可;MPSC 必须 CAS(对比 lockfree_demo.c:22-25 的 mutex 版与 :38-41 的 CAS 循环)。DPDK ring 用 RING_F_SP_ENQ 标志区分(README 04-lockfree:84)。无锁队列与 RCU 的区别? → 队列免锁解决生产消费互斥,读者拿不到就返回;RCU 是发布-订阅,rcu_assign_pointer 发布(rculist.h:84)、call_rcu 延迟回收(tree.c:2835),读者永远无锁无阻塞。数据落点perf-data/P0/04-lockfree/:lockfree_demo.txt(锁 vs CAS 矩阵)、mpsc_test.txt(PASS 记录 + spins 次数)。
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P0-05 · perf 与火焰图:性能分析的起点
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-04 无锁编程 | 下一篇:P0-06 gdb一句话定位perf 三层工作流:perf stat 看宏观指标 → perf record + 火焰图定位热点函数 → perf top 实时观察。它是后面所有 DPDK 调优("单核 14.88 Mpps"那类目标)的起点——先证明慢在哪,再动手改。内核入口:perf 是怎么工作的perf 用户态工具的全部能力,都来自一个系统调用(kernel/events/core.c):SYSCALL_DEFINE5(perf_event_open, struct perf_event_attr __user *, attr_uptr, pid_t, pid, int, cpu, int, group_fd, unsigned long, flags) // core.c:12601-12603 { // ... err = perf_copy_attr(attr_uptr, &attr); // core.c:12624 if (err) return err; /* Do we allow access to perf_event_open(2) ? */ err = security_perf_event_open(&attr, PERF_SECURITY_OPEN); // core.c:12629 // ... if (attr.freq) { if (attr.sample_freq > sysctl_perf_event_sample_rate) return -EINVAL; // core.c:12644-12650 }三个要点:① 用户态把 struct perf_event_attr 拷进内核(core.c:12624)——perf record -F 99 的 99Hz 就是写在这个结构里;② 不是谁都能采样,先过安全闸(core.c:12629);③ 采样频率有上限校验,超了直接 EINVAL(core.c:12644-12650)。事件计数本身由 PMU 硬件计数器完成、溢出时触发采样中断——这就是 perf 采样开销低到几乎可以忽略的原因。实验怎么跑(README 05-perf:21-50)cd source/dpdk-labs/01-cache gcc -O2 -g -o cache_line cache_line.c # 第一层:宏观 perf stat -e cycles,instructions \ -e cache-misses,cache-references \ -e L1-dcache-load-misses,L1-dcache-loads \ -e LLC-load-misses,LLC-loads \ -e branch-misses,branches ./cache_line # 第二层:火焰图 perf record -g -F 99 ./cache_line perf report --stdio | head -60 git clone https://github.com/brendangregg/FlameGraph.git /tmp/FlameGraph 2>/dev/null perf script > out.perf /tmp/FlameGraph/stackcollapse-perf.pl out.perf > out.folded /tmp/FlameGraph/flamegraph.pl out.folded > flamegraph.svg # 第三层:实时 sudo perf top -p $(pidof your_dpdk_app)判读套路直接抄 README(05-perf:31-36):IPC = instructions / cycles,>2.0 是超标量用满,<0.5 说明大量 stall 在等内存;cache-miss% = misses / references,<1% 健康,>10% 就要怀疑 false sharing 或数据布局——然后回到 P0-01 的 pahole 和 P0-02 的 numactl 去找病因(README 05-perf:93-96 的"DPDK 性能问题定位"套路完全一致)。火焰图读法(README 05-perf:99-107):宽度 = CPU 时间占比,高度 = 调用栈深度,颜色无意义。找最宽的"平顶山",从下往上读调用链;优化前后各出一张图,效果一目了然。pahole -C rte_mbuf 那步(README 05-perf:64-70)把 perf 的"果"定位到结构体的"因"。🐧 内核对照perf 功能内核机制出处perf stat -e cache-missesPMU 硬件计数器(每核一组)core.c:12601 的 attr 携带配置perf record -F 99计数溢出 → 采样中断freq 校验 core.c:12644-12650采样内核态需 CAP_PERFMON,安全闸core.c:12629火焰图折叠perf script 输出原始栈用户态后处理,与内核无关面试 3 问perf stat 里 IPC 和 cache-miss% 怎么判读? → IPC<0.5 = stall 严重(等内存);cache-miss%>10% = 布局问题(README 05-perf:31-36)。两者叠加基本锁定 false sharing/跨 NUMA。火焰图的宽和高各代表什么? → 宽 = CPU 时间占比(找最宽的平顶山),高 = 栈深,颜色无意义(README 05-perf:99-107)。perf_event_open 做了哪些关键校验? → 拷 attr(core.c:12624)→ 安全检查(core.c:12629)→ 采样频率上限(core.c:12644-12650)。数据落点perf-data/P0/05-perf/:perf.stat.txt(IPC/cache-miss%)、flamegraph.svg、perf.report.txt(top 热点函数)。
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P0-06 · gdb 实战:调试内核与用户态程序
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-05 perf | 下一篇:P0-07 内核网络栈一句话定位DPDK 程序调试的特殊性在于:网卡被 DPDK 接管后内核都看不到它,gdb 是你唯一能"看见包"的窗口。断点、切线程、检查 mbuf/ring/mempool 这三板斧练熟,后面每次 segfault 排查都靠它。DPDK 调试的特殊性(README 06-gdb:10-20)rte_eal_init 在 main 之前完成 hugepage/设备初始化——断点别设太早,否则先掉进 EAL 内部。worker lcore 是 rte_eal_remote_launch 拉起的线程,每个断点只对当前 thread 生效,要 thread N 切过去。网卡绑到 DPDK 后 ifconfig 看不到它,但内存里的 mbuf 是真实的——gdb 看内存就是看包。真实操作:两条进场路线 + 三组检查命令进场(README 06-gdb:34-47):# 方式 1:直接 gdb 启动(带 DPDK EAL 参数) gdb --args ./my_dpdk_app -l 0-1 -n 1 --no-pci (gdb) break main (gdb) run # 方式 2:attach 到运行中的进程(不用重跑 EAL 初始化) ./my_dpdk_app -l 0-1 -n 1 --no-pci # 终端 1 gdb -p $(pidof my_dpdk_app) # 终端 2三板斧之一:多核切换(README 06-gdb:53-57):(gdb) info threads # 每个 worker lcore 是一个 pthread (gdb) thread 2 # 切到 dpdk-worker 线程三板斧之二:检查 mbuf 内容(README 06-gdb:59-65):(gdb) p/x ((struct rte_mbuf *)mbuf)->pkt_len (gdb) p/x ((struct rte_mbuf *)mbuf)->data_len (gdb) x/64bx ((struct rte_mbuf *)mbuf)->buf_addr # 前 64 字节 = 目的MAC+源MAC+EtherType三板斧之三:检查 ring / mempool 状态(README 06-gdb:68-75):(gdb) p ((struct rte_ring *)ring)->prod (gdb) p ((struct rte_ring *)ring)->cons (gdb) call rte_mempool_avail_count(pool) # gdb 里直接调用 DPDK API (gdb) call rte_mempool_in_use_count(pool)call 这条很值钱——gdb 能在断点现场调用程序里的函数,等于在暂停的进程里加了一段只读的"诊断代码"。三个经典崩溃的排查姿势(README 06-gdb:81-96)现象排查命令根因方向SIGSEGV in rte_eth_rx_burstbt → p port_id → call rte_eth_dev_is_valid_port(port_id)端口没初始化就用SIGSEGV 访问 mbuf->datap mbuf → call rte_mempool_full(pool)mbuf 已释放或 pool 空程序卡死Ctrl-C → info threads → thread apply all btring 满/空导致 busy loop关于 README 06-gdb:99-109 提到的 dpdk.gdb 辅助脚本:它来自旧版 DPDK 的 buildtools/gdb/。我在本机 DPDK 24.11.6 源码树里全局搜过 *.gdb,一个都没有——这个目录在 24.11 已被移除。所以直接用上面的原生 gdb 命令即可,不必折腾脚本。🐧 内核对照用户态 gdb内核调试关键差异gdb -p attachkgdb 串口/网络代理内核调试要停整个系统,代价完全不同p 任意变量/call 任意函数kprobe/bpftrace 动态插桩内核倾向"可编程探测",见 P0-07 的 bpftracethread apply all bt 查卡死sysrq-t + crash 工具思路一致:先拿所有执行流的栈面试 3 问attach 和直接 gdb 启动怎么选? → 能 attach 就 attach:EAL 初始化(hugepage/设备)已跑完,直接从业务逻辑断起(README 06-gdb:43-47)。怎么在 gdb 里看一个 mbuf 指向的包内容? → p/x mbuf->pkt_len 看长度、x/64bx mbuf->buf_addr 直接吐以太头字节(README 06-gdb:61-65)。DPDK 程序卡住不动,第一步做什么? → Ctrl-C 后 info threads + thread apply all bt,看所有 lcore 的栈——多半是某个 ring 满/空导致的自旋(README 06-gdb:92-96)。数据落点perf-data/P0/06-gdb/:gdb-session.log(一次完整的三板斧会话记录)、crash-bt.txt(首个 segfault 的 backtrace 存档)。
2026年08月18日
0 阅读
0 评论
0 点赞
2026-08-18
P0-07 · 内核网络栈初探:一个包的内核之旅
对应路线图:Phase 0 (Day 1-21) | 内核 v6.6 | 上一篇:P0-06 gdb | 下一篇:P1-01 EAL一句话定位一个包进入内核后要过 6 站:XDP → tap(tcpdump)→ ingress qdisc + netfilter → rx_handler → 协议分发 → 投递/丢弃。每一站都是查表和判断——这就是内核"通用性"的代价,也是 DPDK 绕开它的理由。本讲把 netif_receive_skb_core 逐站读完。真实源码精读:收包核心函数逐站过入口三层(net/core/dev.c):netif_receive_skb()(dev.c:5899)打 trace 后进 netif_receive_skb_internal()(dev.c:5824)——这里先做 RPS 判断,把包交给别的 CPU 的 backlog 队列(dev.c:5834-5845 的 enqueue_to_backlog);然后进入主角:static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc, struct packet_type **ppt_prev) // dev.c:5434 { // ... __this_cpu_inc(softnet_data.processed); // dev.c:5461注意 netif_receive_skb_core() 这个公开包装(dev.c:5666-5672)只做一件事:rcu_read_lock() 后调核心函数——后续所有查表都在 RCU 读临界区里进行,和 P0-04 的 list_for_each_entry_rcu 正好接上。第 ① 站 · 通用 XDP(dev.c:5463-5474):do_xdp_generic 执行 eBPF 程序,返回非 XDP_PASS 直接丢弃。这是内核里"最早处理包"的位置,也是 AF_XDP 生态的挂载点。第 ② 站 · tap(dev.c:5488-5498):遍历 ptype_all 列表——tcpdump / AF_PACKET 在这里拿到包的副本。注意这里的模式:pt_prev 先记录、延迟到下一站再真正 deliver,这是收包路径"减少函数调用深度"的经典技巧。第 ③ 站 · ingress qdisc + netfilter(dev.c:5500-5516):#ifdef CONFIG_NET_INGRESS if (static_branch_unlikely(&ingress_needed_key)) { bool another = false; skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev, &another); if (another) goto another_round; // tc redirect 了包 → 从头再走一遍 if (!skb) goto out; // 被 tc 吃掉(限速/丢弃) // ... if (nf_ingress(skb, &pt_prev, &ret, orig_dev) < 0) // iptables PREROUTING goto out; } #endifsch_handle_ingress 是 tc 限速/重定向的落点;nf_ingress 是 netfilter PREROUTING 钩子(README 07-kernel-net:61-89 那张五钩子图里最上面那个)。another_round 意味着包可能被 redirect 后重头再走一遍全部流程。第 ④ 站 · rx_handler(dev.c:5534-5554):网桥、bonding、OVS 这类"设备接管者"。四种返回值:RX_HANDLER_CONSUMED(包归它了,结束)、ANOTHER(再来一轮)、EXACT(只投精确匹配的协议)、PASS(继续往下)。第 ⑤ 站 · 协议分发(dev.c:5593-5603):按 skb->protocol 查 ptype_base 哈希表——ip_rcv 就是在这里被找到并延迟记录的。第 ⑥ 站 · 收尾(dev.c:5610-5625):有 pt_prev 就交给 __netif_receive_skb_one_core 投递;没人认领则 kfree_skb_reason(skb, SKB_DROP_REASON_UNHANDLED_PROTO)。包住的地方:struct sk_buff(include/linux/skbuff.h:843)——next/prev 挂队列、len, data_len 总长与分片长(skbuff.h:892-893)、cb[48] 供各层传私有数据(skbuff.h:876)。对应 DPDK 的 rte_mbuf(rte_mbuf_core.h:467),差异见对照表。实验怎么跑(README 07-kernel-net:46-53, 147-161)# 终端 1:挂探针 bpftrace -e 'kprobe:__netif_receive_skb_core { printf("%s\n", kstack); }' # 终端 2:发一个包 ping -c 1 8.8.8.8 # 预期栈:__netif_receive_skb_core → ip_rcv → ip_local_deliver → icmp_rcv # 丢包与状态 dropwatch -l kas # 内核丢包点实时统计 conntrack -L 2>/dev/null || cat /proc/net/nf_conntrack # 连接跟踪表 ss -s # socket 汇总tc 限速的例子(README 07-kernel-net:94-106)也跑一遍:htb qdisc + u32 filter 把某个源 IP 限到 100mbit——你会直观看到第 ③ 站的 sch_handle_ingress 在起作用。🐧 内核对照DPDK内核关键差异rte_eth_rx_burst 拿 mbuf中断 → NAPI → softirq → skb轮询 vs 中断,见 P0-03收包 1 阶段直达应用8 阶段(README 07-kernel-net:15-29)每站都是查表/判断的通用开销rte_flow 卸载到网卡tc/netfilter 软件过滤硬件卸载 vs 软件逐包KNI 回灌内核(README 07-kernel-net:127-143)原生协议栈DPDK 世界里的"内核通道":rte_kni_tx_burst(:141)送控制包,rte_kni_handle_request(:142)接回复面试 3 问__netif_receive_skb_core 按顺序过了哪几站? → XDP(dev.c:5463-5474)→ tap(dev.c:5488-5498)→ ingress qdisc + netfilter(dev.c:5500-5516)→ rx_handler(dev.c:5534-5554)→ 协议分发(dev.c:5593-5603)→ deliver/drop(dev.c:5610-5625)。RPS 是什么、在哪判断? → 把软中断处理分摊到其他 CPU:get_rps_cpu 选核后 enqueue_to_backlog 入对方队列(dev.c:5834-5845)。是内核版的"RSS 分流"——P1-07 会讲 RSS 与它的对照。为什么内核路径比 DPDK 慢一个数量级? → 8 个阶段每站查表/判断(README 07-kernel-net:15-29),加 3 次拷贝和上下文切换;DPDK 1 次 DMA 直达应用(README 03-pcie:13-25)。不是内核写得烂,是通用性的代价。数据落点perf-data/P0/07-kernel-net/:bpftrace-kstack.txt(真实调用栈)、dropwatch.txt、conntrack.txt、tc-htb.txt(限速前后的 iperf3 对比)。
2026年08月18日
0 阅读
0 评论
0 点赞
①性能工程基础
· 第 1/1 页 · 本页 6 条 · 共 6 条