简介:湖南科技大学2021年操作系统课程设计完整资料包,适合计算机相关专业本科生及正在准备操作系统课设的读者。包内含课程设计指导书、参考源码、扩展资料与已完成实验报告,覆盖进程管理、内存管理、文件系统、设备管理、系统调用与安全保护等核心知识,并配有验证性实验源程序,便于理解进程调度、内存分配等关键模块的具体实现。压缩包共274个文件,以cpp源码、docx实验报告、pdf资料、txt说明为主体,另有exe可执行程序及VS工程配置文件,可直接编译运行、对照分析,整体大小235.2MB。目前已有1353人学习,资料按指导书、代码、报告分层存放,结构清晰,既适合课设选题与代码参考,也有助于报告撰写和操作系统原理的实践复习。
1. 操作系统课程设计资源包里到底装了什么:先看骨架,再决定要不要自己重写
操作系统这门课,很多人是“上课听懂、考试会背,一到课程设计就卡壳”。湖科大2021操作系统课程设计.zip,从标题里的“湖科大”“2021”“课程设计”几个词就能判断,这是一份打包好的操作系统课程设计材料,通常会把进程管理、内存管理、文件系统这几大模块的源代码、课程设计实验报告、运行截图和任务书装在同一个压缩包里。它要解决的问题很具体:操作系统课程设计怎么做才能通过验收,调度算法怎么写才不会卡死,实验报告的数据怎么和代码输出对得上。适合正在赶课程设计的学生、想参考实验报告结构的新手,以及想复用一套调度或内存模拟代码的开发者。下面按“先看结构、再推实现、最后排错”的顺序,把这类资源的打开方式讲清楚。
2. 进程管理与调度:先把 PCB 和状态机写对,再谈算法对比
2.1 为什么 PCB 是调度模块的地基
拿课程设计里最常出题的“模拟进程创建与调度”来说,很多同学拿到题直接去写调度算法循环,结果发现进程的创建、撤销、状态迁移根本没法验证。原因在于,调度算法是跑在进程控制块(PCB)之上的一个“轮转逻辑”,PCB 设计得不对,后面所有算法都是悬空的。
常见做法是先定义一个最小可用的 PCB,把进程标识、状态、优先级、到达时间、剩余服务时间、已占用 CPU 时间放在同一个结构体里。课程投影里那些复杂字段先不用管,够用即可:
typedef struct pcb { int pid; // 进程 ID,从 1 开始自增,唯一标识一个进程 int status; // 0-就绪 1-运行 2-阻塞 3-完成 int priority; // 静态优先级,数值越大优先级越高 int arrive_time; // 到达时间,单位是调度节拍 int need_time; // 还需运行的节拍数,每被调度一次减 1 int used_time; // 已占用节拍数,用于计算周转时间 struct pcb *next; // 指向就绪队列里的下一个进程 } PCB;这段设计的核心是 status 和 need_time 两个字段。status 记录进程当前状态,后面打印状态迁移图和统计各状态停留时间时直接取数;need_time 每被调度一次就减一,减到 0 表示进程运行完毕,可以出队并记录完成时间。priority 字段在时间片轮转里用不上,但保留它就能在同一个程序里多跑一组优先级调度对比,报告里多一张表,性价比很高。
实际写的时候要注意,PCB 链表不能只挂在队头,进程完成或阻塞时要从队列中间摘除,这涉及单向链表的删除操作。常见做法是先实现一个通用的队列插入和删除函数,而不是在每个算法里重写链表遍历。
2.2 时间片轮转和抢占式优先级该怎么落地
先写时间片轮转,因为它的规则最简单:就绪队列先进先出,队首进程运行一个时间片,没跑完就挪到队尾。不含抢占判断,适合作为第一个跑通的代码。
void round_robin(PCB *ready_queue, int time_slice) { PCB *current = ready_queue; while (current != NULL) { // 运行一个时间片 current->need_time -= time_slice; current->used_time += time_slice; if (current->need_time <= 0) { current->status = 3; // 进程完成 record_finish(current); // 记录完成时间,用于算周转时间 } else { current->status = 0; // 回到就绪态,挪到队尾 } current = get_next(ready_queue); // 取就绪队列下一个队首 } }time_slice 是唯一的敏感参数。课程设计里一般让用户从键盘输入,或者设一个默认值,比如 2,再跑 4、8 做对比。这里要注意,need_time 不一定能被 time_slice 整除,最后一次运行时剩余时间小于 time_slice,应该只减剩余量而不是减满一个时间片,否则会出现“进程已完成但仍被多计一个时间片”的问题。
如果做抢占式优先级调度,逻辑多一步:每个节拍都要重新扫描就绪队列,找出当前优先级最高的进程。当前运行进程如果还没做完,先让它回到就绪态再加入队列,然后取出新的最高优先级进程。容易翻车的地方是“同优先级进程怎么处理”,常见做法是按到达顺序轮转,不额外加老化机制,除非题目明确要求。另外,优先级调度必须配合一个“当前还有多少进程未完成”的计数,否则 while 循环会跑死。
2.3 用日志生成甘特图,报告对比数据不靠编
代码跑通后,报告的对比数据不能随手填。我一般会在调度每次切换进程时打印一行日志,包含当前节拍、运行 pid、剩余时间,这样不仅能人工核对执行顺序,还能直接画成甘特图。
t=0 run pid=1 need=4 queue=2,3 t=2 run pid=2 need=3 queue=3,1 t=4 run pid=3 need=2 queue=1,2这份日志原样保留在报告或附录里,比任何文字描述都有说服力。三条调度算法各跑一组固定输入,然后统计平均周转时间和平均等待时间。时间片轮转里时间片越大,上下文切换次数越少,但平均周转时间通常变差;这种情况单独有代表性,写报告时解释一句“时间片过大导致短作业等待过长”即可。优先级调度则重点对比优先级分布对等待时间的影响。
这里有一个容易被忽略的细节:上下文切换次数要从队列操作里自动计数,而不是在报告里手数日志。因为手数一定会漏,评审老师随便抽查一条日志就对不上,整体可信度会立刻下降。
3. 内存管理与页面置换:分配算法选型与结果校验
3.1 动态分区分配:首次适应和最佳适应为什么各实现一个
内存管理模块的课程设计通常有二选一的题目方向:动态分区分配或请求分页模拟。动态分区部分教材会列首次适应、循环首次适应、最佳适应、最差适应四种,我的建议是不要全实现,但至少实现首次适应和最佳适应,理由很简单——这是两个相反的极端,首次适应快、碎片多,最佳适应慢、内部碎片小,一组对比实验就能把差异讲明白。
分配算法的核心是空闲分区链表:
typedef struct free_block { int start; // 分区起始地址,按字编址 int size; // 分区大小,单位是“块” struct free_block *next; } FreeBlock;首次适应按起始地址递增查找,最佳适应按空闲块大小递增查找,两者对同一请求序列的输出不同。实现时,关键差异在“找到满足条件的第一个块”的判定条件。首次适应是地址升序,所以链表保持按 start 排序;最佳适应是大小升序,插入空闲块时要按 size 重排,回收时要合并相邻空闲区再重新排序。
最容易翻车的是回收合并。释放一个分区时,要检查它前后是否有空闲区,如果相邻就要合并成一个大的空闲区,然后再插入链表。很多同学只做了“释放后插入”,忘了相邻合并,导致后续分配结果比预期的碎片多得多,和模拟器一对比就差出来。程序里写两个辅助函数,一个查找前驱分区,一个判断是否相邻,能让合并逻辑清晰不少。
3.2 LRU 用计数器,时钟算法用引用位
请求分页部分,老师通常要求至少实现先进先出、LRU、时钟算法。这里面最常被扣分的不是“算不出来”,而是“结果和标准模拟器对不上”。我处理 LRU 的办法是给每个页面设一个“最近访问计数”,每次内存访问都更新计数,而不是只在缺页时才更新。差一次的核心问题大多出在这里。
int access_page(page_table *pt, int page_no, int access_time) { if (pt->frames[page_no].valid == 1) { // 页面在内存中:即使是命中,也要刷新访问时间 pt->frames[page_no].last_used = access_time; return 0; // 未缺页,缺页次数不变 } // 缺页:找一个 last_used 最小的页面换出 int victim = find_lru(pt); pt->frames[victim].valid = 0; pt->frames[page_no].valid = 1; pt->frames[page_no].last_used = access_time; return 1; // 缺页次数加 1 }access_time 是一个全局自增计数器,模拟“时间戳”。find_lru 要遍历整个页表找最小的 last_used,这很好写但要注意,不能一边遍历一边修改 valid 位,必须先把候选 victim 确定下来再执行替换,否则会出现“本轮换出的页已经被本轮访问过”的逻辑错误。
时钟算法则是每个页框一个引用位,访问时置 1,缺页时用循环指针扫描,扫描过程中把遇到引用位为 1 的页框清 0 并跳过,找到引用位为 0 的页框换出。它和 LRU 的差别在于精度:时钟算法只能表示“最近被访问过”或“最近没被访问过”两个区间,没法区分谁更久没被访问。报告里写对比时,这句话就是你的加分点。
3.3 固定测试序列让三种算法结果可复现
很多课程设计用随机生成页面访问序列,但随机数种子不固定,两次运行结果不同,答辩时你说不清哪个结果是对的。我的习惯是用一组固定的经典序列,内存帧数也固定,把三种算法的缺页次数跑出来放一张表里。
比如访问序列7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1,内存帧数 3,这是一个在很多教材上都出现的序列,非常适合做逐行核对。跑完之后,表格按算法和缺页次数组织:
| 算法 | 缺页次数 | 最后时刻内存页框内容 |
|---|---|---|
| FIFO | 15 | 0 1 7 |
| LRU | 12 | 0 1 7 |
| 时钟算法 | 13 | 0 1 7 |
这里的最后内容以实际输出为准,不同实现的初始装载方式会影响个别页框。要在报告里写清楚“初始时内存为空,每访问一页先判断是否缺页”,这句话能避免评审老师用不同初始条件来质疑你。用固定序列的最大好处是:无论程序跑了多少遍,输出都一样,报告截图和演示输出严格一致,不会出现“答辩现场重跑结果变了”的尴尬。
4. 文件系统与磁盘调度:把“演示程序”升级成“可答辩作品”
4.1 文件控制块和两级目录先设计好,命令解析只是一层皮
文件系统这个方向最容易出现的误区是:大量时间花在命令行的界面美化上,核心的目录和文件管理逻辑反而没写清楚。我建议先把文件控制块结构设计好,再做一个简化的两级目录,最后才是命令解析。
typedef struct file_control_block { char name[8]; // 文件名,短一点无所谓,课程设计够用 int id; // 文件编号,创建时自增 int size; // 文件大小,单位是磁盘块数 int first_block; // 文件在磁盘上的起始块号 int create_time; // 创建时间,可简化为全局递增序号 int is_dir; // 1-目录 0-普通文件 } FCB;两级目录的结构是:一个根目录,下面挂若干一级子目录,子目录下再放文件。这个“两级”是验收时最容易问的点,评审老师一般会问:你为什么要设计两级而不是一级或多级?回答方向是“一级目录查找太慢,多级目录实现复杂度高,两级是演示效果和代码量的平衡点”。这样能引导老师把注意力放到调度和分配逻辑上。
命令解析建议维护一张命令表,把 create、delete、ls、cd、cat 映射到函数指针,而不是用一长串 if-else 堆在 main 里。命令表的好处是新增一个命令只需要加一行表项,出问题也好定位。我一般会把它做成静态数组,每个表项包含命令名、参数个数、对应函数指针。
4.2 位示图分配与释放:20 行代码解决磁盘块管理
文件存储空间管理用位示图最直观,代码量也最小。位示图本质是一个位数组,把整个磁盘的每一块映射到一个二进制位,1 表示已分配,0 表示空闲。分配时找第一个 0 位并置 1,释放时把对应位清 0。
#define BLOCK_NUM 128 #define WORD_BITS (sizeof(unsigned int) * 8) unsigned int bitmap[BLOCK_NUM / WORD_BITS]; int alloc_block() { for (int i = 0; i < BLOCK_NUM / WORD_BITS; i++) { if (bitmap[i] != 0xFFFFFFFF) { // 该字还有空闲位 for (int j = 0; j < WORD_BITS; j++) { if (!((bitmap[i] >> j) & 1)) { // 找到第一个 0 位 bitmap[i] |= (1u << j); // 置 1 return i * WORD_BITS + j; // 返回磁盘块号 } } } } return -1; // 磁盘已满 }分配逻辑按“先找字、再找位”的顺序执行,磁盘块号 = 字下标 × 每字位数 + 位偏移。释放时反向操作:根据磁盘块号算出字下标和位偏移,把对应位清 0。这两段代码加起来不超过 30 行,但覆盖了存储空间管理的核心。
这里有个实际开发中常踩的坑:用 int 数组而不是 unsigned int,位移符在高位时的表现不同,容易把符号位顶进去,导致已分配的块被误判为空闲。我习惯把所有位运算相关的变量都定义成无符号类型,避免这种不确定性。另外,位示图打印函数建议做出来,验收时可以直观看到哪块被分配、哪些空闲,比只看文字输出更有说服力。
4.3 磁盘调度:FCFS、SSTF、SCAN 三组对比的统计口径
磁盘调度部分,最省事的组合是先实现先来先服务和 SCAN 电梯算法,再加一个最短寻道优先做对比。三种算法的输入都是同一组磁道请求序列,输出指标是平均寻道长度和磁头移动方向。
| 算法 | 平均寻道长度 | 磁头移动方向 | 适用场景 |
|---|---|---|---|
| FCFS | 高 | 不固定 | 负载低时公平 |
| SSTF | 低 | 偏向内部磁道 | 响应快但可能饿死 |
| SCAN | 中等 | 单向扫到头再折返 | 大负载吞吐稳定 |
写 SCAN 时最容易错的是边界判断。常见做法是磁头先按一个方向移动,服务完当前方向的所有请求后,再判断是否到达磁盘边界,到达则反向。有人会在“服务完最后一个请求”时就立即反向,这会导致结果和标准算法不一致。报告里统计“平均寻道长度”时,把每次移动的距离累加,最后除以请求总数,代码里用一个全局变量记录总移动距离即可。
磁道请求序列建议用固定的一组,比如55 58 39 18 90 160 150 38 184,初始磁头位置在 100,扇区按教材常见样例处理,这样三种算法的结果可以横向比较,报告的表格也经得起追问。
5. 复现过程中的常见问题与避坑:五条跑不通到对不上的排错记录
5.1 调度程序一运行就死循环,CPU 占用 100%
现象:开始模拟调度后命令行卡住,进程永远运行不完,任务管理器显示 CPU 100%。
原因:就绪队列的指针操作写错了,或者 need_time 没有持续递减。最常见的是 while 循环条件里比较的是“队列不为空”而不是“还有未完成进程”,当进程阻塞后又被错误地重新插入就绪队列,队列永远不会空。
解决:在 while 循环内部打印每一次调度切换的信息,包括当前节拍、运行进程 pid、剩余时间、当前队列长度。一旦发现某次调度后队列长度不减反增,问题基本锁定在队列操作。另外,给模拟加一个“最大调度轮数”上限,比如 500 节拍,到达上限强制终止,避免演示现场卡死。这个上限在答辩时还能解释成“防止死循环的保护机制”。
5.2 LRU 和教材结果对不上,缺页次数总差一次
现象:页面置换模拟跑 LRU 算法,缺页次数比教材上的标准答案少一次或多一次。
原因:访问时间只让每次访问都更新,而不是只在缺页时更新。少一次通常是因为命中时没有更新计数器,导致“最先被换出的页”选择错误;多一次则可能是替换时把刚访问过的页换出去了。
解决:把状态字段改成“每次访问都要先更新时间戳”,再加日志打印每个访问步骤的页框内容和 last_used 值,和教材样例逐行比对。只要日志打印得够细,这一项十分钟能定位完。
5.3 报告截图和最终代码不一致
现象:答辩时老师对照实验报告看演示输出,报告里写的是时间片为 4 的结果,现场跑的却显示时间片为 8。
原因:代码在报告写完后又调过参数,但报告忘了重新截图。这是课程设计最容易被扣分的地方,问题不在技术,而在流程。
解决:先把报告框架和文字写完,代码全部稳定后再统一跑一遍测试,把截图一次性换进去。死记一个原则:报告里的所有数据、截图、表格,必须来自同一个版本的代码输出,不能东拼西凑。我通常在交包之前会删掉旧的 results 目录,重新跑一遍生成新的截图,然后逐张替换。
5.4 换台电脑编译不过,Windows 通 Linux 不通
现象:在 Windows 的 DevC++ 或 Visual Studio 里编译运行正常,换到 Linux 的 gcc 下就报错,甚至头文件都找不到。
原因:常见的是文件名大小写问题,比如#include "PCB.H"在 Windows 不区分大小写,Linux 区分;还有 main 函数写成void main(),gcc 默认不认这种写法;再就是源文件编码问题,Windows 下默认 GBK,Linux 下是 UTF-8,中文注释会乱码甚至造成编译错误。
解决:所有头文件用全小写命名,main 函数标准写法int main(int argc, char *argv[]),源文件统一存成 UTF-8 编码。提交前把整个工程拷到 Linux 或虚拟机里重新编译一遍,当作验收前的最后一道关卡。这样做一次成本不到二十分钟,但能避免答辩环境不兼容的翻车。
5.5 输入非法字符直接崩溃
现象:演示时输入 abc 作为时间片大小,程序直接退出或死循环。
原因:scanf 没有检查返回值,输入解析失败后变量里保留了原来的垃圾值;或者用了 atoi 解析非法字符串。
解决:统一封装一个输入函数,读入失败时给默认值并提示用户重新输入。别小看这个细节,答辩现场老师有时会故意输入异常数据来测试程序的健壮性。只要程序没崩,你就能多拿一个“容错处理”的加分点。
6. 验收与进阶:把课程设计从“能跑”变成“能答辩”
6.1 用一段 bash 脚本固定住所有演示结果
交实验之前,我会跑一个自测脚本,实现“编译、跑测、比对”一条龙。脚本本身很简单,但能保证每次跑出来的结果都在预期范围内:
#!/bin/bash make clean > /dev/null make > /dev/null 2>&1 ./scheduler < testdata/time_slice_2.txt > results/time_slice_2.out ./scheduler < testdata/time_slice_4.txt > results/time_slice_4.out ./scheduler < testdata/time_slice_8.txt > results/time_slice_8.out diff results/time_slice_2.out expected/time_slice_2.exp脚本里每个命令的作用是:先清理并重新编译,保证用的是最新源码;然后用固定输入文件跑三组时间片参数,输出重定向到 results 目录;最后用 diff 和期望输出做比对,不一致会立刻提示。把这个脚本跑完只需要几秒,但它能证明“代码在提交当下是干净的”,而不是“某个时刻曾经跑通过”。
6.2 答辩前两个最小的改动,加起来不到 20 行
第一个改动是把调试日志用宏控制起来,平时开着方便排查,答辩时关掉只看最终结论。比如定义一个#define DEBUG_LOG 1,所有 printf 包在条件下,关掉日志后输出更整洁。第二个改动是把时间片大小、内存帧数等参数改成从文件读,而不是写死在代码里。这样老师问“换个参数会怎样”时,你能立刻现场改参数重新跑,而不是说“这个得改代码重编译”。
这两个改动本身没有任何技术难度,但它们释放的信号是“这个项目是经过工程化处理的”,和只会 Ctrl+F5 跑通一次的同学完全区分开。从那以后我每次交操作系统课程设计之前,都强制自己走一遍编译、跑测、对比结果、重新截图的流程,发现一次不一致就立刻排查,不放过任何一个“它刚刚明明能跑”的瞬间。希望帮到你。
本文还有配套的精品资源,点击获取