简介哈工大操作系统实验李治军完整资料包面向计算机专业学生与操作系统自学者通过真实内核代码与实验任务帮助理解进程管理、内存管理、文件系统、设备与调度算法等核心机制。包内245个文件以C源码、汇编.s、头文件.h、Makefile、目标文件与静态库.o/.a为主包含Linux 0.11内核相关实验代码、构建脚本及文本说明压缩包整体约21.89MB。已有3463人学习下载认可度较高。资源覆盖实验一至实验八包括P/V操作与信号量、虚拟内存与页面置换、文件系统模拟、磁盘调度FCFS/SCAN等、进程调度策略、银行家算法等并附带其他院校补充材料与源码分析便于对照实现和深入剖析。借助这套资料读者可系统掌握操作系统关键模块的实现思路与内核构建方法提升编程与系统设计能力。1. 操作系统实验为什么能让“会背知识点”的人翻车面试官问你进程调度你能把时间片轮转、优先级倒置讲得头头是道可一旦把 Linux 0.11 内核源码摊开让你在sys_call_table里加一个系统调用很多人就卡在“该改哪个文件、表项加在哪、参数怎么传”上。这套以 Linux 0.11 为底子的操作系统实验就是专门治这种“嘴上会、手上不会”的毛病的。说直白一点这不是刷题也不是写个几千行的课程项目而是让你在真实内核里做手术从引导扇区的第一行汇编到文件系统的磁盘布局每一步都要对着源码改、对着模拟器调。课程配套实验由知名高校的系统能力培养体系演化而来国内大量计算机专业学生都在这套 Lab 上熬过夜。它最适合三类人操作系统课上听得懂但不会做题的本科生、准备考研复试想拿项目说话的应届生、以及工作后想补系统底层功底的开发。这套实验真正的价值不在分数而在把一个黑匣子拆开你会亲眼看到“进程到底怎么切换的”“系统调用到底卡在哪条指令上”。下面我用做过一轮完整实验的视角把环境搭建、实验体系、最小可跑通的闭环以及那些能让你少熬几个通宵的坑一次讲清楚。2. 搭建实验环境模拟器加 Linux 0.11 内核的编译链路2.1 环境全家桶为什么用老内核配老编译器这套实验的标配环境是一台 x86 模拟器加 Linux 0.11 源码包。很多人第一反应是装虚拟机我建议你不要。Linux 0.11 是个 1991 年量级的内核现代 BIOS 和虚拟硬件的兼容性极差尤其是引导部分的实模式代码VMware 里经常起不来或者起来就花屏。实验体系选择的是模拟器方案模拟一颗完整的 x86 CPU、中断控制器和磁盘控制器启动过程完全可控还能下断点看寄存器。工具链选型是另一个容易翻车的地方。Linux 0.11 的汇编用了 as86 语法内嵌汇编也是老式 ATT 写法新版 gcc 在编译时会有大量隐式声明错误就算强行编过去生成的代码也可能跑飞。常见的可靠组合是源码自带的 Makefile 配合 gcc 3.4 左右的老版本编译器。如果你用的是现代发行版不要折腾系统 gcc直接找一个老版本的工具链目录在 Makefile 里把CC变量的路径指过去。源码不用刻意下载实验包通常自带完整的内核树。拿到之后你会看到一个linux-0.11目录里面是完整的内核源码旁边是你需要做的实验文档。这套包的优点是够小全部源码加起来也就一万多行一个晚上就能通读关键部分。2.2 编译内核镜像的最小命令序列环境准备好之后第一步不是写代码而是把原始内核编译出来确保“没动过手的环境也是能跑的”。这步的意义是建立基线之后所有实验都是在已知能跑的内核上做增量修改出了问题就知道是改坏了。下面是最小命令序列# 进入内核源码目录 cd linux-0.11 # 清理之前的编译产物保证从零开始 make clean # 编译内核生成 Image 镜像文件 make编译成功的标志是当前目录下出现一个名为Image的文件大小大约几十 KB这是压缩后的内核镜像。注意实验包里可能还带一个make boot或make start目标它负责把Image写入软盘镜像并启动模拟器不同版本叫法不同先看 Makefile 里的目标列表再执行。编译完成后把软盘镜像交给模拟器启动这一步需要一份模拟器配置文件。我一般在实验目录下放一份关键参数就这几个# 模拟器配置文件启动时读取 megs: 32 romimage: file./bios.bin, address0xf0000 vgaromimage: file./vga.bin floppya: 1_44./image.img, statusinserted boot: a log: ./bochsout.log参数说明megs是模拟内存大小32 MB 足够跑起 0.11romimage指定 BIOS 固件镜像和加载地址0xf0000是 x86 实模式的 BIOS 标准入口区域不要乱改floppya把编译好的内核镜像挂载为软盘 Astatusinserted表示软盘已在驱动器里boot: a指定从软盘启动。路径里的文件名是占位换成你实际解压出的 BIOS 文件和编译出的Image路径即可。2.3 验证环境就绪从黑屏到看见[/usr/root]#启动后怎么判断环境是好的打开模拟器电源如果一切正常屏幕上会快速滚过一大段内核启动日志最后停在类似[/usr/root]#的 shell 提示符。能敲命令、能ls、能回车换行这套环境就算通了。这段日志值得读一读它是 Linux 0.11 的“体检报告”内存检测、硬盘分区、挂载根文件系统每一步都打印一行。如果启动中途停住看最后一行日志基本能定位问题。最常见的失败是黑屏或反复重启原因要么是 BIOS 镜像路径配错要么是软盘镜像不是编译生成的Image而是旧的临时文件。还有一种情况是启动了但没有挂载根文件系统屏幕上会出现VFS: Unable to mount root fs这通常是内核编译选项和根文件系统镜像不匹配把实验包自带的根文件系统镜像换成对应版本即可。环境这块我见过太多人卡住其实核心就一句话先别写任何代码把别人的内核原样跑起来再谈改代码。这一步的价值会在后面几个实验里放大无数倍。3. 看懂实验体系从引导扇区到文件系统的 8 个关卡3.1 实验关卡全景表每个 Lab 打在哪个部位这套实验的关卡设计不是随便排的它基本复刻了操作系统的骨架启动、接口、调度、同步、通信、内存、设备、文件。下面这张表是每个 Lab 的落点建议对照着看源码目录实验编号核心对象涉及源码位置知识点上手难度Lab1引导启动boot/bootsect.sBIOS 中断、实模式寻址低Lab2系统调用kernel/system_call.s、kernel/sys.cint 0x80、系统调用表中Lab3进程调度kernel/sched.c时间片、schedule()中高Lab4临界区与信号量kernel/sem.c、kernel/sleep.c睡眠唤醒、临界区保护高Lab5进程间通信kernel/pipe.c管道读写、阻塞唤醒中Lab6内存管理mm/memory.c、include/linux/mm.hLDT、页表、写时复制高Lab7设备驱动kernel/keyboard.S、kernel/tty_io.c中断处理、环形缓冲区中高Lab8文件系统fs/*.cinode、超级块、目录项很高难度评估来自一轮完整做下来的体感Lab1 是热身半个晚上能搞定Lab3 开始要真的读懂调度算法的数据结构和状态机Lab8 涉及整个文件系统子系统的数据一致性是公认最耗时的部分。实验顺序建议按编号走后面的 Lab 会用到前面 Lab 新增代码的成果尤其 Lab2 的系统调用是 Lab4、Lab5 测试程序的基础跳着做容易两头堵。3.2 为什么选 Linux 0.11 而不是新内核这套实验选型最妙的地方在于 0.11 的“小”。新内核动辄几千万行连schedule()都被调度类拆成了好几层抽象根本没法在课程周期里做完整修改。0.11 的kernel/sched.c就几百行所有进程用一个任务数组管理调度就一个函数看明白这一个函数你就掌握了内核调度器的九成功力。另外一个理由是可读性。0.11 时期的内核还没有被大规模重构代码风格直白变量名不绕弯子。配套的注释书把这棵树每一行都讲透了实验手册里的关键词基本都能在书里找到对应段落。新内核里一个spin_lock就牵扯到内存屏障和 SMP 缓存一致性0.11 里一个cli()关中断就完事学习曲线不是一个量级。别觉得学旧东西没用0.11 里的进程结构、文件系统、内存管理模型和现代 Linux 是一脉相承的。你在实验里建立的“系统调用表→处理函数→参数拷贝”的链路认知放到 x86-64 的新内核里依然成立只是表的位置和传参方式变了。3.3 完成标准跑出结果和吃透源码是两码事实验验收通常分两关。第一关是运行结果Lab1 要求在引导时打印指定字符Lab2 要求新增两个系统调用能在用户态调通这一关能过说明代码能编、能跑、逻辑方向对。第二关是讲代码验收老师会让你对着源码解释每一处修改的动机这一关才是真正拉开差距的地方。能跑但讲不清楚分数不会高。我自己的判断标准是“三问”第一问代码在哪个文件、哪个函数、第几行被调用第二问如果没有我这行修改原始内核会走到哪一步、崩在哪第三问如果机器是双核我这套修改会不会出问题三问都能答上才叫吃透了实验。下面一章就按这个标准拆解前两个 Lab把最小闭环走通。4. 走通最小闭环Lab1 引导启动与 Lab2 系统调用的代码级拆解4.1 Lab1 引导在 bootsect.s 里打印你自己的学号Lab1 是所有实验的入口目标是在系统启动的第一阶段、还没有进入保护模式之前让屏幕上出现指定的字符序列。这一步本质上是在和 BIOS 打交道引导扇区被 BIOS 加载到物理地址0x7c00然后 CPU 从0x7c00开始执行你的代码。此时没有操作系统没有中断服务唯一可用的就是 BIOS 提供的中断例程。在boot/bootsect.s里你会看到一段以start:标签开头的汇编。核心逻辑是调用int 0x10中断的0x13号功能打印字符串。最小改动是这样的! bootsect.s 片段在 start 标签后插入 start: mov ah,#0x03 ! 功能号 0x03读取光标位置 xor bh,bh ! 页号 0 int 0x10 ! 调用 BIOS 视频中断 mov cx,#14 ! 要打印的字符数 mov bx,#0x0007 ! 页号 0属性 0x07黑底白字 mov bp,#msg1 ! BP 指向字符串首地址 mov ax,#0x1301 ! 功能号 0x13子功能 0x01写字符串并移动光标 int 0x10 ! 调用 BIOS 视频中断 inf: jmp inf ! 死循环停在当前行 msg1: .ascii Hello, OS Lab ! 要显示的字符串逻辑说明第一段int 0x10是查询当前光标位置返回结果存在 DX 寄存器打印字符串时 BIOS 需要知道光标在哪第二段int 0x10的0x13功能号负责往显存写字符串。CX是字符个数必须和msg1的长度一致数错一个字符就会把后面的乱码也打出来。BP指向字符串注意这里的寻址是相对于CS的所以org 0x7c00这段伪指令不能删——它告诉汇编器代码将来要加载到0x7c00所有标签的地址都要按这个基址计算。最后的inf: jmp inf是死循环意思是引导工作到此为止防止 CPU 继续往下执行到未定义区域。写完make重新生成镜像启动模拟器如果屏幕停留在黑底白字的Hello, OS Lab不再滚动说明你已经亲手完成了内核启动的第一段代码。这个实验看似简单但它把实模式、段寄存器、中断调用、BIOS 服务这几个概念一次性全过了一遍后面的实验都会受益于这里的理解。4.2 Lab2 系统调用从用户态到内核态的完整链路Lab2 是这套实验的分水岭要求你新增两个系统调用一个往内核传字符串一个从内核取字符串。这需要动四个文件缺一个就跑不通。链路是用户程序发中断 → 中断入口查表 → 跳转处理函数 → 从用户栈拷贝参数 → 返回用户态。下面拆开看。第一步在include/unistd.h中申请系统调用号/* 已有的系统调用号到 71 号新号从 72 开始 */ #define __NR_iam 72 #define __NR_whoami 73第二步在kernel/system_call.s的系统调用表中登记! system_call.s 中的 sys_call_table 处 .long sys_iam .long sys_whoami第三步在include/linux/sys.h中声明函数/* sys.h 末尾与其它 sys_ 函数声明排在一起 */ extern int sys_iam(char * name); extern int sys_whoami(char * name, unsigned int size);第四步在kernel/sys.c中实现int sys_iam(char * name) { /* 实际的实现代码从用户地址空间拷贝字符串 */ char tmp[24]; int i 0; /* 这里必须用 get_fs_byte 而不是直接解引用 name 因为 name 指向的是用户态内存内核态不能直接访问 */ while (i 23) { tmp[i] get_fs_byte(name i); if (tmp[i] \0) break; i; } /* 把 tmp 拷贝到内核维护的缓冲区并记录长度 */ copy_to_kernel_buffer(tmp, i); return i; }逻辑说明unistd.h里的编号是用户态和内核态约定的“合同”编号错一个int 0x80的汇编入口就会调到别人的函数上轻则返回错误重则内核崩溃。system_call.s里的.long列表就是那张系统调用表表项顺序必须严格对应编号顺序__NR_iam是 72那这张表里第 72 个.long就必须是sys_iam的地址差一个位置就是灾难。sys.h里的声明是为了让system_call.s通过 C 符号找到函数入口。sys.c里的实现是这套实验最经典的坑用户态传进来的name指针是一个用户空间地址内核态直接解引用会访问到内核页表里不存在的页轻则返回乱码重则触发页错误导致进程被杀。正确的做法是用get_fs_byte()这个宏逐字节从用户空间读数据。这个宏在 0.11 里是关掉段寄存器的用户态数据段选择子后用普通字节访问指令实现的。反过来向用户态写数据要用put_fs_byte()。4.3 把两个 Lab 串起来在模拟器里跑通一次完整调用新增系统调用写完后需要一个用户态程序来验证。实验包一般在lab2目录下给了测试代码核心就是一个main()#include stdio.h #include unistd.h #include string.h int main(void) { char buffer[24]; int len; /* 调用自研系统调用把字符串传给内核 */ len iam(hello-os); printf(iam return: %d\n, len); /* 调用另一个自研系统调用从内核取回字符串 */ len whoami(buffer, 24); printf(whoami return: %d, buffer: %s\n, len, buffer); return 0; }编译这个测试程序然后放到根文件系统镜像里在模拟器里执行。如果你能看到iam return: 8和buffer: hello-os恭喜这条从用户态到内核态的链路已经被你打通了。这里有一个验证技巧别只看返回值和字符串要故意传一个过长的字符串看看内核是截断还是越界。正确的行为是安全截断并返回实际长度。如果你传hello-os返回 8传 30 个字符还能返回 30说明你的拷贝逻辑没有做长度上限判断这是内存安全漏洞。很多人在验收时就是在这个边界条件上被扣分。系统调用实验做透之后Lab3 的进程调度和 Lab4 的信号量都在同一套中断机制上叠加理解深度会完全不同。下面这一章我把从环境到代码最常见的踩坑点汇总一下这些坑几乎每个做这套实验的人都会遇到至少一个。5. 避坑指南环境、编译与调试的 5 个常见问题5.1 新版 gcc 编译直接报错隐式声明与内嵌汇编不兼容现象在实验包目录执行make刷出一堆warning: implicit declaration of function或者直接error: unknown register name编译中途停止。原因Linux 0.11 内核的代码是针对早期编译器写的里面的内嵌汇编格式比如寄存器约束写法和 2005 年之后的 gcc 差异很大。另外 0.11 的 Makefile 里没有-std这种现代编译选项新版 gcc 默认的语法检查更严格把老代码里的“合法 but 不规范”直接判成错误。解决不要和编译器较劲换老版本工具链是最快的路。常见做法是下载一个 gcc 3.x 的工具链放到/opt/下然后修改源码根目录的 Makefile把CC gcc改成CC /opt/gcc-3.4/bin/gcc。改完make clean make。如果下载老工具链不方便另一个办法是给现有 gcc 加-fpermissive和-Wno-implicit编译选项但这种方法治标不治本后面碰到内嵌汇编的硬语法错误还是过不去。提示改 Makefile 前先备份别把原始文件弄丢。几百行的 Makefile 里改错一个变量名编译报错会让你怀疑是源码问题。5.2 模拟器启动后反复重启或停在F000:0000现象模拟器窗口打开后屏幕出现一个光标然后整个机器像掉电一样重启反复循环。日志文件里最后一行是F000:0000之类的十六进制地址。原因F000:0000是 BIOS 复位入口地址模拟器跳回这里说明发生了“三重故障”CPU 无法处理异常被迫复位。最常见的原因不是代码问题而是软盘镜像路径配置错误。模拟器找不到floppya指定的文件或者那个文件是 0 字节的占位文件BIOS 读引导扇区失败后就会复位。解决先确认floppya指向的文件存在且大小正常编译出的Image应该有 KB 级别的大小不是 0 字节。再确认romimage指向的 BIOS 文件是配套版本不同模拟器对 BIOS 校验和的要求不同混用会导致启动流程走到一半直接复位。第三种可能是引导扇区代码自己写飞了导致异常复位此时用调试器在0x7c00下断点单步执行你的引导代码看看是哪条指令触发异常。5.3 改了源码 make 后模拟器跑的还是旧行为现象明明在sys.c里加了打印语句重新编译也成功了启动模拟器后却怎么都看不到打印输出以为代码没生效。原因编译命令没有触发增量编译。make的时间戳检查有时会因为系统时钟设置问题更新不到位或者你改的是头文件而不是.c文件而这个.c文件没有依赖那个头文件。另一个更隐蔽的原因是模拟器加载的软盘镜像是旧的你重新生成了Image但make start没有重新把Image写进软盘镜像模拟器读的还是一张旧盘。解决养成两个习惯。第一修改后先make clean再make别依赖增量编译第二查看 Makefile 里Image到软盘镜像的拷贝目标手动执行一遍再将镜像挂给模拟器。验证方法是在源码里加一行标志性的printk编译后查Image文件的时间戳再确认软盘镜像的时间戳也更新了两步都对了再启动模拟器。5.4 系统调用返回 -1 或 errno38现象用户态测试程序调用iam()后返回 -1perror显示Function not implementederrno 38。原因errno 38 对应ENOSYS内核的意思是“系统调用号没有对应的处理函数”。这基本可以断定系统调用表没配对要么unistd.h里的编号和system_call.s中.long表项的位置错位要么你只加了函数实现忘了在sys_call_table里登记.long项。解决把include/unistd.h里的编号写出来从 0 开始数到 72再数一遍system_call.s里.long表项到第 72 个的位置看是不是sys_iam。注意有些实验版本的系统调用表是从0开始排的有些从1开始排这会导致一个固定的偏移。我自己的排查办法是在system_call.s的表项里临时加一条printk启动后用dmesg看中断入口有没有走到你期望的位置。提示这个坑最容易在跨实验版本参考别人代码时触发因为不同老师封装实验的方式不同一定要以你自己手里的unistd.h和system_call.s为准不要原样抄另一个版本的改法。5.5 调试器下断点后停不下来或地址对不上现象调试器里b 0x7c00下断点按继续执行后没有中断程序直接跑到了 shell或者中断的位置和你预期的源码位置不一样。原因模拟器的调试器默认使用物理地址断点而你下的是逻辑地址另外 0.11 的引导阶段会经历实模式到保护模式的切换同样是0x1000这个数值实模式下是物理地址保护模式下要经过段基址和页表翻译才能得到物理地址。断点设置错了等于没设。解决在实模式阶段断点地址直接用物理地址0x7c00没问题进入保护模式后优先在函数名上下断点比如b schedule调试器会解析符号地址。如果符号表没加载就先执行info reg看当前 CS 和 EIP 的值手动把逻辑地址EIP加上段基址得到物理地址再下物理断点。这个坑在 Lab3 之后频繁出现因为进程切换涉及一堆段寄存器修改。除了上面五个高频问题还有两个小但烦的坑值得提一下。一个是实验包里的Makefile用了古老的 Tab 缩进规则你用文本编辑器改过之后可能把 Tab 替换成了空格执行 make 时报missing separator此时用sed -i s/^ /\t/ Makefile修一下。另一个是 Windows 下解压实验包换行符变成 CRLFbootsect.s每一行末尾多了回车符汇编的时候会报bad character用dos2unix批量转回来就好。6. 让实验变成能讲的系统功底验证方法与进阶调试习惯实验全部跑通只是第一步离“系统功底”还差一个验证闭环。我的习惯是把每个实验写成三张卡片第一张是“它做了什么”用一句话说清功能第二张是“它改了什么”列出文件路径和核心函数的调用关系第三张是“它为什么不会崩”讲清楚异常、中断、保护机制是如何兜底的。三张卡片能默写出来面试或复试被问到内核话题时你不需要背直接就能讲。进阶验证有个很实用的方法故意制造边界条件。拿 Lab2 举例除了正常调用我会写一个传空指针的测试程序看看内核是返回-EFAULT预期还是直接 panic说明错误处理漏了。拿 Lab3 举例我会把时间片改成极小值观察进程切换频率是否异常再从调试器里看任务状态字的切换。这些“反常规测试”比正常路径更能证明你真懂原理。调试能力的提升是最直接的收益。模拟器调试器的几个命令要练熟c是继续执行si是单步指令info reg看全部寄存器x /nwx addr按十六进制查看内存r eip直接改指令指针。这套命令组合覆盖了“程序跑飞了我看着寄存器猜它在干嘛”的场景也是后面 Lab4 信号量实验排查死锁的利器。最后一个习惯每个 Lab 完成时写一段十行以内的修改说明记录改了哪些文件、为什么这么改、踩了什么坑。这套实验有八个 Lab前一个的笔记是后一个的实验手册——尤其是 Lab2 的系统调用表修改记录Lab4 和 Lab7 都会再次用到翻笔记比翻源码快得多。我之前带过 A 同学做这套实验他 Lab1 用了两小时Lab2 卡了三天最后发现是sys_call_table表项位置错了一个偏移——就是上面 5.4 那个坑。解决的那一刻他就说了一句话“原来之前只会调 API不知道这个调用链有多长。”后来他把这八个 Lab 完整做下来复试讲项目的时候直接对着源码讲进程切换那几个提问的老师反而成了听他讲课的人。这套实验真正的回报不是分数是你以后看任何源码时不再发怵。希望你也能把这条链路走通希望帮到你。本文还有配套的精品资源点击获取