HIT-操作系统 | 我在 Linux 0.11 里追着一个进程跑了五次
操作系统实验最容易给人一种错觉:代码看起来不长,真正开始做以后却很难找到落脚点。尤其是 Linux 0.11 这一类教学内核,屏幕上的每一行汇编都像是从很久以前寄来的信,变量名少,注释也不一定能完全对上,偏偏系统还要求你把启动、中断、任务切换、系统调用和进程调度都解释清楚。
我做这组实验时,最大的变化不是记住了多少条指令,而是终于开始习惯追着一个状态往下走:它从哪里来,经过哪一次中断,被保存到了哪一块栈里,又在什么时候被恢复出来。这个仓库里的五个实验,正好记录了我从“看启动代码发懵”,到能够顺着日志分析进程调度的过程。
仓库地址:KALEsky/HIT-Operating-Systems-Labs。
实验一:从 iret 开始,第一次看见特权级切换
实验一分析 Linux 0.00 的启动代码、中断入口和栈结构。刚开始看这部分时,我总觉得每一条指令都在“搬东西”:把寄存器压栈、从栈里弹出来、跳到某个入口,再从某个地方回来。真正的问题是,我知道每条指令在做什么,却不知道这些动作合在一起想解决什么。
后来我把视线从单条指令移到了中断前后。程序在用户态运行时,突然发生中断,处理器会把返回所需的信息压到栈上;内核处理完以后,再通过 iret 把现场恢复出来。iret 并不只是“返回上一行代码”,它会一起恢复代码段、指令地址、标志寄存器和栈相关信息。也就是说,处理器是在借助这份栈帧,重新确认“回到哪个权限、哪个位置继续执行”。
这个实验最折磨我的地方,是栈上的内容看起来特别容易混。一个偏移算错,后面的解释就会全部错位。后来我索性把中断前、进入内核后和 iret 返回前的栈画成三张图,再一项项对应。图不漂亮,但比盯着屏幕猜有效得多。
我也是从这里第一次感觉到,操作系统并不是“程序运行的背景板”。它一直在保存现场、切换权限、安排下一步该由谁继续执行。
实验二:TSS 和任务切换,原来切换任务不是一句“换个函数”
第二个实验把我带到了 TSS 和任务切换。时钟中断到来以后,系统通过远跳转选择另一个 TSS,让处理器保存当前任务的上下文,再恢复另一个任务的寄存器、栈和指令位置。
第一次读到类似 jmpf 0x30:0 的代码时,我只觉得它很神秘。后来把选择子、TSS 和时钟中断放在一起看,才明白这不是一次普通的函数调用。函数调用通常只关心调用者和被调用者之间的约定,而任务切换要处理的是一整套运行现场:CS:EIP、栈、通用寄存器,以及任务下一次从哪里继续执行。
这个实验还让我区分了两个以前容易混在一起的概念:任务切换和中断返回。中断返回是在恢复一次中断发生前的执行现场;任务切换则是在不同的任务上下文之间保存和恢复。它们都和“返回”有关,却不是同一件事。
那一刻我对操作系统的理解开始有点变化。所谓“同时运行多个程序”,并不一定意味着机器真的在同一个核心上同时做所有事情,而是系统在很短的时间里保存、切换、恢复,让每个任务都获得继续前进的机会。
实验三:启动过程,机器上电以后究竟先做什么
实验三继续沿着启动过程往前走:从 boot sector 读入 setup,读取硬件参数,再把控制权交给后续的启动代码。平时按下电源键以后,屏幕很快就亮了,很容易忘记这中间其实没有“操作系统已经准备好”这件事。
我第一次真正观察启动代码时,注意力总被某个地址或者某条跳转吸走。后来我给自己换了一个问题:这一阶段的代码到底在为下一阶段准备什么?答案就清楚了不少。它要把必要的代码放到合适位置,建立后续运行需要的环境,把硬件信息整理出来,并把处理器从最初的启动状态逐步带到能够运行内核的状态。

