资讯详情

操作系统实验实战指南:C语言实现调度算法与同步互斥机制

📅 2026/10/12 17:46:49 | 华诺云谱 👁 阅读
操作系统实验实战指南:C语言实现调度算法与同步互斥机制
简介面向高校操作系统课程学习的实验代码资料包主要针对计算机专业本科生及操作系统自学入门者。内容覆盖杭电操作系统实验中的多个经典模块包含多组实验代码与不同版本实现其中可见简单文件系统相关源码便于理解底层原理并对照实验要求进行调试与分析。压缩包内含28个文件以C源码为主12个.c、4个.h另有txt说明文档、makefile构建脚本及main入口文件资源总大小仅56KB轻便易取txt文档帮助梳理实验要点makefile简化编译流程按实验目录组织便于快速定位目标模块。目前已有244人学习浏览适合缺乏实验参考或想完善实验报告的学生。借助该资料读者可获得完整可运行的实验实现、清晰的目录结构和多种实验版本思路用于自查编码规范、复习关键考点或进行二次开发实验。1. 操作系统实验一门用 C 语言还债的课程先从这个标题说起。所谓“HDU操作系统实验”是某高校HDU操作系统课程里一届届学生传下来的实验资源包最终打包成 zip 分发。很多人以为这是“背背 PV 操作、看看页面置换就交差”的课真正打开那个 zip 才发现五个实验要交的可执行程序和实验报告底层全是 C 语言、Makefile、信号量和调度算法。我的看法是这套实验的及格线是“能跑出对比数据”优秀线是“能解释清楚为什么这个算法比那个算法快”。适合正在被操作系统实验折磨的在校生、想补系统底层知识的后端开发者也适合准备面试前拿真实算法练手的人。这篇不会带你逐行读某个人的源码而是沿着这套实验最常见的组织方式把环境准备、进程与调度、同步与死锁、踩坑排查、验证技巧一次讲透。2. 搭好实验工程为什么必须用 C 和 Makefile2.1 实验环境选型Linux GCC 的理由与边界先定环境。操作系统实验里要求的 fork、信号量、waitpid 这些接口在 Windows 的 C 运行时上要么不存在、要么行为差异很大所以各高校的验收机器普遍是某个 Linux 发行版配 GCC 和 make。我一般会在项目根目录放一份环境清单gcc 版本、make 版本、内核版本都写进去因为不同版本下栈大小、信号量默认行为会有细微差别记录到实验报告里老师追问运行环境时也少一轮来回。关键建议是千万别用 IDE 的图形按钮来编译。命令行 gcc Makefile 有三个不可替代的好处一是 make 按依赖关系只重编改过的文件调度器改一个函数就全盘重编会浪费大量时间二是提交的是一个工程目录Makefile 是验收时最熟悉的构建入口三是后面写自动对比脚本时直接调 make 就好收标准输出。实验环境边界要记住两点互斥锁和线程的编译选项、POSIX 信号量需要的链接库都要在 Makefile 里显式声明不能靠 IDE 悄悄替你补。2.2 工程目录与 Makefile一次编译全部实验我习惯的实验工程目录长这样ostl/ ├── Makefile ├── include/ │ ├── comm.h │ └── pcb.h ├── src/ │ ├── proc.c │ ├── sched.c │ ├── pv.c │ └── banker.c ├── data/ │ ├── sched_input.txt │ └── deadlock_case.txt └── bin/Makefile 需要覆盖三个使用场景单独编某个实验、全部编译、清理。下面这个写法在多数课程里都能直接抄注释里标了每个变量的用途CC gcc CFLAGS -Wall -Wextra -g -Iinclude LIB -lpthread -lrt TARGETS bin/proc bin/sched bin/pv bin/banker all: $(TARGETS) bin/proc: src/proc.c include/comm.h $(CC) $(CFLAGS) -o $ src/proc.c $(LIB) bin/sched: src/sched.c include/pcb.h include/comm.h $(CC) $(CFLAGS) -o $ src/sched.c $(LIB) bin/pv: src/pv.c include/comm.h $(CC) $(CFLAGS) -o $ src/pv.c $(LIB) bin/banker: src/banker.c include/comm.h $(CC) $(CFLAGS) -o $ src/banker.c $(LIB) clean: rm -f $(TARGETS) src/*.o这里的$指目标名本身-Iinclude让编译器去 include 目录找头文件-g是为了后面用 gdb 查段错误。-lpthread和-lrt是给线程和信号量用的不做 PV 实验时删掉也不影响编译但只要你用了sem_open这类实时信号量接口-lrt就必须留这是很多人编译报错后反复查才想起的点。每个实验单独写一条规则而不是写一个通配规则是因为依赖头文件不同写清楚依赖后改 comm.h 只触发相关文件重编spinner 和 scanf 界面那套花活不属于实验省下的等待时间非常可观。注意GCC 链接时库要放在引用它的目标文件之后。如果写成$(CC) $(CFLAGS) $(LIB) -o $ src/proc.c静态链接阶段会因顺序问题报 undefined reference这是一条必踩的链接坑。2.3 公共头文件与日志宏让多个实验的输出格式统一多个实验共用一个日志模块后面的对比脚本才能写。我在 include/comm.h 里放了一套最简单的日志宏诊断信息统一走 stderr#ifndef COMM_H #define COMM_H #include stdio.h #include stdlib.h #include string.h #define LOG(level, fmt, ...) \ fprintf(stderr, [%s] fmt \n, level, ##__VA_ARGS__) #define INFO(...) LOG(INFO, __VA_ARGS__) #define WARN(...) LOG(WARN, __VA_ARGS__) #define ERROR(...) LOG(ERROR, __VA_ARGS__) #endif日志走 stderr 而不是 stdout 是有意为之实验结果数据走 stdout 进管道诊断信息走 stderr 就不会混进去后面用 awk 统计结果时少一层过滤。##__VA_ARGS__是 GNU 扩展允许不传额外参数时自动去掉逗号。这个宏在 proc、pv、banker 三个实验里直接复用实验报告里写一句“日志模块统一”验收观感也好。有一个地方要特别留意你要是把 LOG 的 stderr 改成 stdout验证脚本抓结果时会多出一堆 INFO 行排序和平均周转时间全被打乱属于改一行坑全局的典型。3. 进程与调度实验从 fork 到 FCFS/SJF/时间片轮转3.1 用 fork 和 wait 验证进程语义进程管理第一课是写一个程序让父进程依次创建 N 个子进程每个子进程打印自己的 PID 和父进程 PID然后按创建顺序回收。这个实验看起来简单实际是在验证“进程是资源分配的最小单位”——fork 后父子各有独立地址空间变量修改互不可见子进程不是函数调用。#include stdio.h #include unistd.h #include sys/wait.h #define N 4 int main(void) { pid_t pids[N]; for (int i 0; i N; i) { pids[i] fork(); if (pids[i] 0) { perror(fork); exit(1); } if (pids[i] 0) { printf(child pid%d parent%d index%d\n, getpid(), getppid(), i); _exit(0); } } for (int i 0; i N; i) { waitpid(pids[i], NULL, 0); printf(reaped pid%d\n, pids[i]); } return 0; }子进程里用_exit(0)而不是exit(0)这是个细节exit 会刷新 stdio 缓冲区而 fork 时子进程复制了父进程的缓冲区多个子进程同时刷新可能把同一段数据打出多份_exit直接进内核行为干净。父进程用 waitpid 按 pid 精确回收避免 wait 的“任意回收”顺序和创建顺序错位。参数调整主要在 N 值N 设到 64 以上可以观察并发调度现象子进程的打印顺序不固定那不是代码错是内核调度顺序不确定实验报告里写上这句比只贴运行结果更有价值。3.2 用结构体实现 PCB 与就绪队列FCFS/SJF 的调度内核调度实验的核心是定义进程控制块 PCB 并实现调度算法。PCB 最小字段是到达时间、服务时间、开始时间、完成时间、剩余时间外加一个进程号。FCFS、SJF、时间片轮转共用同一个 PCB算法差异只在“从就绪队列里挑谁”和“是否抢占”两点。typedef struct { int pid; int arrive; int serve; int remain; // 时间片轮转用 int start; int finish; int wait; int seq; // 排序稳定性键 } pcb_t;就绪队列我直接用数组加排序解决不开链式队列因为数据量只有几十个进程排序比指针操作直观日志也好打印。FCFS 先按到达时间稳定排序然后依次执行SJF 每次从“当前已到达且未完成”的进程里挑服务时间最短的int pick_next(pcb_t *p, int n, int cur_time, int *done) { int best -1; for (int i 0; i n; i) { if (done[i]) continue; if (p[i].arrive cur_time) continue; if (best -1 || p[i].serve p[best].serve) { best i; } } return best; }这个函数的意思是只要还有进程未完成、且到达时间已经小于等于当前时刻就挑服务时间最短的那个作为下一步执行对象。如果 best 返回 -1说明此刻没有进程到达调度主循环要把时间直接快进到下一个到达时刻这个“快进”逻辑必须单独写否则就是死循环。上面这段是非抢占式 SJF选中就让它跑完服务时间再回到 pick_next要扩展成最短剩余时间优先 SRTF把比较字段从 serve 换成 remain 即可其余逻辑不动。主循环里记录每个进程的开始时间、完成时间最后计算平均周转时间、平均带权周转时间。输出每行固定为pid arrive serve finish turnaround weight_turnaround五个字段和后面的自检脚本配合时不需要额外解析。3.3 时间片轮转的参数选择与排序稳定坑时间片轮转在同一个 PCB 上加 remain 和排队逻辑。RR 的关键参数是时间片 q我试过 q1、q2、q4 三组数据q 越小短任务响应越快但上下文切换次数变大总完成时间变长q 越大越接近 FCFS。实验报告里放一张三组 q 的对比表结论就是书上那句“时间片应当比一次上下文切换开销大得多”。实现时用“当前时刻 t把所有 arrive t 且未完成的进程按顺序排进队列每轮从队头取一个进程执行 min(q, remain)执行完若 remain0 重新挂到队尾”。这个思路用数组加头尾游标就能做不一定要真链表。这里有个排序稳定坑特别值得记C 的 qsort 不稳定。当多个进程同时到达且服务时间相同时排序后的相对次序可能和输入顺序不一致FCFS 的结果就对不上手算数据。解决办法是 PCB 里的 seq 字段比较时先比 arrivearrive 相同比 seqint cmp_pcb(const void *a, const void *b) { const pcb_t *x a, *y b; if (x-arrive ! y-arrive) return x-arrive - y-arrive; return x-seq - y-seq; }这个改法五行内搞定能避免实验报告里最尴尬的“运行结果和手算顺序不同”。输入数据格式我用每行四个整数进程号、到达时间、服务时间、优先级。优先级字段留给优先级调度算法扩展用不做就先填 0同时保证比较函数里不要因为优先级而改变次序。4. 同步互斥与死锁实验信号量、银行家算法与数据构造4.1 用 POSIX 信号量实现生产者-消费者同步实验首选生产者-消费者模型吞吐量指标直观而且信号量接口能完整覆盖 sem_wait/sem_post 两个核心原语。我用无名信号量加多线程缓冲区大小 BUF_SIZE5生产者和消费者各 N 个线程#include pthread.h #include semaphore.h #define BUF_SIZE 5 #define PRODUCERS 3 #define CONSUMERS 3 #define ITEMS 100 static sem_t empty, full, mutex; static int buf[BUF_SIZE], in 0, out 0; void *producer(void *arg) { (void)arg; for (int i 0; i ITEMS; i) { sem_wait(empty); sem_wait(mutex); buf[in] i; in (in 1) % BUF_SIZE; sem_post(mutex); sem_post(full); printf(produce %d\n, i); } return NULL; } void *consumer(void *arg) { (void)arg; for (int i 0; i ITEMS; i) { sem_wait(full); sem_wait(mutex); int v buf[out]; out (out 1) % BUF_SIZE; sem_post(mutex); sem_post(empty); printf(consume %d\n, v); } return NULL; }信号量初值必须满足 emptyBUF_SIZE、full0、mutex1这三个初值本身就是“有空位才能生产、有数据才能消费、同一时刻只能一个线程动缓冲区”的代码表达。循环次数设成 ITEMS 而不是 while(1)是为了保证线程能退出避免 main 里 pthread_join 超时。环形缓冲区的 in/out 取模是边界条件里最容易被忽略的漏掉% BUF_SIZE数组越界后 mutex 内存被写坏程序会表现出时好时坏的玄学。我踩过这个坑最后用 AddressSanitizer 编译才定位到写越界。4.2 银行家算法安全性检测与预分配回滚死锁实验的标准题型是银行家算法。它维护四个矩阵进程对各类资源的最大需求 Max、已分配 Allocation、还需 Need、系统可用资源 Available。分配前做两步检查Request 是否小于等于 Need是否小于等于 Available都满足就预分配然后调用安全性检测算法判断是否存在安全序列int is_safe(int avail[], int max[][M], int alloc[][M], int n) { int work[M], finish[N]; memcpy(work, avail, sizeof(work)); memset(finish, 0, sizeof(finish)); for (int k 0; k n; k) { int found 0; for (int i 0; i n; i) { if (finish[i]) continue; int can 1; for (int j 0; j M; j) { if (max[i][j] - alloc[i][j] work[j]) { can 0; break; } } if (can) { for (int j 0; j M; j) work[j] alloc[i][j]; finish[i] 1; found 1; printf(safe seq - P%d\n, i); } } if (!found) return 0; } return 1; }这段实现里最关键的教学点是安全性检测必须保证完整一轮都推进不了才返回 0而不是中间某个进程找不到就立即判定不安全。内层遍历顺序会影响输出安全序列的具体排列但不影响“是否存在安全序列”这个结论。Need 直接用 max-alloc 实时计算避免单独维护一个随分配不断变化的矩阵。调用前把预分配的结果填入 alloc 和 avail检测后如果是安全状态才真正提交这次分配否则回滚——这就是“预分配 回滚”语义。实验数据必须准备“分配成功”和“分配失败”各两组样例否则安全性检测返回 0 的分支没有被覆盖代码等于白写。4.3 构造演示数据让三个实验在报告里自洽死锁实验的数据造起来比调度实验讲究。调度实验的数据可以随手生成银行家算法的数据必须保证 Available 接近临界值。我的做法是拿一个 5 进程 3 资源类型的样例Available 设为 (3,3,2)Max 矩阵里有两个进程的最大需求刚好在当前 work 值附近这样才能演示“先执行哪个进程决定了后续是否死锁”。如果所有进程的 Need 都远小于 Available算法一路绿灯报告里看不出安全性检测的判断逻辑。PV 实验的数据靠线程数和 ITEMS 数控制。线程数多、产品数少容易出现某个线程长时间抢不到锁输出看起来像卡死其实是线程饿死。我一般把生产者和消费者线程数都设为 3、ITEMS 设为 100运行时间控制在百毫秒级既能稳定复现又不会让验收时等太久。每个线程的循环结束后打印一行“thread X done”主线程 join 超时时能区分是死锁还是线程未退出排查成本低一个量级。5. 操作系统实验排查六个让实验翻车的具体坑这一章按“现象 → 原因 → 解决”拆开写都是真实调试记录照着查能少走很多弯路。5.1 子进程里改了变量父进程读不到现象子进程修改了一个全局变量父进程打印发现还是旧值。原因fork 后的子进程拿到的是父进程地址空间的拷贝写时拷贝保证物理页在修改时才复制但逻辑上父子已经分家。解决跨进程共享数据要用管道、共享内存或信号量如果只是要并发修改同一份数据把进程实验改成多线程线程才共享地址空间。这个坑在第 3 章的进程管理实验里几乎必现属于对 fork 语义理解不到位。5.2 链接阶段 undefined reference tosem_wait 和pthread_create现象代码编译通过链接报 undefined reference to sem_wait、pthread_create。原因glibc 把线程接口拆到 libpthreadPOSIX 信号量实现依赖实时库 librt链接时必须显式指定。解决Makefile 的 LIB 里写全-lpthread -lrt并且放在源文件之后。这条每个学期都有人翻车最稳的写法就是第 2 章那个 Makefile 格式直接抄不会错。5.3 qsort 排序后输出顺序和手算不一致现象多个进程到达时间相同FCFS 算出的完成时间对但输出顺序和实验报告手算表不一致。原因C 标准库 qsort 是不稳定排序相等元素的相对次序不保证保持。解决给 PCB 加 seq 字段比较函数先比到达时间再比 seq。这样排序结果确定性好多次运行输出不变后面写自动对比脚本时才不会偶发 diff。5.4 银行家算法把“本轮找不到可推进进程”当死锁直接返回现象预分配后明明存在安全序列程序却输出不安全。原因is_safe 的实现里某一次内层循环找不到可完成进程就立刻 return 0忽略了应该继续检查剩余未完成进程的情况。解决每次外层循环重新扫描所有未完成进程只有完整一轮一个都推进不了才返回不安全。同时把“本轮推进了哪个进程”的日志打出来和手算逐行核对定位极快。5.5 自检脚本统计结果飘忽不定平均值忽大忽小现象同一份输入跑脚本平均周转时间每次都不同。原因调试用的 printf 和实验数据都打在 stdout数据行被 INFO 日志冲断awk 统计时把日志行也算了进去。解决日志全部走 stderr数据行走 stdout。判断日志层是否干净很简单终端跑./bin/sched data/input.txt 2/dev/null如果数据行完好说明 stdout 没有混入诊断信息。5.6 段错误环形缓冲区和调度快进的越界现象某个输入下程序直接 Segment Fault生成 core 文件。解决编译保留 -g用 gdb 看 backtrace重点查环形缓冲区取模和调度循环里 pick_next 返回 -1 后的处理。我遇到过的越界基本都长在“队列尾部1”这种代码上逻辑上躲过了检查物理上跨进了相邻数组。AddressSanitizer 编译一次就能直接给出越界行号比肉眼快得多。6. 收尾技巧写一个一键自检脚本比实验报告更值钱最后落地一个自动验证入口。我在每个实验目录放一个 check.sh对所有可执行文件做冒烟测试并检查退出码。这个脚本不算课程要求但改完代码回归一遍只要几秒“运行结果”从此可复现#!/usr/bin/env bash set -euo pipefail cd $(dirname $0)/.. make /dev/null run_exp() { local bin$1 input$2 if [[ ! -f data/$input ]]; then echo skip $bin : missing $input return 0 fi ./bin/$bin data/$input out/$bin.out 2 out/$bin.log echo $bin : exit $? lines $(wc -l out/$bin.out) } run_exp sched sched_input.txt run_exp pv pv_cfg.txt run_exp banker deadlock_case.txt run_exp proc proc_case.txtset -euo pipefail让脚本在管道出错时立刻停下而不是带着脏数据继续跑。每个实验的输出落到 out 目录日志和数据分离后面要做对比表或图表时直接拿 out 下的文件做输入。除了通常样例三种必查边界是空输入文件、全部进程同时到达、故意构造的死锁样例。空输入查边界处理同时到达查排序稳定性死锁样例查 is_safe 的返回值与回滚逻辑。超出这三个边界仍能稳定退出代码基本可以交。个人习惯是每做完一个实验就把当天的 Makefile 和自检脚本复制一份按日期存档。毕业回看时实验报告记录的是“我做了”脚本和代码才记录“我怎么做、哪里卡住过”。当初调银行家算法调了两个晚上最后发现不是算法错而是 Available 样例造得太宽松从此我每写一个判断分支都先问自己这个分支的 false 路径有没有数据能走到。这套习惯从课程带到工作踩过的坑也都留在这些脚本旁边。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