尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

南京大学操作系统实验:源码解析与报告撰写完整指南

发布时间:2026/9/26 2:07:13

资讯中心
01
ARTICLE

南京大学操作系统实验:源码解析与报告撰写完整指南

南京大学操作系统实验:源码解析与报告撰写完整指南
简介这份资源是南京大学操作系统实验的完整资料包面向计算机专业学生及操作系统自学者覆盖进程管理、内存管理、文件系统与I/O设备控制等核心实验主题适合课程作业参考与系统能力进阶训练。压缩包共250个文件以101个h头文件、87个c源文件、32个makefile构建脚本为主另含汇编、Perl脚本及少量可执行与镜像文件整体约191KB结构紧凑便于按实验模块查阅。内容围绕lab1至lab5展开包含进程调度与IPC、页面置换算法模拟、简易文件系统设计、设备驱动与中断处理等实现代码并配有实验报告阐述设计思路、实现过程与结果分析。已有197人学习下载读者可借此对照源码理解操作系统核心机制积累调试与系统编程经验为后续深入研究或开发打下基础。1. 南京大学操作系统实验从源码到报告的完整复现路径如果你正在搜“南京大学操作系统实验内含源码和报告.zip”大概率是两种情况要么课程实验卡在某个系统调用上跑不通要么想找一份能对照的参考实现来理解操作系统到底怎么从一行行 C 代码变成能调度进程、管理内存的实体。这个实验包的核心价值不在于“抄一份能过的作业”而在于它把操作系统最核心的几条主线——进程管理、内存分配、文件系统、中断处理——用可编译、可调试的源码串了起来再配一份实验报告说明每一步的设计取舍。适合谁适合正在上操作系统课、需要动手实现内核模块的本科生也适合工作后想补底层知识、但不想啃完整 Linux 内核源码的工程师。下面我按“先跑通、再改参数、最后排错”的顺序把这份实验材料怎么用、坑在哪讲清楚。2. 实验环境搭建与源码结构拆解2.1 为什么选 QEMU GCC 而不是虚拟机装完整系统南京大学操作系统实验通常基于一个精简的教学操作系统内核常见做法是在 Linux 宿主机上用 QEMU 模拟 x86 或 RISC-V 环境配合交叉编译工具链生成内核镜像。为什么不直接在 VMware 里装一个完整 Linux 再改因为教学内核的代码量通常在几千到几万行编译一次几秒钟而完整系统内核编译动辄半小时调试时反复重启的成本完全不可接受。QEMU 的优势是支持-s -S参数挂 GDB 远程调试能在内核第一条指令处下断点单步跟踪页表切换和中断向量初始化。我一般会先确认宿主机是 Ubuntu 20.04 或 22.04因为工具链版本和 QEMU 的默认配置在这两个版本上最稳较新的 24.04 有时会遇到gcc-multilib依赖冲突。2.2 源码目录的四个核心模块与编译入口拿到实验包后先别急着make花十分钟把目录结构看清楚。典型布局是boot/放启动汇编和链接脚本kernel/放进程调度和系统调用mm/放物理内存管理和页表操作fs/放文件系统实现user/放用户态测试程序。编译入口在根目录的Makefile里面会定义CROSS_COMPILE前缀和QEMU启动参数。下面这段是我常用的环境检查脚本跑一遍能确认工具链是否齐全#!/bin/bash # 检查操作系统实验所需的基础工具链 # 逐项验证缺哪个装哪个避免 make 到一半报错 echo 检查 GCC 交叉编译器 which riscv64-unknown-elf-gcc || echo 缺少 RISC-V 交叉编译器 which gcc-multilib || echo 缺少 32 位编译支持 echo 检查 QEMU 模拟器 which qemu-system-riscv64 || echo 缺少 QEMU RISC-V 版本 which qemu-system-i386 || echo 缺少 QEMU x86 版本 echo 检查调试工具 which gdb-multiarch || echo 缺少多架构 GDB which make || echo 缺少 make这段脚本的逻辑是按“编译→模拟→调试”三条线分别检查。riscv64-unknown-elf-gcc用于生成 RISC-V 裸机代码gcc-multilib让宿主机 gcc 能编译 32 位程序用户态测试用qemu-system-riscv64是模拟器本体gdb-multiarch负责连 QEMU 的调试端口。参数上注意如果你的实验是基于 x86 的把riscv64换成i386即可但不要混用两套工具链否则链接阶段会出现架构不匹配的报错。2.3 首次编译与 QEMU 启动的最小命令环境齐了之后在源码根目录执行make正常会生成kernel.bin或os.img。启动命令通常写在 Makefile 的run目标里但手动敲一遍能让你更清楚每个参数的作用# 启动 QEMU 并加载内核镜像-nographic 表示不弹图形窗口 # -bios default 使用 QEMU 自带的 OpenSBI 固件RISC-V 平台 qemu-system-riscv64 \ -machine virt \ -nographic \ -bios default \ -kernel build/kernel.bin \ -s -S-machine virt指定虚拟平台-nographic把串口输出重定向到终端-kernel加载编译好的内核-s -S是调试开关-s在 1234 端口开 GDB server-S让 CPU 启动后暂停等待调试器。如果你不需要调试去掉-s -S就能直接看到内核打印的启动日志。常见翻车点是-bios default在某些 QEMU 版本上不识别这时换成-bios none并确保你的内核自己处理了 M 模式到 S 模式的切换。3. 进程管理与系统调用的实现细节3.1 进程控制块PCB的数据结构设计操作系统实验里最核心的改动往往在kernel/proc.c。进程控制块决定了你能同时跑多少个进程、怎么保存上下文、怎么切换页表。教学内核的 PCB 通常比 Linux 的task_struct简单得多但几个关键字段不能少pid、state就绪/运行/阻塞、context保存的寄存器组、page_table该进程的页表基址、kernel_stack内核栈指针。我见过不少同学把context定义成struct trapframe却忘了在swtch汇编里按同样顺序压栈结果进程第一次被调度就飞了。下面是一个典型的 PCB 定义和上下文切换的 C 部分// kernel/proc.h 中进程控制块的核心字段 struct proc { int pid; // 进程 ID从 1 开始分配 enum proc_state state; // UNUSED, RUNNABLE, RUNNING, BLOCKED struct context context; // 内核上下文由 swtch 保存/恢复 pagetable_t pagetable; // 用户页表切换进程时写入 satp 寄存器 uint64 kernel_stack; // 内核栈顶指针每个进程独立 uint64 sz; // 进程内存大小用于 sbrk 扩展 };参数说明context结构体里通常只保存ra返回地址和sp栈指针以及被调用者保存寄存器s0-s11因为用户态寄存器在陷入内核时已经由trapframe保存过了。pagetable字段在进程切换时必须同步更新satp寄存器否则新进程会跑在旧进程的地址空间里这种 bug 的现象是“进程 A 的变量莫名其妙被进程 B 改了”。kernel_stack一般指向该进程内核栈的高地址因为 RISC-V 的栈是向下增长的。3.2 系统调用从用户态到内核态的完整链路系统调用的实现分四步用户态把调用号放进a7寄存器、参数放进a0-a5执行ecall指令触发异常硬件跳到stvec指向的陷阱入口汇编代码保存trapframe后根据a7查表调用对应的内核函数返回时恢复trapframe并执行sret。教学实验里最容易出错的是第三步的查表——系统调用号偏移一位或者函数指针类型不匹配都会导致跳到非法地址。下面这段是系统调用分发的核心逻辑// kernel/syscall.c 系统调用分发 void syscall(void) { struct proc *p myproc(); int num p-trapframe-a7; // 从 trapframe 取调用号 if (num 0 num NELEM(syscalls) syscalls[num]) { p-trapframe-a0 syscalls[num](); // 返回值写回 a0 } else { printf(pid %d: unknown syscall %d\n, p-pid, num); p-trapframe-a0 -1; // 返回 -1 表示失败 } }逻辑说明myproc()获取当前进程的 PCBtrapframe是陷入内核时保存的用户寄存器快照。syscalls是一个函数指针数组下标就是系统调用号。参数上注意NELEM宏计算数组长度防止越界访问。返回值必须写回trapframe-a0因为用户态执行ecall后是从a0读返回值的。常见坑是忘记在syscalls数组里给新增的系统调用留位置导致调用号对不上现象是用户态程序打印出“unknown syscall”然后退出。3.3 用 GDB 单步跟踪一次进程切换光看代码很难确认上下文切换是否真的保存了所有寄存器。我一般会在swtch函数上下断点用 GDB 连上 QEMU 后单步执行观察sp和ra的变化。操作步骤先在一个终端跑qemu-system-riscv64 ... -s -S另一个终端跑gdb-multiarch build/kernel.bin然后在 GDB 里执行target remote :1234、b swtch、c。当断点命中时用info registers sp ra看当前值再si单步进入汇编确认sd ra, 0(a0)这类保存指令确实写入了目标地址。这个过程中如果发现sp在切换后没有指向新进程的内核栈说明swtch的参数传错了——第一个参数应该是旧进程的context地址第二个是新进程的context地址顺序反了就会导致栈指针错乱。4. 内存管理与文件系统的关键参数4.1 物理内存分配器的空闲链表实现教学内核的物理内存管理通常用空闲链表启动时把end到PHYSTOP之间的物理页全部串成链表分配时从头部摘一个释放时插回头部。关键参数是页大小PGSIZERISC-V 上通常是 4096 字节和PHYSTOP物理内存上限QEMU virt 平台一般是 128MB。下面是一个简化的分配与释放实现// kernel/kalloc.c 物理页分配 void kfree(void *pa) { struct run *r; if (((uint64)pa % PGSIZE) ! 0 || (char*)pa end || (uint64)pa PHYSTOP) panic(kfree); // 地址不对齐或越界直接崩溃方便定位 memset(pa, 1, PGSIZE); // 填充垃圾数据暴露 use-after-free r (struct run*)pa; r-next kmem.freelist; kmem.freelist r; } void *kalloc(void) { struct run *r kmem.freelist; if (r) kmem.freelist r-next; if (r) memset((char*)r, 5, PGSIZE); // 填充 0x05便于调试 return (void*)r; }参数说明PGSIZE必须是 2 的幂且与硬件页表粒度一致改成 8192 会导致页表项索引计算错误。memset(pa, 1, PGSIZE)在释放时填 1 是为了让 use-after-free 的 bug 更快暴露——如果某个指针在释放后还被读取看到的就是 0x01010101 而不是原来的数据。kalloc里填 5 同理。注意panic在地址越界时直接崩溃这在调试阶段是好事但如果你在kfree里传了一个栈上的局部变量地址系统会立刻挂掉现象是“释放一个临时变量后内核 panic”。4.2 文件系统的 inode 与目录项布局实验里的文件系统通常是简化版 Unix V6 风格超级块、inode 区、数据区三段式布局。inode 里存文件类型、大小、直接块指针和一级间接块指针。目录项是(inode号, 文件名)的数组查找文件就是遍历目录项比较名字。关键参数是MAXFILE最大文件块数和NDIRECT直接块数量。下面这段是 inode 读取的典型流程// kernel/fs.c 根据路径查找 inode struct inode* namei(char *path) { struct inode *dp, *ip; if (*path /) dp iget(ROOTDEV, ROOTINO); // 从根目录开始 else dp idup(myproc()-cwd); // 从当前工作目录开始 while ((path skipelem(path, name)) ! 0) { ilock(dp); if ((ip dirlookup(dp, name, 0)) 0) { // 在当前目录找名字 iunlockput(dp); return 0; // 找不到返回空 } iunlockput(dp); dp ip; // 进入下一级目录 } return dp; }逻辑说明iget从设备号和数据块号获取 inode 缓存idup增加引用计数dirlookup在目录数据块里线性搜索文件名。参数上注意ROOTINO通常是 1因为 inode 0 保留不用。skipelem负责跳过斜杠并提取下一级名字。常见坑是忘记ilock就访问 inode 数据导致多进程并发读目录时拿到脏数据现象是“ls 偶尔少列出一个文件”。5. 实验报告撰写与源码对照的避坑清单5.1 报告里必须写清楚的三类图实验报告不是代码注释的堆砌。我审过不少报告得分高的都有一个共同点把“进程状态迁移图”“页表映射关系图”“系统调用时序图”画清楚了。状态迁移图要标出RUNNABLE→RUNNING→BLOCKED→RUNNABLE的触发条件页表图要画出三级页表的索引位数和satp寄存器的写入时机时序图要标出ecall后硬件自动做的三件事切到 S 模式、关中断、跳stvec。这三张图能画出来说明你对实验的理解已经超过了“能跑就行”的层次。5.2 五个高频翻车点与排查命令现象一内核启动后没有任何输出。原因通常是链接脚本的入口地址和 QEMU 加载地址不一致。解决用riscv64-unknown-elf-objdump -d build/kernel.bin | head -20看第一条指令地址确认是0x80000000QEMU virt 的默认加载地址。如果不是改链接脚本里的. 0x80000000。现象二进程第一次被调度就 panic报“invalid page table”。原因是satp写入的页表物理地址没有右移 12 位RISC-V 的satp低 44 位是 PPN需要把页表基址除以 4096。解决在kvminithart里确认w_satp(MAKE_SATP(kernel_pagetable))宏里做了移位。现象三用户态程序执行printf输出乱码。原因是trapframe里保存的sp和用户栈的实际位置不匹配或者usertrap返回时没有正确恢复epc。解决在usertrap里打印p-trapframe-epc和p-trapframe-sp和用户程序的链接地址对照。现象四make报“undefined reference to memset”。原因是内核编译时用了宿主机 libc 的函数但裸机环境没有 libc。解决在kernel/下自己实现memset、memcpy、strlen或者确认 Makefile 里加了-ffreestanding -nostdlib。现象五文件系统写入后读出来是旧数据。原因是writei写入了缓冲区但没有调用log_write记录日志崩溃恢复后数据丢失。解决确认所有对磁盘的修改都经过日志层begin_op和end_op成对出现。6. 从能跑到能讲清楚一个验证实验是否真正理解的技巧判断自己是不是真的搞懂了这份操作系统实验有一个很土但很有效的办法把kernel/下任意一个.c文件删掉自己从零重写一遍只允许看头文件和实验报告里的接口说明。我当年做的时候把vm.c删了重写第一次卡在walk函数的三级页表索引计算上——(va 12) 0x1FF是二级索引(va 21) 0x1FF是一级索引(va 30) 0x1FF是顶级索引这三个移位量我记反了两次每次都是页表项全零导致panic: walk。后来我在报告里画了一张 39 位虚拟地址的位域图标清楚每个 9 位段对应哪一级页表才彻底记住。另一个验证方法是改参数做破坏性测试。比如把PGSIZE从 4096 改成 8192看哪些地方会崩——通常kalloc和walk会先挂因为页表项索引的移位量写死了 12。再比如把进程最大数量NPROC从 64 改成 2然后跑一个 fork 出 3 个子进程的测试观察allocproc返回空后fork怎么处理错误。这些破坏性测试能让你看清代码里哪些是硬编码假设、哪些是真正可配置的参数。最后一个习惯每次改完代码先跑一遍make clean make再启动 QEMU 看启动日志有没有panic或unexpected trap。不要小看这个步骤我见过太多“改了一行忘了重新编译就调试半小时”的情况。操作系统实验的调试成本很高一次完整的编译加启动可能只要十秒但如果你跳过了后面花的时间可能是十倍。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。