ARTICLE DETAIL

资讯详情

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

操作系统实验全攻略:环境搭建、进程同步与页面置换避坑

操作系统实验全攻略:环境搭建、进程同步与页面置换避坑 简介《计算机操作系统实验指导第3版》是一份面向高校计算机专业学生、考研者及Linux自学者的操作系统实践资料重点围绕Linux环境下的实验环节展开帮助读者将理论概念与真实系统操作联系起来。压缩包共32个文件以20个.h头文件、11个.c源程序文件及1个.cpp文件构成总计15.42MB覆盖进程管理管道通信、软中断通信、存储器管理、设备管理字符设备驱动以及文件系统设计等核心实验的参考代码与程序框架。目前已有1063人学习下载。其中源码可供直接编译验证帮助读者熟悉Linux系统调用、进程同步、内存映射和设备驱动等关键机制同时文件按章节组织、结构清晰便于对照教材进行分步实践。无论是课程实验还是备考复习都能从中获得贴近实际操作的引导降低上手Linux实验的门槛。1. 操作系统实验指导照图施工别先啃理论学期末的操作系统课设总有一批人卡在同一个地方教材看得懂一到上机就不知道从哪一行代码开始写。实验指导书就是干这个用的——它把进程控制、内存管理、文件系统、死锁避免这些章节拆成一个个能在两个小时内跑起来的实验题。手里这本《计算机操作系统实验指导第3版》不是理论补充读物而是照着做的“施工图”每个实验给题目、给流程、给验证方法做完一个就往实验报告里贴一个。这本书适合三类人正在上操作系统课、需要交实验报告的本科生准备考研复试、想快速把经典实验重新过一遍的考生以及刚转行做底层开发、想补一补进程和内存这块基本功的从业者。后面几章我会把环境搭建、每个实验的关键参数、以及我实际跑这些实验时踩过的坑一次说清。2. 先把实验环境搭稳选型理由与工具链一次装齐2.1 选 Ubuntu 而不是折腾别的发行版实验指导里大量操作基于 Linux 系统调用接口做进程、内存实验时需要通过/proc观察内核状态。选 Ubuntu 的理由很功利实验指导上的命令默认就是 Ubuntu/Debian 系的apt 装包比 yum 省事网上搜报错也能直接搜到对应解法。CentOS 和 Arch 不是不行但你把时间花在调包管理器上没有意义。如果你本机是 Windows两条路虚拟机或者 WSL2。两者的差别我用一个表说明。方案启动速度内存占用/proc 真实性图形界面文件互访VMware/VirtualBox慢高建议 4G 起步完整有共享文件夹WSL2快低按需分配基本完整无可用终端/mnt/c 直接访问我做实验那会儿用虚拟机关键原因是页面置换实验里要看dmesg输出和/proc/meminfoWSL2 的内核是微软定制的部分字段和标准内核有偏差可能导致实验结果和理论值对不上。如果你的实验设计只涉及 fork、线程、文件模拟WSL2 完全够用省内存还省启动时间。虚拟机参数按这个底线给2 核 CPU、4G 内存、40G 虚拟磁盘。内存低于 4G 编译大型 C 项目时会频繁触发 swap一次 make 等半分钟。2.2 编译器、调试器、跟踪工具一次装齐Ubuntu 装好后先把工具链打齐。以下是实验期间几乎每天都用的包直接一条命令装完sudo apt update sudo apt install -y build-essential gdb valgrind strace vim git逻辑说明build-essential会拉取 gcc、g、make、libc-dev 这一整套编译工具操作系统实验清一色 C 语言必须装gdb用来调试 fork 和指针问题比盲目加 printf 高效得多valgrind查内存泄漏在多线程实验里几乎必备strace是观察系统调用的利器后面第 3 章还会详细用。参数说明-y是跳过 apt 的确认提示如果只做纯软件模拟实验进程调度、页面置换build-essential加gdb就足够。编译命令建议从一开始就养成带调试信息的习惯gcc -Wall -g -o exp1 exp1.c-Wall打开所有常见警告-g生成调试符号这两项缺一不可。我见过太多同学用gcc exp1.c直接编段错误以后连gdb都用不了只能靠猜。Makefile 写一个最简版就够用CCgcc CFLAGS-Wall -g exp1: exp1.c $(CC) $(CFLAGS) -o exp1 exp1.c clean: rm -f exp1注意Makefile 里的缩进必须是一个 Tab 字符不能用空格替代。这个坑在后面避坑章节还会专门讲。2.3 第一个观察实验用 strace 看 execve 做了什么实验指导里第一个经典实验往往是从fork到exec再到wait的完整路径。在写代码之前我习惯先让学生用strace看一个现成程序做了什么系统调用建立“代码和内核行为”的对应感。比如写一个最简 exec 程序#include unistd.h #include stdio.h int main() { printf(before exec\n); fflush(stdout); execl(/bin/echo, echo, after exec, NULL); perror(execl failed); return 1; }编译后执行gcc -Wall -g -o exec_demo exec_demo.c strace -f -o exec_trace.log ./exec_demo逻辑说明fflush(stdout)必须在execl之前调用否则 printf 缓冲区里的内容会随进程映像替换而丢失execl成功时不会返回只有失败才走到perror。strace -f里的-f是跟踪 fork 出来的子进程-o指定输出文件。打开exec_trace.log能清楚看到execve(/bin/echo, ...)这一行——这就是用户态程序“变身”的现场记录。参数上的一个关键点execl的第二个参数是argv[0]在目标程序里通过getpid()无法感知它但在 shell 脚本里$0会用到这个值。实验指导里涉及 shell 模拟类的题目这个参数经常被传错导致结果异常。3. 进程与线程实验从 fork 到生产者消费者的代码骨架3.1 fork 与 wait先看清父子进程的边界进程控制实验的第一个题目通常是写程序验证 fork 的返回值语义和父子进程的并发执行。代码骨架如下#include stdio.h #include sys/wait.h #include unistd.h int main() { pid_t pid fork(); if (pid 0) { perror(fork failed); return 1; } else if (pid 0) { printf(child pid%d ppid%d\n, getpid(), getppid()); return 0; } else { printf(parent pid%d child%d\n, getpid(), pid); int status; waitpid(pid, status, 0); printf(child exit status: %d\n, WEXITSTATUS(status)); } return 0; }逻辑说明fork 返回后父子进程各自从printf那一行继续执行子进程里pid恒为 0父进程拿到的pid是子进程 PID。waitpid的第二个参数status保存退出状态第三个参数用0表示阻塞等待换成WNOHANG则立即返回。在真实实验里WEXITSTATUS(status)用来提取 exit code很多同学漏掉这个宏直接打印 status 原值得到一个 256 的倍数对着结果一脸懵。一个容易忽略的细节父进程里如果不waitpid子进程会变成僵尸进程ps状态列显示Z。实验报告要求观察僵尸进程时故意注释掉waitpid然后ps -l看状态这份实验才算完整做完——但做完记得加回来否则后续实验里产生大量僵尸进程ps输出全是defunct。3.2 生产者消费者信号量的三个参数线程同步实验里最典型的题目是生产者消费者。实验指导的常见要求是用sem_init创建两个信号量分别表示空位和满位。核心结构如下#include pthread.h #include semaphore.h #define BUFFER_SIZE 5 sem_t empty; sem_t full; pthread_mutex_t mutex; void *producer(void *arg) { for (int i 0; i 20; i) { sem_wait(empty); // 占用一个空位 pthread_mutex_lock(mutex); // 往缓冲区写入 item i pthread_mutex_unlock(mutex); sem_post(full); // 释放一个满位 } return NULL; }逻辑说明sem_wait(empty)若当前空位数量为 0线程挂起等待sem_post(full)让等待在这个信号量上的消费者唤醒一个。这里的核心是让empty的初值等于缓冲区容量BUFFER_SIZEfull的初值为 0再加一个互斥锁保护缓冲区的读写操作。参数说明sem_init的第一个参数是信号量指针第二个参数pshared固定写0表示线程间共享第三个参数是初值。当初值误设成1时生产者和消费者会因为信号量计数不对产生死锁这个我在避坑章节会展开。编译时别忘了-pthreadgcc -Wall -g -pthread -o pc pc.c编译参数-pthread会把线程库的链接选项和宏定义一起加上。漏掉它编译报undefined reference to pthread_create是最常见的入门错误。3.3 实验报告里要写什么对比数据这份实验指导的报告评分通常不是看“能跑”而是看“有没有对比”。最值得做的对比是在同一份代码上控制变量地记录数据。比如生产者消费者分别用单线程、双线程、四线程跑记录完成时间、CPU 占用、线程切换次数。数据用下述表格形式填入报告生产者线程数消费者线程数总耗时(ms)线程切换次数缓冲区利用率112841042%221522768%441334171%额外参数说明缓冲区利用率里“满位信号量最大值 / BUFFER_SIZE”的变化曲线能直观看出同步瓶颈。在写实验结论时用“线程数增加但耗时下降放缓”替代“多线程更快”这种笼统陈述实验报告的分数会有明显差别。4. 内存、文件与死锁实验页面置换、目录模拟与银行家算法4.1 页面置换算法模拟用访问串替代随机数内存管理实验通常会要求模拟 FIFO、LRU、Clock 三种页面置换算法并统计缺页率。我先说明一个设计选择访问串不要用随机数生成器直接产生而是手写一个“带局部性”的序列。原因是随机访问串下 FIFO 和 LRU 的缺页率差异极小实验报告的结论缺少说服力。LRU 算法模拟的核心代码如下#include stdio.h #include string.h #define FRAME_NUM 4 #define REF_LEN 20 int find_page(int frames[], int page) { for (int i 0; i FRAME_NUM; i) { if (frames[i] page) return i; } return -1; } void lru_simulate(int ref[], int n) { int frames[FRAME_NUM]; int timestamp[FRAME_NUM]; memset(frames, -1, sizeof(frames)); memset(timestamp, 0, sizeof(timestamp)); int time 0, faults 0; for (int i 0; i n; i) { int idx find_page(frames, ref[i]); if (idx 0) { timestamp[idx] time; // 命中时更新访问时间 } else { int victim 0; for (int j 1; j FRAME_NUM; j) { if (timestamp[j] timestamp[victim]) victim j; // 找最早未被访问的页 } frames[victim] ref[i]; // 替换 timestamp[victim] time; faults; } time; } printf(LRU缺页次数: %d, 缺页率: %.2f%%\n, faults, faults * 100.0 / n); }逻辑说明frames数组存储当前驻留的页面号timestamp记录每个页面上一次被访问的逻辑时间。页面访问命中时只更新时间戳不产生缺页未命中时遍历所有时间戳选中最早的那个作为替换目标。这里的关键设计是“逻辑时间”用一个每次访问自增1的计数器代替真实系统时钟否则在模拟程序里无法保证时间戳的单调递增。FRAME_NUM是物理页帧数REF_LEN是访问串长度两个参数直接决定缺页率曲线建议做实验时把帧数从 2 一直调到 6记录缺页率变化。和 FIFO 对比时你会看到经典的 Belady 异常现象页帧数增加缺页率反而上升。这个现象只在 FIFO 下出现LRU 和 Clock 理论上不会。实验报告把这段观察写进去基本上一等分水平就有了。4.2 文件系统模拟两级目录怎么用结构体表达文件系统实验题目常常要求模拟一个两级目录文件系统第一级是主目录用户第二级是用户目录文件。我做的方案是用结构体数组模拟内存中的目录项#define MAX_USERS 10 #define MAX_FILES 20 struct dir_file { char name[12]; int size; int first_block; int is_used; }; struct user_dir { char username[12]; struct dir_file files[MAX_FILES]; }; struct user_dir root[MAX_USERS];逻辑说明root数组的每个元素对应一个用户主目录files数组存放该用户下的文件目录项。创建文件时先根据用户名定位到对应user_dir再遍历files找到第一个is_used 0的槽位填入文件名和大小。实验指导里的“打开文件”操作在这里简化成一次结构体查找不需要真正读写磁盘块。这个方法的核心参数是MAX_USERS和MAX_FILES的取值——它们模拟了 i 节点表的容量上限。在删除文件时注意只需要把is_used置 0不需要清空name字段否则会引入不必要的内存写操作。如果实验要求实现文件回收再分配用链表串起空闲槽位更合适。4.3 银行家算法安全序列的输出顺序要小心死锁避免实验几乎必做银行家算法。算法本身不复杂用 Available 表示当前可用资源Max 表示每个进程的最大需求Allocation 表示已分配资源Need 表示还需要的资源。关键是安全检测函数里寻找进度要严格按顺序扫描并且找到一个可满足的进程后必须从头重新开始扫描。#define P_NUM 5 #define R_NUM 3 int available[R_NUM]; int max_need[P_NUM][R_NUM]; int allocated[P_NUM][R_NUM]; int is_safe(int work[], int finish[], int sequence[]) { int cnt 0; while (cnt P_NUM) { int found 0; for (int i 0; i P_NUM; i) { if (finish[i] 0) { int enough 1; for (int j 0; j R_NUM; j) { if (max_need[i][j] - allocated[i][j] work[j]) { enough 0; break; } } if (enough) { for (int j 0; j R_NUM; j) { work[j] allocated[i][j]; } finish[i] 1; sequence[cnt] i; found 1; break; // 跳出 for回到 while 重新扫描 } } } if (!found) return 0; // 本轮没找到任何可满足进程 } return 1; }逻辑说明work数组是可用资源的副本算法运行时不断把已分配资源回收进来。注意内层for循环里每次找到一个可满足的进程后要用break跳出然后while循环重新从进程 0 开始扫描——因为回收资源后之前不可满足的进程现在可能满足了。从进程 0 开始重新扫是必要的。sequence数组记录安全序列这个序列的顺序会直接出现在实验报告里所以建议把found之后break的行为讲清楚不要让序列输出看起来像随机排序。5. 避坑五个翻车场景的现象、原因与解决5.1 fork 之后 printf 输出了两遍现象程序里只有一个printf但运行结果打印了两行相同内容。原因printf默认行缓冲当输出重定向到文件或管道时变成全缓冲fork 时缓冲区被完整复制到子进程于是同一段内容被父子进程各输出一次。解决printf后面立刻加fflush(stdout)或者在 fork 之前把缓冲区清掉。这个现象在实验里看是小事但它直接关系到你对 fork 语义的理解深度实验报告的反思部分提一句往往能加分。5.2 信号量初值设错导致生产者消费者永久挂起现象程序跑起来后生产者或消费者线程卡在sem_wait不动。原因sem_init的第三个参数是信号量初值很多人把empty初始化成 0等于一开始就没有空位生产者永远无法写入。解决empty初值设为缓冲区大小BUFFER_SIZEfull初值设为 0。用gdbattach 到挂起线程bt看到sem_wait那一行基本就能确认是计数问题而不是代码逻辑问题。5.3 LRU 模拟里用真实系统时钟导致替换判断错误现象页面访问序列很短时LRU 和 FIFO 的缺页次数完全一样甚至 LRU 更差。原因模拟程序用time()或clock()获取时间戳精度是微秒级或毫秒级连续访问间隔小于一个时钟刻度时两个页面的时间戳相等替换判断变成随机。解决像 4.1 里的代码那样用每次访问自增的计数器作为逻辑时间保证每个访问都有唯一的单调递增时间戳。5.4 银行家算法把“当前安全”当作“永不死锁”现象算法返回安全后继续执行一段时间某进程申请资源时系统真的死锁了。原因银行家算法保证的是“拒绝会导致系统进入不安全状态的请求”但系统进入安全状态后进程后续的资源请求依然要被逐个检测很多同学只在开始时调用一次安全检测后续请求直接满足把安全状态当成免死金牌。解决每次request到来都做一次试探性分配执行安全性检测不通过则回滚分配并拒绝该请求。5.5 WSL 或 Git 检出代码后 make 报错现象在 Windows 下用 Git 克隆实验代码到 WSL2 里执行make报missing separator或file not found。原因Git 默认把文本文件换行符转换成 CRLFMakefile 的缩进从 Tab 变成“空格 回车”make 无法识别。解决在 WSL 的仓库目录下执行git config core.autocrlf input然后git checkout -- .重新检出或者用sed -i s/\r$// Makefile手工去掉 CRLF 字符。建议在实验开始的第一天就把这个配置做好后面三个实验都会用到 make。6. 用 /proc 与 strace 给实验结果盖章两个验证习惯实验做到能跑只是完成一半。我见过太多同学把截图交上去结果被老师一问就翻车——因为程序能跑不代表行为正确。养成两个验证习惯一份实验报告的“可信度”会完全不一样。第一个习惯是查/proc。fork 实验完成后用ps -l查看进程状态在waitpid被注释掉时能看到Z僵尸状态而加上waitpid后子进程变成S睡眠然后消失。这个动态过程比任何截图都更能说明 fork 语义。线程实验可以看/proc/pid/status里的Threads字段确认创建的线程真的存在。cat /proc/$(pgrep -f pc_program)/status | grep Threadspgrep -f按进程名查找 PIDgrep Threads抓取线程数量字段。如果程序创建了 4 个线程这里应显示4而不是凭感觉认为“应该创建成功”。第二个习惯是strace -c统计系统调用。写一个简单的 shell 脚本包装实验程序跟踪它的系统调用分布能直观看到 fork、wait4、semop 的调用次数。频率异常往往是同步逻辑有误的信号——比如消费者线程里sem_wait和sem_post的次数不匹配一眼就能发现。strace -c -f -o strace_summary.txt ./pc_program cat strace_summary.txt-c让 strace 直接输出系统调用统计表-f跟踪子线程。统计表里semop的次数乘以 2 应约等于写入缓冲区的次数这个关系对不上说明信号量的等待和释放逻辑有问题。从那以后我每次提交实验前都强制走一遍这两步/proc查状态strace -c查频次。数据对不上就回头改代码数据对上了再整理报告省掉了大量答辩时被当场问住的尴尬。希望帮到你。本文还有配套的精品资源点击获取
返回列表