news 2026/11/6 9:33:21

杭电操作系统实验通关指南:从同步互斥到页面置换的C实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
杭电操作系统实验通关指南:从同步互斥到页面置换的C实现

简介:面向杭州电子科技大学操作系统课程的实验资源包,完整覆盖实验要求中的一、二、三、五共四个模块,涉及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。后来改成“先走通再美化”,反而两天就把五个实验全收拾干净了。实验这件事,慢慢地折腾比急着跑完省时间得多,这份经验希望帮到你。

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

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

Spring Boot多环境配置全解:从Profile机制到Nacos实践

做Java后端这些年&#xff0c;几乎每个Spring Boot项目都绕不过多环境配置这道坎。dev、test、uat、prod四个环境的数据库地址、中间件地址、第三方接口域名各不相同&#xff0c;最笨的办法是每次部署前手动改一遍配置文件——我见过不少团队就是这么干的&#xff0c;结果就是每…

作者头像 李华
网站建设 2026/10/22 20:19:10

ArrayList与HashMap底层源码详解:扩容机制、红黑树与实战避坑

开题先说实话&#xff1a;ArrayList和HashMap这两个类&#xff0c;我在给团队做代码评审时几乎每次都会见到&#xff0c;自己写业务代码时也几乎每天都在用。但你要是随便拉住一个写了三五年Java的人&#xff0c;问他“HashMap put一个键值对到底经历了哪几步”“ArrayList扩容…

作者头像 李华
网站建设 2026/11/4 10:37:17

SQL注入原理与实战:从万能密码到防御加固

1. 先说清楚SQL注入为什么会存在 很多初学者第一次接触SQL注入&#xff0c;都是从一句莫名其妙的“万能密码”开始的&#xff1a;在用户名框里输入 or 11 -- &#xff0c;密码随便填&#xff0c;结果居然登录成功了。我当时第一次看到这个操作&#xff0c;第一反应是“这也能…

作者头像 李华
网站建设 2026/10/22 18:43:37

SpringBoot课程设计选题管理系统源码解析与二次开发指南

简介&#xff1a;本资源为基于SpringBoot的课程设计选题管理系统完整项目包&#xff0c;面向计算机相关专业学生、毕业设计开发者及需要课程设计参考的师生&#xff0c;帮助解决选题流程管理、角色权限划分与系统开发学习等问题。压缩包共6个文件&#xff0c;包含zip源码工程、…

作者头像 李华
网站建设 2026/10/25 19:54:43

黔南州12.5米DEM数据处理:解压、配准裁剪与质量验证

简介&#xff1a;贵州省黔南布依族苗族自治州12.5米分辨率的数字高程模型数据包&#xff0c;较常规30米数据可更精细地呈现山脊、山谷与坡度等地形细节&#xff0c;同时附带该州市级范围的矢量边界文件&#xff0c;便于直接按行政区划进行剪切与统计。面向地形分析、区域规划、…

作者头像 李华
网站建设 2026/10/31 3:19:22

顺序表底层原理与手写实现:从ArrayList扩容到面试难点

说实话&#xff0c;顺序表这个名字听起来很有学院派的味道&#xff0c;但你要是在Java里写了两年代码回头再看&#xff0c;就会发现天天都在跟它打交道——ArrayList就是顺序表最标准的工业实现。很多初学者上来就背“顺序表插入O(n)、查找O(1)”&#xff0c;背得滚瓜烂熟&…

作者头像 李华