简介:面向杭州电子科技大学操作系统课程的实验资源包,完整覆盖实验要求中的一、二、三、五共四个模块,涉及setNice/setName进程属性控制、Petree进程显示、模拟Shell、基于消息队列/管道/共享内存的进程通信,以及文件系统实现;同时附带PTA在线题目进程模拟、进程调度、银行家算法的C语言解法。全部源码已调试通过,配套报告详尽说明实验目的、设计与结果分析,且PTA题解代码带注释,便于直接对照算法步骤,适合正在准备实验验收或系统复习操作系统的学生参考。资源共46个文件,以C/C++源码、头文件、Makefile和Word实验报告为主,另有PDF讲义、Markdown笔记与已编译的exe程序,按实验序号分目录组织,包体约4.41MB,文件安排合理。目前已有3690人学习/下载,是一份结构完整、可直接套用的高质量参考。
1. 杭电操作系统实验凭什么能“通过验收”:几个容易被低估的门槛
操作系统实验在杭电的课设里属于那种“代码量不大、但挂人不少”的类型。很多同学从网上找份源码,改个变量名,编译一跑,截图交了,结果验收现场被一句“信号量初值为什么是 5”问得说不出话。我陆陆续续帮人梳理过几轮这类实验,最大的感受是:能不能通过验收,看的不是代码跑不跑得动,而是你有没有把“程序为什么这么写”讲清楚。本文就把整个实验从选型、实现、自测到报告拆开讲,覆盖进程同步、内存管理、页面置换这几块高频题目,照着搭一份能演示、能讲明白的工程,通过验收的把握会大不少。
2. 实验范围与实现语言:先知道老师要验什么,再决定用 C 还是模拟实现
2.1 杭电操作系统实验的常见模块与验收要点
操作系统实验每个学期的题目组合不完全一样,但知识点基本绕不开进程调度、同步互斥、死锁避免、内存管理和文件系统这几块。不用把每个题都做成完整内核,多数情况下是用用户态程序去“模拟”内核行为,再把结果可视化地打印出来。我一般会先做一张对照表,把实验模块和老师可能追问的点列清楚,再决定代码怎么写。
| 实验模块 | 涉及的内核知识点 | 学生端常见实现方式 | 验收时容易被追问的点 |
|---|---|---|---|
| 进程调度 | 时间片、优先级、状态转换 | 模拟 PCB 数组 + 调度算法 | 时间片设多大?进程何时进入就绪态 |
| 同步互斥 | 信号量、P/V 操作、死锁 | pthread + semaphore | 信号量初值为什么是 N?P/V 顺序能不能换 |
| 银行家算法 | 安全序列、死锁避免 | 二维数组维护 Max/Allocation/Need | 不安全状态如何判定 |
| 内存管理 | 分区分配、碎片、回收 | 数组模拟分区表 | 释放后合并了吗?外部碎片怎么解决 |
| 页面置换 | 缺页中断、Belady 异常 | 数组模拟物理块 | FIFO 和 LRU 的缺页次数分别怎么算 |
| 文件系统 | 索引结构、磁盘调度 | 内存里的虚拟文件目录 | 数据是真正落盘还是只存在内存里 |
这张表的价值在于:它决定了你代码里哪些变量、哪些函数是“必须被看见”的。比如做同步互斥,老师不会只看你的输出对不对,他会看你的sem_init参数、看你的wait和signal顺序,这些才是打分点。做内存管理,他重点看分区表结构,而不是看你打印了多少行漂亮日志。所以一开始就按“能讲清楚的数据结构”去写,比写完之后补注释要舒服得多。
2.2 为什么我建议用 C + pthread,而不是改内核或写纯 Java
选语言这件事,很多同学在论坛里问过无数遍。我的建议很明确:如果只想稳过验收,用 C 语言 + pthread 线程库,配上一组模拟数据结构,是最省力的组合。原因有几点:
第一,操作系统课里讲信号量、PCB、页表,用的都是 C 风格的伪代码,你拿 C 写出来的东西和课上的描述一一对应,答辩时顺嘴就能说“先 P 再 V,和课件里的原语一致”,没有任何翻译成本。第二,pthread 是现成的线程库,sem_init、sem_wait、pthread_mutex_lock这些原语可以直接调用,不用自己造轮子,代码量控制在几百行以内,调试起来也快。第三,那种“为 Linux 写内核模块”的路子,编译要内核头文件、调试要靠 dmesg,一个课设周期根本玩不转,出了问题连定位都困难。至于纯 Java 写多线程,逻辑上能跑,但 Java 里没有直接的信号量原语教学配套,写出来的东西更像并发编程作业,离操作系统实验的预期反而远了。
像头歌操作系统这类在线练习平台,用来刷知识点和算法题是挺好的,但课设验收需要的是你本地能编译、能演示、能现场改参数的一份工程。在线平台给的代码往往是片段式的,和验收的“完整演示”场景对不上,所以建议只把它们当练习题,不要直接搬过来当课设提交。
环境方面,建议尽量在 Linux 下编译运行。如果你手头是 Windows,装个 WSL 或虚拟机里的 Ubuntu 都能解决,后文避坑章节还会专门讲这个问题。
2.3 搭一个“好演示”的工程目录
验收时老师会扫一眼你的工程组织方式。把所有实验塞进一个 main.c 里不是不行,但显得乱。我习惯按实验模块拆目录,每个模块独立编译、独立运行,这样现场演示时切换实验只需要换一条命令。
mkdir -p os_lab/{scheduler,sync,banker,memory,report}这个结构里,scheduler放进程调度,sync放生产者消费者,banker放银行家算法,memory放分区分配和页面置换,report放实验报告和运行截图。每个目录里只放一个源文件和一份说明文档,编译命令写在目录内的 README 里。这样做的另一个好处是:答辩时老师问“你的工程怎么组织”,你回答“按实验模块拆分,每个模块独立运行”,本身就是一个加分项。别小看这种工程习惯,它比代码里多写几个注释更能说明你上过道。
3. 三个高频实验的落地代码:信号量同步、可变分区与页面置换的 C 实现
杭电的操作系统实验里,生产者消费者、可变分区分配、页面置换这三道题出现的频率很高。下面给三份能直接编译跑的代码骨架,每一份都在关键位置写了注释,方便你对着讲。
3.1 生产者-消费者:信号量初值与 P/V 顺序的完整演示代码
// sync/pc.c #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h> #define N 5 sem_t empty, full; pthread_mutex_t mutex; int buffer[N]; int in = 0, out = 0; void *producer(void *arg) { for (int i = 0; i < 10; i++) { sem_wait(&empty); // 先申请缓冲区空位 pthread_mutex_lock(&mutex); // 再拿锁,顺序不能反 buffer[in] = i; printf("produce %d -> slot %d\n", i, in); in = (in + 1) % N; pthread_mutex_unlock(&mutex); sem_post(&full); sleep(1); } return NULL; } void *consumer(void *arg) { for (int i = 0; i < 10; i++) { sem_wait(&full); pthread_mutex_lock(&mutex); int item = buffer[out]; printf("consume %d <- slot %d\n", item, out); out = (out + 1) % N; pthread_mutex_unlock(&mutex); sem_post(&empty); sleep(2); } return NULL; } int main() { sem_init(&empty, 0, N); // 空位初值 = 缓冲区大小 sem_init(&full, 0, 0); // 已占初值 = 0 pthread_mutex_init(&mutex, NULL); pthread_t p, c; pthread_create(&p, NULL, producer, NULL); pthread_create(&c, NULL, consumer, NULL); pthread_join(p, NULL); pthread_join(c, NULL); sem_destroy(&empty); sem_destroy(&full); pthread_mutex_destroy(&mutex); return 0; }编译命令:
gcc -o pc pc.c -lpthread ./pc这份代码的逻辑要点在于:empty表示缓冲区还有几个空位,初值必须等于 N;full表示缓冲区里已经放了多少个物品,初值必须是 0。sem_wait(&empty)必须放在pthread_mutex_lock之前,否则可能出现“拿着锁等空位”的死锁场面——生产者占着锁,而消费者等着锁,两边一起卡住。sleep(1)和sleep(2)是为了让现场输出有节奏,验收时你能看到生产一条、消费一条交替打印,老师也能直观看到缓冲区在流动。
3.2 可变分区分配:First Fit 与回收合并的模拟
内存管理的题一般分两步:分配和回收。分配算法看似简单,但很多人栽在“分配后剩余空间怎么保留”和“释放后怎么合并相邻空闲块”这两个细节上。下面这段 First Fit 代码把分配拆成了两部分:找到合适分区后,如果空间有富余,就把富余的部分单独拎出来,留在分区表里继续等分配。
// memory/partition.c #include <stdio.h> #define MAX_PARTS 64 #define MEM_SIZE 1024 typedef struct { int base; // 起始地址 int size; // 分区大小 int free; // 1 = 空闲,0 = 已占用 } Part; Part parts[MAX_PARTS]; int part_cnt = 1; void init_memory() { parts[0].base = 0; parts[0].size = MEM_SIZE; parts[0].free = 1; } int first_fit(int need) { for (int i = 0; i < part_cnt; i++) { if (parts[i].free && parts[i].size >= need) { if (parts[i].size > need) { // 把剩余部分拆成一个新的空闲分区,插到当前分区后面 for (int j = part_cnt; j > i + 1; j--) parts[j] = parts[j - 1]; parts[i + 1].base = parts[i].base + need; parts[i + 1].size = parts[i].size - need; parts[i + 1].free = 1; part_cnt++; } parts[i].size = need; parts[i].free = 0; return parts[i].base; } } return -1; // 分配失败 }参数说明:MEM_SIZE是模拟内存总大小,改成 2048 也没问题;MAX_PARTS是分区表上限,防止碎片多了数组越界。first_fit返回的是分配到的起始地址,-1 表示内存不够。重点关注part_cnt++那一步:它意味着分区表随时在动态增长,所以循环遍历时不要用固定上限,要用part_cnt作为边界。
回收侧的核心是合并,释放时必须先看“相邻的前后分区是否空闲”,空闲就拼成一块:
void release(int base) { for (int i = 0; i < part_cnt; i++) { if (parts[i].base == base && !parts[i].free) { parts[i].free = 1; // 与后一个分区合并 if (i + 1 < part_cnt && parts[i + 1].free) { parts[i].size += parts[i + 1].size; for (int j = i + 1; j < part_cnt - 1; j++) parts[j] = parts[j + 1]; part_cnt--; } // 与前一个分区合并 if (i - 1 >= 0 && parts[i - 1].free) { parts[i - 1].size += parts[i].size; for (int j = i; j < part_cnt - 1; j++) parts[j] = parts[j + 1]; part_cnt--; } return; } } }提示:release 里“先往后合并,再往前合并”的顺序不是固定的,但每次合并后要同步减少
part_cnt,否则分区表里会残留无效项,后面的遍历会出错。
这个实验的答辩点几乎都藏在release的合并逻辑里。老师最爱问的就是“连续分配释放几次之后,空闲分区会不会变成一堆碎块?”,如果你在代码里做了合并,当场演示一遍“分配 200、释放、再分配 300、再释放,最终空闲表项恢复到 1 条”,这一问就算答上了。
3.3 页面置换:FIFO 与 LRU 的缺页次数对比
页面置换实验一般会要求你跑一组访问序列,对比不同算法下的缺页次数。FIFO 的实现很直接:维护一个环形索引,缺页时按顺序换出最早进入的页。
// memory/paging.c #include <stdio.h> #define FRAMES 3 int ref[] = {1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5}; int fifo() { int frames[FRAMES] = {-1, -1, -1}; int idx = 0, faults = 0; for (int i = 0; i < sizeof(ref) / sizeof(ref[0]); i++) { int hit = 0; for (int j = 0; j < FRAMES; j++) { if (frames[j] == ref[i]) { hit = 1; break; } } if (!hit) { frames[idx] = ref[i]; idx = (idx + 1) % FRAMES; faults++; printf("page %d fault -> frames: %d %d %d\n", ref[i], frames[0], frames[1], frames[2]); } } return faults; }LRU 和 FIFO 的差别只在淘汰依据。FIFO 看“谁先进入物理块”,LRU 看“谁最近最久没被访问过”,所以 LRU 需要额外维护时间戳数组:
int lru() { int frames[FRAMES] = {-1, -1, -1}; int last_use[FRAMES] = {0}; int time = 0, faults = 0; for (int i = 0; i < sizeof(ref) / sizeof(ref[0]); i++) { int hit = 0; for (int j = 0; j < FRAMES; j++) { if (frames[j] == ref[i]) { hit = 1; last_use[j] = time++; // 命中必须更新时间戳 break; } } if (!hit) { int victim = 0; for (int j = 1; j < FRAMES; j++) { if (last_use[j] < last_use[victim]) victim = j; } frames[victim] = ref[i]; last_use[victim] = time++; faults++; // 打印当前帧状态 } } return faults; }参数说明:FRAMES是模拟的物理块数,也是这个实验唯一的“旋钮”。改大改小,缺页数会跟着变,验收时可以用它展示“物理块多,缺页率下降”的直觉结论。ref[]是访问序列,建议你自己换一组序列,和手算结果对一遍,确认缺页次数一致再做演示。
3.4 演示顺序建议:先同步、再内存、最后置换
现场演示时,我一般按“进程 → 内存 → 虚拟内存”的顺序来。先跑生产者消费者看交替打印,耗时短且效果直观;再跑分区分配,展示一次分配、一次释放、分区表变化的全过程;最后跑页面置换,把 FIFO 和 LRU 的缺页次数并列打印在同一行,突出对比。这样一套下来,老师对你的逻辑印象会特别顺。
4. 验收前自测与报告组织:让实验经得起追问
4.1 一张可复制的自测清单
验收前焦虑是正常的,提前一天把每个实验当“考试”一样过一遍,比临场祈祷管用。我常用的自测方式是把问题列成表格,逐项跑,逐项打勾。
| 检查项 | 具体做法 | 通过标准 |
|---|---|---|
| 进程调度 | 拉大两个进程的优先级/时间片差距 | 输出顺序与预期一致,切换顺序稳定 |
| 同步互斥 | 把生产者和消费者循环次数都改成 20 | 最终in和out都回到 0,无卡死 |
| 银行家算法 | 构造一个不安全序列 | 程序明确输出“不安全,拒绝分配” |
| 分区分配 | 分配后按“中间释放,再两边释放”的顺序释放 | 空闲表项最终恢复为 1 条 |
| 页面置换 | 换一组新的访问序列,手算一遍缺页数 | 程序输出与手算结果一致 |
这里有个细节:手算缺页数时,很多同学会漏掉“刚启动时物理块全空”的判断。页面置换实验里,刚开始的几次访问必然缺页,因为物理块里没有任何页,这一步要算进缺页总数。自测时如果发现自己手算的结果比程序少,多半是这个原因。
4.2 报告怎么组织才像“自己的”
操作系统实验报告有固定套路,但最忌讳的是从网上抄一段“原理概述”和“实验步骤”,然后贴几张截图。老师常年看报告,一眼就能分辨哪些是抄的、哪些是自己写的。我写报告的习惯是:需求分析只写两三段,重点放在“我设计了什么数据结构、为什么这样设计”。比如你实现了可变分区,就写“分区表采用数组模拟,表项记录 base、size、free 三个字段”,然后把分配和释放的流程图手绘出来,哪怕画得丑,也比网图可信。运行结果截图要有,但每张图下面必须配一段解释,说清楚图中哪些字段发生了变化、为什么变化。最后“遇到的问题”这一节最加分,不要写“没有问题”,要把调试过程中真实遇到卡住的地方写出来,比如“释放时忘了合并相邻分区,导致空间碎片化”,并写出最终怎么解决的。老师翻到这一页,基本就会认定这份作业是你自己做的。
4.3 答辩讲词:先讲你最有把握的那个实验
答辩通常不会让你把六个实验全讲完,时间不允许。老师一般会挑一个实验说“你讲一下”。这时候选你最熟的那个,按“数据结构 → 算法流程 → 关键语句 → 实验结果”四步来。比如讲生产者消费者,你就说:缓冲区用长度为 5 的数组实现,维护读写指针;用两个信号量控制空位和满位;关键语句是生产者先wait(empty)再上锁,消费者对称操作;实验结果从打印日志里能看到生产消费交替进行,并且最终指针回到初始位置。整套说下来不要超过三分钟,句句落在代码上。老师如果追问“为什么空位信号量初值是 5”,你就答“因为缓冲区只有 5 个位置,空位最多 5 个,消费者不可能把空位信号量加到超过 5”,这一句就能证明你是真的懂。顺带一提,操作系统期末复习里反复背的临界区、安全序列、缺页中断这些概念,本质上就是答辩追问的来源,考前拎一遍等于把答辩题库过了一遍。
5. 操作系统实验避坑指南:五个会让验收翻车的现场
5.1 Windows 下 pthread 编译报错,或运行时闪退
现象:在 Windows 的 Dev-C++ 或某些 MinGW 环境里编译,报undefined reference to pthread_create,或者编译过了但一运行程序就退。
原因:pthread 不是 Windows 系统库,MinGW 环境需要额外的线程库支持,很多 IDE 默认没把-lpthread加进链接参数。
解决:编译命令里显式加-lpthread,如果还不行,就说明头文件和库不匹配,别再折腾老旧的 Dev-C++,直接装一个 WSL 或虚拟机里的 Ubuntu。在 Linux 下这份代码就是gcc -o pc pc.c -lpthread一条命令的事,省下来的时间够多写一个实验。
5.2 生产者-消费者运行后没有任何输出,或者打印几条就卡死
现象:程序启动后黑屏,或者打印了三四条就停住不动。
原因:最常见的是 P/V 顺序写反。有人在信号量wait操作之前先去拿互斥锁,一旦缓冲区空位不够,生产者在锁上等待,消费者又在等待生产者释放信号量,两边互相等,直接死锁。
解决:统一规则,先sem_wait(empty)或sem_wait(full),再pthread_mutex_lock。另一个原因是sem_init初值反了:如果empty初值写成 0,生产者第一步就阻塞,所以调试时优先检查两行sem_init的参数对不对。
5.3 内存实验“明明总空间够,却分配失败”
现象:连续分配、释放几轮后,打印空闲分区表,发现零碎的小分区一大堆,总空闲量足够,但请求一个较大的分区时返回 -1。
原因:释放时只把free置为 1,没做相邻合并。相邻的小空闲块各自独立,没法拼成一个大块满足新请求。
解决:在release里补上相邻合并逻辑。自测方法:连续分配 4 个分区,按“中间两个先释放,两边再释放”的顺序做一轮,最后空闲表应该只剩 1 条记录,而不是中间夹着已占用分区的多条碎记录。
5.4 页面置换的缺页计数值和手算不一致
现象:打印出的缺页次数比自己手算的多,或者少几次。
原因:手算时把“初始物理块为空”的首次缺页漏算了,或者代码里判断命中/缺页的逻辑写反。常见错误是遍历frames时没找到就算缺页,但找到之后忘了置hit标记,导致命中也被误判为缺页。
解决:先用一个最简单序列调通,比如{1, 1, 2},手算结果是第一次装 1 缺页,第二次 1 命中,第三次 2 缺页,共 2 次缺页。如果程序输出 3,说明命中判断有问题,直接检查hit的赋值位置。另外 LRU 实验里,命中后必须更新对应帧的时间戳,很多人漏了这一步,导致淘汰对象选错。
5.5 演示时用到文件,换一台机器就读不到
现象:在自己电脑上能读取输入文件,验收教室的电脑上运行就提示找不到文件。
原因:代码里用了绝对路径,比如C:\Users\xxx\Desktop\input.txt,换台机器路径自然失效。
解决:所有输入文件放到源码同一目录,代码里用相对路径读取,比如fopen("input.txt", "r")。更稳妥的做法是让程序支持命令行参数指定文件,演示时在终端里./scheduler input.txt,既灵活又显得专业。
6. 先用最小可运行版本跑通,再补演示效果
最后分享我一个很受用的习惯:任何实验都先做一个“最小可运行版本”。版本一只有最核心的算法逻辑和硬编码输入,能出结果就行;版本二加入交互,让你手动输入分配大小或访问序列;版本三才加打印美化、统计报表和边界处理。不要第一版就奔着完整版去,那是给自己挖坑。
比如生产者消费者,最小版本就是“生产 5 次,消费 5 次,纯数组模拟,不用线程”。跑通后,再把sleep和日志一项项加进去。页面置换同理,先固定住一个序列,确认缺页数算法正确,再去扩展成多组对比。每次改动后都编译运行一次,存一个能跑的版本作为后悔药。我最早给学弟调这个课设时,总喜欢一口气把最终版写出来,然后花一整晚找 bug。后来改成“先走通再美化”,反而两天就把五个实验全收拾干净了。实验这件事,慢慢地折腾比急着跑完省时间得多,这份经验希望帮到你。
本文还有配套的精品资源,点击获取