题记与说明
精读日期:2026-11-05 | 条目前缀:knowledge-a98e_u9b-3 | 类型:主题研究(多来源)
精读对象:Julia Evans(jvns.ca)博客中三篇用图解讲 Linux 系统概念的文章
本次共检索/精读 6 个来源:3 篇正文精读 + 1 篇方法论佐证精读 + 2 个背景来源(归档页、Wizard Zines)
所有引述均为本次 curl 抓取原文后核对的原句(下载于 2026-11-05)
0. 选题与来源清单(检索过程)
从 jvns.ca 首页全站归档(按分类列出全部博文)的 "How a computer thing works"、"Computer networking"、"Kubernetes / containers" 等分类中,按「①讲系统概念 ②以图示/结构化视觉手段为主要教学载体 ③三篇合起来覆盖不同系统层」三条标准,选出以下三篇:
| # | 文章 | 发布日期 | 系统层 | 图解形式(本次抓取核实) |
|---|---|---|---|---|
| 1 | What happens when you start a process on Linux? | 2016-10-04 | 进程生命周期(OS) | ASCII 进程树 + 拟人叙事(无 img 图片) |
| 2 | Async IO on Linux: select, poll, and epoll | 2017-06-03 | 内核 I/O 多路复用 | 系统调用签名对照、性能表格、strace 真实输出、书封图 |
| 3 | How containers work: overlayfs | 2019-11-18 | 容器文件系统 | 手绘漫画(overlay.jpeg)+ 完整 mount 实验终端记录 |
佐证来源:
- How to teach technical concepts with cartoons(2017-09-28):Julia 本人总结"图解技术概念"的方法论,直接解释了上面三篇为什么长成那样。精读。
- Wizard Zines:图解系列的商业化延续,本次核实其 zine 目录(Bite Size Networking! / How Git Works / The Secret Rules of the Terminal 等)。背景来源。
- 归档页证实三篇分类归属与日期(proc 篇归档页标题即为 "What happens when you start a process on Linux?",URL 是
exec-will-eat-your-brain)。
1. 篇一:What happens when you start a process on Linux?(fork/exec 的"吃脑"模型)
链接:https://jvns.ca/blog/2016/10/04/exec-will-eat-your-brain/
核心问题
"启动一个进程"到底是几个系统调用?直觉答案是 start_process(["ls", ...]) 一个调用搞定——Julia 指出这在 Linux 上是错的:实际是 fork + exec 两个系统调用的组合(并坦白 posix_spawn 的存在)。
主要主张
Linux 进程的诞生不是"从无到有生一个孩子",而是"先克隆自己,然后让克隆体被吃掉大脑变成另一个程序"。这个怪异但准确的心智模型,能解释进程树、僵尸进程、环境继承等一系列现象。
方法
- 拟人叙事 + ASCII 进程树逐阶段演示:
my parent my parent my parent my parent
|- me fork→ |- me exec→ |- me wait→ |- me
|-- clone |-- ls (一切归位)
|-- ls (zombie)- 给出可运行的伪代码(C 风格),把三个分支(子/失败/父)标注清楚;
- 列出进程属性清单(打开文件、环境变量、信号处理器、地址空间、寄存器、cgroups/namespace、cwd、用户……)来定义"大脑"到底是什么;
- 用一个真实 bug(Python 忽略 SIGPIPE)证明这套细节有实际后果。
数据与证据
- 引用 Kamal Marhubi 的 shell workshop(写 shell 的练习)与 rachelbythebay 的 "fork can fail";
- 引用 Kamal 的 bug 复盘:Python 把 SIGPIPE 处理器设为忽略,于是从 Python 里启动的子程序与从 shell 启动时行为不同——环境继承是真实的 bug 来源;
- 引用 man7 的 posix_spawn(3),并当场自我更正。
结论
- 每个进程(除 PID 1 的 init)都在进程树里有父进程;
pstree可以直接看这棵树; execve换掉的是内存、寄存器、正在运行的程序,其余几乎全部继承:"The only thing that changes is, well, all of your memory and registers and the program that you're running. Which is a pretty big deal."- fork 不昂贵的秘密是写时复制(copy-on-write):2GB 内存的父进程 fork 子进程时并不复制 2GB,"look at the old process! it's the same!",直到某一方写入才真正拷贝;
- 子进程死后先成为 zombie(等父进程
wait收走返回值),然后才彻底消失。
关键原文引述
"Instead of having children, what I do is you have a child that is a clone of myself, and then that child gets its brain eaten and turns into ls. Really."
"At this point ls is actually a zombie process! That means it's dead, but it's waiting around for me in case I want to check on its return value (using the wait system call.)"
"It turns out that Linux implements 'copy on write' for fork() calls, so that for all the 2GB of memory in the new process it's just like 'look at the old process! it's the same!'"
(fork 伪代码注释)"int pid = fork(); // now i am split in two! augh! // who am I? I could be either the child or the parent"
"So, your program's environment (environment, signal handlers, etc.) can matter! It inherits its environment from its parent process, whatever that was! This can sometimes be a useful thing to know when debugging."
局限
- 文内自认:"I went and looked at the docs and apparently there is a posix_spawn system call that does basically this. Shows what I know."——即标题叙事(fork/exec 是唯一方式)本身在写作过程中被修正过;OS X 上更推荐 posix_spawn,fork/exec 反而被劝退;
- 没讲 vfork/clone3、pidfd、subreaper 等现代机制(2016 年快照);
- ASCII 进程树表达力有限,没有真正意义上的手绘图(本篇是三篇中图解最"轻"的一篇)。
对我的启发
- "克隆自己然后被吃掉大脑"是把最难记的 API 组合压缩成一句话心智模型的范本——解释系统时,先找一个荒谬但精确的比喻;
- 调试"同一个程序在不同环境下行为不同"时,第一反应应是:它继承了谁的信号处理器/环境变量?(对我的 Agent 项目尤其有用:Python subprocess 起的进程会继承 Python 的 SIGPIPE=ignore)
2. 篇二:Async IO on Linux: select, poll, and epoll(事件循环的内核地基)
链接:https://jvns.ca/blog/2017/06/03/async-io-on-linux--select--poll--and-epoll/
核心问题
Web 服务器同时有几千个连接,怎么知道哪个连接有新数据而不浪费 CPU 轮询?Julia 从 Michael Kerrisk 的《The Linux Programming Interface》(TLPI) 第 63 章 "Alternative I/O models" 出发,回答三个问题:select/poll/epoll 有什么区别?为什么 Linux 服务器最终都用 epoll?你的程序什么时候在用 epoll?
主要主张
select/poll 的本质缺陷是内核不记得监视列表——每次调用都要从头检查所有 fd;epoll 用"注册一次、内核记住、返回时只报就绪者"解决了这个问题,万级 fd 下快三个数量级。这是 node.js / Go / gevent / nginx 事件循环的共同地基。
方法
- 从书到内核源码:引用 TLPI 第 63 章后,去 Linux 4.10 源码验证"poll 和 select 用同一套代码"的惊人结论(给出 fs/select.c 行号级链接);
- 从签名反推性能:对比两个函数原型,指出 select 的
nfds参数使它必须循环 0..nfds,而 poll 只遍历稀疏给出的pollfd数组——"poll can perform better than select if you have a sparse set of file descriptors"; - 用 strace 实证:贴出真实 strace 输出,展示 Go 程序做一次 DNS 查询时 runtime 的 epoll 调用序列(epoll_create1 → epoll_ctl ADD fd 3/4 → write 两个 DNS 查询 → read 得到 EAGAIN → epoll_wait);
- 引用性能表格(TLPI,100,000 次监视操作)并自己下结论。
数据与证据
TLPI 性能表(100,000 次监视操作的耗时,原文照录):
| # operations | poll | select | epoll |
|---|---|---|---|
| 10 | 0.61 | 0.73 | 0.41 |
| 100 | 2.9 | 3.0 | 0.42 |
| 1000 | 35 | 35 | 0.53 |
| 10000 | 990 | 930 | 0.66 |
- fd 越多差距越大:10000 个 fd 时 epoll(0.66)比 select(930)快约 1400 倍;
- 书中引文(原文引用):"On each call to select() or poll(), the kernel must check all of the specified file descriptors to see if they are ready."
- 生态证据链(全部附源码链接):node.js → libuv;Python gevent → libev/libevent;Go → runtime/netpoll_epoll.go("it's only about 100 lines which is interesting");nginx → ngx_epoll_module.c。
结论
- epoll 三步走:
epoll_create(拿 id)→epoll_ctl(注册 fd)→epoll_wait(等更新); - level-triggered vs edge-triggered 两种通知模型的区分("get a list of every file descriptor you're interested in that is readable" vs "get notifications every time a file descriptor becomes readable");
- epoll 能监视 pipes/FIFO/sockets/消息队列/inotify/设备,唯独不能监视普通文件——因为"files are weird! You can write to the middle of a file!"(文件没有"有新数据可读"这种事件语义);
- 在 strace 里看到
epoll_wait≈ 你的程序在用事件循环做网络 I/O(Go/node/gevent/nginx 都是)。
关键原文引述
"Instead of spending all CPU time to ask 'are there updates now? how about now? how about now? how about now?', instead we'd rather just ask the Linux kernel 'hey, here are 100 file descriptors. Tell me when one of them is updated!'."
"The first surprising thing I learned from this chapter are that poll and select fundamentally use the same code."(随后用内核源码证实)
"The kernel doesn't remember the list of file descriptors it's supposed to be monitoring!"
(strace 注释)"# then it uses epoll to wait for responses" / "16016 epoll_wait(5, <unfinished ...>"
"Probably there are some wrong things in this post, let me know what they are!"
局限
- 文末自认可能有错,并把多线程场景的坑交给 Marek 的三篇文章(select is fundamentally broken、epoll is fundamentally broken 1/2、2/2)——epoll 对多线程程序的支持历史上不好,Linux 4.5 才有改进;
- 性能表是转引 TLPI(kernel 2.6.x 时代数据),非作者自测;未讨论 io_uring(2019 后的新 I/O 模型);
- 本篇的"图解"主要是表格与真实输出,视觉化程度弱于漫画篇。
对我的启发
- 从函数签名读出性能是极强的方法论:
select(nfds, ...)vspoll(pollfd[], nfds)一个参数差异就泄露了算法差异; - strace 里看到
epoll_wait就知道是事件循环——这给我的程序诊断提供了直接信号; - "内核不记得名单" → "内核记住名单"是理解一切 I/O 多路复用 API 演进(包括 io_uring)的一把钥匙。
3. 篇三:How containers work: overlayfs(容器分层的 mount 实验)
链接:https://jvns.ca/blog/2019/11/18/how-containers-work--overlayfs/
核心问题
Docker 镜像动辄几百 MB(Anaconda 800MB–1.5GB),每个容器都以镜像为"白纸"启动——如果每个容器都拷贝一份镜像,磁盘和速度都不可接受。Docker 是怎么共享镜像内容的?答案是 overlay filesystem。
主要主张
overlayfs(union filesystem)用两个目录就能讲清楚:lower 只读、upper 可写;读文件先查 upper 再查 lower,写文件只写 upper。Docker 的"镜像分层"就是多个 lowerdir + 一个空 upperdir 的 mount 组合。
方法
先画漫画、再做实验。开头就是一篇手绘漫画(overlay.jpeg,后来发展成 zine《How Containers Work!》),然后全文用可复现的终端实验推进:
mkdir upper lower merged work,在 lower/upper 各放文件(含同名in_both.txt);sudo mount -t overlay overlay -o lowerdir=...,upperdir=...,workdir=... merged;- 依次做三个实验:读同名文件(读到 upper 版本)、新建文件(出现在 upper)、删除文件(意外发现!)。
数据与证据
- 镜像体积:alpine 2.5MB / Ubuntu 16.04 ~27MB / Anaconda 800MB–1.5GB(引 Docker Hub);
- 删除实验的意外发现:
rm merged/in_both.txt后,lower 里文件原封不动,upper 里出现一个字符设备文件(c--------- 1 root root 0, 0)——"I guess this is how the overlayfs driver represents a file being deleted."(即 whiteout 机制,作者当场推理而非查文档); - 多层支持:
lowerdir=/dir1:/dir2:...:/dir25——"Docker images are often composed of like 25 'layers'"; - 失败案例同样记录:mount 报错 "special device overlay does not exist" 是个谎言,真实原因是某个目录没写对;btrfs 时代的副本机制会耗尽 btrfs metadata 空间(引 Reddit 案例 + moby#27653);存储驱动选择引官方文档。
结论
- overlayfs 的读/写/删规则:读=upper 优先回落 lower;写=只进 upper;删=upper 里放一个 character device 白洞标记;
- 容器镜像分层 = "unpacks each layer into a separate directory and then asks overlayfs to combine them all together with an empty upper directory";
- Docker 不只有 overlayfs:btrfs 上用 copy-on-write snapshot,驱动可切换;
- 最有价值的元结论:容器的"魔法"可以用一条 mount 命令在没有 Docker 的前提下复现和观察。
关键原文引述
"I wrote a comic about overlay filesystems for a potential future container zine this morning, and then I got excited about the topic and wanted to write a blog post with more details."
"When a process reads a file, the overlayfs filesystem driver looks in the upper directory and reads the file from there if it's present. Otherwise, it looks in the lower directory."
"But what happened in upper is a little strange: there's a file called upper/in_both.txt, but it's a… character device? I guess this is how the overlayfs driver represents a file being deleted."
"I think containers often seem like they're doing 'complicated' things and I think it's fun to break them down like this – you can just run one mount incantation without actually doing anything else related to containers at all and see how overlays work!"
局限
- 未展开 copy-up 细节(改 lower 中文件时先复制到 upper)、rename 跨层行为、
workdir的作用机制; - "I assume that's how containers with many Docker layers work"——分层到 Docker 的映射是推测,未用实验验证;
- 2019 年快照:未覆盖 containerd snapshotter、fuse-overlayfs/rootless 等新进展。
对我的启发
- "先漫画后实验"的内容生产顺序:画图确定心智模型 → 终端实验验证 → 把意外发现(白洞文件)写进文章,意外比正确更有教学价值;
- 调试容器磁盘/文件可见性问题时,直接想到 lower/upper:容器里"删了还在"或"改了没生效"九成是层的问题。
4. 方法论佐证:她自己的"图解词汇表"
来源:How to teach technical concepts with cartoons(2017-09-28,本次精读)
这篇解释了上面三篇的"图解"从何而来。要点:
- 图解 ≠ 绘画:"There actually is a skill to explaining technical concepts to people with drawings... It is just a different skill than like 'drawing an elephant that looks like an elephant'"。她的全部视觉词汇就是几个基础形状(文中 artistic-range.jpg)。
- 六种图解模式(每种配例图):
- Personify:把系统变成会说话的角色(kubelet ↔ API server;laptop/router/OS/program)——"Taking a complicated computer system and breaking down 'ok, these are 3 main important characters in this system' is incredibly useful."
- Show a scene:让角色互动(两个程序抢同一个 mutex,program 2 心想 "not my turn yet");
- Make a list:列表(网络工具清单、Unix 进程属性清单)——"none of them claim to be exhaustive";
- Make a diagram:手绘图(内存碎片、UDP 包结构,用灰色弱化次要字段);
- Make a joke + Paste some computer output:直接把手绘的 strace 输出放进漫画;
- Structure/panels:用分格控制阅读顺序。
- 信息量标准(最重要的判断):图解有两种用途——"draw diagrams / cartoons that make what you're teaching more clear" vs "use drawings to make what you're teaching more fun"。她点名批评《The Children's Illustrated Guide to Kubernetes》:把长颈鹿画全删掉,信息量几乎不变,因为"almost all the actual content is in the text",而且全文没提 etcd(她认为不理解 etcd 就不理解 Kubernetes)。→ 我的图必须删不掉。
- 门槛破除:"drawings don't have to be beautiful to be awesome"——她早期用 Sharpie 画的草图同样获得大量好评;工具只是平板 + Squid 应用。
- 选题方法:"I organize a lot of my comics about some key insight / fact / surprising thing that it took me a long time to learn and was really useful to me."(例:浮点数漫画源于 Stefan Karpinski 给她的三个顿悟)
交叉验证:三篇精读文章恰好是这套方法论的落地——proc 篇 = personify + list(进程属性清单);epoll 篇 = paste computer output + 从签名推理;overlayfs 篇 = 手绘漫画 + 实验。
5. 七维总提取(跨三篇综合)
5.1 核心问题
系统级概念(进程创建、I/O 多路复用、容器文件系统)在 man page 和教科书里抽象、令人生畏,开发者"不知道自己不知道什么"。这三个概念分别决定了:shell 怎么运行命令、node/Go/nginx 为什么能扛万级连接、Docker 镜像为什么能分层复用。如何让普通开发者快速建立正确且可迁移的心智模型?
5.2 主要主张
复杂系统行为可以压缩为极简心智模型 + 亲手实验:
- 进程启动 = fork(克隆)+ exec(换脑)+ wait(收尸);
- epoll = 内核记住你的监视名单(vs select/poll 每次从头全扫);
- overlayfs = lower 只读 + upper 可写的联合目录,Docker 分层只是它的一种用法。
配套主张(方法论篇):图解必须承载信息量(clear > fun),画图不需要绘画技能。
5.3 方法
三段式:(a) 从一个惊讶事实或个人困惑切入("poll 和 select 用同一套内核代码""删除文件会留下字符设备");(b) 最小可复现实证——kernel 源码行号链接、strace 真实输出、mount 实验、TLPI 性能表;(c) 视觉化收尾——ASCII 图/手绘漫画/表格;全程引用一手资料(man pages、内核源码、Docker 文档)并公开标注不确定处。
5.4 数据与证据
- epoll 性能表(TLPI):10k fd 下 100k 次监视操作 epoll 0.66 vs select 930 vs poll 990(μs,约 1400 倍差距);
- strace 输出显示 Go runtime 用 epoll_create1/epoll_ctl/epoll_wait 完成 DNS 查询(A+AAAA 两个查询的猜想);
- overlayfs mount 实验全记录:同名文件读到 upper 版、新文件落 upper、删除产生
c---------字符设备白洞; - 生态证据:libuv / netpoll_epoll.go(约 100 行)/ ngx_epoll_module.c / Docker storage driver 文档;
- kernel 源码行号级引用(fs/select.c 的 select/do_select/poll/do_poll 定义)。
5.5 结论
三篇合起来给出一条主线:Linux 系统是可观测、可实验的对象,不是黑盒。进程环境继承解释"同一程序不同父进程行为不同"类 bug;epoll 的"内核记住名单"是事件循环高性能的原因;overlayfs 的 lower/upper 规则解释容器镜像的共享与隔离。三者都可用一条命令(pstree / strace / mount)直接观察。
5.6 局限
- 作者文内自认(两处):"Shows what I know."(posix_spawn 更正)与 "Probably there are some wrong things in this post";
- 快照时效:2016–2019 年的文章,未覆盖 io_uring、containerd snapshotter、pidfd、epoll 多线程在 Linux 4.5+ 的持续改进;
- 证据强度不一:epoll 性能表是转引(kernel 2.6 时代),overlayfs→Docker 的映射是 "I assume";三篇都是个人学习笔记而非系统评测;
- 图解密度不均:proc 篇无手绘图,仅 ASCII 树。
5.7 对我的启发
- 写作结构可复制:惊讶事实 → 最小实验 → 图,是技术博客的高转化结构;把"意外发现"(白洞文件)当文章高潮而非删掉它。
- 学习顺序可复制:新系统先跑最小实证(strace/mount/dig)再读文档;从函数签名读性能差异。
- 图解标准:画图前自问"删掉这张图,信息少了吗?"——少则留,不少则删(clear > fun 标准)。
- 心智模型库:克隆-换脑(进程)、内核记住名单(epoll)、上下层目录(overlayfs)——这种"一句话 + 一张图"的压缩是可迁移的笔记格式。
- 诚实即可信:公开自我更正(posix_spawn)和标注推测("I assume")不损害反而增强信任,值得在我的笔记/博客里效仿。
- 直接工程收益:Python subprocess 继承 SIGPIPE=ignore 的坑;strace 见 epoll_wait = 事件循环;容器文件异常先查 lower/upper 层。
6. 附:本次未能完成/未验证的事项
- 三篇原文的手绘漫画内容无法直接"看图"核实(本次以文本 + img src 核实了图的存在与位置,如图 overlay.jpeg 位于篇三开头);图内具体画了什么以原文文字与我的既有知识推断,已标注。
- TLPI 性能表为转引(Julia 转引自书),未在本地复测;如需严格验证需自行跑 epoll 基准。
- 未逐条核对 2019 年后 Docker 存储驱动的现状(原文已自带官方文档链接)。
引用来源汇总
- https://jvns.ca/blog/2016/10/04/exec-will-eat-your-brain/ (精读,2016-10-04)
- https://jvns.ca/blog/2017/06/03/async-io-on-linux--select--poll--and-epoll/ (精读,2017-06-03)
- https://jvns.ca/blog/2019/11/18/how-containers-work--overlayfs/ (精读,2019-11-18)
- https://jvns.ca/teach-tech-with-cartoons/ (方法论精读,2017-09-28)
- https://jvns.ca/ (全站归档,选文依据)
- https://wizardzines.com/ (zine 目录,背景来源)
文内二手引用:TLPI (Michael Kerrisk)、man7.org man pages、torvalds/linux fs/select.c、libuv、golang netpoll_epoll.go、nginx ngx_epoll_module.c、idea.popcount.org(Marek 三篇)、kamalmarhubi shell-workshop 及 bug 复盘、rachelbythebay fork 文、docs.docker.com storage drivers、moby#27653。