
简介张亚涛老师的操作系统课程设计实验报告适合正在学习操作系统原理并需要完成课程实验的高校学生内容覆盖进程管理、内存管理、文件系统、设备管理、死锁预防、安全权限与性能分析七个实验主题。压缩包共57个文件核心为8个Markdown实验报告与readme说明辅以36张实验截图、少量Visual Studio项目配置和JSON/SQLite缓存文件整体仅3.14MB下载和查阅都很轻量。当前已有102人学习。实验报告按一至七分篇整理每篇写清实验目的、步骤、关键代码和运行结果截图可用来对照调试Linux系统调用、内核模块或文件操作相关实验也能帮助理解缺页异常、PV操作、死锁检测等重难点。压缩包内md文档与png图片分类存放目录简洁适合作为课前预习参考也可借以梳理课程报告写作思路。1. 这份操作系统课程设计包先看md还是先看源码拿到“张亚涛老师操作系统课程设计实验报告.zip”的时候我最直观的感受是这是一个学生做完七个操作系统实验之后留下的完整现场不是一份“现成答案”。压缩包里七个 Markdown 实验报告、几十张截图、一个 .vs 工程配置、还有大量以“系统调用号”“makefile文件”“全局变量和中断函数表”命名的图片基本把操作系统课程设计里最常碰到的几条主线——进程管理、内存管理、文件系统、设备管理、死锁、安全模型、性能分析——全部覆盖了一遍。对正在做类似课设的学生来说这份资源最大的价值不是抄报告而是能照着图片和文档反推“当时是怎么改内核的、改完跑起来长什么样”。我不是让你偷懒是想告诉你这类实验报告包读法比内容更重要。2. 实验环境与工具链从readme到可编译内核的串联2.1 先读README和md报告结构里藏着评分点压缩包里有README.md和readme.txt两个入口文件加上七个实验的md。我的习惯是先打开readme.txt再看每个实验md里的“实验目的”和“实验结果”两段最后才看代码片段。理由很简单操作系统课设通常是文档占一半分运行结果占一半分。readme里往往写着实验环境、编译方式和提交要求这些信息决定了你后续所有操作的前提。md文件的命名是按实验一、实验二一直到实验七排的每个文件里都嵌入了若干png截图。这些截图大致分成两类一类是代码截图比如makefile文件.png、系统调用号.png、定义数据结构.jpg另一类是运行结果截图比如result.png、menu.png、屏幕快照。代码截图的作用是告诉你“这个实验动了哪些文件”运行结果截图的作用是告诉你“改完之后系统启动起来应该看到什么”。我一般会先扫一遍所有图片文件名在脑子里建立一张“文件 → 实验”的映射表再回头精读md。这里需要提醒一个常见误区不要把md里的代码片段当成完整源码。md只是报告源码需要你自己在实验环境里重建。这份资源里真正有价值的是图片里暴露出来的细节——比如系统调用号加到了多少、全局变量和中断函数表是怎么排的、fprintk在代码里是怎么用的。这些细节在教科书里找不到只有跑过的人才会留在截图上。2.2 构建环境与makefile从源码到可启动内核从压缩包里的“makefile文件.png”和“.vs”目录来看这套实验的工程名是HIT-OS-REPORT对应的主干是教学用类Linux内核实验。这类内核的构建方式非常固定源码根目录下执行make生成Image文件再用虚拟机常见的是Bochs或者QEMU加载启动。我基于截图里常见的菜单界面给你补一个标准的构建流程# 清理旧编译产物避免残留对象文件干扰 make clean # 编译内核生成可启动的Image镜像 make # 如果修改了系统调用表或中断向量只重编对应目标文件 make kernel/system_call.o # 启动虚拟机加载内核镜像Bochs和QEMU二选一 bochs -f bochsrc.txt # 或者 qemu-system-i386 -fda Image -boot a每一条命令都有它的作用make clean是降低“改完代码编译报错说找不到符号”这类玄学问题的概率make是主构建生成的Image就是被虚拟机加载的操作系统镜像单独重编某个.o文件是为了节省调试时间因为你只改了一个系统调用没必要把整个内核都重编一遍最后启动虚拟机时如果用的是bochsrc.txt注意里面配置的磁盘镜像路径和内存大小要和你的目录结构一致。这类内核的编译有个特点它对工具链版本敏感。我见过最多的翻车现场是“gcc版本太新内联汇编语法不兼容”常见做法是装一个老版本的gcc比如gcc-4.8左右或者在makefile里强制指定编译器。如果你用的是现代Linux发行版建议先看截图里的makefile内容确认CC变量的值再动手。2.3 系统调用的公共骨架sys_call_table、系统调用号与handler操作系统课程设计里改动最频繁的就是系统调用。压缩包里“系统调用号.png”“系统调用数量.png”“添加系统调用号.png”“改系统调用数目.png”这四张截图实际上对应四次不同的改动完整覆盖了加系统调用的所有步骤。这类教学内核添加系统调用的标准路径是// 1. 在include/linux/sys.h中声明新函数 extern int sys_mycall(void); // 2. 在sys_call_table数组中添加对应项 // 这个数组是有顺序的位置就是系统调用号 fn_ptr sys_call_table[] { sys_setup, sys_exit, sys_fork, sys_read, sys_write, // ... 中间省略原有项 ... sys_mycall // 新加的系统调用排在最后 }; // 3. 在include/linux/unistd.h中添加系统调用号 #define __NR_mycall 72 // 注意这里的编号要唯一 // 4. 在kernel/system_call.s中把系统调用总数加1 nr_system_calls 73这段代码按顺序做了四件事声明函数、注册到系统调用表、分配系统调用号、修改系统调用总数。四个文件缺一不可少改任何一个用户态程序调这个系统调用都会失败而且失败方式不一样——有的返回-1有的直接触发非法指令中断。我特别想强调系统调用号这个参数。很多新手以为系统调用号是随意填的但实际上它必须和sys_call_table数组中的下标严格对应。如果数组里有73个函数那么最后一项的下标就是72__NR_mycall就必须是72。两张截图“系统调用号.png”和“系统调用数量.png”放在一起看就是在验证这个对应关系。这是整份实验报告里最容易丢分也最容易出bug的位置。3. 核心实验拆解系统调用、内存管理与文件系统的实现细节3.1 实验一进程管理与PV操作临界区怎么用代码说出来实验一对应的截图是“全局变量和中断函数表.png”和带“1、2、3”编号的过程图。从摘要和文件结构看这个实验聚焦进程的创建、调度和同步。进程同步里最经典的手段就是PV操作教学内核里信号量的常见实现是关中断保护临界区因为单核环境下关中断就能阻止进程切换typedef struct semaphore { int value; // 信号量当前值大于0表示可用资源数 struct task_struct *wait; // 等待队列存放因P操作被阻塞的进程 } semaphore; void P(semaphore *s) { cli(); // 关中断进入临界区 s-value--; if (s-value 0) { // 说明资源已被占用进程需要睡眠 sleep_on(s-wait); // 将当前进程挂到等待队列上 } sti(); // 开中断 } void V(semaphore *s) { cli(); s-value; if (s-value 0) { // 说明等待队列非空需要唤醒一个进程 wake_up(s-wait); // 唤醒队首进程 } sti(); }这段代码里有几个容易被忽略的参数细节。cli()和sti()是关中断和开中断指令的内联封装它们保护的是“判断value和修改value”这个原子操作value的初始值决定了信号量的语义初始化为1就是互斥锁初始化为大于1就是计数信号量sleep_on和wake_up是内核里已有的进程调度函数实验报告里通常会要求你验证“两个进程同时访问临界资源时最终打印结果是否符合互斥预期”。做这个实验我一般会额外打印当前进程pid作为佐证比如在临界区里加上类似printk(%d in critical area, current-pid)的日志这样截图里能看到进程交替进入的痕迹报告也更有说服力。3.2 实验二内存管理与TSS、中断表的角色实验二压缩包里有“tss声明.png”、几个数字编号的图片和“定义数据结构.jpg”。TSSTask State Segment在Linux内核里是任务切换时的硬件上下文载体实验二把它和中断表放在一起说明这个实验的重点是理解“进程切换时CPU如何保存和恢复状态”。常见做法是改TSS的字段来观察不同调度策略的效果或者在缺页异常处理里做手脚。页表处理是这个实验的另一半内容。教学内核里每个进程有独立的页目录和页表实验任务通常是扩展虚拟内存区域或者实现一个简单的缺页统计// 缺页异常处理函数记录缺页地址和触发次数 void do_no_page(unsigned long error_code) { unsigned long address current-tss.cr2; // CR2寄存器存放触发缺页的线性地址 static int page_fault_count 0; page_fault_count; printk(page fault #%d at addr 0x%x\n, page_fault_count, address); // 这里可以插入自定义的页分配逻辑 // 常见做法检查address是否在合法的虚拟内存区域合法则分配物理页并建立映射 }参数说明current-tss.cr2是硬件写入缺页地址的寄存器读取它就能知道“哪个地址访问导致了缺页”error_code是CPU压栈的错误码低三位分别表示“是否外部事件”“是否读/写”“是否用户态”。调试时我喜欢用GDB挂到虚拟机上看页表内容(gdb) p/x *(unsigned long *)0x1000 # 查看页目录表第一个表项 (gdb) x/10wx 0x1000 # 连续查看10个表项内容页目录表基地址在CR3寄存器里教学内核一般放在0x1000附近。通过对照TSS声明里的esp0、ss0这些字段能看出中断进入内核栈时栈切得对不对。这个实验的隐蔽坑是改了CR3或者TSS里的字段后一旦没同步修改对应的数据结构系统会在第一次任务切换时直接三重故障。3.3 实验三文件系统与fprintk的内核打印技巧实验三的截图里出现了“fprintk.png”和“main_c.png”这两个文件指向文件系统实验的两条主线一是实现文件读写相关的系统调用逻辑二是用fprintk代替printk做带文件名的调试输出。fprintk是教学内核里一个很实用的辅助函数它比printk多带一个文件参数方便在日志里区分是哪个模块打的点// fprintk的实现通常长这样先构造输出字符串再调用write系统调用 int fprintk(int fd, const char *fmt, ...) { char buf[1024]; va_list args; int len; va_start(args, fmt); len vsprintf(buf, fmt, args); // 格式化到buf va_end(args); // 将buf写入指定文件描述符通常是标准输出或调试文件 return write(fd, buf, len); }注意这里fd参数0代表标准输入1代表标准输出2代表标准错误。做文件系统实验时我习惯把调试信息写到2号描述符这样不会污染正常的数据输出流。文件系统实验本身的实现路径一般是设计超级块、i节点、目录项三张核心数据结构然后实现文件创建、删除、读写的系统调用。压缩包里的“定义数据结构.jpg”对应的就是这三张表的结构体定义。跑这个实验时最容易遇到的问题是“文件写入成功但读出乱码”。常见原因不是代码逻辑而是缓存没有同步——写操作只改了内存中的i节点内容而没有写回磁盘块。解决方法是每次写操作后强制调用一次同步函数比如sync_inode或者直接简化实验不要求磁盘持久化只在内存中模拟但报告里要写清楚这个取舍。4. 剩余实验与内核调试设备、死锁、安全与性能的代码落点4.1 实验四设备管理与中断驱动I/O的模拟思路实验四落在设备管理尤其是I/O控制方式的理解。真实硬件设备的中断驱动I/O在虚拟机里没法直接写常见做法是模拟一个虚拟设备控制器用内存映射寄存器代替真实端口然后用request_irq注册一个中断处理函数// 虚拟设备寄存器结构映射到固定地址 #define DEV_STATUS_REG 0x3000 // 状态寄存器0为空闲1为忙碌 #define DEV_DATA_REG 0x3004 // 数据寄存器写入一个字节 // 中断处理函数设备完成任务后触发 void dev_interrupt_handler(void) { unsigned char data; if (*(volatile unsigned char *)DEV_STATUS_REG 1) { data *(volatile unsigned char *)DEV_DATA_REG; printk(device received: %c\n, data); } // 清中断标志让设备可以继续下一次传输 *(volatile unsigned char *)DEV_STATUS_REG 0; } // 启动一次I/O传输写数据置状态位等待中断 void dev_write(unsigned char c) { *(volatile unsigned char *)DEV_DATA_REG c; *(volatile unsigned char *)DEV_STATUS_REG 1; // 置忙碌触发设备处理 // 这里不等待CPU去干别的数据就绪后由中断通知 }代码里用volatile关键字强制每次读写都访问真实内存地址避免编译器优化掉对寄存器的操作。状态寄存器置1模拟的是“设备开始干活”中断处理函数模拟的是“设备干完活通知CPU”。如果你用过真实的中断控制器比如8259A会发现实验里必须同时处理主片和从片的中断结束命令教学内核里对应的是send_eoi()调用漏掉这个会导致中断只触发一次。4.2 实验五死锁的预防与检测银行家算法的资源分配判断实验五对应的代码落点是死锁的四个必要条件和银行家算法。教学版实现通常只做安全性检查这一步即给定资源分配状态判断是否存在安全序列。核心代码长这样// available: 各资源可用数 // allocation: 各进程已分配资源矩阵 // need: 各进程还需要的资源数 int is_safe(int *available, int **allocation, int **need, int process_count, int resource_count) { int work[resource_count]; // 工作向量初始为available的副本 int finish[process_count]; // 标记进程是否已完成 int safe_seq[process_count]; // 记录安全序列 int count 0; for (int i 0; i resource_count; i) work[i] available[i]; for (int i 0; i process_count; i) finish[i] 0; while (count process_count) { int found 0; for (int i 0; i process_count; i) { if (!finish[i]) { // 判断need[i]每一维是否 work[j] int can_alloc 1; for (int j 0; j resource_count; j) { if (need[i][j] work[j]) { can_alloc 0; break; } } if (can_alloc) { // 假设分配给该进程运行完毕后回收资源 for (int j 0; j resource_count; j) { work[j] allocation[i][j]; } finish[i] 1; safe_seq[count] i; found 1; } } } // 一轮扫描没有找到可满足的进程说明不存在安全序列 if (!found) return -1; } return 0; // 存在安全序列safe_seq数组记录了具体顺序 }这段代码的边界条件很关键need是二维数组、allocation是二维数组传参时要用二级指针work向量是“可用资源已回收资源”的累计值不是固定不变的safe_seq数组在报告里要打印出来作为“存在安全序列”的执行证据。写报告时把多组输入跑一遍一组有安全序列、一组没有对比截图比只贴一种结果更有说服力。4.3 实验六与实验七安全权限模型和系统性能统计实验六偏安全模型理解ACL访问控制列表和用户权限管理。教学实验通常不涉及真实内核的用户管理而是给文件表加一个权限字段在每个文件操作系统调用入口做权限校验。实现时注意权限检查必须在内核态做不能在用户态做完再传结果否则等于没有安全措施。实验七的性能分析是整套实验里最有“数据感”的一个。从“6 3.png、1.png、2.png”这组图片看实验要求用系统调用收集CPU利用率和内存使用情况。我常用的做法是写一个内核线程周期性读取jiffies计算CPU占用率把结果通过proc文件系统暴露给用户态// 内核态采集CPU使用率两个时间点jiffies差值/系统运行总时间 unsigned long total_jiffies jiffies; unsigned long idle_jiffies 0; // 在idle循环里累加 unsigned long cpu_usage (total_jiffies - idle_jiffies) * 100 / total_jiffies; printk(CPU usage: %lu%%\n, cpu_usage);用户态拿到这些数据后我用Python画折线图import matplotlib.pyplot as plt # 从日志文件中解析出时间和CPU占用率数据 times [] usage [] with open(cpu_log.txt) as f: for line in f: t, u line.strip().split(,) times.append(float(t)) usage.append(float(u)) plt.plot(times, usage) plt.xlabel(time (s)) plt.ylabel(CPU usage (%)) plt.title(CPU Usage Over Time) plt.savefig(cpu_usage.png)画出来的图可以直接贴进实验报告比一堆数字直观多了。性能分析实验的加分点是“分析部分”你得解释为什么某个时刻CPU占用率飙升、是不是某段代码处于忙等状态。截图里如果有menu.png说明实验还有个交互菜单的模块通常是用它来触发不同的系统调用制造不同的性能特征。4.4 截图驱动复盘从result.png反推运行步骤整个压缩包里最有价值的一组图是result.png、menu.png、屏幕快照这类运行结果截图。它们的作用不只是给报告当证据还能帮你反推出“怎么把实验跑通”。我的做法是先看菜单截图里有哪些选项再看结果截图里打印了什么信息然后对照md里的代码片段猜测“哪个选项对应哪个操作”。比如menu.png里如果只有三个选项对应的实验代码一般也只注册了三个函数result.png里如果输出了“Hello, kernel!”和一串数字那一定是验证了自定义系统调用的返回值。这种从结果反推过程的方法特别适合你在自己不熟悉的内核版本上做移植——先知道目标长什么样再去代码里找实现路径比从零开始读源码高效得多。5. 课程设计常见问题排查现象、原因与修复记录5.1 编译期的坑头文件与工具链不匹配现象make执行到一半报错提示找不到某个头文件或者内联汇编语法不识别。 原因教学内核是为老版本gcc写的新版本编译器改了汇编语法标准或者include路径里缺少某个自定义头文件。 解决先看报错文件是系统头文件还是内核自带头文件。系统头文件问题就换gcc版本我一般用gcc-4.8内核自带头文件问题检查源码根目录的include是否被正确引用常见做法是在makefile的CFLAGS里加-I标志指向内核include目录。5.2 编译能过、启动黑屏或反复重启现象make顺利生成Image虚拟机启动后黑屏或者不停重启。 原因最常见是CMOS内存设置不符、磁盘镜像路径错误、也可能是改了中断表导致加载完内核就崩。 解决先用最简单的未修改内核验证虚拟机配置。我一般保留一份干净的内核源码做基准任何改动先在干净内核上跑一遍确认环境没问题再移植自己的代码。如果黑屏发生在打印第一行字符之前优先查bochs配置文件里的内存大小和磁盘参数。5.3 系统调用返回-ENOSYS或触发非法指令现象用户态程序调用自定义系统调用后返回-1且errno是ENOSYS或者直接打印出illegal instruction。 原因sys_call_table没有同步更新或者系统调用号超出nr_system_calls的范围。 解决对照“系统调用号.png”和“系统调用数量.png”确认三处一致sys.h里的函数声明、sys_call_table里的位置、unistd.h里的编号。我吃过一次亏改了表但忘了把nr_system_calls加一结果所有系统调用全部失效连printf的底层write都崩了。5.4 运行结果和报告截图不一样现象自己跑出来的菜单、输出顺序和压缩包里的result.png对不上。 原因某些实验需要先执行特定操作比如先创建文件再读取步骤不同菜单状态不同。 解决把md里的实验步骤逐条读一遍按步骤走一遍在关键节点截图。这类教学内核实验的输出顺序通常和操作顺序强相关先做了什么后做了什么输出就长什么样。5.5 TSS或中断表改了之后三重故障现象虚拟机直接重启没有打印任何错误信息。 原因改TSS字段时和进程切换的硬件要求冲突比如esp0指向的内核栈地址非法或者中断门描述符权限位设置错误。 解决用Bochs的调试模式加断点查看CPU停在哪条指令然后反查TSS和中断描述符表。Bochs自带的调试器虽然简陋但看寄存器和内存足够。另一个经验是每次只改一个字段、跑一次验证别一次性改三处。5.6 内核日志里输出乱码现象printk输出的中文字符或者特殊字符变成乱码。 原因虚拟机的串口或显示器输出编码和宿主终端不匹配。 解决把printk里所有中文替换成英文这是最省事的方案。如果非要中文检查终端编码是不是UTF-8教学内核早期版本只支持ASCII和部分扩展字符。5.7 中断只触发一次第二次写设备无响应现象虚拟设备第一个字节传输成功后续写入没有反应。 原因中断处理完成后没有向设备发送EOI中断结束命令设备认为CPU还在处理上一条中断。 解决在中断处理函数末尾对8259A主片和从片分别写入OCW2命令。这个细节在教材里经常被一笔带过但实际实验里几乎必踩。6. 让实验结果可演示三种验证路径与一个记录习惯课设答辩最怕的不是代码写不对而是“跑得出来但说不清楚”。我做过这么多实验总结出三种验证路径按优先级排序第一是编译验证每次改完代码make能过是底线第二是运行验证虚拟机启动后能通过菜单触发新功能并看到输出第三是逻辑验证把实验结果和理论预期对照——比如PV实验里两个进程交替进入临界区理论上输出顺序应该是严格交替的如果出现连续两次同一个进程进入说明互斥失效了。验证时我习惯留一份“运行记录”每次实验建一个文本文件里面存三样东西改动了哪些文件、运行时的完整输出、以及截图对应的命令步骤。这套记录习惯是从这个报告包里的截图命名学来的——你看它每张图片都带数字前缀和含义描述比如“2 系统调用号.png”“5 改系统调用数目.png”这种命名方式本质上是给调试过程留了索引。从那以后我每次做课设实验都强制自己先画一张“文件改动清单表”把系统调用号、函数名、文件路径列成表格改完一项划掉一项全部打勾之后再编译。这个习惯帮我避开了很多“代码和报告对不上”的翻车事故也让我在答辩时不用临时翻代码就能说出每一步做了什么。希望帮到你。本文还有配套的精品资源点击获取