资讯详情

操作系统课设六大实验工程化实现与二次开发指南

📅 2026/10/29 10:32:27 | 华诺云谱 👁 阅读
操作系统课设六大实验工程化实现与二次开发指南
简介面向计算机科学专业学生与操作系统课程学习者这份东北大学操作系统课设资料围绕进程管理、内存管理、文件系统与设备管理等核心模块展开适合用于课程设计参考、实验复现与原理梳理。压缩包共24个文件以21个cpp源码为主体配有头文件、可执行程序和docx课程设计报告整体约2.11MB目录按文件系统功能拆分便于对照阅读实现思路。现有280人学习说明其具备一定的参考价值。读者可从中了解文件创建、读写、删除、目录维护、数据块分配与回收等功能的代码组织方式也可借此理解调度算法、页面替换、中断处理与I/O调度等知识点在代码中的落点并结合报告梳理设计流程、调试要点与系统调用实践为完成同类操作系统课设提供较完整的功能实现框架和排错思路。1. 一份能跑通六个实验的操作系统课设比想象中更能说明问题期末季的课设答辩现场画面往往差不多同学把程序跑起来屏幕上打出一排进程的甘特图导师追问一句“时间片改成 2 会怎样”于是现场改常量、重新编译、再跑一遍。这份计科方向的操作系统课程设计就是按这个节奏组织的——进程调度、生产者消费者同步、银行家算法、页面置换、磁盘调度每个实验编成一个独立可执行文件共用同一套 PCB 结构和输出函数。它不适合拿来当教学视频看适合的是“明天要交、后天要答辩”的开发者代码结构直白输入靠命令行和数据文件输出能直接截图进报告修改算法只需要替换一个比较函数。下面从目录结构一路拆到二次开发讲清楚怎么在本机跑通以及哪几个地方最容易翻车。2. 工程骨架拆解四层目录如何装下六个独立实验2.1 目录布局与 Makefile一个实验一个二进制六个实验各写一份 PCB、各写一份打印函数的做法在第一次改字段时就会付出代价——加一个remain字段要同步改六处漏一处就是编译错误。合格的做法是先立公共层再让各实验只保留自己的算法逻辑目录大致如下。os-coursedesign/ ├── Makefile ├── include/ │ ├── pcb.h # PCB 结构、状态枚举、调度函数声明 │ ├── mem.h # 页表项、物理块、银行家算法用的矩阵上限 │ └── util.h # 打印、输入解析、计时工具 ├── src/ │ ├── common/ │ │ ├── util.c # print_gantt / print_metrics / load_pcb │ │ └── queue.c # 通用链表队列就绪队列和阻塞队列共用 │ ├── sched/ │ │ ├── main.c # 菜单入口把四个算法串起来 │ │ ├── fcfs.c │ │ ├── sjf.c │ │ ├── prio.c │ │ └── rr.c │ ├── sync/producer_consumer.c │ ├── banker/banker.c │ ├── page/page_replace.c │ └── disk/disk_sched.c ├── data/ │ ├── sched1.txt # 进程到达时间、运行时间、优先级 │ └── page1.txt # 页面引用串 └── bin/ # make 产物不提交到版本库include放声明、src放实现、data放测试数据、bin放产物这个划分不是为了好看是为了让每次编译只重新编译改动的那个.c。Makefile 里把公共源文件抽成变量是避免漏链接的关键。CC : gcc CFLAGS : -Wall -Wextra -g -Iinclude -stdc11 COMMON : src/common/util.c src/common/queue.c # 一个实验一个可执行文件改哪个编哪个 all: bin/sched bin/sync bin/banker bin/page bin/disk bin/sched: $(COMMON) src/sched/main.c src/sched/fcfs.c \ src/sched/sjf.c src/sched/prio.c src/sched/rr.c mkdir -p bin $(CC) $(CFLAGS) $^ -o $ bin/banker: $(COMMON) src/banker/banker.c mkdir -p bin $(CC) $(CFLAGS) $^ -o $ clean: rm -rf bin$^是全部依赖文件列表$是目标名两个自动变量把“公共文件 私有文件”拼成一条编译命令不用手写一长串路径。-Iinclude让源码里可以直接写#include pcb.h而不必写相对路径-Wall -Wextra建议一直开着课设里最常见的空指针解引用和未使用参数编译器提前就指出来了。-g是给 gdb 留的调度结果算错的时候单步进去看now和remain比盯着屏幕猜快得多。提示-Wextra会把main(int argc, char **argv)里没用到的argv报成 warning课设报告里如果要求“零警告”加一行(void)argv;即可不要直接关掉这个选项。2.2 公共数据结构PCB 是所有实验的公共语言调度要用 PCB同步实验里的生产者消费者也要用 PCB 表示阻塞进程输出函数要按 PCB 的字段算周转时间。所以pcb.h是这套代码里最不能随便改的文件字段一次定全后面六个实验都省事。#ifndef PCB_H #define PCB_H typedef enum { READY, RUNNING, BLOCKED, DONE } pcb_state; typedef struct pcb { int pid; /* 进程号从 1 开始输出甘特图用 */ char name[16]; /* 显示名报告里比 pid 直观 */ int arrive; /* 到达时间 */ int burst; /* 需要运行的总时间算周转时间用 */ int remain; /* 剩余运行时间只有时间片轮转会改它 */ int prio; /* 优先级约定数值越小优先级越高 */ int start; /* 首次开始运行时刻-1 表示还没开始 */ int finish; /* 完成时刻 */ int wait; /* 已累计等待时间可用于优先级老化 */ pcb_state state; struct pcb *next; /* 挂到队列上时用 */ } PCB; #endif几个字段的取舍值得说明。burst和remain分开存是因为非抢占式调度直接now burst就完事而时间片轮转必须每次减掉一个时间片如果把remain减在burst上后面算周转时间就没基准了。start初始化为-1第一次被调度时才赋now这样算“等待时间 start - arrive”不会把还没跑的进程算进去。wait字段在基础版里其实用不上加它是为了做优先级老化——等待越久的进程优先级数值越小否则低优先级进程在纯优先级调度里会饿死答辩时经常被问这一句。2.3 输出层甘特图和指标表必须是两个独立函数六个实验的报告截图高度雷同一张横条的调度顺序图一张平均周转时间、平均带权周转时间的表。把这两件事抽成print_gantt和print_metrics放进util.c后面每个实验只要把自己的结果数组传进去。/* 打印文本甘特图seq 是执行顺序的 piddur 是每段持续时间 */ void print_gantt(const int *seq, const int *dur, int n) { printf(\n|); for (int i 0; i n; i) printf( P%-3d|, seq[i]); printf(\n0); int t 0; for (int i 0; i n; i) { t dur[i]; printf(%5d, t); /* 每个块的右边界就是累计时间 */ } printf(\n); } /* 计算并打印平均周转时间、平均带权周转时间 */ void print_metrics(PCB *ps, int n) { double sum_turn 0, sum_wturn 0; for (int i 0; i n; i) { int turn ps[i].finish - ps[i].arrive; /* 周转时间 */ sum_turn turn; sum_wturn (double)turn / ps[i].burst; /* 带权周转时间 */ } printf(平均周转时间 : %.2f\n, sum_turn / n); printf(平均带权周转时间 : %.2f\n, sum_wturn / n); }print_gantt用累计时间做右边界dur由调用方保证和执行顺序一一对应这样时间片轮转里同一个进程出现多次也能正确画出多段。print_metrics里带权周转时间要强制转成doubleturn / ps[i].burst如果两个都是int小数部分会被直接截掉平均下来数值明显偏小这个坑几乎每届都有人踩。指标函数只读finish、arrive、burst三个字段所以六个实验可以共用一份不用为每个算法单独写统计。3. 进程调度与同步四个算法共用一条主循环3.1 通用调度骨架先选进程再推进时间非抢占式的三个算法先来先服务、短作业优先、优先级在结构上完全一致差别只在“从已到达的进程里挑哪一个”。把挑选逻辑做成函数指针主循环只写一遍。typedef int (*cmp_fn)(const PCB *, const PCB *); /* 返回负值表示 a 更优先 */ void run_sched(PCB *ps, int n, cmp_fn cmp) { int now 0, done 0; int g_pid[MAX_PROC], g_dur[MAX_PROC], g 0; while (done n) { PCB *pick NULL; /* 1. 在已到达且未完成的进程里按 cmp 选最优的一个 */ for (int i 0; i n; i) { if (ps[i].arrive now ps[i].state ! DONE) { if (!pick || cmp(ps[i], pick) 0) pick ps[i]; } } if (!pick) { now; continue; } /* 当前时刻无进程就绪CPU 空转 */ /* 2. 非抢占式一口气跑到结束 */ if (ps[i].start 0) pick-start now; now pick-burst; pick-finish now; pick-state DONE; g_pid[g] pick-pid; g_dur[g] pick-burst; g; done; } print_gantt(g_pid, g_dur, g); print_metrics(ps, n); }这段骨架有三个参数要知道怎么改cmp决定算法MAX_PROC决定进程数上限课设一般 10 到 20 够用别用变长数组老版本 gcc 下会翻车now那行处理的是进程到达时间存在空档的情况——比如第一个进程 3 时刻才到now必须从 0 走到 3否则周转时间会算少。start 0判断保证只有首次运行时才记录开始时刻这一点在抢占式调度里尤其重要。3.2 四个算法只差一个比较函数/* 先来先服务到达早的优先到达相同则 pid 小的优先 */ int cmp_fcfs(const PCB *a, const PCB *b) { if (a-arrive ! b-arrive) return a-arrive - b-arrive; return a-pid - b-pid; } /* 短作业优先运行时间短的优先 */ int cmp_sjf(const PCB *a, const PCB *b) { return a-burst - b-burst; } /* 优先级调度数值小的优先加 wait 就变成老化版本 */ int cmp_prio(const PCB *a, const PCB *b) { return a-prio - b-prio; }三个函数都是一行减法返回负值就是“a 应该先跑”。这种写法比返回bool好在不会因为相等情况乱序——相等返回 0主循环里的cmp(...) 0就不替换选中的是数组里靠前的那个结果稳定可复现。做优先级老化时把cmp_prio改成return (a-prio - a-wait / 5) - (b-prio - b-wait / 5);每等 5 个时间单位优先级提升一级答辩演示时能直观看出饥饿被缓解。时间片轮转没法塞进这套骨架因为它会在一个进程没跑完时切走得单独写。/* 时间片轮转q 为时间片长度remain 初值等于 burst */ void run_rr(PCB *ps, int n, int q) { int now 0, done 0; while (done n) { int idle 1; for (int i 0; i n; i) { if (ps[i].arrive now || ps[i].state DONE) continue; idle 0; if (ps[i].start 0) ps[i].start now; int slice ps[i].remain q ? ps[i].remain : q; /* 最后一片可能不足 */ now slice; ps[i].remain - slice; if (ps[i].remain 0) { /* 正好跑完 */ ps[i].finish now; ps[i].state DONE; done; } } if (idle) now; /* 一轮下来没人可跑时间推进 */ } }slice取remain和q的较小值是为了让最后一片不必填满整个时间片否则甘特图的总长度会凭空多出一截周转时间也跟着变大。这个写法按数组下标顺序轮转而不是按到达顺序维护真正的就绪队列在“所有进程同时到达”的课设数据下结果一致但如果到达时间分散严格来说应该先把新到达的进程入队再出队差异在答辩追问时是会被抓住的。3.3 生产者消费者手搓一个精简信号量同步实验要求用 P、V 操作实现有界缓冲区的互斥与同步。Linux 上可以直接用sem_t但课设报告通常要求把 P、V 的逻辑写出来所以一般自己定义一个最小结构。typedef struct { int value; /* 大于 0 表示还有可用资源小于 0 的绝对值表示阻塞进程数 */ int blocked; /* 仅用于统计打印阻塞队列长度 */ } mysem; void P(mysem *s) /* 申请资源先减再判 */ { s-value--; if (s-value 0) s-blocked; /* 简化版只记数不真的挂起线程 */ } void V(mysem *s) /* 释放资源先加再判 */ { s-value; if (s-value 0) s-blocked--; /* 唤醒一个阻塞进程 */ }P里必须先减后判V里必须先加后判顺序反过来语义就错了如果先判再减两个进程可能同时判断“还有空位”然后一起进去互斥失效。这个细节是同步实验的核心考点写反了在单线程模拟里看不出来但报告里的伪代码会被逐行检查。#define N 5 int buf[N], in 0, out 0; mysem empty { N, 0 }; /* 空位数 */ mysem full { 0, 0 }; /* 已放产品数 */ mysem mutex { 1, 0 }; /* 缓冲区互斥锁 */ void producer(int item) { P(empty); /* 有空位才能放注意顺序先资源信号量再互斥量 */ P(mutex); buf[in] item; in (in 1) % N; V(mutex); V(full); } void consumer(int *item) { P(full); P(mutex); *item buf[out]; out (out 1) % N; V(mutex); V(empty); }P(empty)必须写在P(mutex)前面这是一个经典死锁点如果先拿互斥锁再等空位缓冲区满的时候生产者会抱着锁一直等消费者永远进不来整个系统卡死。把这条写进报告的“死锁分析”小节比单纯贴代码更能说明理解到位。4. 内存与存储银行家算法、页面置换、磁盘调度4.1 银行家算法安全性检查写成独立函数银行家算法分两段请求合法性检查请求是否超过声明最大值、是否超过当前可用以及安全性检查试分配后能否找到一个安全序列。把安全性检查拆成独立函数主流程只负责打印逻辑清楚得多。/* 返回 1 表示安全并把安全序列写进 safe_seq */ int is_safe(int n, int m, int avail[], int max[][MAX_RES], int alloc[][MAX_RES], int safe_seq[]) { int work[MAX_RES], finish[MAX_PROC] { 0 }; for (int j 0; j m; j) work[j] avail[j]; /* 工作向量 当前可用 */ 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 (max[i][j] - alloc[i][j] work[j]) { ok 0; break; } } if (ok) { found i; break; } } if (found 0) return 0; /* 找不到可满足的进程不安全 */ for (int j 0; j m; j) work[j] alloc[found][j]; /* 假设它跑完并归还资源 */ finish[found] 1; safe_seq[k] found; } return 1; }work数组不能用avail直接改试分配要能回滚所以必须复制一份。finish用数组而不是复用 PCB 的state是因为安全性检查可能在一次请求里被调用多次状态要能重置。MAX_RES和MAX_PROC建议在mem.h里定义成宏比如 10 和 20比写死成[][3]更安全——资源数改成 4 的时候不会越界。资源请求的入口函数要先把“请求 需求”和“请求 可用”这两种情况直接拒绝再进入试分配avail - request; alloc request; need - request;调一次is_safe安全就保留分配不安全就把三个数组减回去。回滚这段代码是报告里最容易写漏的部分。4.2 页面置换三种算法共用一套物理块数组FIFO、LRU、OPT 的差别只在“选哪一页淘汰”命中判断和缺页统计完全一样。用一个replace()函数按算法编号分派公共部分只写一次。/* algo: 0FIFO, 1LRU, 2OPT返回缺页次数 */ int page_replace(int *ref, int len, int frames, int algo, int *faults) { int blk[MAX_FRAME], load_t[MAX_FRAME]; /* 物理块内容与装入/访问时刻 */ int cnt 0, fidx 0; /* cnt 当前占用块数fidx 为 FIFO 指针 */ for (int i 0; i frames; i) { blk[i] -1; load_t[i] 0; } *faults 0; for (int i 0; i len; i) { int p ref[i], hit -1; for (int j 0; j cnt; j) if (blk[j] p) { hit j; break; } if (hit 0) { /* 命中只更新访问时刻 */ load_t[hit] i; continue; } (*faults); if (cnt frames) { /* 还有空块直接装入 */ blk[cnt] p; load_t[cnt] i; cnt; } else { int victim 0; if (algo 0) { /* FIFO轮转指针 */ victim fidx; fidx (fidx 1) % frames; } else if (algo 1) { /* LRU访问时刻最早的 */ for (int j 1; j frames; j) if (load_t[j] load_t[victim]) victim j; } else { /* OPT向后找最远才用的永不使用给 len */ int far -1; for (int j 0; j frames; j) { int k i 1; while (k len ref[k] ! blk[j]) k; if (k - i far) { far k - i; victim j; } } } blk[victim] p; load_t[victim] i; } } return *faults; }load_t一个数组兼两个用途LRU 里存最近一次访问的时刻FIFO 里其实用不到靠fidx指针OPT 里也用不到靠向后扫描。这样写省掉了一套额外结构代价是看代码的人要理解“同一个数组在三种算法下语义不同”报告里最好加一句注释说明。OPT 里k - i在“后面不再出现”时会等于len - i天然比任何真实距离都大正好当选淘汰对象不需要额外写哨兵。4.3 磁盘调度SSTF 与 SCAN 的磁头移动量磁盘调度实验的核心指标是“总寻道长度”算法本身不复杂难点在 SCAN 要判断移动方向。/* SSTF每次挑离当前磁头最近的请求 */ int sstf(int *req, int n, int head) { int used[MAX_REQ] { 0 }, total 0; for (int k 0; k n; k) { int best -1, bd 1 30; for (int i 0; i n; i) { if (used[i]) continue; int d abs(req[i] - head); if (d bd) { bd d; best i; } } total bd; head req[best]; used[best] 1; } return total; }bd初始化成一个足够大的数而不是req[0]是因为req[0]可能已经被访问过。SCAN 则要先把请求排序从head位置分成两段按当前方向先服务一侧到头再折返扫另一侧。折返点取最大磁道号还是 0取决于题目约定报告的“算法说明”里要写清楚否则算出来的总寻道长度和参考实现对不上。5. 跑起来与踩坑编译、数据文件和结果校验5.1 三分钟跑通从 clean 到看到甘特图拿到源码后别急着看代码先把程序跑起来心里有个“正常输出长什么样”的基准。# 1. 清掉可能残留的旧产物 make clean # 2. 全部编译出现 error 先解决再往下走 make # 3. 逐个实验跑一遍调度模块直接读数据文件 ./bin/sched # 4. 其他模块是菜单式交互跑起来后按提示输入 ./bin/banker ./bin/page ./bin/disk编译这一步如果报undefined reference九成是 Makefile 里某个目标漏了$(COMMON)如果报MAX_PROC undeclared是include路径没生效检查-Iinclude有没有写。运行调度模块前先打开data/sched1.txt看一眼格式通常是每行“进程号 到达时间 运行时间 优先级”四列改数据的时候保持列数和分隔符一致。输出验证有个简单办法手算一遍 FCFS。第一个进程 0 时刻到达、运行 3第二个 2 时刻到达、运行 6那么完成时间分别是 3 和 9周转时间是 3 和 7平均 5。程序算出来的平均周转时间如果对不上问题一定在now的推进或者start、finish的赋值上用 gdb 在while循环里打now就能定位。5.2 五条踩坑记录从现象倒推原因现象一make报undefined reference to print_gantt。原因很直接某个目标的依赖列表里只写了src/sched/xx.c忘了$(COMMON)链接阶段找不到util.c里的符号。解决是把公共源文件统一放进COMMON变量并拼到每个目标后面不要手写路径手写早晚漏。现象二时间片轮转算出来的平均周转时间比先来先服务还差。第一反应是代码写错了实际上是时间片设成了 8而所有进程的运行时间都不超过 6等于每个进程一片就跑完退化成了 FCFS 加大量空转。把q改成 2 或 3 再跑指标立刻符合预期。这类结论不要直接写进报告容易被反问“那时间片趋于无穷时会怎样”。现象三银行家算法输入 4 个资源类型就崩溃。数组声明成int max[][3]但输入里资源数填了 4写越界把栈上的其他变量踩了程序不一定当场崩可能只是安全序列算错属于典型的黑匣子问题。解决办法是把资源数做成变量传入同时在读入时校验m MAX_RES超了直接报错退出而不是让它悄悄越界。现象四OPT 的缺页率算出来比 LRU 还高。OPT 理论上是最优的出现这种结果一定是实现有问题最常见的是页面引用串用0结尾、循环条件写成ref[i] ! 0而页面号本身包含 0读到第一个 0 就提前结束。把长度单独传参别依赖哨兵值这是血泪经验。另一种情况是物理块数改过但MAX_FRAME没跟着改。现象五数据文件从 Windows 拷过来解析出来的到达时间全是 0。用fscanf(%d)读数字其实不受换行符影响但如果代码里混用了fgets再手工strtok行尾的\r会让atoi返回 0。解决方式是编译前跑一次dos2unix data/*.txt或者解析时把\r一起当分隔符处理。现象六附带甘特图在终端里对不齐。进程名里用了中文或者全角括号printf的宽度按字节算一个汉字占三字节但显示两列必然错位。输出层统一用 ASCII 的P1、P2中文留在报告正文里。6. 二次开发把菜单程序改成能批量评测的小框架这套代码最实用的改造方向不是加算法而是把“人工点菜单、肉眼对结果”变成“读文件、批量跑、输出结构化结果”。答辩现场被要求换三组数据重跑的时候这个改造能省下大量时间。第一步是给每个可执行文件加命令行参数把交互式输入变成可选。以调度模块为例用getopt支持-f指定数据文件、-a指定算法、-q指定时间片/* ./bin/sched -f data/sched1.txt -a rr -q 3 */ int opt, algo 0, quantum 2; const char *file data/sched1.txt; while ((opt getopt(argc, argv, f:a:q:)) ! -1) { switch (opt) { case f: file optarg; break; case a: algo parse_algo(optarg); break; /* fcfs/sjf/prio/rr */ case q: quantum atoi(optarg); break; default: usage(); return 1; } }第二步是把print_metrics的输出从printf改成写文件格式用 CSV一行一个算法一组参数字段是“算法, 时间片, 进程数, 平均周转时间, 平均带权周转时间, 缺页次数”。这样同一份数据可以循环调用不同参数结果直接丢进表格工具出对比图报告里的“算法性能对比”一节就不用一个个手抄了。第三步是补一组回归用例。做法很土但有效把当前正确输出存成expected/*.txt写个 shell 脚本跑一遍diff改代码之后先跑这个脚本。改造过程中最容易破坏的是“所有进程同时到达”和“存在空闲时间段”这两个边界各留一组数据改完立刻知道有没有踩坏。改造点改动位置收益命令行参数各模块main.c免去手工输入可脚本调用CSV 输出common/util.c结果可直接做对比表回归脚本根目录test.sh改算法后快速验证没跑偏参数校验load_pcb等入口越界输入直接报错不静默算错第四步如果想让报告更有说服力可以给调度模块加一个随机数据生成器给定进程数范围和到达时间分布生成 100 组数据每个算法跑一遍取平均。单个例子容易被质疑“你这组数据刚好对某个算法有利”跑一百组取均值就没这个问题了。生成器本身二十行代码比手工编数据快得多。注意加参数解析之后原来的菜单交互路径最好保留用一个-i开关切过去。答辩时导师往往还是习惯看交互式演示把菜单删了反而尴尬。这套代码我前后改过几版最大的教训是别在答辩前一晚动公共结构体。有次为了加一个“进程类型”字段改了pcb.h结果六个实验里有两个忘了重新编译演示时数据对不上现场找了十分钟才发现是旧的二进制在跑。从那以后我每次改完公共头文件都强制走一遍make clean make再用两组边界数据跑一遍确认输出没变最后才敢改下一个模块。希望这些能帮到你把课设跑通只是第一步把它改成自己能随手复用的工具才是这份代码后面真正用得上的地方。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