简介:南京大学操作系统实验lab1至lab5的完整源码与实验报告合集,面向操作系统课程学习者与需要完成课程设计的学生,全面覆盖进程管理、内存管理、文件系统及I/O设备控制等核心实验主题。压缩包共250个文件,以C语言源文件(87个.c、101个.h)和makefile为主,另含启动汇编、Perl辅助脚本、编译中间文件与镜像文件,整体仅191KB,轻量紧凑,按lab1至lab5分别组织实验项目。每个lab均配有对应实验报告,详细阐述实验目的、设计思路、实现过程及结果分析;源码部分可直观体验进程调度模拟、页面置换策略、简化文件系统模型等关键实现。已有198人学习下载,适合希望通过亲手编写和调试代码来巩固操作系统理论、提升系统级编程能力的学生。
1. 一份“南京大学操作系统实验”压缩包:源码是主菜,报告是配菜
一份名为“南京大学操作系统实验内含源码和报告.zip”的压缩包,解压后通常是一个能编译的教学操作系统工程加一份现成的实验报告。很多人拿到它的第一反应是“报告改个名字就交”,而我的建议正好相反:源码才是这份资源里最值钱的部分,报告只能当参考。这篇笔记要解决的就是三件事——把工程在本地跑起来、把核心源码读懂、把报告重写成你能扛住追问的版本。适合正在做课程实验、想复现教学操作系统设计思路的在校生,也适合想快速了解一个小型内核怎么组织的工程师。别急着提交,先花一个下午把代码跑通。
2. 先把工程跑起来:环境选型、首次编译与启动的最小路径
2.1 这类实验包的典型结构:源码目录、报告正文与 Makefile 各自的作用
拿到压缩包后不要急着make,先解压,把目录结构看一遍。南大这类课程实验包的布局在不同版本之间会有差异,但大方向很接近,你大概率会看到这样的结构:
os-lab/ ├── Makefile # 顶层构建脚本,直接 make 就能编译整个内核 ├── boot/ # 引导扇区、启动汇编,负责把内核从磁盘加载进内存 ├── kernel/ # 内核主体:初始化、中断、调度、内存管理 ├── user/ # 用户态程序与测试用例,编译后作为镜像的一部分 └── docs/ # 实验报告与说明文档先对照自己的压缩包确认三件事:第一,是否有Makefile;第二,内核源码在kernel/还是直接在根目录;第三,docs/里的报告是什么格式、对应哪个实验。
我一般会先读Makefile再读README(如果有的话)。教学操作系统的构建方式和 Linux 内核不一样,不同实验包的make目标差异很大——有的直接make qemu,有的需要先make生成镜像再单独启动模拟器。搞清楚这些之后再动手,能省掉后面一大半的排错时间。
2.2 环境选型:Ubuntu 虚拟机 + 32 位工具链 + QEMU 的组合
这类教学内核的构建脚本几乎都是为 Linux 工具链写的,Makefile 里大量假设了sh、gcc、ld的标准行为。常见做法是装一台 Ubuntu 虚拟机,版本不用太挑,20.04 或 22.04 都行;如果你本来就在用 Linux,直接在当前环境装依赖即可。不建议在 Windows 原生环境硬跑——不是跑不了,而是会撞上一堆跟代码无关的环境坑。
WSL2 也能凑合,但图形窗口和串口交互偶尔会被终端代理干扰,排查起来比虚拟机麻烦。我一般直接给虚拟机分配 2 核 2G 内存,干净利落。
需要安装的依赖如下:
sudo apt update sudo apt install -y build-essential gcc-multilib gdb qemu-system-i386build-essential提供gcc、make、ld等基础工具;gcc-multilib是重点——很多教学内核保留 32 位编译目标,而 64 位 Ubuntu 默认不装 32 位启动库,缺了它编译会在链接阶段报错;qemu-system-i386是模拟器,用于运行编译出来的内核镜像。
装完后验证一下工具链:
echo 'int main(){return 0;}' > /tmp/t.c && gcc -m32 /tmp/t.c -o /tmp/t && file /tmp/t # 输出里能看到 "ELF 32-bit" 就说明 32 位编译链可用这一步值得花两分钟做,因为后面很多编译报错都跟 32 位库缺失有关,提前确认能直接跳过一类坑。
2.3 首次编译与启动:五条命令跑通最小路径
环境就绪后,按下面的顺序操作:
cd os-lab make clean # 第一次编译前必做,清掉可能残留的旧产物 make # 生成内核映像 ls -lh *.bin *.img 2>/dev/null # 确认内核镜像确实生成了 make run # 或者查看 Makefile 里 run 目标的实际命令make clean是最容易被跳过的一步。压缩包里的源码如果带时间戳缓存,增量编译可能什么都不做,直接让你误以为“编译失败”或“代码没问题”。make如果没有任何输出,多数情况下不是坏消息——去目录里找找有没有新生成的内核映像文件,有就说明构建成功了。
make run的具体命令以你手上的 Makefile 为准,有的包直接调qemu-system-i386,有的需要先挂载镜像。启动成功的标志是:QEMU 窗口弹出,终端里滚动内核启动日志,最后出现一个 shell 提示符,能输入命令并得到返回。
如果make报错,不要看刷屏的几百行输出,直接看第一行真正的错误位置。绝大多数首次编译失败都集中在“找不到头文件”和“32 位库缺失”两类,前者检查路径,后者检查 2.2 的依赖有没有装全。
3. 读懂实验源码:阅读顺序、调度器改动与自测方法
3.1 阅读顺序:从 boot 到 init,再到 trap 和 syscall 的调用链
教学操作系统源码通常在几万行量级,从头到尾读不现实。我的习惯是只追三条主线:启动、中断、进程调度。
| 主线 | 入口位置 | 重点关注 |
|---|---|---|
| 启动 | boot/下的启动汇编、kernel/下带init字样的 C 文件 | 从实模式到保护模式、页表初始化、内核栈建立 |
| 中断 | kernel/下 trap 相关文件 | 中断描述符表、异常分发、系统调用入口 |
| 进程 | kernel/下 task / proc / sched 相关文件 | 进程控制块结构、就绪队列、上下文切换 |
一个典型教学内核的入口函数长这样(具体函数名不一定相同,去kernel/目录找带init字样的文件):
void kernel_main(void) { console_init(); // 1. 串口与屏幕输出 memory_init(); // 2. 页表与物理内存管理 trap_init(); // 3. 中断描述符表 task_init(); // 4. 进程控制块与第一个用户进程 sched_init(); // 5. 调度器 intr_enable(); // 开中断,准备进入用户态 launch_first_task(); // 拉起第一个用户进程,不再返回 }这个调用顺序就是实验报告“设计思路”部分的写作顺序。先初始化控制台,是因为后面所有printf都依赖它;先初始化内存,是因为创建进程需要分配页表和内核栈;中断和调度放在最后,因为用户进程一旦跑起来就必须靠时钟中断抢占。
读这一层的目的不是背代码,而是能徒手画出“启动到用户态”的依赖关系图。答辩时被问“第一个用户进程是怎么被拉起来的”,你能顺着这个顺序讲清楚,比背一万行代码都有用。
3.2 挑一个实验动手:调度器源码怎么改、参数怎么调
调度器是教学操作系统实验里最常被要求改动的部分。先找到负责“选下一个任务”的函数,名字可能叫pick_next_task,也可能叫schedule_next,职责都一样——从就绪队列里挑一个进程,切换上下文。
一个典型的基于时间片轮转的实现:
// 简单轮转:从就绪队列头取任务,再把它挪到队尾 static struct task *pick_next_task(void) { struct task *next; if (list_empty(&ready_queue)) { return current; // 没有任务可切就继续跑当前任务 } next = list_first_entry(&ready_queue, struct task, node); list_del(&next->node); list_add_tail(&next->node, &ready_queue); // 轮转:放到队尾 next->state = TASK_RUNNING; return next; }这里有两个关键点。第一,list_empty判断不能省——如果就绪队列为空而强行取队首,内核会直接崩溃。第二,先list_del再list_add_tail的顺序不能反,否则同一个节点把自己链到了自己后面,队列遍历会进死循环。
调参的重点是时间片长度。时间片通常由时钟中断频率和当前任务的ticks计数器共同决定,在调度器初始化函数里找一个类似time_slice = 10的赋值,把它改成 1 或 50,能直观感受到两个任务切换频率的变化。修改后配合一个死循环打印的用户程序,观察两个任务是否交替输出,就能验证调度器在正常工作。
如果实验要求在此基础上扩展,最常见的升级方向是把单个就绪队列改成多级反馈队列:准备三个队列,优先级从高到低,高优先级队列的时间片短,低优先级队列的时间片长。实现上只需要把ready_queue从list_head换成数组,再在pick_next_task里按优先级从高到低扫描。代码量不大,但能展示你对“饥饿”和“响应时间”这两个概念的理解。
3.3 自测套路:新增系统调用、加打印、写用户态测试程序
改完调度器或内存管理代码后,怎么确认它真的生效?我习惯用一个最小实验:加一个系统调用,然后从用户态调用它。
用户态的封装函数通常在user/或lib/目录下,类似这样:
// syscall.c —— 用户态侧的系统调用封装 int sys_getpid(void) { return syscall(SYS_getpid, 0, 0, 0); }内核侧的分发逻辑在 trap 处理函数里,通常是一个switch语句:
// trap.c —— 内核态的系统调用分发 case SYS_getpid: regs->eax = current->pid; // 返回值写在 eax 寄存器里 break;改一个系统调用要动三个地方:用户态封装、系统调用编号定义(一般在syscall.h里)、内核分发处的case分支。漏掉任何一处,测试程序的表现都是“编译通过但运行无输出”,而且不会有任何报错——这种黑匣子问题最浪费时间。
排查这种问题不要盯着代码猜,加打印是最直接的手段。教学内核通常自带cprintf或printk之类的调试输出函数,在分发函数的入口打一行“syscall number = xxx”,运行后看这行打印有没有出现、参数对不对,两步就能定位问题。
验证时写一个极简用户程序:
// test.c —— 放到 user/ 目录下,加进 Makefile 的编译列表 int main(void) { int pid = sys_getpid(); cprintf("my pid is %d\n", pid); return 0; }编译进内核镜像后在 QEMU 的 shell 里执行它,看到正确的 pid 输出,就说明整条用户态到内核态的路径是通的。这个小实验值得每一个做操作系统实验的人先跑通一次——它验证的不只是某个功能,而是你对整个系统调用链路的理解。
4. 避坑排查:编译失败、启动黑屏与报告审核的 5 个典型问题
下面这五条,是这类课程实验里最常见、也最容易耽误一整个下午的坑。每条按现象、原因、解决来写,看完直接对号入座。
4.1 编译期:缺 32 位库,报错指向stubs-32.h
现象:make编译到某个 C 文件时报错,输出里有一行类似/usr/include/gnu/stubs-32.h: No such file or directory,或者在链接阶段报找不到crt1.o。
原因:你的宿主系统是 64 位 Ubuntu,但教学内核的 Makefile 用了-m32编译选项。系统里只有 64 位的 glibc 开发文件,32 位编译必需的启动文件和头文件没装。
解决:执行sudo apt install gcc-multilib,装上之后重新make clean && make。如果是在精简容器里,可能还需要libc6-dev-i386。验证方法:用 2.2 里的gcc -m32测试命令编译一个空程序,能出 32 位 ELF 就说明没问题。
4.2 平台期:Windows 下运行镜像报“不是有效的应用程序”
现象:把编译出来的内核镜像或某个程序拷贝到 Windows 上,双击想运行,弹窗提示“指定的可执行文件不是此操作系统平台的有效应用程序”。这个提示跟文件名没关系,你不用管它叫claude.exe还是kernel.bin。
原因:Windows 的 PE 加载器无法识别 Linux 下的 ELF 格式,更识别不了教学内核自定义的镜像格式。这个操作本身没有意义——内核镜像从来不是被双击运行的,它是被 QEMU 虚拟机加载的。
解决:全程在 Linux 虚拟机或 WSL 里操作,用file命令确认产物类型,通过make run启动 QEMU,把内核镜像作为虚拟机的“磁盘”或“内核”加载。记住一个原则:宿主程序是 QEMU,内核镜像是 QEMU 读入的数据文件,两者不是同一个平台的东西。
4.3 启动期:QEMU 黑屏或卡在 Booting
现象:make run后 QEMU 窗口弹出来了,但屏幕黑着,或者卡在Booting from Hard Disk...就不动了。
原因:三个最常见原因。第一,镜像路径不对,QEMU 没找到磁盘文件;第二,启动参数里format=raw与镜像实际格式不匹配;第三,图形输出方式问题,某些环境里-nographic会禁用图形窗口。
解决:先看 Makefile 里run目标的真实命令,优先用原装命令启动,不要自己另写。手动排查时按这个顺序:
# 检查镜像文件是否存在 ls -lh os.img # 用显式格式参数启动 qemu-system-i386 -drive file=os.img,format=raw -m 128M # 还黑屏就换成标准 VGA qemu-system-i386 -drive file=os.img,format=raw -m 128M -vga std如果用的是-nographic,改成图形窗口,或者反过来把图形输出重定向到串口。哪种模式能跑取决于实验包的启动实现,不是玄学,看 qemu 的报错输出最快。
4.4 改码期:改了源码再 make 没有任何反应
现象:修改了kernel/下的某个 C 文件,保存后重新make,终端没有任何编译输出,运行结果还是旧行为。
原因:教学内核的 Makefile 依赖关系写得不完整,或者源文件时间戳没变化导致增量编译跳过了重编。还有一种可能是你改的副本不在 Makefile 实际编译的路径里——比如压缩包里有两份源码目录,你在根目录那份上改了,Makefile 编的是kernel/那份。
解决:第一条路是直接make clean强制全量重编,干净利落,代价是编译时间多几十秒。第二条路是确认改对了文件位置,用grep在 Makefile 里搜你要改的文件名,看它实际引用的是哪个路径。增量编译这种事不值得花时间研究,clean 一下永远是最快的解法。
4.5 提交期:原版报告最危险的不是查重,而是答辩追问
现象:把压缩包里的报告改了名字就提交,或者只改了目录结构和排版,结果被老师或助教追问“你这个调度器怎么选下一个进程的”答不上来,严重一点的直接被判定为雷同作业。
原因:原版报告是基于原作者的实现写的,里面的设计决策、函数名、测试数据跟你的实际代码不一定对得上。你没有跑通过他的代码,只是在“搬运”,任何深入一点的提问都会露馅。
解决:把报告当技术文档重写。先用 2.1 到 3.3 的步骤把代码跑通、读懂,再按自己的话把实验目的、设计思路、核心代码、测试结果写一遍。重点准备三个问题:调度器怎么选下一个任务、中断从哪里进入内核、内存是怎么分配的。这三个问题能讲清楚,说明你真的理解了这个实验——也正是这份压缩包存在的意义。
5. 把“内含报告”变成验收成果:报告结构、自测清单与一个加分扩展
源码读懂了,代码也跑通了,最后一步是把报告重写成自己的成果。一份能扛住提问的报告,按下面五段来写:
- 实验目的:这个实验解决什么问题,对应课程哪个知识点。一两段就够,不要抄教材定义。
- 设计思路:讲清楚内核是怎么分层的,启动到用户态的依赖顺序,附一张调用链示意。这是报告的核心,也是答辩提问的重灾区。
- 核心代码:贴你觉得最值得讲的改动,逐行注释,说明为什么这么写,而不是把整个文件粘进去。
- 测试与结果:给 QEMU 运行截图或串口日志,说明验证了哪些场景、边界在哪里、失败是什么表现。
- 问题与总结:写你卡住的地方、怎么排查的、最后改进了什么。这一段最有说服力。
提交前过一遍自测清单:
make clean && make # 1. 编译零报错 make run # 2. QEMU 能进 shell,输入命令有返回 # 3. 把你改动的功能点对应的测试程序在 QEMU 里跑一遍 # 4. 关掉屏幕,能徒手画出 启动→中断→调度 的调用链做到第 4 条,这份实验才真正属于你。如果还想在答辩上多一个亮点,给调度器加一个多级反馈队列,代码量不大,但能展示你对优先级反转和饥饿问题的理解。
我最早拿到这类压缩包时,第一反应也是赶紧把报告改了交上去。后来被问了一个“调度器怎么选下一个任务”就卡住了,老老实实回来把入口函数从头跟了一遍,反而觉得实验比想象中简单。从那以后我养成了一个习惯:压缩包里的源码永远比报告值钱,报告只是你读懂源码之后的副产品。希望帮到你。
本文还有配套的精品资源,点击获取