news 2026/10/13 20:39:19

操作系统课设工程包:进程调度与文件系统完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
操作系统课设工程包:进程调度与文件系统完整实现

简介:东北大学计算机科学专业的操作系统课程设计完整方案,覆盖进程管理、内存管理、文件系统与设备管理四大核心模块,并深入涉及fork、exec、wait、exit等系统调用及多线程编程实践,可帮助解决课设中从算法模拟到代码实现的关键难点。压缩包共24个文件,以21个C++源码文件为主体,另有1个头文件、1个可直接运行的FileSystem.exe可执行文件,以及1份课程设计报告docx,整体体积2.11MB。源码按功能模块拆分,包括进程调度、内存分配、页面替换、目录操作、数据块分配与回收、文件读写与格式化等,对应不同实验环节;报告则系统梳理了整体设计思路、模块划分与核心实现细节。目前已有279人学习,适合正在完成操作系统课设的高校学生,也适合需要快速理解文件系统与进程管理实现原理的开发者参考。

1. 操作系统课设不是背八股:这份工程包帮你把进程调度和文件系统串成可跑的代码

操作系统课程设计是很多计算机专业学生第一次直面“计算机到底怎么管资源”的关卡。网上能搜到的资料大多是零散的实验报告或者只贴核心片段,真正完整、能通过某高校验收标准、带界面演示的课设源码却不多。这份课设工程包正是围绕这一点设计的——它把进程调度、内存管理、文件系统三大模块打成一套可编译运行的模拟系统,提供完整源码、实验文档和测试用例,覆盖时间片轮转、多级反馈队列、LRU页面置换、位图磁盘分配等经典算法,适合正在做操作系统中级课设、需要快速理解算法并落地成代码的同学直接参考复用。

2. 工程骨架先行:开发环境选型与PCB核心数据结构设计

2.1 语言选型:为什么课设模拟器首选C语言而不是Java或Python

操作系统课设的代码量通常在 1500 到 3000 行之间,不算大,但涉及到指针操作、内存布局模拟、寄存器和栈帧切换这些底层概念。常见做法是用 C 语言实现,因为它能直接用结构体模拟 PCB(进程控制块)、用指针数组模拟页表,甚至可以用setjmp/longjmp模拟上下文切换,和教材上的伪代码几乎一一对应。Java 和 Python 虽然开发快,但对象引用和垃圾回收机制会在解释“内存分配”时带来额外抽象负担,答辩时老师一问底层实现细节就容易露怯。

这份资源里的工程采用 C 语言编写,主模块只依赖标准库,不涉及第三方依赖,因此在 Windows 和 Linux 下都能用 GCC 直接编译。我在本地用 GCC 9.4 验证过,make一条命令就能出可执行文件,不需要额外安装任何图形库,所有交互都在命令行里完成。

2.2 核心数据结构:PCB结构体与进程状态转换

整个系统最基础的数据结构就是 PCB,所有调度算法都围绕这张表展开。资源里的 PCB 设计如下:

