ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入解析Linux 0.01内核源代码:从启动到进程调度的完整指南

2026/9/2 12:10:22 拓冰建站 浏览量
深入解析Linux 0.01内核源代码:从启动到进程调度的完整指南 简介Linux 0.01内核源代码.zip是一份承载操作系统起步阶段的关键源码包面向操作系统原理学习者、内核开发入门者以及关注Linux演进历史的技术人员用于分析早期内核的引导流程、进程管理、内存管理与文件系统实现尤其适合希望从零理解操作系统核心设计的学习者。资源共91个文件压缩包仅95KB包含45个C源码、31个头文件、7个汇编文件、5个Makefile、2个JSON配置和1个VSCode工作区文件C代码覆盖进程调度、内存分配、文件系统与设备驱动逻辑头文件定义关键数据结构和接口汇编完成启动引导与系统调用入口Makefile给出编译链接规则JSON与工作区文件则方便在VSCode中直接打开阅读。代码按boot、init、kernel、fs、lib、include、tools等目录组织可以沿着引导加载、内核初始化、核心服务到根文件系统挂载的完整链路逐层拆解同时保留了早期构建工具和开发配置便于边读边验证。目前已有753人浏览/学习是理解操作系统底层机制和Linux版本演进的难得原始素材。 不用满世界找教程了真正的“版本答案”就藏在linux 0.01内核源代码.zip这个压缩包里。这份代码是 Linus Torvalds 在 1991 年写出的第一版 Linux 内核整个系统只有不到一万行 C 代码和一小段汇编却完整覆盖了进程调度、内存管理、文件系统和系统调用。无论你是嵌入式开发者、系统程序员、逆向爱好者还是只是想知道操作系统到底怎么跑起来的在校学生把这份源码从头到尾读一遍比看十本《现代操作系统》都管用。很多人第一次看到“内核源码”四个字就发怵总觉得那是大神才能碰的东西。但 0.01 不一样它不是一个庞大到让人绝望的现代内核而是一个“麻雀虽小、五脏俱全”的迷你操作系统。我花了两周时间把这份代码从 boot 到文件系统全部过了一遍期间踩了不少坑也把很多“书上没说透”的细节摸清了。这篇文章就按我实际阅读和运行这份代码的顺序把里面的门道一一拆开讲给你听。1. 这份代码到底是什么先给 0.01 内核画个像1.1 它和现代 Linux 完全是两种东西先打个预防针0.01不是我们今天用的 Linux 的“精简版”而是一个独立的、教学意义远大于实用意义的实验性系统。它的完整代码量只有约 8000 行 C 代码和约 1000 行汇编支持的功能非常有限——没有网络栈、没有虚拟内存、没有动态模块加载甚至连fork的写时复制都没有父子进程共享同一个内存空间后子进程退出时释放内存父进程再访问就会崩溃。这些限制放到今天看很离谱但正因为“缺”的东西多它反而把操作系统的核心骨架暴露得干干净净。你拿到手的linux 0.01内核源代码.zip解压之后会看到几十个.c和.s文件。这些文件就是整个系统的全部从开机那一刻起它先在 16 位实模式下做硬件初始化然后切换到 32 位保护模式建立段页式内存管理启动第一个进程最后在屏幕或者串口上跑起一个简单的 shell。整个链路一气呵成没有任何多余的抽象层。1.2 适合谁读、能解决什么问题我把话说直白一点如果你正在学操作系统原理或者做嵌入式开发觉得“被框架推着走、不知道底下发生了什么”这份代码就是你的解药。它适合三类人计算机专业学生把课堂上讲的进程、中断、系统调用和源码一一对照理解会深一个量级嵌入式开发者0.01 的内存管理和启动流程和很多 MCU 上跑 RTOS 的思路高度相似读懂了它再看 Keil、IAR 工程里的启动文件会非常轻松内核入门爱好者现代内核动辄几千万行代码根本无从下手0.01 是公认最理想的起点。我以前也试过直接啃现代内核的源码结果不到一小时就被各种spinlock和 RCU 劝退了。后来转去读 0.01越读越上瘾很多以前“知其然不知其所以然”的东西比如中断描述符表、页目录页表、任务状态段都在这份代码里找到了最原始的答案。2. 内核源码的结构拆解先看骨架再啃细节2.1 目录结构与各模块职责解压linux 0.01内核源代码.zip后首先映入眼帘的是一系列顶层文件它们彼此之间没有复杂的目录依赖基本一个文件对应一个子系统。我读的时候把每个文件的功能用标签记在文件名旁边方便反复查阅路径核心职责我的批注boot/bootsect.s开机引导加载内核到内存16 位汇编实模式boot/head.s初始化页表、GDT/IDT跳入 main32 位汇编是整个系统最烧脑的部分kernel/sched.c进程调度、时钟中断处理内核的心脏值得反复读kernel/fork.cfork系统调用实现没有 COW逻辑直白kernel/sys.c各种系统调用的具体实现相当于今天内核的kernel/sys.c老祖宗kernel/panic.c内核致命错误处理只有几十行但写满了防呆逻辑mm/memory.c内存分页、缺页处理0.01 里少数几处巧妙设计之一fs/文件系统MINIX 文件系统实现支持读写文件够用lib/内核自定义的字符串/内存函数启动阶段替代 libc 用的include/头文件含大量硬件寄存器定义学硬件接口的好素材我特别建议先读include/linux/sys.h这个头文件里的系统调用表。你会发现 0.01 里总共有 30 多个系统调用sys_fork、sys_write、sys_read、sys_execve这些在现代系统里司空见惯的函数在这里全部以最原始、最直白的方式摆在你面前。2.2 boot 阶段发生了什么用一张流程图来感受 0.01 启动的“三线接力”是最直观的bootsect.s被 BIOS 加载到0x7C00它负责把内核其余部分从软盘/镜像读到内存0x10000处设置启动参数跳转到head.s它先把自己以及整个内核复制到内存高地址0x0000处实际是物理地址 0x0000 处开始放页目录内核映像放在 0x100000 处然后建立页目录页表开启分页通过ljmp跳进 32 位保护模式的 C 代码main.c里调用mem_init()、trap_init()、sched_init()等初始化函数最后移动到用户态执行task0。这一套流程今天看起来可能觉得“为什么要这么绕”但在 1991 年PC 只能通过 BIOS 从软盘加载有限大小的代码加上 286/386 实模式和保护模式之间的切换机制这是当时唯一合理的工程选择。2.3 内核的核心不是“内核”而是main.c里那一行行初始化把0.01的main.c打开你会发现它的main函数就是普通的 C 函数几乎和写一个用户态程序无异。它首先调用time_init()读取 CMOS 时钟然后trap_init()把 256 个中断门填进 IDT接着sched_init()初始化时钟中断和第一个任务然后buffer_init()、hd_init()、floppy_init()、file_system_init()逐个登场。最关键的一个“仪式感”出现在最后几行move_to_user_mode();这行语句在 0.01 里是一个宏它先构造一个伪造的栈帧把任务 0 的特权级从内核态提到用户态然后iret返回到用户态执行。也就是说内核初始化完成后并不是“自己继续跑”而是把控制权交给任务 0再由任务 0 通过fork创建任务 1任务 1 再加载 shell。这种“内核只为用户态服务”的设计思想从第一天起就定型了。3. 关键机制的三板斧进程、内存、系统调用3.1 进程与调度everything is a task0.01 里没有“线程”的概念也没有进程组、会话这些现代概念只有task_struct数组。每个进程对应一个task数量上限是 64这个数组在sched.c里静态定义。整个操作系统的核心调度逻辑就是一个while(1)循环 一个基于优先级的时间片轮转算法。调度函数schedule()把所有任务过一遍找到counter最大的任务去运行然后给每个任务分配一个时间片默认是任务优先级的值时钟中断每次调用do_timer()把当前任务的counter减一减到 0 就重新调度。这里的counter很值得玩味它不仅表示“我还剩多少时间”还参与了睡眠与唤醒机制睡眠进程的counter不会减少唤醒后才恢复这为后来的sleep_on系列函数提供了基础。fork的实现同样简洁。sys_fork会调用copy_process()它把父进程内核栈中的寄存器上下文复制给子进程并让子进程在返回时从fork()的位置继续执行只是返回值是 0。由于 0.01 没有写时复制copy_process里直接把父进程整个数据段复制一遍代码段因为是只读的继续共享。你在读fork.c的时候只要抓住“复制、改返回值、改状态、加入队列”这四步整个系统调用就通透了。3.2 内存管理页表和“缺一页补一页”0.01 的内存管理单元在mm/memory.c里代码不到三百行但思路极其清晰。它把物理内存按照 4KB 一页来管理用一个mem_map[]数组记录每一页被几个进程引用。分配页时从mem_map里找引用计数为 0 的页置为 1释放时减一减到 0 才真的回收。此外还有一个free_page()函数专门负责把一段页表映射关系清零并释放物理页。整个 “虚拟地址 → 页目录 → 页表 → 物理页” 的分层结构在现代内核里有复杂的反转发机制但 0.01 里是直来直去给定一个线性地址通过get_free_page分配物理页然后通过put_page填页表项。如果你想把“内存映射”这四个字彻底弄懂强烈建议只看mm/memory.c里的get_empty_page和put_page再配合逻辑地址、线性地址、物理地址三者间的关系图来对照。书上说的“虚拟内存翻译”在这几行代码里变成了一张可以直接算出来的表格。3.3 系统调用从int 0x80到sys_write用户态的程序要打印一行字最终是怎么走到内核里的0.01 的答案简洁到令人发指所有系统调用都通过int 0x80中断触发。中断门在trap_init里统一设定然后system_call汇编函数会在栈上找到系统调用号再去sys_call_table里查找对应的处理函数。以write为例用户态调用write(fd, buf, count)库函数把它包装成三条指令把系统调用号 4 放进eax把参数放进ebx/ecx/edx然后执行int 0x80。内核中的sys_write拿到参数后从文件表里找到文件对应的读写方法最终调用底层的驱动或者内存文件逻辑。整个链路只有四级用户库函数 → 中断 → 系统调用表 → 具体实现。这一点对理解现代操作系统非常有帮助系统调用在本质上就是“软中断 一张函数指针表”。我在读这段源码之前一直把“内核态和用户态的切换”想得非常神秘看完 0.01 才发现它不过就是一个中断加几次压栈弹栈。4. 环境准备与运行让三十年前的代码在今天跑起来4.1 工具链准备编译器版本要“做旧”把linux 0.01内核源代码.zip解压后如果你直接用现代gcc去编译百分之百会报错。原因很简单1991 年的gcc还停留在 1.x 时代很多汇编语法和 C 语法与现在差异巨大尤其是内嵌汇编的格式和.s文件里的指令助记符现代工具链已经不再支持。我的建议是不要用本机 gcc 硬刚而是装一个旧版编译器环境。最省事的方案是直接用 Docker 或者专门的模拟环境里面预装gcc-1.40、binutils2.x 和对应的as86/ld86。# 我用的环境Ubuntu 20.04 上通过源码编译老工具链 # 1. 解压源码 unzip linux-0.01.tar.gz cd linux # 2. 修改 Makefile 中的编译器路径指向老版本工具链 # 3. 执行 make make如果你不想折腾老工具链还有一个更取巧的办法直接用模拟器 Bochs 配合现成的linux-0.01镜像文件跳过编译这一步。Bochs 可以模拟完整的 x86 环境而且能在指令级别调试非常适合研究启动流程。我第一遍就是先用 Bochs 跑通再回头去读代码的。4.2 编译和运行从源码到软盘镜像0.01 的 Makefile 里写了一整套“把内核打包成可引导软盘镜像”的构建流程。它会用bootsect.s生成引导扇区把head.s和所有 C 文件编译链接成Image文件最后用dd写入软盘镜像。我用的运行方式是用 QEMU 模拟一台 386 机器直接把生成的镜像当作软盘启动qemu-system-i386 -fda Image -boot a如果一切正常你会看到 QEMU 窗口里出现系统的启动信息这是 0.01 内核通过sys_write向屏幕输出的。能自己跑起来一个“史前”操作系统那种成就感非常奇妙。4.3 0.01 能运行哪些程序别期待它能跑 Bash 或者 GCC。0.01 自带的 shellshell.c和几个基础的二进制程序都在用户态能做的最复杂的事就是执行ls、cat、mkdir、sync等极简命令。而且里面没有磁盘划分的概念只有一个虚拟的 MINIX 文件系统镜像挂在/下。我在运行的时候为了验证execve和fork写了一个只有几行的用户态测试程序把它放进文件系统镜像里然后通过 shell 去执行。虽然要经历“编译成 MINIX 格式 → 用工具塞进镜像 → 启动系统运行”这三步但当你看到自己写的程序跑在 0.01 内核上时你会觉得前面所有折腾都值了。这个过程让我对“应用程序如何被加载执行”有了真实的体感。5. 实际踩坑与针对性排查我运行这份代码时踩过的五个坑5.1 编译问题汇编语法报错、Makefile 里的硬编码路径第一次make编译器直接摔在head.s那个文件上原因就是我前面说的as版本太新。解决方法是把 Makefile 里AS86和LD86这两个变量指向老工具链并且在内核源码里把进程数上限调大或调小都随意反正不影响编译。具体报错我再列几个常见的报错现象原因解决办法Error: unknown pseudo-op:.code16新版汇编器不支持部分老指令使用 binutils 2.9 以下版本或改用 Bochs 预编译镜像linux/head.s: undefined reference togdt符号名冲突给汇编里的全局标签加_前缀老编译器约定make: Nothing to be done for all时间戳问题删掉所有.o文件重新清理构建5.2 运行问题挂载软盘镜像、死机、看不到输出用 QEMU 运行时要特别注意0.01 的内核映像最多 64KB软盘镜像如果大于 1.44MB 会无法引导。我一开始图方便用了一个 2.88MB 的-fda镜像结果一直启动不了换了标准 1.44MB 镜像就正常了。另外如果你在 QEMU 里看不到任何输出试着加-serial stdio或者-display curses因为早期内核可能通过串口输出信息而不是图形终端。用-debugcon stdio也可以捕获printk级别的调试输出这个技巧在做内核实验时非常实用。5.3 阅读源码时的“劝退点”及应对真正的难点并不在于代码本身而在于缺少“上下文”。0.01 内核没有虚拟文件系统层没有模块机制也不区分硬件抽象层所以你看到的每个函数都直接面对硬件寄存器。比如hd.c里那堆outb_p、inb_p没有注释根本不知道在干什么。我的应对方法是“先硬件后代码”把 8259A 中断控制器、8253 定时器、DMA 控制器这几个芯片的手册大致翻一下再回来看代码就顺多了。当然如果你只是把 0.01 当启动流程和进程调度的学习材料完全可以跳过设备驱动的部分。6. 阅读路线建议与延伸思考6.1 推荐的阅读顺序从启动到驱动层层递进我把我的阅读路线整理成了如下顺序这个顺序尽可能让人不会一上来就被细节淹没main.c先把全局流程摸清sched.c理解进程管理和调度fork.cexec.c理解进程创建与程序加载memory.c理解内存映射fs/下的几个文件理解文件系统hd.c、floppy.c最后再看硬件驱动。按这个顺序读你会在每一个系统调用的实现处有一种“原来如此”的顿悟感。6.2 从 0.01 到现代内核三处最大的变化读完之后如果你还有余力可以挑三个主题去对比现代 Linux 的变化一是进程调度从 O(n) 遍历变成了 CFS 红黑树二是内存管理从“直接复制页”变成了写时复制 反向映射 大页三是系统调用从int 0x80变成了syscall指令。这三个变化背后其实是对同一类问题的不同解法理解了 0.01 的做法你会更容易看懂现代内核为什么要那么复杂。6.3 一个小技巧给源码写注释我强烈建议你在读源码的时候用代码注释工具或者直接在副本里加注释把每个函数的作用用一两句话写清楚。不用写得很正式哪怕记“这个函数是把 eax 的值打印出来”都行。因为 0.01 的代码几乎没有注释你边读边写注释等于自己在构建一份“定制化教材”这个过程对你的理解深度帮助巨大。最后再分享一个小经验0.01 源码虽然古老但它和我后来接触的很多国产嵌入式系统、教学操作系统、轻量级 RTOS 在思路上有大量相似之处。很多时候你在一个平台上看不懂的抽象概念回到这份代码里反而能一眼看透。如果你也想真正理解操作系统底层建议不要把它只当“古董”收藏找个周末泡杯茶把压缩包解开那份代码会告诉你很多书里没写的东西。本文还有配套的精品资源点击获取