glm5.1转写的字数很多,但是可读性没有opus4.6那么强,因此又用opus转写了一遍。目前一共是三个版本:sonnet、glm5.1、opus4.6。大家如果想比较一下三种模型的智能的话,可以对比观看。 我个人感觉目前opus4.6的模型能力还是独一档,它对文字的掌控力是很强的。
阶段小结
不知不觉就已经讲到 Virtualization 部分的最后一节课了,进度有一点快,因为每一次课都在给大家调代码,每一次课都会有新的工具和知识。所以今天 Hacking Day 也算是一个回顾,给大家讲一讲在操作系统上面,应用生态是怎么构建起来的。今天大部分内容应该都不会在考试里出现,但可能会对大家比较有用。
我们先复习一下。我们花了好多次课讲虚拟化,虚拟化到底讲了什么?我们从程序和进程开始,讲进程的地址空间,里面有游戏的修改器;然后再讲到文件描述符,我们怎么去打开操作系统里面各种各样的对象。你们做了实验,做了 PS3,做了 Sperf,也知道 C 的标准库是怎么样一层一层实现起来的。
在上一次课的时候,我合上了整个 Virtualization 部分的最后一片拼图——链接和加载。但其实我整个学期到现在,所有的时间我都是在讲系统调用,就是在讲操作系统给应用程序提供的各种各样的 API。所以上节课讲的其实是 execve 系统调用的行为。
你们有可能没有反应过来,但我今天这样一讲,你再回头想一下。所谓 execve,我们说在任何一个程序执行一条指令的时候,它都有一个确定的行为。如果它执行的是 execve,它的行为就是把 execve 的那个 path 对应的二进制文件——它描述的一个可执行文件的初始状态——搬到这个进程的内存里。
这里面包含了什么呢?寄存器里面有一个 stack pointer,不管是 x86、ARM 还是任何一个体系结构都有一个 stack pointer。Stack pointer 里面会有初始的进程的栈,包括上面的 auxiliary vector、一个初始的内存布局。我们甚至看了 Linux 内核里面加载器的代码,你知道了这里面的每一项都是有代码写进去的,没有任何的魔法。
除此之外我们还讲了加载——有静态的链接和加载,动态的链接和加载。execve 的行为就是把 a.out 的所有——就是 ELF 文件里面用 readelf 看到的那些 PT_LOAD 段——代码、数据、未初始化的数据都加载到进程的地址空间里。这就是 a.out 的内存映射。
此外我们还有 interpreter。我们讲了一个动态链接的 a.out,在你用 starti 调试它的第一条指令的时候,这两部分的内容是在进程的地址空间里的;但是像 libc 这些都不在进程的地址空间里。如果用 ldd 查看的话,它可能会用到很多动态链接库,但是在第一条指令执行的时候,它们都不在地址空间里,而是由我们的加载器再用系统调用——用 mmap 再把那些二进制文件、ELF .so 文件里面的一段一段内存搬到地址空间里。
等到所有的这些东西都准备完毕以后,还会有一个 PC,对应的是 interpreter 里面的第一条指令。那这个程序就开始往后执行,放到 CPU 上往后执行。所有的行为就是你们在 strace 里面能看到的。所以其实我讲的是 execve 系统调用的行为。
再回过头去把那些代码在 AI 的帮助下看一下,你会有一个惊喜:这么复杂的东西其实在概念上也没什么困难的。你在任何时候想要知道任何程度的细节,你就去跟 AI 交互,建立了正确的概念,你就能知道里面每一个字节是怎么来的。你提出正确的问题,AI 会帮你找到答案。
这个学期我到现在一直都是在讲系统的行为。后面我会讲一些具体的由这些系统调用引发的更大的麻烦,比如说并发,比如说持久性的文件系统。但我们前半部分的课程传递的一个 message 是:你们可以通过阅读手册,指导 AI 来写代码,去理解任何你在计算机系统里面看到的、不理解的东西,并且你还可以回过头来问"为什么要这样设计"。
我讲动态加载的时候,其实给了足够多的动机——为什么需要动态链接库。比如我要证明在整个计算机系统里面 libc.so 就只有一份只读的副本:我创建了非常多个打开同一个动态链接库的程序,但是它们并没有占用过多的内存。
这就是我觉得这门课最希望告诉大家的。就算这门课到现在就结束了,我觉得也 OK。因为你们如果再去回看一下,确信自己掌握了这个技能以后,你们就有一定的真正的编程的成熟度——因为你不怕这东西,搞不懂没关系,你就去问就行了。
从 UNIX 到 Linux(毒鸡汤)
今天我会讲一点系统调用之外的内容。我们现在已经知道各种各样的系统调用它的行为是什么了,然后我们再讲这个系统调用是怎么来的,再讲系统调用来了以后它又是怎么样把应用的生态世界给长起来的。这当然也是跟系统调用很有关系的故事。
早期 UNIX 的演化
故事的第一个部分是一篇论文,叫做 The Evolution of the Unix Time-Sharing System。我强烈推荐每一个同学都去阅读一下。有点长,我知道同学们一定不会愿意在课后去读这样的阅读材料,不用着急,我们一会儿再回到这个文档。
它很经典,传递了一个什么样的 message 呢?它告诉你 UNIX 怎么从零开始一点一点实现成现在这个样子的。最早的那个版本的 UNIX 甚至没有 fork。它创建进程的方式不是像 fork + execve 或者我们说的那种更合理的 spawn 可以直接 spawn 一个程序。
早期 UNIX 的 shell 是这样的:如果你要运行一个外部程序,比如说 ls,那个时候还没有管道,它就会把所有打开的文件都关闭,然后把 0、1 这两个文件描述符打开到终端,然后把自己退出——shell 就被从内存里面卸载了。卸载以后用 execve(那个时候的 execve)把一份代码加载到内存里,然后执行。
也就是说在最早的 UNIX 上,一台终端里面最多只有两个程序:一个是 shell,一个是可执行文件。可执行文件退出以后,shell 再被重新加载回来,就来回交替——shell、可执行文件、shell、可执行文件……就像你们大一的时候做的一个小练习那么难的程序。
后面还有很多有意思的故事。比如 Thompson 用 27 行汇编实现了一个 fork。因为在早期的简单进程模型底下,所有的内存可能就是几个指针就描述了——我的代码从哪开始到哪结束,数据从哪开始到哪结束——就是一个 struct proc,跟我们写的那个小的 Crazy OS 一样。在这样简单的模型下,他想要实现一个进程的复制就非常非常简单。
这给我们一个启发:所有复杂的东西在刚开始的时候都是很简单的。你看今天大语言模型已经很可怕了,如果你不停地往前溯源,我们会发现早期它们其实没有现在这么复杂。现在为了把这么大的模型高效地 serve 出来,有各种 PD 分离、KV cache 等等技术,但最早的时候都是你们能理解的最简单的样子。
Minix:操作系统的教学典范
在最早的 UNIX 火了以后,有一位非常有名的教授 Andrew S. Tanenbaum,他写了一个操作系统叫 Minix。Minix 3 是一个非常成熟的操作系统,成熟到什么程度呢?它真的能把一个模仿 Windows XP 的用户界面完整地跑起来。
而且它一度是世界上最 popular 的操作系统——为什么?因为 Intel 的芯片组上有一个额外的 CPU,那个 CPU 上运行的就是 Minix 3,后来才被人发现。也就是说,不管你实际的 CPU 上运行的是 Windows、Linux 还是别的系统,里面都有一个 Minix 3 在跑。
Minix 最早是 1987 年发布的。Tanenbaum 做了一件事,就是写了一本书叫 Operating Systems: Design and Implementation。这本书算是我梦开始的地方。我像你们这么大的时候,就拿着一本操作系统这本书来学习。它前一半是教你操作系统原理,后一半是代码。那个时候我们有惊人的注意力,可以在书上一边看理论,放个书签在前面,然后翻到后面去找代码,一行一行人肉解释。它算是我操作系统的启蒙老师。
Minix 1987 年做到了 UNIX V7 兼容,1997 年的 Minix 2.0 做到了 POSIX 兼容——POSIX 是 UNIX 的标准。2006 年发布了 Minix 3。因为这个兼容性,它还是非常了不起的一个项目。
今天我给大家准备了一个 Minix 的演示环境。有爱好者已经把 Minix 项目完整地存档下来了,你们把这份代码下载下来以后直接 make minix2,就可以从 GitHub 上面把 Minix 的源代码下载下来,包括 Minix 1 和 Minix 2。
编译完成以后会生成 images,包括 Minix 1.7 的 64MB 镜像和 Minix 2.0 的 64MB 镜像——在今天看非常小,但在那个时候其实已经是非常大的了。感谢这个世界里有很多随手可得而且开源免费的工具,我们又用到模拟器了,可以直接 make run。
这是一台 ARM 的树莓派,整个计算机系统都在这个键盘里。但它完全不吃力,可以运行 Minix 1.7.5。它是随书送的代码,每一个买那本 Tanenbaum 的操作系统教科书的人都可以使用这份代码。
你看它这里有些日志信息,比如找到了 HD0,是一个 QEMU hard disk。然后显示 "Minix executing in 16-bit protected mode"。16 位意味着什么?你们没有用过 16 位机,但这是一个如假包换的、UNIX V7 兼容的 Minix 1.7。如果是 Minix 2.0 的话,它甚至是 POSIX 兼容的——和你们在 Linux 里面的体验几乎是一样的。
把时间回到 1987 年,你可以得到一个这样的系统。这个系统里面有几乎所有的 UNIX 体验。比如我可以用 VI 编辑器——VI 不是现在才有的,它是从 ex(一个基于命令的编辑器)变成一个 visualized 编辑器。我可以编辑一个 C 文件,然后为了证明机器确实是 16 位的,我打印 sizeof(x)——只要写这样一段代码就可以知道它是不是真的 16 位的。
这时候所有 UNIX 里面的工具都可以正常使用。我可以 cat a.c,然后管道给 wc。你看,我们今天日常用的现代 shell 里的管道命令,从那个时代就拥有了。然后我们可以用 cc 命令来编译,真的编译出来一个 a.out。用 file a.out 可以看到它是 Minix PC 16-bit 的 executable。运行它,打印出来 sizeof 的值。
这是一个完整的 UNIX 体验。有多完整呢?我们甚至可以用 man 来查看手册,竟然还是有颜色的。man 1 user commands、man 2 syscalls、man 3 library routines。我们还可以 man -k 查看系统里面所有的工具、系统调用、库函数。甚至里面还有完整的源代码——整个 Minix 世界里所有的源代码都在里面。
比如说在 usr/src 里你可以找到所有的源代码,包括内核的实现和所有工具的实现。这是一个完整的、非常可用的操作系统。我们说它是 UNIX V7 兼容,这不是开玩笑的——在 1987 年的时候你找一台它能支持的机器,接一个终端,看到的就是这个东西。
你可以在 Minix 里面修改 Minix 自己的内核或者操作系统的功能,然后在 Minix 里面编译它,编译完以后把内核镜像烧录到磁盘上,启动加载器可以把它加载起来,这个系统就真的可以运行了。你可以用它来做任何事——打印文档、连接打印机,都是可以做到的。
Linux 的诞生
最早的时候 Linux 就是从这里诞生的,因为 Minix 已经非常完整了,但是它有一个致命的缺点——慢。
Tanenbaum 是一个大学教授,他的想法总是比较疯狂和超前。在 1987 年计算机处理器还很慢的时候,他就想做一个只有一个系统调用的操作系统,这个系统调用就叫 send and receive——我可以发一条消息,然后等一条消息回来。
操作系统内部有一些线程或者服务,甚至可以运行在用户态。你跟那些服务去交换消息就行了——相当于 A 同学负责文件系统,B 同学负责管 CPU,C 同学负责管内存。我作为一个进程要申请内存,就跟负责内存的同学发个消息,他做好了给我。它们都以普通进程的方式运行在用户态,这就是当时微内核的想法。
确实是非常超前的想法。我觉得在今天这个想法其实挺好的,可能微内核很快就要复活了。但在那个时候,计算机很慢——你每执行一条系统调用,如果像 Linux 一样,一条 syscall 指令到内核里,没过几个时钟周期就开始执行了;但如果你要发消息的话,你先要把消息准备好,然后通过系统调用发给另外一个进程,那个进程执行完以后再返一个消息给你——性能很低。
所以 Tanenbaum 一直说:我做这个操作系统是为了上课的,是为了教学生什么是操作系统、怎么设计和实现一个操作系统,性能不是我的首要目标。
这就有了 1991 年 8 月 25 日的一个里程碑式的事件。在那个时候有一个来自芬兰的 21 岁年轻人——跟你们在座的差不多大。芬兰是一个非常冷的地方,一年四季有相当长的时间都是冬天,大雪封门,只能窝在温暖的家里从事脑力活动。
然后就有一个天才少年——你们知道这个名字的——Linus。Linux 就是从他这儿来的。你看 Minix 的 X 就是 UNIX,Linux 的 X 也是 UNIX,他把自己的名字放了进来。
他在 Minix 的邮件列表 comp.os.minix(你们可以理解为百度贴吧"Minix 吧")里面发了这样一个帖子:
同志们好,你们这些用 Minix 的人。我现在在做一个 free 的 operating system,just a hobby,won't be big and professional like GNU。
那个时候 GNU 也在开发自己的内核,结果 GNU 的内核到现在都还是难产状态,Linux 已经成为事实标准了。
他说他从来没有想过要支持别的体系结构,就因为手上有一台 386,所以就想做一个 386 上面能取代 Minix 的操作系统。"我从 4 月份就开始搞了,马上就可以给你们用了。"很快他就把最早的一批源代码发了上来。
他早期的 Linux 还需要依赖 Minix 的工具链,因为没有自己的文件系统实现——为了运行 Linux,你需要把磁盘格式化成 Minix 文件系统。上面运行的软件包括了 GNU 的 GCC、Bash 等早期版本。
所以有的时候觉得就是这样——在一个合适的时间、合适的地方,有一个合适的人,就做成了一件事。
在正确的时间做正确的事
我觉得更有意思的是,这个世界上有更多可能合适的人在不合适的时间做了合适的事,然后这个事情就可能被雪藏几十年。比如 Frank Rosenblatt 的 Perceptron 论文——他受到人类神经元的启发,提出了感知机的概念:能不能在机器里也做出一个像人类神经元一样的东西?一个个 neuron,接收来自其他神经元的输入,再输出送给其他的神经元,权重是学出来的。这不就是我们今天的深度神经网络吗?
但是到 2012 年才有 AlexNet。这就是有合适的人想出了合适的东西,但在不合适的时间——那个时候没有算力,也没有氛围。虽然这个东西曾经火过,但后来被泼了冷水,进入了人工智能的寒冬。现在又是人工智能的热潮。
我们需要正确的时间、正确的地点、合适的人,就把一件事情给做成了。做成的就是你知道的——今天上课的 Linux。你们的每一个服务器,不管是打开抖音还是淘宝,背后提供服务的服务器,还有你们 Android 手机,背后都是 Linux。确实是一件很了不起的事情。
名场面
在这个过程中也诞生了很多名场面。比如 2012 年,Alex Krizhevsky、Ilya Sutskever 和 Geoffrey Hinton 搞 AlexNet 的时候,NVIDIA 在 2006 年推出了 CUDA——就是现在消耗了全球最多电力的那个编程框架。那个时候我还在读 PhD,还在跟同学们抱怨说 NVIDIA 野心很大但做了一个反人类的东西出来。当然到今天大家还是在抱怨 CUDA 反人类,所以现在有 Megatron、Triton 这些抽象层把 CUDA 藏到下面去。
就在那个时候,Linus 说:"NVIDIA is the single worst company we've ever dealt with。"然后做了一个手势——就成了名场面。现在 NVIDIA 已经是地球上市值最高的公司了。
Linus 是一个非常有个性的人,有很多名言,包括他很自负地说"世界上没有人能写出完美的代码,除了我"。后来还有人写论文的时候 diss 他,因为 Linux kernel 的邮件列表是永久保存的,里面那些口出狂言也都被记录下来了。
他是怎么成功的?
Just for fun。他有一本回忆录叫 Just for Fun: The Story of an Accidental Revolutionary——"偶然的一个革命"。没有人想到那个 21 岁的少年在 1991 年 8 月 25 日说的"我就是一个 hobby,做了一个 386 上自己用的操作系统,给你们看看、玩玩",在贴吧上发了个帖子,没想到诞生了今天整个计算机世界的基座。
书上说:"革命不是 planned 的,不是 managed 的,它们 just happen。"
贴吧论战
随着关注 Linux 的人越来越多,comp.os.minix 上关于 Linux 的讨论也越来越多。Tanenbaum 甚至在贴吧里开始和 Linus 这个小伙子论战。
1992 年 1 月 29 日,Tanenbaum 站在道德制高点批判 Linus:
"To me, writing a monolithic system in 1991 is a truly poor idea."
这句话攻击性非常强。还有一些 defensive 的话,有点阴阳怪气:"So don't get me wrong, I am not unhappy with Linux...but in all honesty, I would suggest that people who want a more than free OS look around"——来看我的,我这个才是对的。
Linus 这人大家都知道嘴臭,肯定要喷回去的。
更有意思的是,Ken Thompson 也来了——这相当于什么呢?Ken Thompson 大概已经在 1983 年因为 UNIX 获得了图灵奖,在中国的地位至少相当于中科院院士。一个院士级的人物,在大学教授和毛头小伙子的论战之间,实名在贴吧上发表自己的言论。
他说他同意微内核可能是未来的方向("microkernels are probably the wave of the future"),但也说了一些正确的预言。结果这些话到今天都成真了——到今天我们开始有鸿蒙微内核了。Linux 在有一段时间里确实经历过一个非常大的 mess。
Linux 从最早一个 hobby project 开始,后来越来越多厂商入场,IBM 等公司加入,Linux 开始成为服务器上可以严肃运行的东西。2.0 引入了多处理器支持,2.4 Linux 内核才能真正用多处理器并行。2003 年左右 Linux 2.6 发布,正好是大家开始做云计算基础设施的时候,源代码经历了巨大的膨胀。好几个子系统经历了巨大的重构,逐渐变成了现代的、非常先进的操作系统。
里面有意思的是,院士竟然可以下场到贴吧来回复——在我们这是一件政治不太正确的事。你们校长突然到百度贴吧来回你的帖子,你会觉得不太可能。如果有一天我们真的可以变成这样——我去贴吧跟你们论战,你们有同学写了一个 Crazy OS,然后我就跟你们互喷,喷完了以后这个系统越变越好——我觉得这绝对是值得的。
AI 时代:从零到零点一
这些伟大的东西其实都是从零开始的。2009 年,我在学一本过时的教科书来学习操作系统。到今天 2026 年,你们坐在这儿手上有最好的大语言模型——中国也有,所有东西都是最好的。课程也是最好的,你可以在计算机系统基础课上写 emulator。
你们如果有一个想法,说今天想去百度贴吧做一个 Crazy OS,你们也可以做成。
在 Linus 那个时候,必须是一个天才才能实现。但今天不需要了——只要你的想法足够偏门,但实现起来对一个领域的专家没有显著的门槛,从零到零点一前所未有的容易。靠 AI 烧点 token 肯定给你做一个原型出来,界面还做得漂漂亮亮的。甚至我觉得可能在不太长的时间里面,从零点一到零点九五可能都变得容易了,这个世界上创新的模式就变了。
比如我们自己的工作——我们做编译器测试。你们的 GCC、LLVM 在编译程序的时候都有可能编译错。在 GPT-4 的时代,我们通过好的 prompt engineering,用一个 prompt 给 GCC、LLVM 找到了 50 个 bug——而且还是在 prompt 写得很差、GPT-4 能力比现在差很多的情况下。现在如果重新去做这份工作,我觉得找到一两千个 bug 绝对不是困难的事情。
再比如这两天有一个很有意思的项目叫 Caveman,它用极致的 prompt 去压缩大语言模型的输出。大语言模型被微调成了一个"舔狗",每次说话都要说好多好话,但这些话对完成任务没有实际帮助。Caveman 做的事情就是用 prompt 告诉模型:你把所有没有必要的东西能用文言文就用文言文,能写多短写多短,哪怕语法不正确。
精妙之处在于它利用了 Agent 的容错性——做错了不要紧,Agent 会再试一次。他用 Agent 的自主性去弥补偶尔会做错的问题,只要准确率没下降太多,从 overall 来看就能节省 token。
但从零到零点一变得前所未有的容易之后呢?又怎么样呢?我最近在审稿,审了一些论文,发现里面做了好多东西,效果可能都不如 Caveman——一个小小的 idea,我不知道为什么一定要为了写一篇论文而写一篇论文,浪费每一个读者的时间。
说了这么多,其实我就是要说:你们可能在高中的时候已经坐牢坐了三年,已经 burn out 了,但你们现在还有机会把自己解脱出来。你们可以像 Linus 那样,在 AI 的帮助下做一点了不起的事情。
从 initramfs 开始构建 Linux 世界
进程的初始状态
前面讲了操作系统是怎么从最早的 UNIX 一点一点变成今天的 Linux 的。现在我们来看一个真正的 Linux——我们能不能从零开始真正地掌控它。
先回顾一下关于进程的初始状态。execve 的时候给进程的初始状态包括 argc、argv[0]、envp、auxiliary vector,然后 path 和 interpreter 被加载到内存,设置好正确的 PC。
我们的硬件也有一个 CPU reset 状态,寄存器和内存里面的值是确定的,PC 指向 firmware。我们讲过可以感染 firmware 的病毒——它在 4 月 26 号把你的 firmware 给改掉,你的电脑就真的变砖了。
Firmware 执行后,里面有一个 bootloader(启动加载器)。启动加载器会加载操作系统,操作系统可能也是一个 ELF 文件,是一个静态链接的 ELF,PC 指向里面的一条指令,然后操作系统就开始运行了。
操作系统运行以后,它会加载世界上的第一个进程。那第一个进程是什么呢?如果你看进程树的话,你会发现所有的进程都有一个共同的根叫 systemd。那第一个进程是 systemd 吗?还是别的进程呢?
今天这个技术部分的内容就来讲操作系统上面的第一个进程。因为操作系统加载了第一个进程以后,它就会变成系统中的服务提供者——你 system call 调进来会调动操作系统,它会响应中断,在后台也可能会处理一些还没完成的事情,比如把缓存写到磁盘里。
控制第一个进程
从第一个进程长出了你看到的操作系统世界全部的进程。它到底在哪里?它做了什么?我们能不能控制它?
你只要能问出这样一个问题——"我们能不能把操作系统加载的第一个进程换成我们的进程"——当然是可以的。
我这里给大家准备了一个最小的 Linux 的案例。首先我们在上课的时候讲过一个最小的 x86-64 二进制文件,它就用系统调用来打印 "this is a minimal binary",然后再调用一个 exit,程序就退出了。我当然可以编译它,它是一个 64 位的、x86-64 的、静态链接的 minimal 程序。
在我的树莓派上也可以运行它——为什么?因为我的系统里有 QEMU。如果运行一个别的体系结构的二进制文件,它默认会进入模拟器模式执行——模拟执行所有用户态指令,当调用系统调用的时候直接调用本机操作系统的系统调用。
initramfs 是什么
如果你想要控制第一个运行的进程,你首先需要准备一个东西叫 initramfs——initial RAM filesystem,即初始的内存文件系统。
回想一下 execve 的参数,第一个参数是一个 pathname,指向系统里面的一个文件。所以为了能够运行一个程序,你必须要有一个文件。但是在内核刚开始的时候,你还不知道文件系统是在 U 盘上还是磁盘上。那最早期的文件在哪里呢?就在 initramfs 里——这是你可以控制的、你可以构建的。
如果你用 apt update 去更新软件包,有的时候会看到一个耗时特别长的操作叫 "update initramfs"。所有操作都很快,唯独那个操作最慢,就是因为有些软件更新后要重新打包回这个初始文件系统里。
你知道这一点以后,就可以让 AI 帮你 build 一个 initramfs。做一个目录树,里面有 /bin 之类的目录,bin 里面可以有 bash、有一个 init。Linux 内核代码里面硬编码了一系列路径,会按照 /sbin/init、/etc/init、/bin/init 一个一个尝试,试到一个存在的就 execve 加载——第一个进程就被加载到 Linux 的世界上了。
我们完全可以控制它。这份代码会把一个目录树用 cpio 归档然后用 gzip 压缩,生成一个 initramfs.cpio.gz。最终 QEMU 在启动的时候用 -kernel 加载内核,用 -initrd 加载这个打包好的 initramfs,还可以加一个参数 rdinit=/init 来 override 默认行为。
运行最小的 Linux
我 make run,在 QEMU 模拟器里运行。因为是在 ARM 上模拟 x86,比正常 Linux 启动慢一些。你可以看到 Linux version 6.1.0 amd64,运行在 QEMU Standard PC 上。
Linux 开机时会不断滚动一些日志消息。在内核运行了大约四秒半以后,Linux 说 "Run /init as init process",然后打印出来 "this is the minimal binary"——就是我那个汇编程序打印的一模一样的红色字。
有意思的是,那个程序执行了 exit 系统调用。在我正常的操作系统上这没问题,这个进程退出了就退出了,因为系统里还有其他进程。但如果 init 退出了——操作系统内核就直接罢工了,叫 "kernel panic: Attempted to kill init"。整个操作系统就挂掉了。
所以我们确实控制了 Linux 加载的第一个进程。
探索真实系统的 initramfs
在 AI 的帮助下我们可以干更多的事。比如我告诉 AI,我是一台树莓派,我想把它的 initramfs 给解出来。它找到了 /boot/initrd.img,然后解包了。但它犯了一个错误——它说这是一个 minimal 的 initrd,解出来总共只有 7.3MB 的文件,但那个 initrd 有足足 23MB 那么大。
你只要发现这个不一致,就可以跟它说肯定有一些东西没有解。在整个过程里面,我对具体的细节、文件结构一无所知,我有的只有一个基本的观察——解出来 7MB,但文件有 23MB,一定还有东西没解出来。AI 开始写脚本来解,找到了在 trailer 后面还有 19MB 的额外数据——是 ZST 压缩的第二段 CPIO。这是 Linux 内核复杂的地方,虽然概念上 initramfs 是一个目录树,但压缩文件可以分几段来压缩。
这次解到一个完整的小系统,它里面有一些基础工具,目录结构非常像你的根目录——有 lib、usr、bin。
这个 initrd 里面不仅有 init,还有别的东西。比如如果你的磁盘发生了损坏,文件系统挂不上的时候,你会进入到一个 initrd 的模式。这时候只要 init 文件系统没有损坏,你依然可以得到一个命令行体验——因为 Linux 内核已经加载了,所有系统调用都是正常工作的。
它里面有核心的启动脚本、NTFS 支持(ntfs-3g)、各种配置文件、键盘布局、字体配置等等。用户空间工具 usr/bin 和 usr/sbin 下面有大量的工具,都是 BusyBox 提供的精简版。还有必要的动态链接库——我们的 init 其实可以是动态链接的,因为这时候已经有文件系统了。还有内核模块信息和一些固件的二进制——它需要加载必要的驱动才能访问某些设备。
最后有一句话引起注意:Switch Root——切换到真实根文件系统。
从 initramfs 到真实系统
想一想,这个 initramfs 你们从来没有在 Linux 系统上看到过一个长成这样的文件系统,但你又确信它是真实存在的。那这个东西到哪去了呢?
答案是:Linux 系统在启动的时候,虽然看起来 systemd 是你整个进程树的根、是你的一号进程,但不完全是这样的。Linux 在 initramfs 阶段的最后,会执行一个叫 pivot_root 的系统调用。用 man 2 pivot_root 可以看到它叫 "change the root mount"。这个 pivot_root 重新创造了以 systemd 为根的系统——实际上是因为你系统的根目录下的 /etc/init 就是 systemd,然后 systemd 再创建了你进程树里面整个操作系统进程的世界。
动手构建一个真正的 Linux
我也给大家准备了一份脚本。这份脚本里面有两个部分的 init——一个是 initramfs 阶段的,一个是 pivot_root 之后的。
在 initramfs 阶段,我的世界上就只有 BusyBox——只有一个 /bin/busybox。我把这个目录树打包成一个 initramfs,还加了一些驱动程序(QEMU 必须要这些驱动才能正确运行)。
Linux 加载以后会把这个 initramfs 当成文件系统,然后执行里面的 init。这个 init 的解释器是 /bin/busybox sh——用 BusyBox 的 shell 来解释这个脚本。
我编译并运行它。这次有图形界面,因为是树莓派做全系统模拟,稍微慢一些。几秒钟以后 Linux 正常启动了,加载了 initramfs。
这时候系统里几乎什么也没有。find / 会发现只有 /bin/busybox 和我加的一大堆驱动。甚至 /bin/ls、/bin/sh 都没有,只有一个 /bin/busybox。
我的脚本还没完。执行到第 13 行之后:/bin/busybox --list 会把 BusyBox 里面所有支持的命令都列出来——有这么多命令都集成在一个 BusyBox 二进制文件里。然后我用三行脚本创建符号链接:对于 list 里的每一个 command(比如 ls),就执行 busybox ln busybox /bin/ls,创建一个符号链接。这样就在 /bin 里面创建出了一大堆二进制文件。
接下来用 mkdir、mount 这些命令开始创建操作系统里的对象——创建一个 procfs 的对象、一个 sysfs 的对象,用 insmod 安装一大堆驱动,用 mknod 创建一大堆设备。
日志打印 "init ok, launch a shell, initramfs busybox sh"。这时候 procfs 里面有东西了,可以运行 ps 了。用 ls -l /bin 查看,每一个像 wc、wget 都是指向 /bin/busybox 的符号链接。
ps 显示 1 号进程是 init busybox sh /init——这是 initramfs 最早加载的那个进程,是所有进程的祖先。系统里就只有我的 BusyBox shell 和 ps 进程。
计算机系统里面没有任何的魔法。还有一点要强调:你看到的所有操作看起来调用的是命令行工具,但它们背后全部都是系统调用。比如 ln -s 背后是一个创建符号链接的系统调用;mkdir 背后是一个 mkdir 系统调用;mount -t proc proc /proc 会调用 mount 系统调用,在 /proc 目录下创建出 procfs 的所有对象;mknod 也有对应的系统调用——比如创建 /dev/vda,需要指定主设备号和从设备号。你们 /dev 里面的每一个文件都是以这种方式创建的。比如 random 主设备号 1、从设备号 8,urandom 是 1、9,null 是 1、3,tty 是 4、1。
pivot_root:切换到真实世界
如果我的 shell 退出了,真正神奇的事情才开始。
倒数 3、2、1 以后,我把 /dev/vda(一个虚拟磁盘)挂载到 /mnt。磁盘也是一个操作系统对象,磁盘里面有一个文件系统的目录树,挂载以后就出现在了 /mnt 里面。
这是我准备的另外一个目录,fsroot 里面有一个 e1000.ko(网卡驱动)和另一个 init 脚本——这个 init 会打印 "jyy's minimal linux"。
退出 shell 以后,脚本启动了网卡。有一个日志叫 "goodbye QEMU console"——串口的 console 不能用了,但是 VGA 显示设备可以用了。我们回到了最早和 Minix 一样的状态——QEMU 模拟出来的一个 VGA 显示设备。之前一直在模拟的串口对话,现在切换到了图形输出。
现在有了 /dev/tty。如果我 echo hello > /dev/tty,就是 Linux 用驱动程序画出来的——每一个像素都是画出来的。这里有我打印的 "jyy's minimal linux"。
这个 init 是 pivot_root 系统调用之后运行的。它换了一个文件系统,因为新文件系统是空的,所以还需要把 BusyBox 的所有符号链接重新创建一遍,procfs、sysfs 也重新创建一遍,/dev 里面也要重新创建一遍。
然后我配置了网络——安装了 e1000 模拟网卡的驱动,设置了 IP 地址。在这个 Linux 虚拟机里,我启动了一个 httpd 在 8080 端口。
我的 Makefile 里把虚拟机的 8080 端口映射到了外面的 8080 端口。理论上访问 localhost:8080 就可以访问到虚拟机里面的网页——首先是 404,因为没有 index.html。但虚拟机的文件系统里有一个 init 脚本,所以访问 localhost:8080/init 就能看到这个 init 脚本——我看到了!这证明了我的 Linux 是真正点亮了。
一切都是系统调用
我们经历了一个漫长的过程。从最早的 initramfs 里面只有一个 BusyBox,到可以在 initramfs 里加载驱动、做各种系统调用允许的事情,到 pivot_root 切换到真实文件系统,最终到我可以启动一个 HTTP 服务器、在浏览器里看到网页——所有的这一切都是用系统调用实现的。
从第一个进程——initramfs 加载的第一个进程开始,之后所有的事情都是用系统调用实现的。这就是系统调用这个抽象真正厉害的地方。
总结
操作系统一定会到达一个确定的初始状态。这个绝对确定的初始状态由 CPU reset 决定,然后 firmware 把操作系统加载好,操作系统把第一个程序加载好。从第一个程序的初始状态开始,剩下的整个世界就只有一个东西——操作系统对象和 API。这就是操作系统给应用世界提供的一切。
想象一下:你拿起一个 Android 手机,下意识地打开抖音的时候,再想到操作系统课上面我跟你说过——它是从 initramfs 开始的,之后你抖音上看到的每一个像素点都是系统调用实现的。这还是有一点惊叹的。
从 UNIX 到 Minix 到 Linux,我们看到了历史是怎么走过来的——从最早 27 行汇编代码的 fork,到 Minix,到论战,到 Linux,到今天成熟稳定的状态。
但故事并没有结束。在 Linux API 上没有什么东西是不能做的——大语言模型、Agent、抖音、淘宝,全部都是在 Linux API 上做出来的。
应用生态
光有操作系统的 API 是不够的。操作系统的 API 很大程度上是和使用它的应用程序一起演化的——应用程序发现性能不够时,操作系统会给它新的 API、新的对象。它们共同进步,最后变成了今天操作系统的样子。
应用生态成就了操作系统的繁荣。操作系统自己本身什么都不是,它就是一个 API。但因为厂商、个人开发者、GitHub 上每天都在发布新的应用,操作系统不仅需要实现一套 API,还需要一套核心的工具集来支撑开发者。这些也是操作系统的一部分。
所以操作系统有狭义的操作系统和广义的操作系统。我们讲操作系统原理最主要的还是讲操作系统的 API。但当我们谈论"为什么中国没有国产操作系统"的时候,很多时候谈论的是生态——它需要核心工具集、基本运行库 libc、3D 库、图形库,还有各种辅助工具、版本管理、系统管理。
Linux 内核看起来 2000 万行代码已经是庞然大物了,但相比于应用生态来说根本就是一点点——一个复杂的业务应用就可能是上千万行代码。
在早期 DOS 的时代我们还是用软盘和光盘发布软件的,CD-Key 写在光盘上或者贴在里面的一张纸条上。但进入互联网时代以后,我们有了更好的应用生态分发方法——App Store、apt、rpm、Python 的 PyPI、npm、Hugging Face、Ollama,都是内容分发的生态。
操作系统是一层一层的——内核 API 是最小的一层,上面有库函数、shell、核心工具集、整个应用生态。
比如说 Debian 系统,它说:
"Our mission is creating a free operating system. When we use the word 'free', we are not talking about money. It is software freedom."
所有的软件都是可见的,你可以看到每一行源代码,你可以理解它,也可以为它做出贡献。
1998 年 apt 被发明出来的时候——苹果的 App Store 2000 年代才出来——你想装 Firefox 只要 apt-get install firefox 就装好了。apt 有一整套开源供应链的管理流程,从 unstable 分支到 testing 分支,有志愿者愿意尝鲜,慢慢一点一点进入稳定状态。
你甚至可以让 AI 帮你解开一个 .deb 包,然后问"安装这个软件包到底意味着什么"——就可以知道应用生态是怎么完整构建起来的。
这就是操作系统——对象和系统调用。它从上面长出了完整的应用生态的世界。