项目总览与工程规范
1. 为什么必须做项目
系统知识有一个残酷的特点:读懂了不等于会做,会做不等于做对。
你可以背出 epoll 的 LT/ET 区别,但只有当你写一个 1 万连接的 echo 服务器,看到 CPU 100%、Recv-Q 持续增长、epoll_wait 返回 EPOLLHUP 却没收尾时,才会真正理解「边缘触发必须读到 EAGAIN」不是一句口号。你可以画出 TCP 状态机,但只有当你亲手实现一个用户态协议栈,被重传、乱序、窗口更新折磨过,才会记住 TIME_WAIT 的意义。
因此本书把第 6 篇写成四条项目主线、11 个可运行项目。每个项目都对应一个真实系统,但不要求你复刻它的全部功能;要求的是把核心机制做对,并用测试与压测证明。
2. 四条主线
| 主线 | 项目 | 真实对应物 | 核心机制 | 主要能力 |
|---|---|---|---|---|
| A 云原生 | A1 mycontainer | Docker / runc | namespace、cgroups、overlayfs、OCI | 隔离、资源、生命周期 |
A2 myhttpd | nginx / lighttpd | epoll、HTTP、零拷贝、线程池 | 高并发、协议、性能 | |
A3 mybpf | bpftrace / Pixie | kprobe、tracepoint、map、libbpf | 可观测性、内核 | |
| B 存储 | B1 myredis | Redis | RESP、事件循环、持久化、复制 | 数据结构、I/O、一致性 |
B2 mydb | SQLite / LevelDB | B+ 树、buffer pool、WAL、恢复 | 存储引擎、崩溃一致性 | |
| C 网络 | C1 mytcp | lwIP / Linux 栈 | 以太网、IP、TCP 状态机、重传 | 协议实现、调试 |
C2 myproxy | HAProxy / Envoy | 连接池、负载均衡、超时、零拷贝 | 代理、并发、稳定性 | |
C3 mysniff | tcpdump / Wireshark | AF_PACKET、解析、流重组 | 抓包、协议分析 | |
| D 工具链 | D1 mysh | dash / bash | 词法、展开、管道、作业控制 | Shell、进程、信号 |
D2 mybox | BusyBox | applet 框架、选项解析、文件操作 | CLI 工程、可移植性 | |
D3 mygit | Git | 对象模型、哈希、索引、packfile | 数据模型、二进制格式 |
不要 11 个全做。按你的路线选 2–3 个:应用/运维选 D2 + A2;系统编程选 A2 + B1 或 B2;内核/性能选 A1 + A3;网络方向选 C1 + C2 + C3(C3 可作 C1 的调试工具)。
3. 项目难度与投入
| 项目 | 难度 | 预计投入 | 前置章节 | 最小可交付 |
|---|---|---|---|---|
A1 mycontainer | ★★★★ | 30–40 h | 2.7、2.19、3.4、5.1–5.5 | 能 run 一个 busybox,带 PID/mount/net 隔离与内存限制 |
A2 myhttpd | ★★★★ | 30–40 h | 2.16–2.18、4.5、4.8–4.10 | 静态文件 + keep-alive + 1 万连接压测 |
A3 mybpf | ★★★★ | 20–30 h | 3.3、3.15、4.8 | 追踪 openat 与 TCP 收包,导出统计 |
B1 myredis | ★★★★★ | 40–60 h | 2.14、2.16–2.18、4.5–4.6 | RESP + GET/SET/EXPIRE + AOF + 主从 |
B2 mydb | ★★★★★ | 40–60 h | 2.2–2.3、3.12–3.13 | B+ 树 + WAL + 崩溃恢复 + 简单 SQL |
C1 mytcp | ★★★★★ | 50–70 h | 4.2–4.6、2.18 | 能 ping、能三次握手、能传文件 |
C2 myproxy | ★★★☆ | 25–35 h | 2.16–2.18、4.7 | HTTP 反向代理 + 健康检查 + 压测 |
C3 mysniff | ★★★ | 15–25 h | 4.2–4.5、2.18 | 抓包、解析 TCP/UDP、导出 pcap |
D1 mysh | ★★★★ | 30–40 h | 2.7–2.10、2.19 | 管道、重定向、内建命令、作业控制 |
D2 mybox | ★★★ | 20–30 h | 0.3、2.2–2.6 | 10 个 applet + 统一选项解析 |
D3 mygit | ★★★★ | 30–40 h | 1.7、2.2–2.3 | init/add/commit/log/checkout 与真 git 互操作 |
4. Stage 模型
每个项目都按 Stage 递进。Stage 不是「章节」,而是可验收的里程碑:
Stage 0 骨架与构建:能编译、能跑、有测试入口
Stage 1 最小闭环:核心路径通,能演示
Stage 2 正确性:边界、错误、并发安全
Stage 3 性能:有基线、有优化、有数据
Stage 4 可观测:日志、指标、trace
Stage 5 交付:容器镜像、CI、文档、复盘
每个 Stage 的页面包含:
- 目标:这一阶段结束时,什么命令能跑出什么结果;
- 关键概念:需要回看的章节与 API;
- 代码:完整可编译的核心片段,不是伪代码;
- 验证:命令、预期输出、失败时怎么查;
- 常见错误:3–6 条,含错误信息与修复。
规则:Stage N 没通过验收,不要开 Stage N+1。系统项目最常见的失败不是「不会写」,而是「在错误的地基上堆功能」。
5. 目录规范
每个项目一个独立目录,建议放在仓库外(避免把几千行代码塞进教程站),或放在 linux/code/<project>/(本书附录给链接):
myhttpd/
├── Makefile
├── README.md
├── include/
│ ├── http.h
│ ├── buffer.h
│ └── log.h
├── src/
│ ├── main.c
│ ├── event.c # epoll 封装
│ ├── http.c # 解析/响应
│ ├── file.c # 静态文件/零拷贝
│ └── log.c
├── tests/
│ ├── test_http.c
│ ├── test_buffer.c
│ └── run.sh
├── bench/
│ ├── wrk.lua
│ └── report.md
└── docs/
├── design.md
└── stage-notes.md
三条纪律:
- 一个模块一个文件,一个文件一个职责;超过 800 行的
.c要拆。 - 公共头文件只放声明;内部结构体放
internal.h,不要污染公共 API。 - 每个 Stage 打一个 git tag(
stage-0…),方便回滚与对比。
6. 验收标准
每个项目按六维打分,每维 0–2 分,总分 12:
| 维度 | 0 分 | 1 分 | 2 分 |
|---|---|---|---|
| 功能 | 跑不起来 | 主路径可用 | 主路径 + 边界都可用 |
| 正确性 | 有已知崩溃/泄漏 | 常规测试通过 | 错误注入/并发/崩溃测试通过 |
| 性能 | 无数据 | 有基线 | 有优化前后对比与瓶颈解释 |
| 可观测 | 只有 printf | 有结构化日志 | 有指标/trace,能定位一次真实故障 |
| 代码质量 | 难读、重复 | 模块清晰 | 有测试、有注释、无警告 |
| 文档 | 无 | README 能跑 | 设计文档 + 复盘 + 压测报告 |
及格线 8 分,优秀 10 分以上。功能满分但正确性 0 分的项目,按 0 分处理——系统程序的第一美德是正确。
7. 安全与伦理
- 容器/namespace/网络实验会改宿主状态:先打快照,在虚拟机里做;不要在公司/共享机器上
--privileged。 - 端口扫描、抓包、压测只对你自己的实验环境做;不要对公网目标做。
rm -rf、mkfs、dd、ip link del这类命令,执行前先echo确认变量非空,再用--分隔。- 内核模块崩溃会拖垮整机;用 QEMU,不要在宿主上直接
insmod未知模块。 - 项目代码不要引入 GPL 代码到闭源产品;本书代码默认 MIT,引用第三方库时看清许可证。
8. 时间管理
一个 40 小时的项目,建议切成 8–10 次,每次 3–4 小时:
| 次数 | 内容 | 产出 |
|---|---|---|
| 1 | 读前置章节 + 设计 | design.md、接口草图 |
| 2 | Stage 0 骨架 | 能编译、能跑空循环 |
| 3–4 | Stage 1 最小闭环 | 能演示核心功能 |
| 5–6 | Stage 2 正确性 | 测试、错误注入、修复 |
| 7 | Stage 3 性能 | 基线、优化、数据 |
| 8 | Stage 4 可观测 | 日志/指标/trace |
| 9 | Stage 5 交付 | 镜像、CI、文档 |
| 10 | 复盘 | retro.md、演讲 10 分钟 |
每次结束前留一个可运行的状态,不要停在「改了一半编译不过」。这是工程习惯,也是防止项目烂尾的最有效手段。
9. 怎么写复盘
复盘不是流水账,回答四个问题:
- 最初的设计假设是什么?哪一条被证伪了?
- 最难的 bug 是什么?现象、假设、证据、根因、修复、如何防止复发?
- 性能瓶颈在哪?怎么定位?优化前后数据?
- 如果重做,哪三件事会不同?
模板:
# 复盘:myhttpd
## 1. 目标与结果
- 目标:1 万连接、静态文件 5 GB/s、P99 < 10 ms
- 结果:8 千连接、3.2 GB/s、P99 18 ms
## 2. 关键假设与证伪
- 假设「epoll ET + 4 线程足够」→ 证伪:单 acceptor 成为瓶颈
- 证据:`perf top` 显示 `accept4` 占 38% CPU
## 3. 最难 bug
- 现象:压测 30 分钟后 fd 耗尽
- 根因:`EPOLLHUP` 分支忘记 `close`
- 修复/防复发:事件循环统一 cleanup + fd 计数指标
## 4. 性能
- 基线/优化后/瓶颈解释(表格)
## 5. 如果重做
- 先做 acceptor 多线程
- 先加 fd/连接数指标
- 先写压测脚本再写业务
10. 与正文的配合
每个项目页开头会给「前置章节」与「回看时机」:
- 做 A2 前读 2.16–2.18、4.5、4.8–4.10;
- 卡在
epoll事件丢失时回看 4.10 的ep_poll_callback; - 卡在
TIME_WAIT时回看 4.5; - 卡在内存增长时回看 3.10 与 2.3 的
mmap。
不要等「读完再做」。项目是检验理解的手段:读一章、做一段、卡住、回看,这个循环比线性阅读有效得多。
下一站:工程手册给出构建、测试、压测、CI 的具体模板;然后从四条主线里选一条开始。