ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

杭电操作系统实验通关指南:从同步互斥到页面置换的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信号量初值为什么是 NP/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表示缓冲区还有几个空位初值必须等于 Nfull表示缓冲区里已经放了多少个物品初值必须是 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。后来改成“先走通再美化”反而两天就把五个实验全收拾干净了。实验这件事慢慢地折腾比急着跑完省时间得多这份经验希望帮到你。本文还有配套的精品资源点击获取
返回列表