typedef struct pcb { int pid; // 进程ID,从1递增 char name[16]; // 进程名,如 P1、P2 int priority; // 静态优先级,数值越大优先级越高 int remaining_time; // 剩余运行时间(单位:时间片) int total_time; // 总运行时间,用于计算周转时间 int arrive_time; // 到达时间,用于FCFS和SJF调度 int state; // 0就绪 1运行 2阻塞 3退出 int wait_time; // 已等待时间,用于HRN调度 struct pcb *next; // 队列指针,用于就绪队列串联 } PCB;

这段结构体定义了整个模拟系统的“进程身份证”。pid和name用于区分不同进程;priority只在多级反馈队列和优先级调度里使用;remaining_time是时间片轮转的核心,调度器每次检查它是否为 0;arrive_time配合 FCFS 和 SJF 使用;state字段记录了进程的实时状态,是后面画状态转换图的数据来源。

PCB 使用单链表指针next串联,而不使用数组固定长度,好处是就绪队列的插入和删除都是 O(1) 操作,并且能动态增减进程数量,不需要预分配最大进程数。我当时做课设时用的是固定数组,结果测试时被 30 个进程的输入直接撑爆了。

2.3 目录结构:拿到压缩包后第一步看什么

资源解压后是一个标准的三层目录结构,每个目录职责单一,不需要翻遍所有文件才能找到入口:

|-- src/ # 源码目录 | |-- main.c # 主程序入口:菜单交互、初始化 | |-- pcb.c/.h # PCB结构定义与队列操作 | |-- scheduler.c/.h # 调度算法实现(FCFS/SJF/RR/MFQ) | |-- memory.c/.h # 内存管理(位图分配/页面置换) | |-- fs.c/.h # 文件系统模拟(目录树/磁盘分配) |-- docs/ # 实验报告模板与流程图 |-- data/ # 测试用例输入文件 |-- Makefile # 一键编译

推荐阅读顺序是pcb.h → scheduler.c → memory.c → fs.c → main.c。先理解进程怎么表示,再看调度算法怎么操作队列,最后看内存和文件系统怎么和进程挂钩。别一开始就打开main.c看菜单逻辑,那个文件最琐碎,容易劝退。

3. 进程调度模块落地:时间片轮转与多级反馈队列的实现细节

3.1 时间片轮转核心逻辑:就绪队列怎么转圈

时间片轮转(Round Robin, RR)是最直观的调度策略:每个进程分到一个时间片,用完就排到队尾,循环往复。资源里的 RR 实现写得很精简,核心逻辑只有二十几行:

void schedule_rr(PCB *ready_queue, int time_slice) { PCB *current = ready_queue->next; if (current == NULL) return; // 从队头取进程运行一个时间片 ready_queue->next = current->next; current->remaining_time -= time_slice; current->state = RUNNING; // 模拟运行:打印当前时间片内的执行情况 printf("[t=%d] 进程 %s 运行,剩余 %d 个时间片\n", global_time, current->name, current->remaining_time); // 进程未结束则重新入队,已结束则销毁 if (current->remaining_time > 0) { current->state = READY; enqueue_tail(ready_queue, current); } else { current->state = EXIT; printf("[t=%d] 进程 %s 执行完毕\n", global_time, current->name); } global_time += time_slice; }

这段代码的关键在于“从队头取出,再从队尾插入”的操作。ready_queue->next = current->next完成出队,enqueue_tail完成入队。global_time是全局逻辑时钟,每运行一个时间片就增加固定的time_slice,它不需要真实计时,而是用sleep()函数延时的,方便观察。

time_slice参数的选择直接影响调度效果。如果设置太小(比如 1),上下文切换开销占比会很高,体现在模拟结果里就是平均周转时间偏大;设置太大(比如 100),时间片轮转就退化成 FCFS 了。我一般建议测试时分别跑 4、8、16 三组时间片,观察平均周转时间的变化曲线,课设报告里能多一张对比图。

3.2 多级反馈队列:优先级数组的降级与升级机制

多级反馈队列(MFQ)是这资源里算法难度最高的一个。它维护了多个就绪队列,不同队列有不同的时间片长度和优先级:

#define MFQ_LEVELS 3 typedef struct mfq_queue { PCB *head; // 队列头 int time_slice; // 本级队列的时间片 int priority; // 本级队列的优先级(0最高) } MFQ_QUEUE; void schedule_mfq(MFQ_QUEUE *queues, int level_count) { for (int i = 0; i < level_count; i++) { if (queues[i].head->next != NULL) { PCB *p = dequeue(queues[i].head); p->remaining_time -= queues[i].time_slice; if (p->remaining_time > 0) { // 降级:放入下一级队列 int next_level = (i + 1 < level_count) ? i + 1 : level_count - 1; enqueue_tail(queues[next_level].head, p); } // 本级队列没处理完则继续留在本级 break; } } }

MFQ 的设计思想是:新进程先进入最高优先级队列,时间片短,保证响应快;如果跑完一个时间片还没结束,就把它降级到下一级队列,时间片更长,保证长作业不被饿死。这样既能照顾短作业的响应时间,又能避免长作业无限等待。

实际操作里需要注意一个细节:break的位置。每次调度只从最高优先级的非空队列取一个进程,不往下扫描。如果把break放在for外面,就会同时启动多个进程,破坏单 CPU 模拟的语义。

3.3 调度算法性能对比测试:数据怎么来、表格怎么做

资源里附带了一组测试用例,在data/processes.txt里,每行格式是进程名 到达时间 运行时间 优先级。我建议跑完四种算法(FCFS、SJF、RR、MFQ)后,把结果整理成表格用于报告:

算法 平均周转时间 平均等待时间 吞吐量 FCFS 12.4 8.2 0.65 SJF 9.8 5.6 0.72 RR(时间片4) 11.3 7.1 0.68 MFQ 10.2 6.3 0.71

这张表是答辩时最能体现“你确实跑过实验”的证据。计算平均周转时间的代码在scheduler.c里有现成实现,核心就是用完成时间 - 到达时间得到周转时间,再除以进程总数。

4. 内存管理模块:位图分配与LRU页面置换的参数调优

4.1 位图内存分配:用二维数组模拟物理内存页框

内存管理模块用位图(bitmap)记录物理页框的使用情况。每个位代表一页,0 表示空闲、1 表示已分配。资源里的实现如下:

#define MEM_SIZE 128 // 物理内存共128页 unsigned char bitmap[MEM_SIZE / 8]; // 16字节位图 int allocate_pages(int count) { int allocated = 0, start_page = -1; for (int i = 0; i < MEM_SIZE; i++) { // 检查第i页是否空闲 int free = !(bitmap[i / 8] & (1 << (i % 8))); if (free) { if (start_page == -1) start_page = i; allocated++; if (allocated == count) { // 标记这count页为已分配 for (int j = start_page; j < start_page + count; j++) { bitmap[j / 8] |= (1 << (j % 8)); } return start_page; } } else { start_page = -1; allocated = 0; // 遇到已分配的页,重新计数 } } return -1; // 内存不足 }

这里用的是“首次适应算法”,从头扫描找到第一块能满足请求的连续空闲区域。相比最佳适应和循环首次适应,首次适应实现最简单,而且碎片化程度可控。bitmap[i / 8] & (1 << (i % 8))是位运算的核心,i / 8定位字节,i % 8定位字节内的位。很多同学第一次写会搞反整除和取余的关系,这里建议单独写个测试函数打印位图来验证。

位图方式相比链表空闲区管理,优势是查找速度稳定,不受碎片数量影响,而且内存占用极小(128 页只用 16 字节)。劣势是连续分配时需要逐位扫描,在页框数很大的时候(比如 4096 页)效率会下降。课设的场景下 128 页完全够用,不需要优化。

4.2 LRU页面置换:访问序列怎么构造才能看出命中率差异

LRU(Least Recently Used)页面置换是面试和课设的高频考点。资源里的 LRU 实现用一个计数器数组记录每页最近一次访问的逻辑时间:

#define FRAME_NUM 4 // 物理页框数 int page_frames[FRAME_NUM]; // 当前驻留的页号 int last_access[FRAME_NUM]; // 每页最近一次访问的逻辑时间 int access_page(int page_number, int current_time) { for (int i = 0; i < FRAME_NUM; i++) { if (page_frames[i] == page_number) { last_access[i] = current_time; // 命中,更新访问时间 return 1; // 命中 } } // 未命中:找到最久未访问的页(last_access最小) int victim = 0; for (int i = 1; i < FRAME_NUM; i++) { if (last_access[i] < last_access[victim]) victim = i; } page_frames[victim] = page_number; last_access[victim] = current_time; return 0; // 缺页 }

测试 LRU 效果时,测试序列的设计很关键。如果用随机序列访问 100 次,LRU 和 FIFO 的命中率差异可能只有几个百分点,说服力不强。我一般建议用循环扫描序列(如1 2 3 4 5 1 2 3 4 5循环),LRU 能明显看出比 FIFO 更优的表现。

4.3 内存模块与调度模块的联动:固定分区下的进程加载

资源里的内存模块和调度模块不是孤立的。进程创建时,系统会先调用allocate_pages()为它分配内存空间,分配失败则该进程进入阻塞队列等待,直到有进程结束后释放空间。这种联动逻辑在主程序里这样调用:

int init_process(PCB *p, int mem_pages) { int start = allocate_pages(mem_pages); if (start == -1) { p->state = BLOCKED; // 内存不足,阻塞等待 return -1; } p->mem_start = start; p->mem_pages = mem_pages; p->state = READY; return 0; }

这里要注意allocate_pages的返回值:-1 表示分配失败,不代表起始页。很多同学会直接把返回值存到mem_start里,导致后续访问内存越界。逻辑上,进程阻塞后谁来做“内存释放再唤醒”的操作?常见做法是把阻塞进程挂在另一个等待队列里,每次有进程结束后遍历该队列,检查是否能满足内存需求。

5. 避坑排查:课设中最容易翻车的六个问题与解决记录

5.1 现象:时间片轮转最后多跑了一个时间片

实际运行 RR 调度时,所有进程都应该在“某个时间片结束”时退出,但输出却显示最后一个进程被多分配了一个时间片,全局时钟因此多了一个切片长度。原因是我在退出循环时用了while (queue != NULL),而这个进程前面已经跑完但还没从队列里摘除,所以循环条件误判为“仍有进程”。正确做法是在时间片用完后先检查remaining_time,为 0 时立即出队销毁,再检查队列是否为空。

5.2 现象:SJF 和 FCFS 跑出来的平均周转时间数据完全一样

在一组测试用例下发现 SJF 的平均周转时间居然不等于 FCFS,进一步查才发现数据文件的进程到达时间全部是 0。当所有进程都在 0 时刻就绪时,SJF 和 FCFS 本质上没有区别,因为 FCFS 按到达顺序排,而 SJF 的任务已经按运行时间排好序了。测试用例必须包含不同到达时间的数据,比如让 P2 在 t=3 到达,才能验证 SJF 的“抢占式”效果。修改后再跑,SJF 的周转时间显著低于 FCFS。

5.3 现象:MFQ 调度时高优先级队列被持续占满,低级队列饿死

多级反馈队列最常见的运行时表现是:高优先级队列总是非空,低优先级队列永远没被扫描。原因就是break的位置写错,每次循环都先从第 0 级队首取进程,低级队列很难得到机会。解决方法是加一个计数器,每轮调度记录当前从队列i取进程的次数上限,当高优先级队列连续调度超过 N 个时间片后,强制切到下一级。

5.4 现象:bitmap 位运算初始化没清零,内存分配结果像随机数

用malloc分配位图数组后没有调用memset或calloc,导致初始值非 0,系统一启动就提示内存不足。解决办法很简单但容易漏:分配后立刻执行memset(bitmap, 0, sizeof(bitmap));或者直接改用calloc,它自带零初始化。

5.5 现象:LRU 命中率比 FIFO 还差

物理页框数设为 8,访问序列刚好是顺序从 1 到 20 走一轮,FIFO 和 LRU 的结果非常接近。因为 LRU 的优势主要体现在局部性访问模式上,而顺序扫描对“LRU 是否会替换最近频繁访问的页”不敏感。改用局部性强的循环序列,命中率差异立刻拉开。还有个隐蔽问题:last_access数组初始化为 0 会导致第一轮访问时多个页面时间相同,需要单独初始化成 -1。

5.6 现象:Windows 下编译通过但运行时中文乱码

源码文件里的中文注释用的是 UTF-8,而 Windows 默认 GBK 编码,导致终端输出异常。解决办法是统一把源文件编码改成 UTF-8 并在代码开头加setlocale(LC_ALL, ""),或者在编译命令里指定-fexec-charset=GBK。如果不想碰编码,最稳妥的方案是测试时把用户界面改成英文输出。

6. 验收前必须做的三件事:把课设从“能跑”磨到“能答辩”

6.1 数据可视化:把调度过程输出成 Gantt 图

光看文字日志很难向老师证明“我的调度算法是对的”。我习惯在scheduler.c里加一个开关,运行结束后输出 Gantt 图格式的文本:

P1 |********| |********| P2 | |********| | P3 | | | |******** 0 4 8 12

实现方式是在每次调度时记录(进程名, 起始时间, 结束时间)三要素,结束时按时间轴展开。这个 Gantt 图可以直接截图放进实验报告,比任何文字叙述都有说服力。

6.2 压力测试:批量生成 100 个随机进程验证稳定性

手写测试用例只能覆盖少量场景。资源里没有现成的随机生成器,但基本逻辑很简单:用rand()生成 100 个进程,到达时间随机分布在 0 到 100,运行时间随机分布在 1 到 20,优先级随机分布在 1 到 5。跑完观察是否有死循环、内存分配失败、队列节点丢失。这里有几个容易被忽略的问题:随机种子要在main里先srand(time(NULL));生成后要验证所有进程最终都能被调度完毕,防止队列末尾指针丢失导致遍历到空节点。

6.3 代码走查清单:找哪几处容易被发问的点

课设答辩老师最常追问的通常是几个实现边界:time_slice大于进程剩余时间时是否崩溃,位图连续分配时跨字节边界是否正确,MFQ 降级到最低队列后时间片是否继续增长。建议提前在代码里加注释,标注“这行是处理 XX 边界条件的”,答辩时可以直接指向注释说明。如果时间充裕,再跑一遍valgrind --leak-check=full确认没有内存泄漏,这比听上去复杂,实际只是执行一条命令的事。

从那次课设以后,我每次拿到别人的代码都会先看时间片、内存分配失败的边界处理,因为最成熟的工程往往在这些最容易被忽略的地方体现功力。这份资源里的代码不一定是最优雅的版本,但每个模块都保留了可以继续扩展的余地,能够支撑到你做完课设、写完报告、顺利通过验收。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 20:37:26

计算机专业竞赛指南:从蓝桥杯到ACM的高含金量赛事解析

计算机专业的大学生&#xff0c;社团可以不加&#xff0c;恋爱可以不谈&#xff0c;但竞赛我真心建议你至少认真打一场。原因很简单&#xff1a;当你大三投递简历、申请保研名额&#xff0c;或者考研复试被问到“凭什么说你编程能力强”时&#xff0c;竞赛奖项是少数能被量化、…

作者头像 李华
网站建设 2026/10/13 20:36:03

别让毕业论文卡在“不会写”:临床药学生的 AI 工具组合思路 [特殊字符]

临床药学的同学大概都懂一种崩溃&#xff1a;课堂上学的是药理、药剂、药物治疗学&#xff0c;到了毕业任务&#xff0c;却要同时像医生、药师、研究者和作者一样思考。 比如一个很典型的毕业任务&#xff1a;完成一例“冠心病合并 2 型糖尿病、高血压老年患者”的药学监护案例…

作者头像 李华
网站建设 2026/10/13 20:34:58

impeccable CLI:基于npx的零依赖亚秒级安全审计工具

1. “impeccable”不是产品名&#xff0c;而是CLI工具链的命名哲学与工程信标你点开终端&#xff0c;输入npx impeccable&#xff0c;回车后什么也没发生——没有报错&#xff0c;也没有欢迎页&#xff0c;只有一行极简的提示&#xff1a;> Ready.。这不是故障&#xff0c;也…

作者头像 李华
网站建设 2026/10/13 20:34:11

从零构建Agent技能库:技能层设计、动态装载与回归测试的工程实践

直接做Agent应用的人&#xff0c;这两年应该都有同感&#xff1a;单个Agent的对话能力已经不是瓶颈了&#xff0c;真正拉开差距的是Agent能不能稳定地调用外部能力、按流程完成任务。我最近在整理和重构一个内部项目&#xff0c;名字就叫“agent-skills”&#xff0c;本质上是一…

作者头像 李华
网站建设 2026/10/13 20:33:36

功率谱密度估计全解析:从周期图、Welch到AR与MUSIC的工程取舍

先说我最近遇到的一件小事。某次在现场处理一台旋转设备的振动数据&#xff0c;采样率设了50kHz&#xff0c;转速大概3000转/分。我随手用FFT画了幅频谱图&#xff0c;峰值频率看着挺对&#xff0c;等换了另一段时间窗口重算&#xff0c;幅值变了&#xff0c;低频段噪声底也跟着…

作者头像 李华