这两天claude sonnet和opus降智了,转写出来的稿子遗失了一些信息,所以jyy老师的讲稿又用最新出的国产模型glm5.1转写了一遍。
阶段小结
不知不觉,我们已经讲到了虚拟化部分的最后一节课。进度之所以这么快,是因为每次课都在帮大家调试代码,每次课都会引入新的工具和知识。今天的 Hacking Day 也是一个回顾,我会讲讲在操作系统之上,应用生态是如何构建起来的。虽然今天的大部分内容不会出现在考试中,但我相信这对大家会非常有用。
我们先复习一下花了好几次课讲的虚拟化,到底讲了什么。我们从程序和进程开始,讲到了进程的地址空间,里面有游戏的修改器。接着讲到了文件描述符,以及如何打开操作系统里的各种对象。你们做了实验,做了 PS3 和 SPerf,也知道了 C 标准库是如何一层一层实现出来的。
在上一次课,我合上了整个虚拟化部分的最后一块拼图,讲的是链接和加载。但回顾整个学期,其实我一直在讲系统调用,也就是操作系统给应用程序提供的各种 API。所以上节课讲的是 execve 系统调用的行为。大家可能没有反应过来,但听我这样一讲,再回头想一下我调试的那些东西,就会明白所谓 execve 的含义。
当我们在任何程序中执行一条指令时,它都有一个确定的行为。如果它执行的是 execve,它的行为就是把 execve 的 path 参数对应的二进制文件——也就是一个可执行文件描述的初始状态——搬入这个进程的内存里。这里面包含了寄存器里的 Stack Pointer,不管是 x86、ARM 还是任何体系结构都有这个指针。Stack Pointer 里面会有初始的进程栈,包括上面的 Auxiliary Vector,这就是一个初始的内存布局。
我们甚至看了加载器——Linux 内核里加载器的代码,知道了这里的每一项都是有代码写进去的,没有任何魔法。除此之外,我们还讲了加载,有静态的加载和链接,也有动态的加载和链接。execve 的行为就是把 a.out 里所有在 ELF 文件(用 readelf 能看到)里标记为 PT_LOAD 的段,也就是代码、数据、需要初始化的数据,都加载到进程的地址空间里。
这就是 a.out 的内存映射,除此之外还有解释器。我们讲过动态链接的 a.out,当你在 gdb 里调试它的第一条指令时,a.out 本身和解释器这两部分内容是在进程地址空间里的。但像 libc 这些库,在第一条指令执行时并不在地址空间里。如果用 ldd 查看,a.out 可能会链接很多动态链接库,但在执行最开始它们都不在内存里。
这时需要由加载器通过系统调用,用 mmap 把那些二进制文件——ELF .so 文件里的一段段内存——映射到地址空间里。等到所有这些都准备完毕后,还会设置 PC 指针。PC 指向的是解释器里的第一条指令,这个程序就开始在 CPU 上往后执行了。所有的行为就是你们在 strace 里面能看到的。
所以,我讲的其实是 execve 系统调用的行为。你可以回头再把代码看一下,在 AI 的帮助下阅读一下,你会发现这么复杂的东西在概念上其实没什么困难。你在任何时候想要知道任何程度的细节,就去跟 AI 交互。只要你建立了正确的概念,就能知道里面每一个字节是怎么来的。你提出正确的问题,AI 会帮你找到答案。
所以说,这个学期我一直都在讲系统的行为。我们整个课程的目标,现在虚拟化部分快要结束了,后面我会讲一些由这些系统调用引发的更大的麻烦,比如并发、持久化的文件系统。但我们前半部分的课程传递的一个核心信息是:你们可以通过阅读手册来理解计算机系统。我不断试图把手册打印出来,然后通过管道传给 AI 工具去问这个工具是干什么用的。
你可以阅读手册,指导 AI 来写代码,去理解你在计算机系统里看到的任何不理解的东西。并且,你还可以回过头来问为什么要这样设计。比如我讲动态加载的时候,其实给了足够多的动机,解释为什么需要动态链接库。我要证明在整个计算机系统里,libc.so 只有一份只读的副本。我创建了非常多个打开同一个动态链接库的程序,它们并没有占用过多的内存等等。
这就是我觉得这门课最希望大家掌握的。就算这门课到现在就结束了,我也觉得 OK。因为如果你再回看,确信自己掌握了这个技能,你就具备了一定的真正的编程成熟度。因为你不再害怕这些东西,搞不懂也没关系,去问就行了。
从 UNIX 到 Linux(毒鸡汤)
今天我们会讲一点在系统调用之外的内容。我们已经知道各种系统调用的行为是什么了,现在要讲这些系统调用是怎么来的,以及系统调用出现以后,应用生态世界是如何生长起来的。这也是和系统调用很有关系的故事,所以我们今天就来讲讲故事。
故事的开始是一篇论文,叫《The Evolution of the UNIX Time-Sharing System》。我强烈推荐每一个同学都去阅读一下。这篇论文有点长,我知道同学们一定不愿意在课后去读这样的阅读材料。如果我在一节课上放了十来个链接,每个链接都有那么长的文档,你们马上就会觉得头大。不用着急,我们一会儿再回到这个文档。
这篇论文非常经典,它传递了一个什么样的信息呢?它告诉你 UNIX 是如何从零开始,一点一点实现成现在这个样子的。最早版本的 UNIX 甚至没有 fork,它创建进程的方式不是像 fork/execve 或者更合理的 spawn 那样直接生成一个程序。早期的 UNIX Shell,如果要运行一个外部程序——比如那时还没有管道的 ls——它会先把所有打开的文件都关闭,把 0 和 1 这两个文件描述符打开到终端,然后把自己退出。
这个 Shell 就从内存里被卸载了。卸载以后,它用 execve(也就是我们今天的 execve)让加载器把一份代码加载到内存里执行。也就是说,在最早的 UNIX 上,一台终端里面最多只有两个程序:一个是 Shell,一个是可执行文件。可执行文件退出以后,Shell 再被重新加载回来。来回就是 Shell、可执行文件、Shell、可执行文件。
就像你们可能在大一大二的时候,做一个很小的练习,虽然要写汇编,但也没那么难。后面还有很多有意思的故事,比如他们脑洞大开,用 27 行汇编——Thompson 用 27 行汇编实现了一个 fork。因为在早期,在那样简单的进程模型底下,所有的内存可能就是几个指针描述的:代码从哪开始到哪结束,数据从哪开始到哪结束。
这就是 struct task_struct,跟我们写的那个 crazy os 一样。他想要实现一个进程的复制,就非常简单,用 27 行汇编实现了一个 fork。这给我们一个启发:所有复杂的东西在刚开始的时候都是很简单的。你看今天大语言模型已经很可怕了,但如果你不停地往前溯源,我们会回到早期的时候,那时候其实都没有现在这么复杂。
现在为了把这么大的模型高效地 serve 出来,我们有各种技术,比如 PD 分离、KV Cache 等等,把 LLM serving 这件事做得很好。但以前最早的时候都不是这样的,都是你们能理解的最简单的样子。在最早的 UNIX 火了以后,这也是一个非常有名的教授 Andrew S. Tanenbaum,他写了一个操作系统叫 MINIX。
你可以看到这里有个链接 minix3.org。MINIX 3 是一个非常成熟的操作系统,成熟到什么程度呢?你看这是一个模仿 Windows XP 的界面,它真的能把完整的用户界面跑起来。它甚至一度是世界上最流行的操作系统,为什么呢?因为英特尔。它的许可证比较宽松,MINIX 3 的许可证很宽松,它就运行在英特尔自己的 firmware 里。
在你的每一台电脑里,除了有一个主 CPU,CPU reset 时执行 firmware 之外,还有一个芯片组,那个芯片组上还有一个 CPU。那个 CPU 上就要运行一个操作系统,运行的就是 MINIX 3。没有人知道,后来被人发现了才发现。有一个帖子说“我们发现了原来 MINIX 3 才是世界上最流行的操作系统”,因为那时英特尔如日中天,每一台英特尔芯片组的计算机上都有一个 CPU 在运行 MINIX 3。
不管你的实际 CPU 上运行的是 Windows、Linux 还是别的系统,它里面都有一个 MINIX 3。很有意思。MINIX 最早是 1987 年的。1987 年的时候,MINIX 走了一条非常有趣的路线,它要做到与 UNIX 完全兼容。UNIX 早期火了以后,因为受到贝尔实验室的 Dennis Ritchie 和 Ken Thompson 需要挣钱赚钱的约束,UNIX 没有办法广泛流传。Andy 就做了一件事,他写了一本书叫《Operating System: Design and Implementation》。
这本书算是我梦开始的地方。我像你们这么大的时候,就拿一本操作系统《设计与实现》。这书写得非常非常好,虽然在今天来看阅读价值已经越来越低了,但那时真是如获至宝。书的前一半教你操作系统,后一半是代码。我们那时有惊人的注意力,可以在书里放个书签,读一下书说操作系统这部分是怎么实现的,然后翻到后面去找代码,看一行一行代码,人肉解释。至今它都算是我操作系统的启蒙老师。
MINIX Book 1987 年是 UNIX V7 兼容,到 1997 年做了 MINIX 2.0,是 POSIX 兼容——POSIX 是 UNIX 的标准。到 06 年发布了 MINIX 3。因为这种兼容性,它是和标准兼容的,所以是一个非常了不起的项目。今天我给大家准备了一个 MINIX。有爱好者已经把 MINIX 这个项目完整地存档下来了。
你们把这份代码下载下来以后,直接 make minix2,它就可以从 GitHub 上把 MINIX 的源代码下载下来,包括 MINIX 1 和 MINIX 2。这里面有一个编译脚本,如果你的环境配置正确,里面的 Makefile 可以正确执行。执行完了以后你会生成很多 images,比如 MINIX 1.7 的 64 兆镜像,还有 MINIX 2.0 的 64 兆镜像。这在今天看非常小,但在那时看其实已经是非常大的镜像了。
感谢软件世界里有很多随手可得且开源免费的工具,我们这次又用到了模拟器。我们可以直接 make run。这是一台 ARM 的树莓派,像小霸王一样的设备,所有的整个计算机系统都在这个键盘里,但它完全不吃力,可以运行大家看。你们看这个 MINIX 1.7.5,它是随书送的代码。每一个买了 Andy Tanenbaum 的操作系统教科书的人,都会得到这份代码。
所以这个 MINIX 系统也是 Linux 系统实现的一个原点。看它这里有些日志信息,比如它找到了 HD0,这是一个 QEMU hard disk。相应的 memory 有一些日志说“MINIX executing in 16-bit protected mode”,它在 16 位模式下运行。16 位意味着什么?你们没有用过 16 位机,没有真正在 16 位机上写过程序,但这是一个如假包换的 UNIX V7 兼容的 MINIX 1.7。
如果是 MINIX 2.0 的话,它甚至是 POSIX 兼容的,这和你们在一个 Linux 里的体验几乎是一样的。你可以把时间回到 1987 年,你可以得到一个这样的系统,里面有几乎所有的 UNIX 体验。比如我可以用一个 vi 编辑器。vi 不是现在才有的,vi 是一个非常早的从 ex 一个命令行编辑器变成一个 Visualized 编辑器的。
我可以编辑一个 C 文件。为了证明机器确实是 16 位的,我可以打印 sizeof(void*)。只要写这样的一段代码,就可以知道它是不是真的是 16 位的。这时候所有 UNIX 里的工具都是可以正常使用的,但是 Shell 不支持我重复上一条命令。我可以 cat a.c 然后把它管道给 wc。
看到这老到就是我们今天上课的时候,或者你们今天每一个同学日常在你们的 ZSH、Fish 里面用的现代 Shell 里的管道命令,对吧?我现在经常管道给 cloudflare,但是那个功能就是从那个时代就拥有的。我们可以用一个 cc 的命令来编译它,它就真的编译出来有一个 a.out。我可以真的运行它 ./a.out,打印出来。所以这是一个完整的 UNIX 体验。
UNIX 体验有多完整呢?我们甚至可以用 man 来查看手册,竟然还是有颜色的。今天 Linux 的手册也长这样。man 1 user command,man 2 syscalls,man 3 library routines。我们还可以 man -k,没有 less 只有 more。你可以看到这个系统里面有非常全的工具、系统调用、库函数,甚至这里面还有完整的源代码。
比如我可以 ls /usr/src,所有的 MINIX 世界里的源代码都在这里面。当然你不需要在 MINIX 里面体验这些功能,你可以在本地上看。在 MINIX 1.7 里面,你可以找到比如 /usr/src,里面有内核的源代码实现,也有所有工具的源代码实现。包括刚才我用的 vi 或者 cc,所有的这些源代码的实现都是有的。
这是一个非常非常可用的操作系统,我们说它是 UNIX V7 兼容,这不是开玩笑的。这意味着你可以在系统里,它可以真的连到一个物理的终端上。不像今天我们必须在模拟器里运行,在 1987 年的时候,你找一台它能支持的体系结构的机器,拿一个终端,你在终端上看到的就是这个东西。在这个地方你可以编辑它的源代码,它是可以自己编辑自己的。
你可以在 MINIX 里面修改 MINIX 的整个内核或者操作系统的功能。比如说你想给它加一个功能,加一个系统调用,这些都是 OK 的。然后你可以在 MINIX 里面编译它,编译完了以后把这个内核镜像烧录到磁盘上。烧录到磁盘上以后,它里面有加载器,全部都有。这个启动加载器、firmware 可以把这个东西加载上去以后,这个系统就真的可以运行了。
而且你可以改它,改它以后你改的系统也可以运行,一个非常完整的体验。你可以用它来做任何事:打印,生成文档,连接一个打印机,把文档从打印机里面打印出来,这都是可以做到的。所以最早的时候 Linux 就是从这诞生的,因为它已经非常完整了。
但是它有一个致命的缺点:慢。Tanenbaum 是一个大学教授,是一个像我这样的人。大学教授的想法总是比较疯狂和超前的,有的时候我觉得我上课时一些想法也比较疯狂,比如我们要做一个 crazy os。他那时的想法也很疯狂:在 1987 年计算机处理器还很慢的时候,他想要做一个只有一个系统调用的操作系统。
这个系统调用就叫 send 和 receive。我可以发一条消息,然后等一条消息回来。Linux 实际上是有 send 和 receive 两个系统调用的。只要有两个系统调用,操作系统内部有一些线程或者服务——这些服务甚至可以是运行在用户态的服务——你跟那个服务去交换些消息,就相当于我们有三个同学:A 同学负责文件系统,B 同学负责管 CPU,C 同学负责管内存。我作为一个进程要申请内存,我就跟负责内存的同学发个消息,他做好了给我。
这就是他当时的想法,确实是非常超前的想法。我觉得在今天这个想法其实挺好的,我觉得很快微内核就要复活了。我记得前两年 OSDI 有一篇文章就讲鸿蒙的微内核是怎么实现的。在今天微内核又可以复活了。但在那时,这是一个非常超前而且不被大家看好的想法,为什么呢?因为那时计算机很慢。
你每执行一条系统调用,如果像 Linux 那样,一条 syscall 指令到内核里,内核代码马上没过几个时钟周期它就开始执行了。但如果你要发消息的话就不行。发消息的话,你先要把那个消息准备好——比如你 read/write 的那些参数、字符串都要写到消息里——然后通过系统调用发给另外一个进程。另外一个进程运行了以后,执行完以后再返回一个消息给你。它的性能很低。
所以这个操作系统对于 Andy 来说,Andy 一直说:我做这个操作系统(Crazy OS,也就是 MINIX)是为了上课的,我是为了教学生什么是操作系统,怎么设计和实现一个操作系统呢?性能不是我要考虑的,未来有一天性能会是问题的。所以这就有了在 1991 年 8 月 25 号的一个里程碑式的事件。
那时有一个来自芬兰的年轻人,二十一岁,跟你们在座的差不多大。芬兰是一个非常冷的地方,一年四季有相当长的时间都是冬天,大雪封门。你可能会去滑雪参与这种冬季运动,但是很多时候你可能只能宅在家里取暖,对吧?那你就只能在家里从事一些脑力活动。在那个时候就有一个天才少年,你们知道这个名字的,对吧?Linux 就是从他这来的。
你看 MINIX 那个 X 是 UNIX,Linux 的那个 X 也是 UNIX。那他把自己的名字放进来,所以这个意义上我觉得 Andy Tanenbaum 起名字的时候更像一个老实气的名字。Anyway,这个不重要。他在 MINIX 的邮件列表——叫 comp.os.minix,你们可以理解为就是“MINIX 吧”——里面发了这样一个帖子。
他说:“同志们好,你们这些用 MINIX 的人,我现在在做一个 free 的 operating system,just a hobby,won't be big and professional like GNU”。那时 GNU 也在开发自己的内核,结果 GNU 的内核到现在都还是难产的状态,MINIX 已经成为事实标准了。他说:“我这个 Linux 从来没有想过我要支持什么另外一个体系结构”。
就像今天你看我的树莓派上是 ARM64,而我在这儿还可以用 QEMU 跑一个 32 位或者 16 位的 x86 系统,他从来没有想过这件事。他说就因为他手上有一台 386,所以他说好,我现在就想做一个 386 上的能够取代 MINIX 的操作系统。我从 4 月份就开始搞了,然后我现在马上就可以给你们用了。
很快,他把最早的一批源代码就发上来了。很有意思,发在“百度贴吧”上。他早期的 Linux 不像现在所有东西都是 GNU 的,可以 self-contained。他还需要依赖 MINIX 的工具链,因为他没有自己的文件系统的实现。为了运行 Linux,你需要把你的磁盘格式化成 MINIX 文件系统,就是我刚才给大家看的那个终端里的 MINIX 文件系统。
然后 Linux 还能用,他上面运行的软件包括了 GNU 的 GCC、Bash,当然这都是今天有的非常早期的版本。所以有的时候也是这样,在一个合适的时间、合适的地方,有一个合适的人,然后就做成了一件事。我觉得更有意思的是,这个世界上有更多可能合适的人,在不合适的时间做了合适的事,然后这个事情就可能会被雪藏几十年。
比如 Frank Rosenblatt 的 Perceptron Paper,他提出了一种叫感知机的东西。这个东西你们看是不是有点熟悉?他说他受到人类神经元的启发,我们能不能把这个机器里面也做出一个像人类神经元一样的东西——一个一个 neuron,能够接收来自其他神经元的输入,然后再输出一些输出送给其他的神经元。当然这些权重是学出来的。
这不就是我们今天的深度神经网络吗?但是后来你看到 2012 年的时候才有 AlexNet。这就是有合适的人想出了合适的东西,但是在不合适的时间。因为那时没有算力,那时也没有一个氛围,大家虽然愿意(其实这个东西火过),但后来又被一些人泼了冷水。他们试图证明比如说这样两层——就是一个 hidden layer 的感知机——它的计算能力很弱等等。
多层的能力是强,但是大家又不会训练,然后就进入了人工智能的寒冬,对吧?然后现在又是人工智能的热潮。我们需要正确的时间、正确的地点、合适的人,然后就把一件事情给做成了。你知道的就是我今天上课的 Linux,然后你们的每一个服务器,不管是你今天打开抖音还是淘宝,他们背后给你提供服务的服务器,或者你们的 Android 手机,它背后都是 Linux。
所以确实是一件很了不起的事情。在这个过程中也诞生了很多名场面。比如又回到 2012 年。2012 年我那时在读 PhD,Alex Krizhevsky、Ilya Sutskever 这两个人非常有名,还有一个是他们的导师 Geoffrey Hinton,他们在搞 AlexNet。然后 NVIDIA 在 2006 年推出了 CUDA,就是现在所有的、消耗了全球最多电力的那个东西。
它是一个非常反人类的编程框架。那时我还在读 PhD,还在跟我的同学们抱怨说,你看 NVIDIA 野心很大,但是他做了一个烂东西出来。当然到今天大家还是在抱怨 CUDA 变得反人类。所以你看现在有什么 Megatron、Triton 这些抽象层,把 CUDA 藏到下面去。就在那个时候,2012 年又是一个正确的时间,他们在搞 AlexNet。然后 Linus 在说 NVIDIA 是“The single worst company we ever dealt with”。
你们可能有同学知道这个梗吗?然后他就做了一个手势,说了那个“colorful world”的 NVIDIA。当然这就成了名场面。现在 NVIDIA 已经是市值地球上最高的公司了。就是这样一个非常有个性的人,他有很多名言,包括他很自负地说“这个世界上没有人能写出完美的代码,除了我”,各种名人名言。
后面还有人写论文的时候还会 diss 他,因为 Linux kernel 的邮件列表是永久保存的,你可以翻出历史上所有的那些邮件。所以他里面那些口出狂言、大放厥词也都被记录下来了。我觉得在那个时代的背景下,我觉得还是有一定的道理,虽然你可能是在现在的意义上就觉得有点开玩笑了。
他是怎么成功的呢?他是怎么成功的?Just for fun。就我刚才说到,他是来自芬兰的一个天才男孩,他在冰天雪地里什么事也干不了,他只能窝在温暖的家里面。Linus Torvalds 写了一本回忆录,这本书就叫《Just for Fun》,名字就叫《The story of an accidental revolutionary》——偶然突然的一个革命。
但没有人想到,对吧,那个 21 岁和你们一样大的少年,在 1991 年 8 月 25 号的时候,他说:“I'm just a hobby,我就做了一个 386 上的我自己用的操作系统给你们看看、给你们玩玩”,在贴吧上发了个帖子。没想到,诞生了今天整个计算机世界的基石。
所以这本书上说,革命不是 born 的,can be planned,can be managed,they just happen。我觉得我们处在一个很有意思的交叉路口上。第一个,这是我梦开始的地方(2009 年),但这时这本书已经出来很多很多年了,已经是一个过时的教科书了。也就是说我在 2009 年的时候,我在学一本过时的教科书来学习操作系统是什么。
等到今天,2026 年,你们坐在这儿,手上有最好的大语言模型,中国也有,所有东西都是最好的,课程也是最好的。你可以在计算机系统基础课上面写 emu,这就是世界上最好的学习方式。也就是说我们现在已经有一个土壤了。你们如果就是 just for fun,有一个同学说今天你想去“百度贴吧”做一个 crazy os,你也可以做成。
但与此同时呢,我们又很奇怪,对吧?最近也有一些有意思的帖子。包括我的博导也是干摩托车发动机的,为什么他没干出来?没看过那个很火的公众号文章《铁》已经被广泛流传的。以及你看在 2025 年 12 月的时候,全国高校科技创新工作会议暨基础学科交叉学科突破计划启动部署会召开。他们要求我们高校的老师提高政治站位,勇担国家使命,精心谋划一批重大战略任务、重大政策举措和重大工程项目。
我觉得从国家的背景上,我觉得这也无可厚非,因为他们也没有办法说“你们每个人都躺平吧,然后去做 crazy os”。国家有钱,所以他必须要要求高校来做这件事。但是从另外一个角度,如果我们被迫要做这件事的时候,我就什么也做不出来了。因为我必须要写一个非常长的计划,论证这件事情是可以的。等这东西写出来的时候,我要做事情可能已经过时了。
所以有一些有意思的事情,就是 Linus 这个少年在一个好的土壤里、无所谓的土壤里面,经历了很长的一段时间的发展。后来随着关注 Linux 的人越来越多,这个 comp.os.minix 上关于 Linux 的讨论也越来越多。这里面甚至因为刚才我说到 MINIX,Andy Tanenbaum 官方就以一种官方的方式在“百度贴吧”里开始和 Linus 这个小伙子开始论战。我觉得这也好玩,大家可以看。
这是 Andy Tanenbaum 在“百度贴吧”上,1992 年 1 月 29 号。因为有一种什么感觉呢?MINIX 很好,刚才我们看的 MINIX 真的是很好,它很能用,但是它就是性能不行。它做得太超前了。所以你看 Andy 那个时候就很有教授的架子,他在那站在道德制高点批判 Linus 说:“To me, writing a monolithic system...”。
MINIX 内核是像 UNIX 一样的,所有的代码都写在一起的。在 1991 年的时候,“It's a truly poor idea”,这句话攻击性非常强。还有一些这种 defensive 的话,我觉得就有点阴阳怪气的感觉:“So don't get me wrong, I am not unhappy with Linux... I'm very happy... but in all honesty, I would suggest that people who want a more than free OS look around mine, this is right.”
然后这个 Linus 大家都知道这人嘴臭,对吧?那你肯定是要喷回去的。你看他就喷回去了。我刚才说到一个创新的矛盾,这里面在贴吧上面还有一个人回了,这是 Ken Thompson。Ken Thompson 相当于什么呢?那时 Ken Thompson 大概在十年前因为 UNIX 获得了图灵奖(1983 年),就相当于是他在中国的地位比中国科学院院士还要再顶层一点,至少是院士级的。
然后一个院士在一个大学教授和一个毛头小伙子的论战之间,过来发表在贴吧上发表自己的言论,实名在贴吧上发表自己的言论。当然这个言论也是他自己的工程经验。你看他说:“To Jerry, I agree that microkernels are probably the wave of the future”。
我觉得真的,你看到今天,我们开始有鸿蒙微内核了,对吧?“However, it is unlikely that they will be popular in the PC market... in fact, the way Linux is turning into a mess in a hurry... I see them as being modified to be more monolithic”。结果就是这几句话都成真了。
当然他也说了一些正确的废话。Linux 在有一段时间里面其实经历过一个非常大的 mess。我这张图上虽然没有显示,但在 4.x 的时候,Linux 内核代码经历了一个断崖式的下降。Linux 从最早的时候是一个 small project,很小,后来随着越来越多的厂商开始入场,IBM 那些开始入场,Linux 开始成为一个服务器上可以严肃运行的东西了。2.0 引入了多处理器,2.4 Linux 内核才能用多处理器并行,否则只有用户态的程序能多线程运行。
然后 2003 年的时候 Linux 2.6 发布,大概在那个时候正好是大家开始做云计算的基础设施的时候,Google 开始在那个时候做云计算的基础设施,开始起飞。源代码经历了一个巨大的膨胀,然后基本上是稳定了好几个 subsystem 经历了巨大的重构,包括什么 USB 子系统等等,它就已经陷入了一个泥潭。
很多 Linux 早期说的那些不重要的话,最后也都被打脸了,然后逐渐变成了现代的现在的一个有点像微内核又有点像宏内核的、不知道应该怎么叫它的、但是一个现代的非常先进的操作系统。这就是 Linux。我觉得里面有意思的是,院士竟然可以下场到贴吧来回复,对吧?在我们这是一件政治不太正确的事情。
你们想嘛,校长突然到百度贴吧来回你的帖子,你会觉得 not like it happened。那如果我们有一天真的可以变成这样,对吧?我就去贴吧跟你们论战。你们有同学说你们写了一个 crazy os,然后我就跟你们互喷。喷完了以后,这个系统最后越变越好,那我觉得这绝对是值得的。
这样的故事其实一直都在发生,它不是个例。如果你们回过头来想,为什么我需要老一点的人像我上年级的人来教你们呢?不是同学和同学之间学习。同学和同学之间学习我觉得有一点好,就是你们年轻人有热情、有荷尔蒙。我现在到我这个年纪没什么荷尔蒙了,所以我看什么事情都比较平常,我已经没有兴奋不起来了。
但你们不一样,你们像 Linus 那时候,你看了 Andy 竟然要抱怨我的系统,那你一定要喷回去。现在我看了你们要喷我,我又不会喷回去,我是平常心。就是这种热血,就是因为你们有热血,年轻人之间有热血,所以你们可以把一件事情义无反顾地做下去。可能很多人都失败了,但最后有一个人成功,就彻底改变了世界。
所以这些伟大的东西其实都是从零开始的。从 Firing 的那个讲个人电脑的故事,从最早的时候的 Altair 8800,Bill Gates 要带着一卷纸去运行他的 BASIC,到《The Evolution of the UNIX Time-Sharing System》。在我这儿,我现在就可以告诉大家我是怎么读这些文章的。
大家还记得我刚开始上课的时候,我用豆包,用豆包的表现不尽如人意,经常翻车。从上一次还是上上次课开始,我换成了千问的 Qwen 2.5 72B。但我没有 coding plan,所以我是 API billing 的,我只能在上课的时候烧些钱,多花些钱来获得比较好的上课体验。
这个不一样,这个是我用豆包帮我做的。我现在没有 Lobster(可能是某种 AI 工具的误听),因为我喜欢自己控制我的代码。我有 Cloud Code,我如果想读一篇文章——不管是我今天有一篇论文,甚至有的时候比如说一百来页的那种博士论文,一个 PDF——我直接往我的 iCloud 共享文件里一丢。丢完以后,后台就会自动把那个文档处理好。它会一页页 OCR,然后烧掉豆包的 token。
处理完以后,它会得到好几个级别的 summary。包括一个一句话的——如果你是一个专家,你只需要用这样一句话就可以把所有的东西给重述出来的最短的 summary;以及一个可能是再长一点的摘要;还有一个一页纸的,这里面就有一些关键的信息。比如早期的 UNIX 是怎么实现的:从他们淘了一台垃圾开始,实现一些最简单的;早期的版本甚至连路径名都不支持,将目录视为特殊的文件;用 27 行汇编实现了 fork,结合 Shell 已有的能力有了 fork/execve 的模型;然后到管道,1972 年多次提案以后终于实现。
这个竖线是怎么来的,结合你今天看到的比如说上课的时候讲的那些例子,最重要的都在这儿。而且我还有他的原文。所以我只要在 Cloud 里面打开一个终端,我就可以自由的对话,说出任何我想要的东西。然后我自己的这些对话的历史的知识也会被作为 memory 记录下来。
所以我们在现在这个时代,你们可以装一个 Lobster 做这件事。但我觉得 Lobster 很多时候对我来说不是那么可控。但至少我是这样学习的。这样学习就意味着我可以在几分钟的时间里面可能看完一个几百页的博士论文。因为我可能对这个领域有一定的熟悉。这是一件我觉得以前无论如何也做不到的事情,但今天你们是可以做到的。
也许你们突然有一个同学说,我要回去做一个比 Lobster 更好的东西,这个学习口派的你们可能就可以去贴吧发帖子了,说:“我现在正在做,请大家来用”。所以这些伟大的故事,都是从零开始的。而且在今天,在现在这个时候,从零到零点一,从完全没有——你只有一个想法,像 Linus 当时说“我就要复刻一个在这个 386 上复刻一个 UNIX 操作系统,然后它性能很好,我可以用”——这样的一个想法,在那个时候必须要一个天才才能实现,但今天不需要了。
只要你的这个想法足够偏门,但是如果实现起来没有对一个领域的专家有显著的门槛,从零到零点一变得前所未有的容易。你马上靠扣的烧点扣肯定给你做一个原型出来,界面还做得漂漂亮亮的。甚至我觉得可能在短时间内,从零点一到零点九五可能都变得容易了。
然后这个世界上创新的模式就变了。我们可以做很多事。我其实在想,crazy os 可以把它变成什么?我想到的是我们可以在硬件上面,通过增加指令集的方式让硬件能够高效地运行 crazy os。那这就有可能真的是可以做一种新型的计算机。我可以做这个 hardware/software co-design,去彻底重新做一种操作系统,至少可以从头再做一遍。从零到零点一非常容易。
最近也有一个大新闻,Anthropic 发了新的模型已经变成 Claude 3.5 了,从 Opus 变成 Claude 3.5 了。然后他说“Finding bugs is easy”。当然马上大家就揪出来他是过度营销,他找的那个 ffmpeg bug 是一条根本就不可能进得去的路径。但确实是从零到零点一变得前所未有的容易。
比如我们自己的工作,我们也做测试。你们的编译器其实有 bug,你们的 GCC、LLVM 在编译程序的时候都有可能会编译错。我真的在实际项目当中遇到过编译器编译错的情况。那真的很离谱。因为编译器给你编译错了,你为了把这个 bug 找到,你第一个怀疑都是我自己我的逻辑写错了,你不会去怀疑编译器给你代码翻译错了。但编译器真的会翻译错。
我们在 GPT-4 的时代,我们通过非常好的 prompt engineering,用一个 prompt 给 GCC/LLVM 找 50 个 bug,而且是在那个 prompt 写得很差的情况下,效果都非常好。但现在各种榜单,一个 20B 的小的开源模型都会吊打 GPT-4。GPT-4 是那时最大的模型。也就是说,如果我现在重新去做这份工作,我觉得我找到个一两千个 bug 绝对不是一件困难的事情。
但又怎么样呢?我就说从零到零点一会变得前所未有的容易。再比如说,这两天有一个很有意思的项目,你们看到了吗?叫 Caveman。他去用极致的 prompt 去压缩你的输出。你看大语言模型被微调成了一个“舔狗”,他每次说任何话的时候都是要把主人伺候的好好的。但这个事情对于 code 来说是浪费,是一种浪费 token 的行为。
你要说好多好话,但这些话对于完成任务有没有实际的帮助?没有。所以他做的事情就是用 prompt,他甚至是用 prompt 一个 skill 说:“你把你所有没有必要的东西,你能用文言文讲就用文言文讲;然后你所有哪怕语法不正确,你能写多短写多短,反正你大模型能看懂”。Caveman 我觉得其中精妙之处在于他利用了 Agent 的容错性。
因为你想,如果 prompt 被缩减了,它肯定会丢信息。丢了信息以后肯定会做错,它的准确率肯定会下降。有偶尔之前吐了好多废话能做对的,现在说的少了做不对了。但是这不要紧,为什么呢?因为如果做错了,你的那个任务、任务的描述、程序的输出这些东西没变,Agent 会再试一次的。Agent 是有自主性的,他发现做不对的时候,他就再做一次,就能做对了。
他用 Agent 的这种自主性去弥补偶尔会做错——他的准确率可能下降了,只要下降得不多,从 overall 来看,他就能省 token。它背后的道理就像我自己的 Cloud MD 里面也有一个 prompt,让他答得简单一点,用中文。你们看到的,我所有的问答都是用中文问的,但是我的 Cloud Code 输出全是中文,而且确实输出得比较短。其实是和 Caveman 一样的想法,只是他的这个做得更极致一点。
我觉得很有意思。从 AI 时代,从零到零点一变得前所未有的容易。然后我们又发现另外一点,因为我最近在审稿一些论文,我发现里面他做了好多好多东西,甚至我觉得他的效果可能都不如 Caveman。他可能说他做了 abcde 加在一起,做了什么事情,效果可能真的不如 Caveman。我不知道他为什么要为了写一篇论文而浪费每一个读者的时间。
所以,说了这么多,其实我就是要说,你们可能在高中时候已经“坐牢”坐了三年,已经 burn out 了,但是你们现在还有机会把自己解脱出来。你们可以像 Linus 那样,去想想自己还可以在 AI 的帮助下做一点了不起的事情,找到一个状态。可能在现在的版本答案里,你不是我,你拿一个南大的学位,然后你可以去老家找一个公务员的工作,躺到退休。
因为可能所有的岗位都面临被取代的时候,我们自己——包括我自己,比如我在这儿能走多远我还不知道——可能你们要往前走得更远一点。
从 initramfs 开始构建 Linux 世界
好,前面是说历史上我们的系统调用或者说操作系统是怎么从最早的 UNIX 一点一点变成今天的 Linux 的。现在我们就来看一个真正的 Linux,我们能不能从零开始真正地掌控它。我先再回顾一下关于进程的初始状态。这是 execve 的时候给进程的初始状态,有 argc, argv, envp, auxv,然后有 path 和 interpreter 被加载到内存,设置好正确的 PC。
我们的计算机系统、硬件也有一个 CPU reset 状态,寄存器和内存里面就确定了,然后 PC 指向的是 Firmware。我们还讲过可以感染 Firmware 的病毒,它在 4 月 26 号的时候把你的 Firmware 给改了,你的电脑就真的“砖”了。Firmware 会执行,它里面有一个叫 Bootloader 的启动加载器。启动加载器和操作系统里面的 ELF loader 类似,启动加载器会加载操作系统。
操作系统可能也是一个 ELF(os.elf),它是静态链接的。它的 PC 会指向里面的一条指令,然后操作系统就开始运行了。操作系统运行以后,它会加载世界上的第一个进程。那第一个进程是什么呢?很好的问题。你在 PS3 里面看到,如果你看进程树的话,你发现所有的进程都有一个共同的根叫 systemd。那第一个进程——它加载的这个 Linux 操作系统从硬件固件把它加载运行以后运行的第一个进程——是 systemd 吗?还是别的进程呢?
我们今天这个技术部分内容就来讲这个操作系统上面的第一个进程。因为操作系统还是会加载一个进程,然后之后在第一个进程加载以后,操作系统就会变成一个系统中的服务提供者,给大家提供服务。系统调用调进来会调用操作系统,它会响应中断,在后台也可能会处理系统中一些还没有完成的事情,比如把缓存写到磁盘里。
这些长出了你看到的操作系统世界的全部进程,它到底在哪里?它做了什么?我们能不能控制它呢?你只要能够问出这样的一个问题——我们能不能控制 Linux 加载的第一个进程——那么合理的事情就一定能做到。你马上就可以让 AI 帮你去把这件事情搞定。
我们能不能把操作系统加载的第一个进程换成我们的进程呢?当然是可以的。我这里就给大家准备了一个最小的 Linux 的案例。这个最小的 Linux 我是这样构想的:我们在上课的时候讲过一个最小的 x86_64 的二进制文件。这个二进制文件首先做一个系统调用,比如打印一个 "Hello World"——这里打印 "this is a minimal binary"。它不需要任何额外的库函数,它就用系统调用,然后调用 exit,这个程序就退出了。这是一个 x86 的最小的二进制文件。
那我当然可以编译它。这是一个 64 位的 x86_64 静态链接的 minimal。那如果我在我的电脑上运行它,也是可以运行的。为什么呢?这不是一个 x86 的——哦,因为我的加载器知道我的系统里面有 QEMU。如果我运行一个别的体系结构的二进制文件的话,它默认会进入模拟器的状态执行。它会模拟执行所有的用户态的指令——那些 move、计算和内存的指令——然后当调用系统调用的时候,它会直接调用我这个操作系统上的系统调用。
这是一个很有意思的特点。你如果问 AI 的话,AI 会给你大概会给你一段这样的代码,它说:如果你想要控制第一个运行的进程的话,你首先需要准备一个东西叫 initramfs。这个词是这样的:Initial RAM Filesystem,初始的时候的内存里的文件系统。
还记得 execve 的参数吗?execve 的第一个参数是一个 pathname。pathname 指向了系统里的一个文件。你 execve 一个 /bin/ls、/bin/bash 都是可以的。所以你为了能够运行一个程序,你必须要有一个文件。但是在内核刚开始的时候,你还不知道你的文件系统是存在你的 U 盘上,还是存在你的磁盘上,还是在哪里。那这个最早期的这个文件在哪里呢?
它就在一个叫 initramfs 的东西里。这是你可以控制的,你可以构建的。所以如果你用 apt update 去更新的话,你会看到这个文件是在哪里呢?更新你的 Linux 系统上面的软件包的时候,你会看到一个耗时特别长的操作叫 update-initramfs。所有的操作都很快,唯独那个操作是最慢的。因为有些软件更新了以后,它们要被重新打回到这个最初始的文件系统里,然后这个文件系统里要有你要加载的第一个程序。
你知道到这一点以后,你就可以让 AI 帮你。AI 当然也知道这个概念,AI 就可以帮你去 build 一个 initramfs。initramfs 也可以给你一个脚本打包,你可以直接问它。我们可以因为这是一个 fs(文件系统),我们可以做一个目录树。目录树里面我们可以有什么 /bin、/sbin,任何一个你想要的东西。bin 里面可以有 bash,可以有一个 init。
Linux 的操作系统的代码里硬编码了一系列的路径,它会按照 /sbin/init, /etc/init, /bin/init 一个一个从这个 initramfs 里面一个一个去尝试。如果 /sbin/init 不存在,它就会是 /etc/init,再是 /bin/init。总之试完了试到最后试到一个有了,execve 相当于执行 execve 成功了,这个第一个进程就被加载到 Linux 的世界上了。
这就是第一个进程怎么来的。然后我们完全可以控制它。这份代码就控制它。它会把这个目录树——一个目录树——打包,用 cpio 归档,然后用 gzip 压缩,然后生成一个 initramfs.cpio.gz。最终 QEMU 在启动的时候,用 -kernel 加载内核,用 -initrd 加载这个打包好的 initramfs。
好,我还可以加一个参数 rdinit 等于 /init。我可以给内核一个参数去 override 它默认的行为——它是硬编码的一些路径——但我可以在配置里面给 Linux 内核加一个命令行参数,指定第一个进程是 /init。AI 解释得很好。我可以 make run,这个时候我就是在 QEMU 的模拟器里运行了。
我用了 nographic 的选项,所以没有那个图形界面的 QEMU,你确实可以看到这个比你们的 Linux 启动还慢很多,因为这是模拟出来的。我在 ARM 上模拟了一个 x86。你可以看到 Linux version 6.1.0 amd64,这是我从我另外一台电脑上面考出来的一个 Linux 内核的镜像,然后我运行了一个 QEMU standard PC。
你现在知道了在我 Linux 开机的时候,有不断的滚动一些消息,然后滚到某一个时刻的时候,它突然屏幕会闪一下,然后你的字体、分辨率就都正常了。你观察到这个现象,它的启动是分好几级的。在 minimal 的时候——就在我这个 minimal 加载的时候——在这里内核运行了 4.5 秒以后,Linux 说“Run /init as init process”。正确,这是我刚才指定的行为。然后它打印出来“this is a minimal binary”。
这就是我的汇编刚才运行那个 minimal 的时候打出来的一模一样的红字。有意思的是,刚才我的这个执行了几条指令以后,它执行了一个 exit,对吧?它执行了一个 exit syscall。在我的操作系统上这是没问题的,这个进程就退出了,因为我的系统里面还有其他的进程。但是如果我的 init——我的 init 退出了——这个操作系统内核就直接罢工了,叫“Kernel panic - Attempting to kill init”。它说因为你的 init 进程退出了,整个操作系统就已经挂掉了,有一个 core dump。
所以你看,我们确实控制了 Linux 加载的第一个进程。在 AI 的帮助下——这是我往年没有的内容——我们可以干更多的事。比如 kernel unpack 这个 initramfs。我告诉他我是一个树莓派,我想把他的这个 initramfs 给解出来。他找到了这个 /boot/initrd.img。你甚至都不需要知道它运行了什么。因为你只要有一个概念:这个世界上有一个打包好的目录树。
我们刚才打的一个包是我们自己打的。在真实的系统里面应该有一模一样的包,只是它的目录树是固定的。然后他其实犯了一个错误,他把它解包了。他说这是一个 minimal 的 initrd,仅包含内核模块,没有 init 的脚本,他翻车了。我今天在准备这个的时候,我就走过一遍这个流程,所以我也知道他翻车了。比如你可以发现这里面总共只有 7.3 兆的文件,但是这个 initrd 有足足有 23 兆那么大。所以这件事情不对。
你只要发现这个不一致,你就可以跟他说肯定有一些东西没有解。也就是说其实你们需要的是一种敏感性,随时随地 AI 都可能会犯错,AI 都可能会做错。这个时候 AI 开始干各种骚操作,你看他甚至直接开始读这个二进制文件了,他觉得这里面可能会有后面有更多的数据,他还没有解压出来。注意在整个过程里面,我对具体的细节的流程——他使用的工具、文件的结构——是一无所知的。我有的只有一些最基本的观察:我解出来了一个 7 兆的文件,但那个文件有 23 兆,那那个 23 兆的文件里面一定还有一些东西没有解出来。
他开始写脚本来解了,他开始写代码来解了。这就是 Coding Agent 向人类学习的地方。你看他找到了在 trailer 后面还有 19 兆的额外数据。让我检查后面的内容。如果你能够有敏锐的观察,当然我也相信有一天 AI 也可以有自己敏锐的观察,那他就真的可以把要做的事情给搞定。
他找到了 ZST 压缩的第二段 CPIO。这是 Linux 内核复杂的地方。虽然我有一个 initrd——initramfs 的概念上是一个目录树——但这个压缩文件可以分几段来压缩。他这次解到了一个叫 initrd4 的目录里,你就看到这是一个有点像完整的小系统。这个目录结构长得非常像你的 /,对吧?你有 lib,有 usr,有 bin。
然后我可以问。这就是我觉得 Coding Agent 真正强大的地方:他可以像人一样探索,只要有足够好的直觉。当然我相信更好的模型他应该自然而然会产生这个直觉:这个 initrd 应该有全部的文件。他会自己再探索一下。现在我做的任务就是让他解释一下一个真实的系统里面的初始的这个文件系统的目录树。
他不仅有说明,这个时候文件里面不仅有 init,他应该还有别的东西。比如如果你的磁盘发生了损坏,那个文件系统挂不上的时候,你们有可能会进入到一个 initrd 的模式,对吧?那这个时候,只要这个 init 文件系统没有损坏,你依然可以得到一个命令行的体验,因为 Linux 内核已经加载了,所有的系统调用都是正常工作的。在这个时候虽然没有磁盘——你的磁盘上面可能有非常多的二进制文件——但是你的系统其实已经可以工作了。
我们来看看它里面有什么。它里面有核心的启动脚本,启动脚本告诉你按照什么样的顺序来启动。甚至还有包括 ntfs-3g,它有 NTFS 的支持,你可以从一个 NTFS 的驱动器里面读到数据。然后有各种各样的配置文件,你看这里有配置文件,甚至有键盘布局、字体配置等等。
然后有一个用户空间工具。你看 /usr/bin 和 /usr/sbin 下面有大量的工具,都是 BusyBox 提供的精简版,有点像刚才我们看到的 Linux 里的,我有全套的命令行工具在这里。你有一个 BusyBox,你也有所有的全套的命令行工具,还有一些库。我们的 init 其实可以是动态链接的,因为在这个时候已经有文件系统了。虽然刚才我那个 initramfs 里面我只有一个 minimal,它是静态链接的,但是如果我的文件系统里面有动态链接库,我的操作系统完全是可以支持 ELF 的动态加载的。
然后这里也有必要的动态链接库,还有一些内核模块的信息,还有固件的二进制——也是驱动,它需要加载必要的驱动才能访问起一些设备。这些都是在这个 initramfs 里的。而且它知道是我的树莓派上面的特定的 initrd。它的主要职责是加载内核模块、检测并且把你的实际的文件系统要挂起来,然后它还会显示一个启动画面。
最后它有一句话引起了你的注意,叫“Switch root”——切换到真实的根文件系统。这个文件系统你们回头想一想,这个 initramfs 你们从来没有见过。我现在用 tree 打印,你们从来没有在你们的 Linux 系统上面见到一个长成这样的文件系统,但是你又非常确信这个就是系统的,因为系统里面可能只有这一个文件。如果你看 Linux 内核启动的日志的话,它也确实指向了这个文件是 initramfs。
那这个东西到哪去了呢?你从来没有见过,你又知道它是必须的。答案就是实际上,Linux 系统在启动的时候——虽然看起来 SystemD 是你的整个进程树的根,是你的 1 号进程——但是不是这样的。你的 Linux 整个系统在启动和加载的过程当中,在 initramfs 阶段的最后,它会 initramfs 阶段你可以甚至可以弹出个命令行 Shell,然后你可以跟它交互说你要干什么,你可以重配置。比如说找不到那个找不到启动盘的时候,你在那个 /etc/fstab 里面写一个启动盘。
然后它找不到那个启动盘的时候,你甚至可以在这里说我可以换一块盘来启动。但最终当所有的事情都完工以后,它会执行一个 Syscall。这个 Syscall 在这里会执行一个叫 pivot_root 的 Syscall。你看我 man 2 pivot_root,它说叫“change root mount”。最后会有一个 pivot_root。但是你们不需要严格知道它的行为是什么,你知道的就是这个 pivot_root 重新创造了你以 systemd 为根的。
但是这也是可以指定的。实际上是因为你的系统的根就是你的现在的那个 /sbin/init 是 systemd,然后这个 systemd 再创建了你世界上进程树里你刚才看到那个进程树的整个操作系统的进程的世界。
那我们来可以真实的来看一下一个真正的 Linux。我也给大家准备了这样的一份脚本。这份脚本里面有两个部分的 init,它们都叫 init。可能名字有一点点容易混淆,这两份脚本是不一样的。这一份脚本——这个 init 是在 initramfs 阶段的。在 initramfs 阶段,我的世界上就只有 busybox。我可以在这加一个 sh。在这个例子里,我打包了这样的一个初始的文件系统,它有一个 /lib,然后有一个 /bin/busybox。然后除此之外,我还有一些驱动程序。我今天发现我的 QEMU 必须要这些驱动才能正确运行,我现在把这些东西打包成一个 initramfs。除此之外这个世界里面就没有任何东西了。
那我现在根据我们知道的信息,我们的 Linux 在加载以后会把这个 initramfs 给解开——或者说会把这个 initramfs 当成一个文件系统——然后当成文件系统也会执行这里的 init。然后这个 init 是什么呢?我这个 init 的解释器是 /bin/busybox sh。我会用 shell 来解释这个。解释这个以后,我会把这个 /init 先删掉,删掉以后执行一个 sh。
然后呢,我编译它,然后我运行它。这次是有图形的,当然因为我是树莓派,所以我用它来做全系统的模拟。我用它来做全系统模拟的时候稍微有点慢,所以 x86_64 的 QEMU 启动可能需要那么个几秒钟。几秒钟以后,我们的 Linux 就正常启动了。这个时候它会加载这样的我的 initramfs,然后刚才那个脚本就启动了。
这个脚本启动以后,这个系统里面其实什么也没有。如果我 ls /,现在这个系统里面就只有 /bin/busybox,然后还有我加的一大堆驱动。没有这些驱动我没有办法做下一件事。这是 /,这个 root 是 shell 创建的吗?最开始是没有的,还有一个 /dev/console。你看到的所有的你今天——比如说我在这个看到这些东西——都不存在,甚至那些二进制文件、/bin/ls 都没有,/bin/sh 都没有,只有一个 /bin/busybox。
我的脚本没有完。我现在运行到 13 行。你看如果我的 shell 退出了——shell 退出以后——我这里面会打一个日志,打一个 message 以后,会进行一段非常神奇的脚本。这段脚本 for command in $(busybox --list),我们来看看 /bin/busybox --list 的功能是把 busybox 里面所有支持的命令都列出来。你看有这么多命令都被打包在——都集成在了一个 busybox 二进制文件里。就是说 busybox 一个文件就有这么多功能。
然后我会执行 busybox 的符号链接,创建一个符号链接。比如说这里如果有一个 command 是 ls,我就会执行 busybox --install -s /bin。所以相当于是我用这样的三行脚本,我就创建出了 /bin 里面的一大堆二进制文件。然后接下来我就开始用 mkdir、mount 这些命令,开始创建操作系统里面的对象,创建一个 procfs 的对象,创建一个 sysfs 的对象。
然后再用 insmod 在内核里面安装一大堆的驱动,再用 mknod 创建一大堆的设备。然后最后打一个 init ok, launch a shell (initramfs busybox sh)。所以我现在要从这个 shell 里面退出。退出以后它就开始做剩下的事情了。你看到对吧,这些日志都打印出来了,insmod 成功了。
看到这张红色的 init ok, launch a shell (initramfs)。然后这个时候你就可以运行像 ps 了。procfs 里面有东西了,我们可以用 ls /proc 来查看一下现在有的功能。这些文件都存在了,每一个都是符号链接,对吧?每一个像 wc、wget,甚至还有一个用来处理互联网功能的 wget,都是指向 /bin/busybox 的符号链接。然后这个时候这个系统其实已经有一点能用了,对吧?
你看我的 ps,它有 ps aux 吗?没有,它只有 ps。但是这个 ps,1 号进程是 init (busybox sh-init)。这是最早的——initramfs 最早的时候操作系统加载的那个进程文件——是所有的进程的祖先。还有一些这是操作系统自己的线程。然后接下来系统里面就只有我的 busybox sh 还有刚才的那个 ps。
OK。所以计算机系统里面没有任何的魔法。还有一点我要强调的,刚才你看到的所有的这个世界——我们做的——在这个程序被加载以后,我看起来调用的是命令行的工具,但是他们的背后全部都是系统调用,对吧?
执行 23 行的时候,我说调用 busybox --install -s,那我就会 fork 一份我当前的 shell,fork 完以后会执行一个 execve 运行 busybox,然后这个 busybox 的 --install 它就会执行一个做符号链接的对吧——ln -s——它做完整解析以后执行一个符号链接的系统调用。这个系统调用会在文件系统里面创建一个符号链接。
同样的,你看起来这里是一个 mkdir 的命令,这个背后也是一个系统调用——一个 mkdir 的系统调用——它就可以在文件系统里面(当然是在这个初始的内存里的文件系统)创建一个目录。mount 也会创建很多的操作系统里面对象,对吧?我把 procfs——mount -t proc proc /proc——就是把整个 procfs 里面所有对象都在 /proc 这个目录里面创建出来。所以这样的一个命令,它调用其实调用那个 mount 系统调用,在我的操作系统里面又创建出了很多的对象。
相应的这些每一个命令都对应的系统调用,包括比如说这个 mknod。mknod 也有一个对应的系统调用。比如说我现在要创建一个叫 /dev/vda 的磁盘,这个 vda 是一个虚拟磁盘,是一个 virtio 的 QEMU 的磁盘。如果你们在阿里云上租一个,你们也能看到 /dev/vda。我们需要指定一个主设备号、指定一个从设备号,这个设备文件才能被创建出来。然后你们的 /dev 里面的每一个文件都是以这种方式创建的。
你看比如说 random,random 就是主设备号是 1,从设备号是 8;urandom 是 1、9;tty 是 4、1。这些设备,你也可以在 ls -l 的时候直接看到它们都是设备文件,对吧?这些是字符设备,这是一个块设备。它们现在是可以正常工作的。如果你看操作系统的日志的话,现在我在这个状态,我在 shell 里,这个 shell 它的功能就相对来说已经比较完善。
比如说我可以 vi a.txt,a.txt 我就可以看到,然后我也可以 cat a.txt。同样的,我可以用管道,这都是标准的。如果我的 shell 退出以后,真正神奇的事情才真正开始。我会倒数三秒钟,倒数一个 321 以后,我会把 /dev/vda——我现在就可以挂载它,我可以创建一个叫 /mnt 的目录,然后我可以 mount -t ext4 /dev/vda /mnt。好,它成功了。
这个时候一个 /dev/vda 对应一个磁盘,这个磁盘磁盘也是一个操作系统的对象,但是磁盘里面有一个文件系统的目录树,然后这个文件系统的目录树就被放到了系统里的 /mnt。这是我给大家准备的另外一个目录,在 fsroot 里面。你看有个 e1000.ko 是网卡的驱动,然后 init 是另外一个 init——是一个另外一个 init。你看这里面会打印一个“JYY's minimal Linux”,对吧?
然后我们来看会发生什么。我先把这个文件系统给解挂了,之后再看,这里就没有我刚才挂载的那个文件系统了。好,我现在可以退出 shell。321 倒数计时。它又花了一些时间,对吧?你看它启动了网卡,然后在这里有一个日志叫“goodbye QEMU console”。在这里我的 console——这个 console 不能用了——但是这个东西可以用了。我们回到了最早的时候和 MINIX 一样的,这是 QEMU 模拟出来的一个 VGA 的设备,一个显示设备。
刚才我们一直是在模拟的一个串口,相当于一个调试串口,跟 /dev/console 对话。然后我们的这个(VGA)一直是黑的,如果你们留意到我有几次不小心切到这个中间的话,在这里一直是处于一个什么什么输出也没有的状态。但是现在有了 /dev/tty 了,对吧?然后如果我 echo hello 打印到 /dev/tty,那就是我现在的这个。所以这是 Linux 用驱动程序画出来的,对吧?每一个像素都是画出来的。这里有我打印的“JYY's minimal Linux”。
这个 Linux 就是一个真正可用的 Linux。这个 init 是我们的操作系统在 initramfs 的最后阶段——就是 pivot_root 系统调用之后——它会重新换一个 init,换一个文件系统。换了以后就到了我现在的这个程序所做的事情。因为这个文件系统现在是空的,所以我还需要把所有的 busybox 全部都里面的那些什么 ls、grep 这些东西全部都创建一遍,再把 procfs、sysfs 这些东西再创建一遍,设备 /dev 里面也要再重新创建一遍。
然后在这里我配置了网络,我安装了一个 e1000 的模拟出来的网卡的驱动,然后我设置了一个 IP 地址。然后 IP 地址我可以干这样的一件事:在这个 Linux 里——这是一个虚拟机,但是它可以在理论上说是没有任何问题在一个真实的计算机上运行的——我可以在这里启动一个 httpd,我启动 httpd 在 8080 端口。
httpd 在 8080 端口,好。你明显发现这个终端打印的速度变慢了,对吧?因为这是一个图形,它是一个一个像素点画出来的,它不再是刚才那个硬件模拟出来的一个字符字符接口,它真的模拟的一个图形的图形的设备。好这个时候网络应该能够正常工作了,因为你看这边已经看到网卡“link becomes ready”了。
那么我的 Makefile 里面还有给大家留了一个彩蛋,我用 QEMU 给大家做了一个端口映射,我把里面的 8080——就是虚拟机里的 8080 端口——映射到了外面的 8080 端口。这也就意味着理论上说我可以访问 localhost 的 8080 端口,我就可以访问到虚拟机里面的网页。
这是一个 404 是对的,因为我没有 index.html。那我的虚拟机里面有什么呢?虚拟机的文件系统里面有什么呢?文件系统里面有一个 init。所以理论上说,如果我实现的正确,如果我 localhost:8080/init,我就应该能看到这个 init 脚本。我看到了,这证明了我的这个 Linux 是真正点亮了。
我们经历了一个非常漫长的过程,所有的脚本你都看到了。我们从最早的这个 initramfs 里面只有这个东西,到我可以在 initramfs 里面加载驱动,从做一些 Linux 允许的系统调用的事情,都是系统调用。你看到的所有的这一切,从第一步,然后到 pivot_root,最终到你看到一个我可以在里面启动一个 httpd——一个网络服务器——然后甚至能在外面能够访问这里面的网络服务。我真的在浏览器里面真的看到了我的那个网页。
所有的这一切都是用系统调用实现的。从第一个进程——我们说 initramfs 加载的第一个进程开始之后——所有的事情都是用系统调用实现的。这个就是系统调用这个抽象它的真正厉害的地方。这是我刚才说的,对 initramfs 其实不是我们看到的 Linux 的世界。有兴趣的同学也可以去看一下相关的这些文件。
所以这算是一个故事的结束,对吧?这就是应用视角的操作系统。我花了那么长的时间,有点绕,但回来的时候什么基本原理呢?操作系统一定会到达一个确定的初始状态。这个绝对确定的初始状态是由 CPU reset 决定的,然后 Firmware 会把操作系统加载好,操作系统会把第一个程序加载好。然后从第一个程序的初始状态开始,剩下的整个世界就只有一个东西:操作系统对象和 API。
这就是操作系统给应用世界提供的一切。然后你可以想象这件事情还是挺惊叹的,对吧?你从你的你拿起一个 Android 的手机,下意识地打开抖音的时候,你再想到操作系统课上面我跟你说过它是从 initramfs 开始,之后你抖音上面看到的每一个像素点都是系统调用实现的,对吧?这还是有一点点惊叹的,而且你知道这个事情确实是这样的。
这是一个进程所走的路,到你最后看到屏幕上的像素点。然后从一个进程,从 Linux 到 MINIX 到 Linux,我们看到了这个历史是怎么走过来的,对吧?从最早的一个 27 行汇编代码的 fork,到 MINIX,到论战,到 Linux,到今天一个成熟稳定的状态,一个很精彩的故事。但其实没有结束,因为我们说精彩的故事在这个系统调用的对象和 API 之上还在延续,没有停止。
在 Linux API 上没有什么东西是不能做的,对吧?大语言模型、Agent、抖音、淘宝这些东西全部都是在 Linux API 上做出来的。所以我最后一个要稍微讲一点的就是应用生态。光有操作系统的 API 是不够的。这个操作系统的 API 很大程度上是和使用这个操作系统的应用程序一起演化的。应用程序发现操作系统里面的性能不够的时候,操作系统会给它一个新的 API、性能更好的 API,或者一个新的文件、一个新的对象。它们就这样共同进步,最后变成了今天操作系统的样子。
所以其实是应用生态成就了操作系统的繁荣。操作系统自己本身它什么都不是,它就是一个 API。但是因为厂商、个人开发者——每一个人,GitHub 上每天都在发布新的应用——所以操作系统其实不仅仅需要实现这一套 API,还需要一套核心的工具集来支撑这些厂商和个人开发者。这些也是操作系统的一部分。
所以你看操作系统有狭义的操作系统和广义的操作系统。我们的讲操作系统原理最主要的还是讲操作系统的 API。但当我们谈论操作系统的时候——为什么中国没有国产的操作系统呢——我们很多时候是谈论这个生态。它需要一套核心的工具集、基本的运行库(libc)、什么 3D 的库、图形库,这就麻烦死了。还有核心的工具集,这里面一点安全漏洞都不能有一个,安全漏洞就把你整个系统都拖垮了。
除此之外还有各种辅助工具,版本管理、系统管理,这都是代码。你看起来 Linux 内核好像 2000 万行代码,那已经是个庞然大物了,但相比于这个应用生态来说根本就是一点点。你一个复杂的业务代码很有可能就是上千万行的,就一个应用程序就是上千万行的,然后它们之间还要互相协作。
这个应用生态也不是就像今天一下马上就变成今天这个样子了。在早期 DOS 的时代,我们还是用软盘和光盘发布软件的,那个光盘的 CDKey 就写在光盘上,或者一张纸条贴在光盘里面,所以很容易破解。但进入了互联网的时代以后,你看到我们现在有更好的应用生态的分发方法了:App Store,apt,rpm,Python 的 Packaging Index,npm,Hugging Face,Ollama,都是这种内容分发的生态。
所以你看到我们的操作系统是一层一层的。内核 API 是最小的一层,上面有库函数、Shell、核心的工具集,整个应用的生态。举个例子,比如说我现在这个树莓派上跑的这个 Debian 系统,它说:“Our mission is creating a free operating system”。当我们使用“free”这个词时,我们不是在谈论钱,不是免费的操作系统,是自由操作系统,是 software freedom。是你的所有的软件都是可见的,你可以看到它每一行源代码,你都能看,你也可以理解,你也可以为它做出贡献。
所以我觉得 CS 能够发展这么好,也有很大一个原因像 Linus 这样的人,那个天才少年,他不藏着掖着,他说我有一个好东西,我就迫不及待的想要和别人分享。然后每一个人看了他们分享了好的东西,你做出好东西,你也自然而然的愿意和别人分享。在 1998 年的时候,apt 被发明出来。
这是 1998 年。如果你想装 Firefox,你只要 apt-get install firefox 你就装好了。你看苹果的 App Store 什么时候才出来?2000 几年的时候才有 App Store,而在 1998 年的时候就有了 Debian 的 Advanced Packaging Tool。apt 有一整套开源供应链的管理流程。你的包从 unstable——不稳定的,从 GitHub 上面每天你都往上面交的代码——直到一点一点一点被合进 test 的分支,然后有一些志愿者就愿意尝试,你愿意尝鲜、愿意获得更新的版本功能的人可以尝鲜,然后慢慢一点一点进入稳定的状态。
后面还有一些细节,比如说软件的生态是怎么构建的。我们现在可以直接让 AI 帮我们:你随便找一个——比如说我现在有一个 deb 包——你的系统里面可能也有,不管是 rpm 还是 deb——你都可以让 AI 直接把这个包解开,然后问安装这个软件包到底意味着什么。你就可以知道这个应用生态是怎么完整的构建起来的。我觉得这是一个很有意思的话题,你们就是操作系统上面的对象和系统调用,但是它从上面长出了完整的应用生态的世界。