这张启动输出图对我来说很有纪念意义。它不是什么复杂的可视化结果,但它把一段原本只存在于源码里的流程,变成了屏幕上可以观察的东西。看到系统真的走到那里,和在报告里写“启动完成”,完全是两种感觉。
实验四:给 Linux 0.11 加两个系统调用,用户程序怎样找到内核
第四个实验要求为 Linux 0.11 增加 iam 和 whoami 系统调用。表面上看,一个负责写入名字,一个负责读出名字,功能很小;但它把用户态到内核态的完整链路暴露了出来。
我需要先在用户程序里发起调用,再让系统调用号能够被识别,接着修改内核中的系统调用表和实现函数,最后让编译出来的内核和测试程序真正接上。中间任何一处没有对齐,都会出现很令人困惑的现象:程序能编译但运行不了,调用号对了却返回错误,或者数据明明写进去了,读取时却像没有发生过。
这个实验让我第一次从“调用一个 API”的角度,看到 API 背后其实有一条很长的路。用户程序并不是直接跳进内核函数,而是通过约定好的入口、寄存器和调用号,把请求交给内核,再由内核决定实际执行哪个实现。
现在回头看,iam/whoami 当然是一个很小的功能,可它比很多复杂功能更适合拿来理解系统调用:动作足够简单,链路却足够完整。
实验五:追踪进程运行轨迹,终于能看见调度不是玄学
第五个实验是我对这门课最有感觉的一次。需要在内核中维护进程运行日志,记录进程从创建、就绪、运行、等待到退出的状态变化,再统计等待时间、周转时间、运行时间和吞吐量。
实验里用 N、J、R、W、E 这些标记描述进程状态。以前看调度时,我脑子里的画面很抽象:时间片到了,系统切换任务,某个进程继续运行。真正把日志打印出来以后,才发现一个进程的生命线其实是由很多细小的状态变化组成的。
我当时最关心的是修改时间片以后,数据会怎样变化。时间片更短,切换可能更频繁;时间片更长,某个进程可能连续运行更久,但其他进程需要等待。它不是简单的“越大越好”或者“越小越快”,而是在响应性、切换开销和吞吐量之间做取舍。

看到统计脚本把日志变成平均等待时间、平均完成时间和吞吐量时,我第一次觉得调度策略不再只是课本里的概念。它会留下可以比较的痕迹。哪怕这些数字只是历史实验记录,它们也能帮助我把“调度好不好”从一句空话变成几个可以观察的指标。
五个实验串起来,才看见一条从启动到调度的路线
实验一和实验二让我先理解中断、栈和任务上下文;实验三把视线拉回系统启动;实验四让我从用户程序跨进内核,观察系统调用如何连接两种执行环境;实验五则把进程状态和调度结果记录下来。
它们连起来以后,我对操作系统的认识终于不再是“有内存管理、有进程、有文件系统”这种名词列表,而是一条可以追踪的时间线:机器启动,内核建立环境;程序发起请求,系统通过中断进入内核;任务在不同上下文之间切换;进程在调度器安排下不断变化状态。
现在回头看,最难的其实是坚持把每个状态解释清楚
这组实验的代码不算庞大,但它要求你不能只看最后有没有输出。你得知道为什么跳到这里,为什么从这里返回,为什么某个进程现在是等待状态,为什么改了时间片以后统计数字发生了变化。
我以前遇到不懂的代码,容易先记住结论:这里是中断入口,那里是任务切换,这个函数是调度器。做完这组实验以后,我更愿意多追几步。结论当然重要,但真正能留下来的,是你知道这个结论是怎么从栈、寄存器、日志和状态变化里一步步推出来的。
仓库地址:HIT-Operating-Systems-Labs。这里保留了五个实验、历史报告和相关说明,也明确写出了旧环境和当前不可一键复现的边界。
说明:本文根据个人历史实验材料和公开仓库整理,部分文字经过 AI 辅助润色;文中启动输出和调度数据属于历史实验记录。课程资料仅用于学习回顾和个人作品整理,不用于当前课程作业或考试。

浙公网安备 33010602011771号