简介:这份华中科技大学计算机学院操作系统课程设计报告,面向计算机相关专业学生及操作系统自学者,提供一份完整的课程设计参考范例。报告围绕Linux系统展开,涵盖Linux编程环境搭建、内核代码结构分析、添加系统调用、设备驱动程序开发、/proc文件系统解析等核心实验内容,并包含文件拷贝程序与GTK+图形界面并发进程展示等实践环节,适合作为课程设计模板与系统级编程入门参考。资源包内含1个doc文档,压缩包大小约1.22MB,结构完整、内容详实,便于直接查阅与借鉴。目前已有403人学习下载,读者可从中获取实验环境配置思路、内核编译流程、系统调用实现方法及驱动开发要点,同时了解Ubuntu下依赖管理与开发工具安装的常见问题处理方式,为操作系统课程实践与系统开发能力提升提供切实帮助。
1. 一份操作系统课设报告,为什么值得当成工程模板来拆
很多人看到“操作系统课程设计报告”这几个字,第一反应是学生作业,跟一线工程没多大关系。但如果你真带过校招新人,或者自己回头复盘过当年那份文档,会发现它其实是一份被严重低估的工程模板:它逼着你在没有现成框架兜底的情况下,把进程调度、内存管理、文件系统这些底层机制讲清楚、跑通、测出来。这恰恰是现在很多业务开发最缺的能力——大家习惯了调库,一旦遇到性能抖动、资源争抢、死锁排查,就只剩重启和加机器两招。
这份报告的核心价值不在“报告”两个字,而在它背后那条完整的落地链路:选题、环境搭建、模块拆分、编码实现、测试验证、数据记录、结论收敛。你把它当成一个最小可用的系统工程项目来读,就能看出哪些环节是真正卡人的,哪些参数是必须交代清楚的,哪些结论是拍脑袋写上去的。适合谁看?适合正在做课设但不想糊弄的学生,也适合工作两三年、想补一补底层系统思维的开发者。接下来我不讲空话,直接按“怎么选、怎么搭、怎么写、怎么测、怎么避坑”这条线,把一份能拿得出手的操作系统课设报告拆成可复现的步骤。
2. 选题与实验环境:先定边界,再谈实现
2.1 三个主流选题方向的实际工作量对比
操作系统课设的选题通常集中在进程调度、内存管理、文件系统、设备管理这几个方向。很多人一上来就选“实现一个完整文件系统”,结果两周过去还在纠结磁盘块分配。我的建议是先看工作量分布,再结合自己手头的时间选。
| 选题方向 | 核心模块 | 最小可交付 | 常见坑点 | 建议周期 |
|---|---|---|---|---|
| 进程调度模拟 | PCB 结构、就绪队列、调度算法 | 支持 3 种调度算法并输出甘特图 | 时间片边界、队列排序稳定性 | 1~2 周 |
| 内存管理模拟 | 页表、地址转换、置换算法 | 支持分页 + FIFO/LRU 置换 | 缺页率统计口径、页框分配 | 2~3 周 |
| 简单文件系统 | 超级块、inode、目录项、块分配 | 支持创建/读写/删除文件 | 空闲块管理、目录遍历效率 | 3~4 周 |
| 生产者消费者 | 信号量、互斥锁、缓冲区 | 多线程下无死锁、无丢失 | 条件变量误用、虚假唤醒 | 1 周 |
选进程调度的人最多,因为容易出可视化结果;选文件系统的人最少,因为调试成本高。如果你时间紧,优先选调度类,把算法对比做扎实,报告反而更有说服力。
2.2 用 C + Makefile 搭一个可复现的实验骨架
不管选哪个方向,环境一定要统一。我一般会要求用 C 语言加 Makefile,不依赖 IDE,这样换机器也能跑。下面是一个最小骨架,包含主程序、调度模块和测试入口。
// main.c #include <stdio.h> #include "sched.h" int main(int argc, char *argv[]) { // 从命令行读取调度算法类型,默认 FCFS const char *algo = (argc > 1) ? argv[1] : "fcfs"; printf("running scheduler: %s\n", algo); // 初始化进程集合,实际项目中应从配置文件读取 init_processes(); // 根据算法类型分发 if (strcmp(algo, "fcfs") == 0) { run_fcfs(); } else if (strcmp(algo, "sjf") == 0) { run_sjf(); } else if (strcmp(algo, "rr") == 0) { run_rr(4); // 时间片固定为 4 } else { fprintf(stderr, "unknown algo: %s\n", algo); return 1; } print_stats(); return 0; }# Makefile CC = gcc CFLAGS = -Wall -g -O0 OBJS = main.o sched.o sched: $(OBJS) $(CC) -o $@ $(OBJS) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f *.o sched这段代码的逻辑很直白:主程序只负责解析参数和分发,具体调度逻辑放在sched.c里。参数说明上,-Wall -g -O0是为了调试期能看到所有警告和符号,等最终跑数据时再换成-O2。run_rr(4)里的 4 是时间片,这个值后面做对比实验时要能改,所以不要写死在函数内部。
2.3 进程数据结构怎么定义才不返工
PCB 的定义决定了后面所有代码的写法。我见过太多人一开始只写pid和burst_time,做到一半发现要统计等待时间、周转时间,又回头改结构体,牵一发动全身。下面这个版本是我踩过坑之后固定下来的。
// sched.h #ifndef SCHED_H #define SCHED_H #define MAX_PROC 32 typedef struct { int pid; // 进程号 int arrive_time; // 到达时间 int burst_time; // 需要运行的总时间 int remaining_time; // 剩余运行时间,RR 调度用 int start_time; // 首次开始运行时间 int finish_time; // 完成时间 int wait_time; // 等待时间 = 周转 - 运行 int turnaround; // 周转时间 = 完成 - 到达 } PCB; void init_processes(void); void run_fcfs(void); void run_sjf(void); void run_rr(int quantum); void print_stats(void); #endif关键字段是remaining_time和start_time。前者让 RR 调度不用反复计算已运行时间,后者用来判断进程是否已经启动过。wait_time和turnaround不直接存原始数据,而是最后统一算,避免中间过程写乱。参数上MAX_PROC取 32 是够用的,课设数据量一般不超过 10 个进程,留点余量方便做压力测试。
3. 核心模块实现:把调度算法写成可对比的实验
3.1 FCFS 与 SJF 的代码差异其实只有一行
先看 FCFS 的实现,它按到达时间排序后依次执行,逻辑最简单。
// sched.c 片段 #include <stdio.h> #include <string.h> #include "sched.h" static PCB procs[MAX_PROC]; static int n; void init_processes(void) { // 模拟 5 个进程,到达时间和运行时间固定,方便复现 PCB data[] = { {1, 0, 7, 7, 0, 0, 0, 0}, {2, 2, 4, 4, 0, 0, 0, 0}, {3, 4, 1, 1, 0, 0, 0, 0}, {4, 5, 4, 4, 0, 0, 0, 0}, {5, 6, 3, 3, 0, 0, 0, 0}, }; n = sizeof(data) / sizeof(data[0]); memcpy(procs, data, sizeof(data)); } void run_fcfs(void) { int time = 0; for (int i = 0; i < n; i++) { // 如果当前时间小于到达时间,CPU 空转 if (time < procs[i].arrive_time) { time = procs[i].arrive_time; } procs[i].start_time = time; time += procs[i].burst_time; procs[i].finish_time = time; procs[i].turnaround = procs[i].finish_time - procs[i].arrive_time; procs[i].wait_time = procs[i].turnaround - procs[i].burst_time; } }SJF 的区别只在选择下一个进程时,从“按到达顺序”改成“在已到达的进程里选 burst_time 最小的”。如果你把进程按到达时间排好,SJF 只需要在循环里加一个查找最小值的逻辑。很多人把这两个算法写成完全独立的两套代码,后期改 bug 要改两遍,不划算。
3.2 RR 调度的时间片参数怎么定
RR 的核心是时间片quantum。设得太小,上下文切换开销占比高;设得太大,退化成 FCFS。课设里一般取 1 到 4 之间做对比。下面是一个可运行的 RR 实现。
void run_rr(int quantum) { int time = 0; int completed = 0; int queue[MAX_PROC]; int front = 0, rear = 0; int in_queue[MAX_PROC] = {0}; // 先把到达时间为 0 的进程入队 for (int i = 0; i < n; i++) { if (procs[i].arrive_time == 0) { queue[rear++] = i; in_queue[i] = 1; } } while (completed < n) { if (front == rear) { // 队列空,时间推进到下一个到达点 time++; for (int i = 0; i < n; i++) { if (!in_queue[i] && procs[i].arrive_time <= time) { queue[rear++] = i; in_queue[i] = 1; } } continue; } int idx = queue[front++]; if (procs[idx].start_time == 0 && procs[idx].remaining_time == procs[idx].burst_time) { procs[idx].start_time = time; } int run = (procs[idx].remaining_time < quantum) ? procs[idx].remaining_time : quantum; time += run; procs[idx].remaining_time -= run; // 新到达的进程入队 for (int i = 0; i < n; i++) { if (!in_queue[i] && procs[i].arrive_time <= time) { queue[rear++] = i; in_queue[i] = 1; } } if (procs[idx].remaining_time == 0) { procs[idx].finish_time = time; procs[idx].turnaround = time - procs[idx].arrive_time; procs[idx].wait_time = procs[idx].turnaround - procs[idx].burst_time; completed++; } else { queue[rear++] = idx; // 没跑完,重新入队 } } }这里有几个参数要盯住:quantum直接决定切换频率;in_queue数组防止同一个进程重复入队;start_time只在第一次运行时记录。跑完之后用print_stats输出平均周转时间和平均等待时间,这两个指标是报告里必须有的对比数据。
3.3 用脚本批量跑对比实验并生成表格
手工改参数跑多次很容易漏记录。我一般写一个 shell 脚本,把不同算法和时间片组合跑一遍,输出到 CSV。
#!/bin/bash # run_experiments.sh echo "algo,quantum,avg_turnaround,avg_wait" > result.csv for algo in fcfs sjf rr; do if [ "$algo" = "rr" ]; then for q in 1 2 4 8; do output=$(./sched rr $q) # 假设程序输出格式为 avg_turnaround=xx avg_wait=yy ta=$(echo "$output" | grep -o 'avg_turnaround=[0-9.]*' | cut -d= -f2) wt=$(echo "$output" | grep -o 'avg_wait=[0-9.]*' | cut -d= -f2) echo "$algo,$q,$ta,$wt" >> result.csv done else output=$(./sched $algo) ta=$(echo "$output" | grep -o 'avg_turnaround=[0-9.]*' | cut -d= -f2) wt=$(echo "$output" | grep -o 'avg_wait=[0-9.]*' | cut -d= -f2) echo "$algo,0,$ta,$wt" >> result.csv fi done这个脚本的关键是让程序输出可解析的格式,不要用中文描述。grep -o提取数值,cut去掉前缀。跑完直接拿result.csv画图或贴表,报告里的数据部分就不用临时编了。
4. 报告撰写与数据呈现:让结论站得住脚
4.1 实验数据表的字段设计
报告里最容易被挑毛病的地方是数据表。字段太少显得单薄,字段太多又没人看。我建议固定这几列:进程号、到达时间、运行时间、开始时间、完成时间、周转时间、等待时间。下面是一个填写示例。
| 进程 | 到达 | 运行 | 开始 | 完成 | 周转 | 等待 |
|---|---|---|---|---|---|---|
| P1 | 0 | 7 | 0 | 7 | 7 | 0 |
| P2 | 2 | 4 | 7 | 11 | 9 | 5 |
| P3 | 4 | 1 | 11 | 12 | 8 | 7 |
| P4 | 5 | 4 | 12 | 16 | 11 | 7 |
| P5 | 6 | 3 | 16 | 19 | 13 | 10 |
平均周转时间 = (7+9+8+11+13)/5 = 9.6,平均等待时间 = (0+5+7+7+10)/5 = 5.8。这两个数要跟算法理论值对得上,如果差太多,先检查是不是把到达时间算错了。
4.2 甘特图用文本画比截图更稳
很多人喜欢截图放报告里,但截图在文档里容易糊,而且改一个参数就要重新截。用文本画甘特图,改起来快,复制到任何地方都不失真。
FCFS: | P1 | P2 | P3 | P4 | P5 | 0 7 11 12 16 19 RR (q=2): | P1 | P2 | P1 | P3 | P4 | P1 | P5 | P2 | P4 | P5 | P1 | 0 2 4 6 7 9 11 13 15 17 19 20画的时候注意每个格子的宽度要跟时间成比例,不然视觉上会误导。RR 的图里同一个进程会出现多次,这是正常的,不要为了好看合并。
4.3 结论部分只写三件事
结论不要复述过程,只写三件事:哪个算法在当前负载下平均等待时间最短;时间片从 1 变到 8 时 RR 的切换次数和平均等待时间怎么变;你的实现跟理论值的偏差在哪里。比如“RR 在时间片为 4 时平均等待时间比时间片为 1 时低 12%,但切换次数减少一半”,这种结论才有信息量。
5. 避坑与排查:那些让课设翻车的细节
5.1 进程按到达时间排序后忘记处理相同到达时间
现象:两个进程到达时间都是 0,跑出来的顺序跟预期不一致。原因:排序算法不稳定,或者比较函数只比了到达时间。解决:在比较函数里加第二关键字,比如按 pid 升序,保证结果可复现。
5.2 RR 调度里新到达进程入队时机错误
现象:某个进程明明在时间片结束前到达,却被排到了很后面。原因:入队检查放在了时间推进之前。解决:每次time变化后立刻扫描未入队进程,条件用arrive_time <= time,不要用< time。
5.3 平均周转时间用整数除法
现象:算出来平均周转时间是 9,实际应该是 9.6。原因:int除法截断。解决:累加时用double,或者最后乘 1.0 再除。这个坑很小,但报告里数据对不上就很尴尬。
5.4 忘记初始化 remaining_time
现象:RR 跑第一轮就把进程标记为完成。原因:remaining_time默认是 0,没有从burst_time拷贝。解决:在init_processes里显式赋值,或者用memcpy后统一初始化。
5.5 测试数据只有一组
现象:换一组进程数据,结果完全不合理。原因:代码里写死了进程数量或时间片。解决:把进程数据抽到独立数组或配置文件,主逻辑只依赖n和procs,不依赖具体数值。
6. 从课设到工程:把报告里的验证习惯带走
课设做完,报告交完,大多数人就把代码扔了。但真正有用的东西是那套验证习惯:先定边界,再写最小可运行版本,然后用脚本批量跑对比,最后用数据说话。这套习惯放到工作里一样成立。比如你优化一个接口的响应时间,不是改完就说“快了”,而是固定输入、跑多组参数、记录 P99、对比前后差异。下面这个表格是我现在做性能对比时常用的记录格式,跟当年课设报告里的字段几乎一样。
| 场景 | 并发数 | 平均延迟 | P99 延迟 | 错误率 | 备注 |
|---|---|---|---|---|---|
| 优化前 | 50 | 120ms | 450ms | 0.2% | 基线 |
| 优化后 | 50 | 85ms | 210ms | 0.1% | 缓存命中率提升 |
| 优化后 | 200 | 160ms | 520ms | 0.5% | 高并发下退化 |
还有一个习惯是给每个实验留“后悔药”:参数不要写死在代码里,用命令行或配置文件传入。当年我做 RR 调度时把时间片写成宏,后来想对比 1 和 8 的差异,只能改代码重编译,浪费了不少时间。现在不管写什么,只要是可能变的数值,一律走参数。最后说一个我自己的教训:报告里的每一个结论,都要能在代码里找到对应的输出行。如果找不到,那个结论就是拍脑袋写的,答辩时一问就露馅。希望帮到你。
本文还有配套的精品资源,点击获取