第25讲 · 文件系统 API(2)
2026 南京大学《操作系统原理》第二十五讲 · 课堂实录重构
| 主讲 | 蒋炎岩(Yanyan Jiang) |
| 视频 | https://www.bilibili.com/video/BV12gVs6SExQ/ |
| 课程主页 | https://jyywiki.cn/OS/2026/ |
| 本讲讲义 | https://jyywiki.cn/OS/2026/lect25.md |
| 讲义 / 幻灯片作者 | 蒋炎岩,https://jyywiki.cn/ |
| 讲义 / 幻灯片许可 | CC BY-NC 4.0 |
| 图片来源 | 书中插图为课堂演示与讲义渲染图;演示类截图为视频画面 |
对应讲次:第25讲(南京大学 操作系统原理)
制作模型:deepseek-flash(coding-agent / pi;据 ~/.pi/agent/settings.json)
编者说明:本书由课堂录像的语音字幕重构而成,术语、章节骨架与参考链接已按课程官方讲义(
lect25.md)校准。讲师的口语风格、类比与案例被尽量保留;编辑性的补充一律以「编者说明」或「编者拓展」标出。
引子
(参考时间: 00:00)
上节课讲了文件系统增删改查的 API(mkdir、link、unlink、symlink)、软/硬链接、以及文件的属性(mode、metadata、extended attributes)。
今天在 CRUD 的基础上,设计真正的「文件系统级」API。 思路回到根本:文件系统是一个数据结构(Abstract Data Type)——既然是数据结构,就可以定义各种更「花哨」的操作。本次课讲四件事:
- 监控(inotify,→ eBPF)
- 快照(persistent data structure,→ git、btrfs)
- 覆盖/联合(overlay、union fs,→ Docker)
- 终极虚拟化(既然文件系统是数据结构,凭什么不能自己写一个 → FUSE)
编者拓展|数据结构里的「全局操作」trick:讲师举了个例子——如果数据结构要支持「对所有元素加 10」,你既可以遍历一遍逐个加,也可以把「+10」悄悄记在数据结构旁边(惰性标记):取元素时再加上,插入 9 时插 −1。「数据结构里可以干很多 fancy 的事情,凭什么文件系统不能干?」
一、监控文件系统的变化
1.1 需求:文件变了通知我
(参考时间: 00:04)
典型场景:Web Server 的 debug/reload——代码改了要重新加载(比如 Next.js:你一保存 CSS,网页瞬间就变了);让 coding agent 一步一步改前端,你能看界面一点点变化。「这是一个非常实用的需求。」
原则上不用操作系统帮助也能实现——轮询 + diff:
# 用 stat 打印所有文件的修改时间,睡眠两秒后再打印,然后 diff
stat --format='%n %Y' * # 立即得到所有文件的 mtime
sleep 2
# 修改 a.txt,再 stat 一次
diff name1 name2 # 没改就什么也不返回
📷 轮询 + diff:一行脚本就能实现「哪些文件改了」
回到原视频 (00:06:30)
但问题:如果目录里有几百万个文件,每次遍历就是几百万次元数据查询——效率太低。
1.2 inotify:让文件系统主动通知你
(参考时间: 00:09)
现代操作系统提供了机制——inotify_init 和 inotify_add_watch:
int inotify_init1(int flags); // 返回一个文件描述符
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
int inotify_rm_watch(int fd, int wd);
// 配合 read() 读出 struct inotify_event
- 它返回一个文件描述符——「Linux 的设计里所有东西都用 fd 实现」——所以你可以用
poll/select/epoll去监听它,甚至同时持有多个 inotify fd; - 注意:它是不递归的。
使用场景:文件管理器的自动刷新、编辑器(VSCode)的自动重载、systemd 监控单元文件、tail -f……
📷 inotify:返回 fd,可以用 poll/epoll 监听文件系统事件
回到原视频 (00:09:30)
Watchdog 演示(一个 Python 脚本,封装了 inotify):
# web-code 出来的 watchdog:递归 watch 一个目录
# 创建目录、touch a.txt 时,能看到 created / modify 事件
📷 watchdog 演示:touch 一个文件时,收到 created,并且它的父目录 mtime 也变了
回到原视频 (00:12:00)
编者说明:一个你可能没留意过的细节——你改一个文件的 mtime,它上一级目录的 mtime 也会变。但不会一层层向上传递:「
t/1/2里 touch 一个文件,只有t/1和t/1/2变了。」(因为只有直接父目录被open/修改。)
1.3 从 inotify 到 eBPF
(参考时间: 00:13)
回忆上节课的 block I/O trace:你能看到系统里所有进程执行块设备 I/O 的操作(读、写、同步、落盘)。「这很有启发性」——我们真正想监控的不只是文件变化,而是整个操作系统这个「程序」运行时的各种状态变化。「本质上操作系统也是代码。」
strace:告诉你应用程序调用了哪些系统调用;ltrace:告诉你调用了哪些库函数;inotify:告诉你什么时候调用了文件系统相关的系统调用(「一种全局的 strace」);- 它们本质都是从操作系统内核的执行中提取信息。
自然的想法:既然想要 trace 操作系统的代码——能不能写一段代码,插到内核里,让内核每个函数执行时都跑一下它?(类似 LD_PRELOAD 的 hooking,但这次是在内核里。)「恭喜你,你发明了 eBPF(extended Berkeley Packet Filter)。」
📷 eBPF 的动机:把 strace 搬进内核;网络设备不能 read/write(速度太快,用 ring buffer + 中断)
回到原视频 (00:19:30)
编者拓展|eBPF 是什么:「你听它的名字就知道——它最早用在网络子系统上。」因为网络是世界上最快的设备之一——「数据中心的网卡,你看它的包计数,甚至会觉得 64 位整数都快溢出了」,所有 CPU 一起响应中断才跟得上。「最终所有 I/O 设备都活成了网卡的样子。」
eBPF 的关键特性: - 是一个虚拟机(RISC-like 指令集,11 个寄存器,R0 是返回值;每次调用入口 R1 指向一个 context 结构体,包含函数参数、function name 等); - 有一条特殊指令
bpf_callhelper(如bpf_get_current_pid_tgid),还能访问 BPF map(预实现好的数据结构); - 有非常严格的验证器:只允许 bounded execution(不能有 unbounded 循环),所以无法随意修改内核内存,也无法自己实现数据结构(要用 map); - 通过bpf()系统调用把字节码注入内核,verify + JIT 成本地指令,勾住关键的 hook point / trace point。编者拓展|eBPF 在生产中的一个真实 bug:讲师讲了自己学生写的 BPF program——「里面有个
printf,我在前面加一个循环,那个printf就不执行了。但根据语义它百分百是要执行的——编译器不知道为什么把它优化掉了(分析有错,认为那是死代码,但它显然不是)。」
eBPF 能做什么?答案是任何事。 比如一个 Y 字开头的 eBPF 程序捕获你所有键盘按键和鼠标移动;再结合 execve 等系统调用的 trace,就能完整复原你在 Linux 电脑上干的所有事的轨迹。
📷 用 eBPF 捕获键盘/鼠标事件,可以做「学习轨迹复原」(甚至让大模型点评你 Vim 的操作)
回到原视频 (00:23:00)
编者拓展|「Specification is all you need」:讲师说,以前开发 web 后端需要懂很多库和机制,今天你只需要一个 AI——把功能描述清楚,「哪怕你写一个性能奇烂无比但正确的 prototype,让 AI 尽可能借用操作系统机制做高效实现就行」。他甚至引用了 Ion Stoica 的论文 The Time is Here for Just-in-Time Systems:「未来系统可以 synthesize from scratch,specialized to environment、workload and required system properties。」——「如果你对现在的操作系统不满意,就直接说『帮我把内核改一下吧』,它 on the fly 就改好了。我真的觉得这件事很快会发生。」
二、快照:persistent data structure
2.1 从二叉树到 git
(参考时间: 00:31)
Random read + append-only write = 任何持久化数据结构。 比如在一棵二叉树上插入节点:只把从根到叶的那条路径复制一份($O(\log n)$),旧版本和新版本都保存下来了。
你从第一节课就在用的工具就是这么干的——git。
编者拓展|「什么是 git?」:讲师说,研究生复试时问「什么是 git」,有人摇头不知道,有人说是「从网上下载代码的」,没有一个能答出 rebase 是干什么的。「我当时看到就雷倒了。」
git 就是一个 persistent data structure——所有东西都在 .git 里:
- 你创建、提交一个文件,得到
.git/objects下的一个对象(a6/9...); - 三大对象:blob(文件内容)、tree(目录树,含 mode + 名字 + 指向 blob/tree 的 hash)、commit(含 message、author、时间戳、一个 tree object、若干 parent object);
- 对象都是压缩存储的——讲师让 AI 现场写了一个
git cat-file类似工具,把对象内容打印出来,看到旧版的hello和新版的hello world真的都在 objects 里。
📷 git 的三大对象:blob / tree / commit,全部压缩存储在 .git/objects
回到原视频 (00:37:15)
$ git cat-file ... # 现场用 AI 写的工具解出对象内容
hello # 旧版本
hello world # 新版本
git 剩下的一切都是「文本形式的指针」:
.git/HEAD -> refs/heads/master
refs/heads/master -> <commit hash>
所以:一个分支就是一个指向某个 commit object 的指针;commit 时指针往后移;HEAD 也是(往后退的)指针。你可以依葫芦画瓢造一个 tail 指向旧的提交,然后 git diff HEAD tail。
📷 git 的 head/tail 与 refs:分支、HEAD 都是指向 commit 对象的文本指针
回到原视频 (00:39:00)
编者说明:讲师现场让 AI 把某个项目的 commit history 对象图画出来(Mermaid / SVG)——「你会得到一个很不错的结构图。」
📷 用 AI 从 git 对象生成流程/结构图,帮助理解项目到底发生了什么
回到原视频 (00:44:00)
2.2 merge / rebase / stash / worktree
(参考时间: 00:45)
分布式版本管理:一个 commit 对应一个 tree,你在一台机器上发展出 A、B,别人发展出 C、D——不再是线性历史,git 会做合并:
- merge:加一个节点 E(两个 parent,三方合并:公共祖先 + B + D);
- fast-forward:本地没有 A、B,直接把远端的 C、D 拉过来;
- cherry-pick:任何一个提交本质是一个 diff(A→B),可以把这段 diff 挑过来;
- rebase:先把 C、D 复制过来,再在上面依次 cherry-pick Δ1、Δ2,得到 A'、B'——「就好像别人先做完,你在他的基础上接着做」。
编者拓展|慎用 rebase:「一旦 cherry-pick 发生冲突,基本上就要放弃 rebase,还是得 merge。因为你 resolve 了这个冲突,下一个又冲突了,两条线时间拉得越长就越痛苦。而且即便 auto rebase 成功了,也可能隐藏逻辑冲突——你最好回过头来 review 这三个 diff。」
stash 也是一个 commit object(有两个 parent:HEAD 和 next stash),所以它是一个栈,可以 push/pop。
📷 git 的三种合并:merge(新节点 E)/ fast-forward / cherry-pick 与 rebase
回到原视频 (00:47:00)
编者拓展|历史总在重演:git 是为单线程设计的(假设开发者一次做一件事)——「就像早期的 UNIX 只有进程、没有线程。」当你开始 multitasking(老板不断打来电话让你切换 feature 分支、hot fix),
stash是个栈,设计之初就没让你多线程工作。于是 2015 年 git 增加了 work tree:「既然这是一个 persistent data structure,我们为什么不能把树直接复制一份呢?因为复制是没有代价的——我永远不修改已有对象,只要一个指针指着某个位置就可以了。」
$ git worktree add ../hot-fix # 创建一棵独立的工作树(共享 .git 对象池)
worktree 里的 .git 不是一个目录,而是一个文件——里面是一个指针(指向真实的 .git 与 HEAD)。「这是 persistent data structure 带来的一个免费的 feature。」——而今天,worktree 成了 Agent 时代非常标准的特性(可以同时放一堆 agent 在同一个项目上)。
📷 git worktree:因为 persistent data structure,复制一棵树只需要一个指针
回到原视频 (00:56:30)
2.3 btrfs:把整个文件系统做成 persistent 结构
(参考时间: 00:58)
既然 git 是 persistent data structure,能不能把文件系统整个变成 persistent?「没有道理不能。」
btrfs(B-tree FS,十几年前就可以在生产用了)——「Everything is a b-tree」:
- 为了在块设备上构建,它从零实现一个块设备上的 persistent data structure——用 B 树(4096 叉树) 把索引层数压得很浅;
- 所有东西(volume、文件系统目录树……)都以 append-only / copy-on-write 的方式做;
- 支持免费快照:只需要一个 fd,一个
ioctl(BTRFS_IOC_SNAP_CREATE)——「只是把指向当前 root 的指针记下来、起个名字」。「恭喜你,又发明了一个改变世界的技术。」
📷 btrfs:Everything is a b-tree,copy-on-write,一个 ioctl 创建免费快照
回到原视频 (00:59:30)
三、Overlay / Union FS
3.1 把两个目录「叠」起来
(参考时间: 01:01)
编者说明:这一切最早追溯到 UnionFS 的论文。文件系统是目录树——除了增删改查和监听,还能设计什么不一样的机制?
Union FS 的想法:一个挂载点里有 upper 和 lower 两个目录。创建一个虚拟目录 merge:
- 你问
merge要文件时,先去upper看,有就直接返回; upper里没有,就去lower找;- 两边都没有,
merge里也没有。
「upper 拼在上面,lower 拼在下面。」于是 lower 可以是一个「系统盘」,upper 里对 lower 做修改,而 lower 本身保持不变。
📷 Union FS:upper 拼在 lower 上面;读时优先 upper,写只落到 upper
回到原视频 (01:01:30)
两个 Killer Application:
- 刻盘:
lower放 600MB 相同的文件,upper里放每张光盘不同的 CD-Key(1MB)——省下大量空间,可以并行刻 16 张盘; - 防「成砖」:把整个根文件系统当作
lower,在另一块盘上做一个空的upper,然后apt dist-upgrade——所有写入都落到upper(apt的 trace 本质就是写文件、写系统状态)。「成砖了就回到 initramfs 模式,把 overlay 拿掉,你的系统又回来了。」
3.2 overlayfs 与 Docker
(参考时间: 01:08)
Linux 的机制叫 overlayfs,而且做得更彻底:可以挂载不止一个 lower(这正是 Docker 的关键):
- 可以挂多个
lower,但只能挂一个upper,还需要一个临时的workdirectory(必须为空); - 有意思的行为:
- 修改一个
lower里的文件 → overlay 变了、lower没变,upper里多了一个改过的副本; - 删除一个
lower里的文件 →lower永远不变,upper里产生一个字符设备文件(whiteout,「你cat它会 permission denied——这是一个假装出来的不存在的文件」)。
📷 overlayfs 演示:修改反映到 upper,删除产生 whiteout 字符设备
回到原视频 (01:09:30)
编者拓展|「网吧管理器」与考试竞赛管理:「网吧里用户会任意破坏你的电脑,你甚至愿意给他高权限——不要紧,把系统放在
lower,给他一个upper,下机了直接把upper删掉。」讲师说自己在做大规模考试/竞赛管理时也用这套:「把/home做成 overlay,把实际数据放在用户访问不到的地方,上午场在upper1工作,下午场挂另一个upper——下午场看不到上午的工作目录,但最终回收 U 盘时你还能看到所有人的东西。」
Docker 就是一个又一个 layer:Dockerfile 里每个 RUN 都会启动一个容器、创建一个 upper,底层是上一层的 lower;命令跑完,upper 就「固化」成一个新的 layer。一层层叠上去(又是一个 persistent data structure:底层是 append-only,只有最上面可变)——「你改了一条 RUN,就必须从上一条命令对应的 layer 重新 replay。」
📷 Docker:每个 RUN 产生一个 layer;多个 upper 共享一个 lower = 容器
回到原视频 (01:13:00)
四、终极虚拟化:自己写一个文件系统
(参考时间: 01:15)
前面不论 inotify、快照、还是 CRUD,都可以用增删改查实现;但 overlay 改变了文件系统的语义。再往后想一步:文件系统就是一个数据结构,数据结构可以随便实现——「为什么我不能直接写一个 fs.c 来实现一个文件系统?」
编者说明:
/proc就是内核里的一个文件系统——它的getdents64由它自己提供,所以有些目录你能cd进去,但ls看不到。「这个行为完全由文件系统的实现决定。」
FUSE(Filesystem in USEr space):内核把 lookup/read/write 转发到 /dev/fuse,你只要实现一个 fuse_operations 结构体(open、read、readdir、甚至 set extended attribute……)。「从此文件系统再也不是 CRUD。」
📷 FUSE:实现一个 `fuse_operations` 结构体,在用户态「虚构」出任意目录树
回到原视频 (01:17:30)
GGFS(Galgame FS):还记得那个用符号链接做的 Galgame 吗?这次用 FUSE 实现一个文件系统:
- 不需要真实文件——
readdir/read/write返回什么完全由你的代码决定; - 「往任何一个文件里写入数据」就会回调你的实现,驱动游戏运行——目录变化、打字机效果……
- 「代码是 Turing complete 的,理论上你可以做任何事。」
编者拓展|FUSE 能做的疯狂事情:把远程 SSH 目录映射成本地目录(
sshfs);把远程 git 仓库变成目录;用数据库存文件系统(db-fs,用 SQLite 引擎实现,靠 append-only 保证一致性);反过来把一个不是文件系统的东西挂载成文件系统——把一个 JSON / key-value 变成目录树(key 变目录、value 变value.txt,写入即更新 JSON)……「你可以把一份『学习资料』真正隐藏起来——让
getdents64/readdir看不到它,但你可以cd进去。只有你知道名字才能进去,没有任何人能进去。」
总结
- 监控:轮询 + diff → inotify(返回 fd,可用 poll/epoll;不递归)→ 本质都是从内核执行提取信息 → eBPF(把代码注入内核,hook 关键点);
- 快照:persistent data structure(路径复制)→ git(blob/tree/commit,一切皆文本指针,merge/fast-forward/cherry-pick/rebase/stash/worktree)→ btrfs(Everything is a b-tree,copy-on-write,免费快照);
- Overlay/Union FS:upper 叠 lower(读优先 upper,写只落 upper);overlayfs(多 lower、一个 upper、whiteout);Docker layer;
- 终极虚拟化:文件系统就是数据结构 → 在用户态实现 FUSE(GGFS、sshfs、db-fs、json-fs……);
- Takeaway:「文件系统是一个数据结构」——就这一句话,从增删改查开始,你只要有想象力、借助操作系统机制,就可以做任何事情。
编者拓展|Everything is a file 的终极实现:这一讲的四个层次层层递进——监控(改变「谁通知你」)、快照(改变「时间维度」)、overlay(改变「目录树的语义」)、FUSE(改变「文件系统本身是什么」)。「你从增删改查开始,你只要有想象力,借助操作系统机制,就可以做任何事情。」
附:官方参考与延伸阅读
本讲的讲义见课程主页,以下链接按书中出现顺序整理:
| 主题 | 链接 |
|---|---|
| eBPF 官网 | https://ebpf.io/ |
BPF 手册(bpf(2)、bpf-helpers(7)) |
https://man7.org/linux/man-pages/man2/bpf.2.html |
| git 对象模型(Pro Git: Git Internals) | https://git-scm.com/book/en/v2/Git-Internals-Git-Objects |
| git worktree | https://git-scm.com/docs/git-worktree |
| btrfs | https://btrfs.readthedocs.io/ |
| overlayfs(内核文档) | https://docs.kernel.org/filesystems/overlayfs.html |
| Docker(容器与 layer) | https://docs.docker.com/get-started/docker-concepts/building-images/ |
| FUSE | https://github.com/libfuse/libfuse |
| GGFS / Galgame FS(persistence 演示) | https://jyywiki.cn/OS/demos/persistence/ |
| Ion Stoica, The Time is Here for Just-in-Time Systems | https://www.cs.utexas.edu/~eunsol/ |
| OSTEP(第 37 章 Files and Directories) | https://pages.cs.wisc.edu/~remzi/OSTEP/ |
阅读材料:读 git 的 Pro Git「Git Internals」一章,理解三大对象;在自己的机器上跑一次 git worktree、overlayfs、sshfs。
版权说明:本讲讲义与幻灯片作者为蒋炎岩,采用 Creative Commons BY-NC 4.0 许可,来源于 https://jyywiki.cn/OS/2026/。视频画面与字幕版权归原作者所有。本书为课堂内容的学习性重构。