ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HUST操作系统实验与课设:从解压到跑通全程避坑指南

HUST操作系统实验与课设:从解压到跑通全程避坑指南 简介这是一份华中科技大学操作系统实验与课程设计资料包面向计算机科学与技术专业学生覆盖进程管理、内存管理、文件系统、设备管理、死锁避免、线程同步、调度策略、系统调用与网络编程等核心主题旨在通过实际编程与系统设计帮助学习者理解操作系统核心机制与设计思想适合正在学习操作系统原理并希望动手实践的人使用。压缩包共249个文件容量41.66MB主要包含C/Java源码以及编译后的class、jar、exe等产物另有makefile、xml配置、txt说明与pdf文档便于按模块对照阅读和编译验证。目前已有169人学习下载适合作为实验报告或课程设计的参考资料。通过完成这些实验读者可亲手实现简单调度器、文件系统或设备驱动从调度算法到内存布局逐层深入理解用户态与内核态切换、中断处理等底层机制从而提升系统编程能力和问题排查能力。1. 拿到 HUST Operating System Codes先别急着解压想清楚这套代码能给你什么很多人在期末周把「华中科技大学操作系统实验与课设」这个 zip 包下载下来第一反应是赶紧找个目录解压、找一份很像的实验报告改改交差。但我建议你先忍住这套东西的价值远不止一份作业答案。它是一套完整的操作系统教学实验代码覆盖进程调度、同步互斥、内存管理、文件系统这些核心模块哪怕你没在华中科技大学读过一天书也可以拿它当训练场把实验跑起来、改参数、看现象再用自己的话把代码讲明白。这个过程比背两遍教材有用得多尤其适合正在准备考研复试、项目答辩或者想补系统能力的人。本文就顺着这套代码从拆包到跑通的路径把常见做法、参数和踩坑一次讲清楚。2. zip 解压与目录探路把压缩包变成可构建的工程骨架2.1 解压前先看包封用 unzip -l 确认编码与顶层目录拿到一个 zip 文件不要急着双击或者用 Python 的 zipfile 一把梭。我先用 unzip 的列表模式看看里面到底是什么结构避免解压后一堆 .c 文件散落在当前目录把工作区搞得一团糟。打开终端进入下载目录执行unzip -l HUST Operating System Codes 华中科技大学操作系统实验与课设.zip | head -50-l参数只列出压缩包里的文件清单并不实际解压。这能让你一眼看清有没有一个顶层目录。如果所有路径都带一个类似hust-os-lab/的前缀那解压是安全的如果路径直接是lab1/、lab2/这种你需要手动新建一个目录再解压。顺带说一句这个压缩包的文件名里带着中文说明它多半是在 Windows 环境打包的要留意后面可能出现的编码问题。看完顶层结构我还习惯用zipinfo -v查看单个文件的压缩方式和解压后大小。如果里面有 Linux 可执行文件可以留意一下文件模式是否保留了rwx权限。很多跨操作系统压缩的包在解压后没有执行权限这也是后面踩坑的高发区。2.2 解压并修正中文乱码用 -O 参数避免文件名变“锟斤拷”确认结构之后正式解压。这里有一个典型的坑压缩包在 Windows 下用 GBK/CP936 编码保存中文文件名而 Linux 默认用 UTF-8直接 unzip 会得到一堆乱码文件名。那些写着“实验三”的目录会变成“瀹為獙涓”你找都找不到文件。常见做法是给 unzip 指定解码字符集# 先建一个干净的目录避免原本没有顶层目录的文件散落一地 mkdir -p hust-os-lab cd hust-os-lab unzip -O CP936 ../HUST Operating System Codes 华中科技大学操作系统实验与课设.zip-O CP936告诉 unzip 用简体中文的 GBK 编码去解释文件名解压出来就是正常的“进程调度实验”一类的目录名。如果你用的 unzip 版本不支持-O参数可以用 Python 的zipfile配合iconv做二次改名但优先推荐换一个支持该参数的 p7zip 或者 unzip。解压完成后立刻统计文件数量验证有没有解压中断find . -type f | wc -l如果数量对不上压缩包里的记录说明磁盘空间不够或者路径过长需要清理后重新解压。这一步值得多花两分钟因为后面的所有实验都建立在一份完整的源码树之上。2.3 识别实验环境从 Makefile 与头文件判断目标平台解压完成别急着make。我一般先看目录里的 README、Makefile 和.h头文件判断这套代码是给真机 Linux 运行还是要配合 QEMU、RISC-V 模拟器之类的环境。操作系统课设通常有两类一类是纯用户态模拟写一个调度器或内存管理器直接用 gcc 编译成可执行文件另一类是基于 MIT 的 xv6 或类似的教学内核需要交叉编译并在模拟器里引导启动。两种环境的跑法和调试方式完全不同。find . -name Makefile | head -10 head -50 $(find . -name Makefile | head -1)看 Makefile 里的编译目标如果出现gcc -o scheduler scheduler.c那是普通用户态程序如果出现riscv64-unknown-elf-gcc、qemu-system-riscv64这类工具说明是内核实验。这套包如果以课程设计命名我大概率会押在用户态模拟实验上因为自带启动引导的内核工程通常会有更复杂的目录结构。再file一下有没有现成二进制file $(find . -name *.bin -o -name *.elf | head -3)这一步能帮你快速确认代码是 32 位还是 64 位是 x86 还是 ARM避免后面用错了编译参数。遇到 32 位代码在 64 位机器上编译还要记得安装gcc-multilib。环境识别的结果直接决定你接下来是打开一个终端跑起来还是先启动 QEMU。3. 进程调度与同步互斥从实验代码到可调参数的调度模拟器3.1 把调度实验拆出主线从 PCB 结构体到调度序列进程调度是大多数操作系统实验的第一仗也是这套代码包里的核心内容。打开进程调度相关的目录你通常能看到PCB结构体、create_process()、schedule()这几个函数。我以前指导学生时第一步不是看完整代码而是先画调用链什么数据表示一个进程的 CPU 时间什么时候决定切换就绪队列用什么数据结构。如果这套实验是用户态模拟PCB 一般是一个结构体里面有 PID、到达时间、服务时间、剩余时间、状态字段。struct pcb { int pid; int arrive_time; // 到达时间单位可以是秒或时间片 int burst_time; // 需要占用的 CPU 总时长 int remaining; // 剩余时间用于时间片轮转 int start_time; // 第一次被调度的时间算响应时间用 int state; // 0 就绪 1 运行 2 等待 3 结束 };这段结构体的重点在remaining和start_time。很多实验只要求算平均周转时间和平均带权周转时间没有start_time你就算不出响应时间。而remaining是实现抢占式调度所必需的没有它就没法支撑时间片轮转。写实验报告时你有责任解释清楚每个字段为什么存在而不是把它当成黑匣子一样背下来。调度主体常见的做法是写一个模拟时钟循环把时间单位当整数每个单位检查一次进程状态决定是否切换。我用 C 写模拟器时习惯把每个调度算法独立封装成一个函数输入是 PCB 数组和进程个数输出是调度甘特图。这样既方便对比结果也方便测试不同参数。3.2 从 SJF 到时间片轮转你至少要看懂这三段代码以最短作业优先SJF为例它的非抢占版实现很简单在每次进程结束时从就绪队列里挑一个服务时间最短的进程运行。这份实验包的实现可能用了链表但原理一样我把它展开成最简单的数组版本方便你对照阅读void sjf_schedule(struct pcb *procs, int n) { // 按到达时间先排一次保证初始就绪队列正确 for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (procs[j].arrive_time procs[j1].arrive_time) { struct pcb t procs[j]; procs[j] procs[j1]; procs[j1] t; } } } int current -1, done 0, now 0; while (done n) { // 在当前时刻挑一个 burst_time 最小且已到达的就绪进程 int best -1; for (int i 0; i n; i) { if (procs[i].arrive_time now procs[i].remaining 0) { if (best -1 || procs[i].burst_time procs[best].burst_time) best i; } } if (best -1) { now; continue; } // CPU 空闲时间推进 procs[best].remaining--; now; if (procs[best].remaining 0) { procs[best].state 3; // 结束记录完成时间 procs[best].burst_time now; // 这里临时借字段存完成时间 done; } } }代码逻辑不复杂按时间逐个推进每次只执行一个时间单位后重新做选择。为什么要这样写因为后续改成时间片轮转时你只需要把“选择最短作业”换成“取队首元素”并且加一个时间片计数。这种结构最贴近实际调度器的中断驱动模型每次时钟中断就是一次重新决策的时机。注意上面我用burst_time临时存完成时间这在正式做工程时会用一个单独finish_time字段实验代码经常偷懒混用阅读时要能识别出来。时间片轮转RR的实现重点在于就绪队列循环。如果实验代码用循环链表别太担心如果希望看到更清晰的轮转效果可以直接用数组加环形游标#define TIME_SLICE 2 void rr_schedule(struct pcb *procs, int n, int time_slice) { int now 0, done 0, cur 0; while (done n) { int ran 0; for (int used 0; used time_slice; used) { // 当前游标指向的进程未到达或已结束则跳过 if (procs[cur].arrive_time now || procs[cur].remaining 0) break; procs[cur].remaining--; now; ran 1; if (procs[cur].remaining 0) done; } cur (cur 1) % n; // 游标轮转 if (!ran) now procs[cur].arrive_time now ? procs[cur].arrive_time : now 1; } }必须说明真实的时间片轮转还得考虑“进程在当前时间片内结束则立即切换”以及新进程加入时是否抢占。在这个简化版里我用一个游标替代链表队首省了内存管理代价是进程很多时效率低。做实验验证完全够用跑一百个进程毫无压力。TIME_SLICE是你最值得改的参数把它从 1 改成 4平均周转时间的曲线变化会非常明显这也是你实验报告里“分析时间片对调度性能影响”的素材来源。3.3 生产者消费者里的信号量顺序counter 和 mutex 谁先谁后同步互斥实验比调度更容易翻车。常见题目是生产者消费者代码包会给出sem_wait/sem_post的封装有时直接让你在 Linux 线程上补全。这里最经典的错误是先获取互斥锁再判断缓冲区是否满导致生产者在锁内阻塞消费者永远拿不到锁形成死锁。正确做法是先对空槽位信号量做 P 操作再去拿互斥锁。void *producer(void *arg) { for (int i 0; i ITEMS; i) { // 先申请一个空槽位阻塞到有位置再对缓冲区加锁 sem_wait(empty); pthread_mutex_lock(mutex); buffer[in] i; in (in 1) % BUFFER_SIZE; pthread_mutex_unlock(mutex); sem_post(full); } return NULL; }这段代码的要点是sem_wait(empty)必须放在pthread_mutex_lock(mutex)之前。如果顺序反了生产者拿着锁等空槽位消费者又没有锁释放空槽位程序就会卡死在谁也唤醒不了对方的状态。我在实验课上见过十多个组踩同一个坑最后都是靠打印日志定位到“锁内的 sem_wait”才缓过神。注意这里empty和full的初值empty等于缓冲区大小full等于 0它们分别代表空槽位和已生产项的数量。改变这两个参数的初值程序行为会完全不同你要能解释原因。3.4 多级反馈队列的层数与降级策略实验报告多写两页的细节如果课设要求实现多级反馈队列MLFQ千万不要只写两层。常见做法是设计三到四个队列高优先级队列时间片短低优先级队列时间片翻倍。实验代码里一般会有队列数组和level字段调度时从最高优先级往下找。这里的参数是每层队列的时间片基数base_slice[]以及降级阈值age_limit。我把这两个参数直接定义成宏方便反复改#define NUM_QUEUES 3 #define BASE_SLICE {1, 2, 4} #define AGE_LIMIT 3 // 运行超过3个时间片未完成就降级MLFQ 的实验代码里最微妙的是“降级时机”必须在进程让出 CPU 或时间片耗尽时判断不能在中途中断时降级否则高优先级进程会被频繁降级失去优先级意义。验证方法也很简单制造一个 CPU 密集的长任务观察它是否从 queue[0] 一路降到 queue[2]打印每次切换的队列号。整个过程你能看到调度算法在动态平衡响应时间和吞吐量这比单看教材上的数据有说服力得多。4. 内存管理与文件系统把银行家算法和页面置换拆开验证4.1 动态分区分配首次适应与最佳适应的内存碎片对比内存管理实验如果做的是动态分区代码包里通常有一段模拟内存分配的程序有一个空闲分区表每个分区记录起始地址和长度。最先能做到实验报告里的是首次适应算法它从低地址开始找第一个够大的空闲区。这里要留意的参数是“碎片阈值”小于某个尺寸的空闲区就不再分配直接并入相邻区否则你的内存会被切得粉碎。struct free_block { int start; int size; }; void first_fit(struct free_block *p, int n, int req) { for (int i 0; i n; i) { if (p[i].size req) { // 从低地址分配 p[i].start req; p[i].size - req; break; } } }最佳适应则是遍历全表选一个 size 最小的合适分区。很多实验代码用链表维护空闲块我这里为了行文清晰用数组示意。真实跑起来你可以让一串进程交替进行请求和释放最后打印空闲区分布图。这个分布图非常直观地说明为什么最佳适应看起来更节约空间却会产生更多无法利用的微小碎片。写实验报告时别只说“最佳适应性能更好”要给出你的分配序列和最终碎片数量。4.2 页面置换LRU 的计时器实现和计数器实现的差别页面置换实验里 LRU 是最常被考到的算法。实验代码里如果用了计数器法会在每个页表项里加一个last_used字段每次访问页面时更新为当前时钟。淘汰时扫描全部页表项找最小的last_used。这个实现简单直观但有个坑用整数计时可能溢出实验规模小没关系但你要在报告里提一嘴。更常见也更省事的是用一个“访问栈”或者“时间戳数组”。#define FRAME_NUM 4 int page_table[FRAME_NUM]; // 页号到帧号的映射 unsigned long last_used[FRAME_NUM]; unsigned long clock; void access_page(int page_no) { // 查页表未命中则按 LRU 淘汰最早使用的帧 int victim -1; unsigned long oldest ULONG_MAX; for (int i 0; i FRAME_NUM; i) { if (page_table[i] page_no) { victim i; break; } if (last_used[i] oldest) { oldest last_used[i]; victim i; } } page_table[victim] page_no; last_used[victim] clock; }这段代码把“命中”和“缺页”一起处理了如果命中直接更新该页的last_used如果缺页就找最小last_used的帧淘汰。请注意LRU 在实际系统中会用硬件引用位近似实现不会真去扫全表实验代码里的“真 LRU”更多是为了让你理解算法逻辑。和 FIFO 比LRU 在顺序访问下几乎没有优势在循环访问下优势明显。你可以把访问序列放在一个文件里统计缺页次数观察不同帧数FRAME_NUM下的缺页曲线。4.3 文件系统理解多级索引结构与磁盘块的分配文件系统实验不如前两个模块花哨但代码量往往最大。课设代码包里出现的文件系统模拟一般分成两层一层是磁盘块管理另一层是文件目录树。磁盘块管理里你至少要知道block_size和inode里的直接块、一级间接块、二级间接块数组。模拟多级索引的代码通常不复杂复杂的是你要算清一个文件最大能到多大。#define BLOCK_SIZE 1024 #define ADDR_PER_BLOCK (BLOCK_SIZE / sizeof(int)) #define DIRECT_BLOCKS 12当实验要求写bmap()函数时核心逻辑就是按文件偏移量找到它在第几级索引。很多初学者在这里翻车把直接索引表和间接索引表混在一个数组里。我习惯在纸上先画一个三层树文件偏移 0 到 11 走直接块12 到12ADDR_PER_BLOCK-1走一级间接再往上是二级间接。这样做的好处是代码写起来每个分支都能对应纸上的一行。如果实验包里已经给了完整的 inode 结构你要重点看它是否支持超过 4GB 的文件如果不支持说明索引层级不够这也是一个可以写进报告的改进点。4.4 银行家算法的安全序列预分配和回滚逻辑的验证内存管理有时会结合死锁避免要求实现银行家算法。实验代码会给你当前可用资源Available、每个进程的最大需求Max和已分配Allocation让你计算Need并判断系统是否处于安全状态。这里最容易出错的是“试探性分配”后的回滚找到安全序列时你要把满足条件的进程资源在系统模拟上释放掉而不是真的结束进程。很多人忘记释放导致安全序列找不全。int safe_check(int *avail, int **max, int **alloc, int n, int m) { int need[n][m]; int work[m]; int finish[n]; // 初始化 work avail, finish 0 for (int i 0; i n; i) { finish[i] 0; for (int j 0; j m; j) need[i][j] max[i][j] - alloc[i][j]; } for (int i 0; i m; i) work[i] avail[i]; // 每次找一个能完成的进程 for (int k 0; k n; k) { int found -1; for (int i 0; i n; i) { if (finish[i]) continue; int ok 1; for (int j 0; j m; j) if (need[i][j] work[j]) ok 0; if (ok) { found i; break; } } if (found 0) return 0; // 找不到安全序列 finish[found] 1; for (int j 0; j m; j) work[j] alloc[found][j]; // 释放已分配资源 } return 1; }银行家算法的代码量不大但逻辑严谨性要求高。上面的safe_check里最不显眼也最关键的是work[j] alloc[found][j]这行模拟了“进程跑完归还资源”。如果你在实验报告里论证某组数据不安全就用这段代码跑一边把每一步找到的进程序列列出来。相比纯文字描述这个安全序列列表可信度高很多。5. 避坑 / 常见问题 / 排查从解压到跑通这五个坑我最想替你踩5.1 现象zip 解压后目录名全是乱码找不到实验三在哪很多人在 Linux 上直接unzip那个中文名压缩包结果所有中文目录变成了“鏂囦欢/”一类的乱码。原因就是 Windows 下压缩时文件名用的是 GBK/CP936 编码Linux 下 unzip 默认按 UTF-8 处理于是字节被强行解码成乱码。解决办法是用unzip -O CP936重新解压。这个-O参数在部分发行版自带的 unzip 上无效如果报“invalid option”可以安装 p7zip 并用7z x -mcp936处理。要注意的是重新解压前先把乱码目录整个删掉避免新旧文件混杂。5.2 现象Makefile 报错“missing separator”明明代码看着没问题这套代码包如果在 Windows 上被编辑过换行符通常是 CRLFmake 在解析规则时会把\r当成内容于是报missing separator。另一个常见原因是缩进用了空格而不是 tabmake 规定规则命令行必须用 tab。排查方法是用cat -A Makefile看行尾有^M就是 CRLF。解决方法是sed -i s/\r$// Makefile把回车去掉再检查配方行是否以 tab 开头。这个坑特别隐蔽因为编辑器默认显示会把 tab 显示成空格。5.3 现象编译通过运行一秒钟就段错误实验代码里最常见的段错误是越界访问比如就绪队列用固定数组进程数一多就越界或者页面置换里page_table数组下标越界。排查时不要只盯着代码看直接用 gdb 跑起来它会精确指到越界的行。gdb ./scheduler进去之后run崩溃后bt看调用栈。很多时候你会发现是某个链表节点没有判空比如调度器取队首时直接head head-next却没有检查head NULL。养成在每次指针操作前判空的习惯能挡掉一大半问题。5.4 现象生产者消费者程序挂起没有任何输出这是典型的死锁。原因十有八九是信号量顺序错了先拿互斥锁再等待空槽位。定位方法是在每个sem_wait前后加printf(... in sem_wait for empty)看到卡在哪一行。解决方法是调整顺序让sem_wait(empty)在pthread_mutex_lock(mutex)之前。注意如果你用的是sem_init销毁时要sem_destroy否则第二次执行时会复用一个已损坏的信号量同样会卡住。5.5 现象代码在 Ubuntu 上没问题换到 Kylin Linux 上编译就报错有些国产OS比如 Kylin Linux Advanced Server内核基于 Linux 4.x版本较老实验代码里用到的 glibc 函数或内核头文件位置可能不一致。最常见的是syscall函数定义从sys/syscall.h换到了unistd.h或者某些系统调用号变了。解决办法是检查#include的顺序和宏定义必要时用#ifdef __NR_xxx做适配。这不算代码包的问题而是不同发行版之间的生态差异遇到别慌先把gcc -v和uname -a输出打出来对比。6. 把实验代码变成自己的课设验证调度器正确性的三个技巧与我的教训课程设计答辩时老师最不喜欢听你讲“我抄了别人的代码”。你要拿出自己的验证方法证明你不仅让代码跑起来了还能解释它的行为。这里我分享三个我在做实验时觉得特别有用的技巧。第一个技巧是故意构造极端输入。比如验证 SRTF最短剩余时间优先时我构造了一批同时到达但服务时间差距极大的进程一个要 100 个时间片几个要 1 个时间片。正确调度器应该先执行短进程然后才开始长进程。如果你的代码输出不是这样说明调度算法实现有问题而不是数据问题。这种“对抗性测试”比随机测试更容易暴露边界 bug。第二个技巧是打印调度甘特图。不要只看最后算出来的平均周转时间要把每个进程的时间片分配打印出来。我习惯在调度函数里维护一个全局日志数组记录[时间点, pid, 运行长度]最后统一输出。这样你一眼能看出时间片轮转是否按照预期轮转SJF 是否真的按顺序执行。用脚本或者表格整理日志你还会发现一些逻辑边界比如进程到达时间相同时应该按 PID 排序否则每次输出都不一样。第三个技巧是把代码包里的核心算法单独编译一个基准测试版本。我不直接把课设代码拿来跑而是把所有算法封装成纯函数输入输出都从文件读取。然后写一个小测试框架生成多组随机数据调用函数并统计结果。这个过程能让你对每个函数的参数边界理解得更透彻答辩时被问到“如果到达时间都是 0 会怎样”也能立刻答上来。我自己的教训是当年做页面置换实验时把 LRU 和 LFU 混为一谈。我以为只要记录访问次数少的就是最近最少使用结果代码写出来缺页率比 FIFO 还高。后来我把访问序列在纸上画出来用时间戳标注每次访问才发现 LRU 的核心是“最近”不是“频次”。这个教训告诉我黑匣子里的代码再熟练没有亲手在纸面推演一遍面试时照样露馅。希望这套 HUST Operating System Codes 能帮你在真正理解操作系统这条路上省下不必要的折腾也希望我的这些踩坑记录让你在实验里少赔几个晚上的睡眠。本文还有配套的精品资源点击获取
返回列表