# uNOS 学习方法论 — 三层剥皮法 > **创建日期**: 2026-08-01 > **适用**: 系统编程学习(DPDK / 内核 / TCP 协议栈 / PMD 驱动) > **关联**: `uNOS_REQUIREMENTS.md`、`uNOS_DESIGN_SPEC.md`、`lazy-puzzling-rocket.md` --- ## 一、核心公式 ``` 掌握 = 内核为什么慢 → DPDK 怎么解决的 → 我亲手写一遍 → 性能数据证明 ────────── ────────────── ────────── ───────────── Layer 1 Layer 2 Layer 3 Layer 4 (理解问题) (理解方案) (内化知识) (验证理解) ``` 每一层不可跳过: | 跳过 | 后果 | |------|------| | Layer 1 | 不知道为什么需要 DPDK,面试回答停留在"因为快" | | Layer 2 | 只会调 API,不理解设计决策 | | Layer 3 | "看懂了但不会写",面试一问细节就崩 | | Layer 4 | "写出来了但不知道自己写对了没" | --- ## 二、四层详解 ### Layer 1: 内核为什么慢(20 分钟,理解问题) **做什么**:读内核源码,找到瓶颈的**精确位置**。 **方法**: 1. 先确定要研究的子系统(如:包分配、路由查找、TCP 状态机) 2. 在 Linux 内核源码中找到对应的 C 文件 3. 跟踪一个完整的数据路径(如:收包 → 分配 skb → 协议栈 → 应用) 4. 标注每一步的延迟来源:中断、锁、内存分配、cache miss、上下文切换 **产出标准**:能用一句话解释"内核做 X 要走多少步、每一步慢在哪"。 **示例**(mempool): ``` 问题: 内核是怎么分配网络包的? 精确到源码: 收包: net/core/dev.c → netif_rx() → enqueue_to_backlog() → 每个包触发一次 ksoftirqd 调度 分配: include/linux/skbuff.h → alloc_skb() → kmem_cache_alloc() → slab allocator → 每次分配都要获取 per-cpu 锁 为什么慢: 1. 每包一次中断 → 每包一次上下文切换 2. slab allocator 有锁 → 多核竞争 3. kmem_cache 有冷热分离 → 热路径 miss 时有 cache miss 4. sk_buff 结构体 200+ 字节,跨多个 cacheline ``` --- ### Layer 2: DPDK 怎么解决的(20 分钟,理解方案) **做什么**:读 DPDK 源码,找到优化的**精确手段**。 **方法**: 1. 在 DPDK 源码中找到对应的库/驱动 2. 读头文件的 struct 定义和函数注释 3. 读实现文件的几个关键函数 4. 画出"数据路径图":一个包从进入到离开,经过哪些结构体、哪些函数 **产出标准**:能用一张图画出关键路径,标注每一级的延迟量级。 **示例**(mempool): ``` 方案: rte_mempool 精确到源码: lib/mempool/rte_mempool.h line 230 → struct rte_mempool lib/mempool/rte_mempool.h line 90 → struct rte_mempool_cache 三级路径: L1: mbuf 从 per-lcore cache 拿 → 零锁,L1 cache 常驻 (~5ns) L2: cache 空了 → 从 ring 批量补 256 个 → CAS 一次 (~50ns 均摊) L3: ring 也空了 → 从 hugepage 拿 → 很少触发 (~200ns) 为什么快: 1. 预分配 → 收发过程零分配,只从池里取 2. per-lcore cache → 无锁,无竞争 3. ring 批量操作 → 分摊 CAS 开销 4. mbuf 固定 128B,2 个 cacheline → prefetch 友好 ``` --- ### Layer 3: 亲手写一遍(60 分钟,内化知识) **做什么**:关掉所有参考代码,自己从头写。 **方法**: 1. 读完 Layer 1-2 后,**立即关掉 DPDK 源码** 2. 打开编辑器,自己写 3. 编译 → crash → gdb backtrace → 读报错 → 修 → 继续 4. 跑通后用 gdb 单步跟踪关键路径:打印 struct 内容、看指针偏移 5. 用 `pahole` 看结构体 cache line 布局 **产出标准**:能在 gdb 里打印出关键结构体的内部状态,能解释为什么这么快。 **示例**(mempool): ```c // 自己写: // 1. 创建 mempool struct rte_mempool *pool = rte_pktmbuf_pool_create( "my_pool", 8191, 256, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id() ); // 2. 分配/释放循环,测性能 uint64_t start = rte_rdtsc(); for (int i = 0; i < 1000000; i++) { struct rte_mbuf *m = rte_pktmbuf_alloc(pool); rte_pktmbuf_free(m); } uint64_t cycles = (rte_rdtsc() - start) / 1000000; printf("avg cycles per alloc+free: %lu\n", cycles); // 3. gdb 验证 // (gdb) p *pool // (gdb) p pool->cache[0] ← per-lcore cache 里有多少个 mbuf? // (gdb) p pool->cache[0].len ← 当 len > 0 时 alloc 走 L1 // (gdb) p pool->cache[0].len ← 当 len = 0 时 alloc 走 L2 补货 ``` **禁止事项**: - ❌ 复制粘贴 DPDK examples 代码 - ❌ 先看答案再写 - ❌ crash 后直接问 AI 怎么修(先自己 gdb 至少 5 分钟) --- ### Layer 4: 性能数据证明(20 分钟,验证理解) **做什么**:用数据证明你写的东西确实比内核快。 **方法**: 1. `perf stat` 对比内核路径 vs DPDK 路径 2. `perf record` + FlameGraph 生成火焰图 3. 填一张对比表 4. 写入 `docs/` 文档 **产出标准**:有一张表格,有数字,有火焰图链接。 **示例**(mempool): ``` │ 指标 │ 内核 (slab) │ DPDK (mempool) │ │ alloc+free 延迟 │ ~200ns │ ~15ns │ │ L1 cache miss │ ~8% │ ~0.5% │ │ 锁竞争 │ 有 (per-cpu) │ 无 (per-lcore) │ │ 火焰图 │ [链接] │ [链接] │ ``` --- ## 三、每阶段时间分配 ``` 总时间: 2 小时/阶段 Layer 1: 20 分钟 (读内核源码 + 理解问题) Layer 2: 20 分钟 (读 DPDK 源码 + 理解方案) Layer 3: 60 分钟 (手写 + gdb 跟踪) Layer 4: 20 分钟 (perf 验证 + 写文档) 输入 (Layer 1-2): 30% 输出 (Layer 3-4): 70% ``` --- ## 四、应用到 uNOS 每个 Phase ### Phase 1: L2 MAC 转发 | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读 `net/core/dev.c` NAPI 收包路径 | 理解中断驱动的瓶颈在哪 | | 2 | 读 DPDK PMD 轮询模式: `rte_eth_rx_burst` | 理解零中断轮询 | | 3 | 手写 L2 转发循环,gdb 跟踪 mbuf 从口0到口1 | 能跑通,iPerf3 双向通 | | 4 | `perf record` 火焰图 | CPU 100% 有用功,中断开销 0% | ### Phase 2: L3 路由转发 | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读 `net/ipv4/fib_trie.c` 内核路由表 | 理解 FIB trie 的结构 | | 2 | 读 `lib/librte_lpm/rte_lpm.c` DIR-24-8 算法 | 理解 LPM 的 2 级 Trie | | 3 | 手写 LPM lookup + TTL 减一 + checksum | 跨网段 ping 通 | | 4 | rdtsc 测量单次 lookup 延迟 | 内核 ~500ns vs DPDK ~200ns | ### Phase 3: ARP 协议 | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读 `net/ipv4/arp.c` 内核 ARP 实现 | 理解 ARP 表的维护逻辑 | | 2 | 读 `lib/ring/rte_ring.h` 无锁 CAS 原理 | 理解 ring 的 MP/MC 模式 | | 3 | 手写 ARP 请求/应答 + pending 队列 | arping 能收到回复 | | 4 | 连续 ARP 风暴,检查 mbuf 泄漏 | 内存无泄漏 | ### Phase 4: 控制面框架 | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读内核 workqueue / tasklet 机制 | 理解异步任务调度 | | 2 | 读 `rte_eal_remote_launch` + `rte_timer` | 理解 DPDK 的多核 + 定时器 | | 3 | 手写 Pipeline 框架 + Interface Manager | 两机多 lcore 运行 | | 4 | 控制面定时器无影响数据面延迟 | 数据面延迟不因定时器而抖动 | ### Phase 5: NAT + 连接跟踪 | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读 `net/netfilter/nf_conntrack_core.c` | 理解内核 conntrack 的锁竞争 | | 2 | 读 `lib/hash/rte_cuckoo_hash.c` Cuckoo hash | 理解 2-bucket 开放寻址 | | 3 | 手写 SNAT/DNAT + rte_hash 连接表 | 内网 curl 外网能通 | | 4 | 100K 连接查找延迟 + 哈希冲突率 | 查找 O(1),冲突率 < 5% | ### Phase 6: ACL + QoS | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读内核 `net/sched/` TC 调度框架 | 理解多队列调度的复杂度 | | 2 | 读 `lib/acl/rte_acl.h` + `lib/sched/` | 理解硬件加速分类 + 调度 | | 3 | 手写 5 元组 ACL + 8 队列 SP/WRR | 满载大包时小包延迟 < 2ms | | 4 | iperf3 大包 + ping 小包 CDF 曲线 | 内核 p99 > 80ms vs uNOS < 2ms | ### Phase 7: IPv4/IPv6 + ICMP + Fragmentation | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读 `net/ipv4/ip_input.c` IP 收包路径 | 理解 IP 层在内核中的位置 | | 2 | 读 RFC 791, RFC 792, RFC 815 | 理解协议规范本身 | | 3 | 手写 IP 封装/解封 + ICMP Echo Reply | 能 ping 通你的栈 | | 4 | 分包重组 2000 字节包 | 正确重组 | ### Phase 8: TCP 状态机 | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读 `net/ipv4/tcp_input.c` tcp_rcv_state_process | 理解内核 11 状态 | | 2 | 读 RFC 793(逐字读) | 画出自己的状态转换表 | | 3 | 手写 11 状态 + scapy 逐状态触发 | gdb 单步跟踪每个转换 | | 4 | scapy 100 万 TIME_WAIT → 时间轮 O(1) | 内核链表 O(n) vs 时间轮 O(1) | ### Phase 9: 重传引擎 | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读 `net/ipv4/tcp_output.c` tcp_retransmit_skb | 理解内核重传逻辑 | | 2 | 读 RFC 6298 (RTO), RFC 2018 (SACK) | 理解重传算法 | | 3 | 手写 RTO + 快速重传 + SACK | tc netem 丢包 5% → 正确补偿 | | 4 | rdtsc 测量 RTO 精度 | 误差 < 10ms | ### Phase 10: 拥塞控制 | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读 `net/ipv4/tcp_cubic.c` 内核 CUBIC | 理解三次函数窗口增长 | | 2 | 读 RFC 8312 + 2008 年 CUBIC 论文 | 理解设计决策 | | 3 | 手写滑动窗口 + CUBIC | wireshark 验证窗口曲线 | | 4 | 内核 TCP vs 自研 TCP 吞吐对比 | 1G: 940M vs 940M (持平) | | | | CPU: 50% vs 8% (6x 优) | ### Phase 11: Socket API | Layer | 内容 | 产出 | |-------|------|------| | 1 | 读 POSIX socket 语义 | 理解 API 设计哲学 | | 2 | 读内核 `net/socket.c` socket 系统调用 | 理解 fd 到 sock 的映射 | | 3 | 手写 tcp_socket/bind/listen/accept/send/recv | HTTP Server 跑在自研 TCP 上 | | 4 | wrk 压测 HTTP QPS | 自研栈 ≥ 内核栈 QPS | ### Phase 12-17 (应用层/管理面/生产级增强) 每个 Phase 同样应用四层框架。Layer 1-2 以阅读对应 RFC / 工业标准实现为主。 --- ## 五、方法论红黑榜 ### ✅ 做这些 ``` 每个概念都按 1→2→3→4 顺序来,不跳层 Layer 3 时关掉所有参考,完全自己写 每次 crash 先 gdb 至少 5 分钟再搜答案 每层产出必须可验证(代码能跑 / 图表能看 / 数据能复现) 每天产出至少 50 行可运行的代码 ``` ### ❌ 不做这些 ``` 跳过 Layer 1 直接看 DPDK 文档 → 不知道为什么需要 跳过 Layer 3 只看不写 → "我看懂了"(其实没有) Layer 3 复制粘贴 examples → 没有内化 不写文档 → 一个月后全忘了 反复讨论设计但不写代码 → 设计永远不会完美 不跑 perf 不生成数据 → 证明不了自己写的东西好 ``` --- ## 六、每日日志模板 ``` 日期: 2026-08-XX 阶段: Phase X - XXX Layer 1 (20min): 读了: [内核文件/函数] 理解: [一句话瓶颈描述] Layer 2 (20min): 读了: [DPDK 文件/函数] 理解: [一句话优化描述] 画了: [数据路径图] Layer 3 (60min): 写了: [文件路径,代码行数] Crash: [次数 + 最关键的 bug] gdb 发现: [最关键的 insight] Layer 4 (20min): perf 数据: [关键对比数字] 火焰图: [链接] 文档: [docs/ 文件路径] 明天计划: [下一阶段的 Layer 1 要读什么] ``` --- > **文档结束** > > 这个方法不是学知识的,是逼你写代码的。 > 系统编程不能靠"看懂",要靠"调通"。 > 每层 20/20/60/20 分钟,一天 2 小时一个模块。