简介:操作系统实验全套资源来自山东大学课程实践,覆盖进程控制、线程与管道通信、MSH Shell编写、进程同步与互斥五大实验模块,面向需要完成操作系统课程设计或巩固并发编程基础的高校学生。包内共51个文件,以13个C源码、9个目标文件、5份docx实验报告及Makefile构建脚本为主,C源码对应实验核心实现,docx方便整理报告,Makefile负责自动编译,压缩包仅1.37MB,便于按模块定位和复现。已有3753人学习/下载。通过这套资源,可直观理解进程生命周期与系统调用、多线程及管道通信机制、简易Shell的命令解析与执行流程,还能结合理发店问题、抽烟者问题等经典案例掌握信号量、互斥锁等同步原语的使用,避免死锁与数据不一致。每个实验附带的文档和代码注释,有助于整理实验报告与排查实现思路,适合入门到进阶的系统层实践。
1. 实验整体设计与思路拆解
说起操作系统实验,很多人第一反应是“不就是调几个API、跑几个函数吗”。真上了山东大学操作系统实验的战场,你会发现完全不是这么回事。这门实验课的核心目标不是让你用一遍Linux命令,而是让你亲手触碰操作系统的关键子系统:进程管理、内存管理、文件系统、同步互斥、系统调用。每一个实验都对应着理论课上一个重要章节,实验代码量不大,但涉及的概念深度和对系统机制的理解要求相当高。
刚开始做这套实验时,我最大的困惑是“实验到底要交什么”。后来才摸清,实验考核的不只是最终运行结果,更是你对实验过程的理解。比如一个进程管理实验,如果只贴出来PID和父进程ID的输出截图,分数大概率不高;但如果你同时说明了进程创建时内核做了什么、fork返回值为什么是这样设计、调度器又是如何选中新进程运行的,思路一下就打开了。
从我个人的学习路径来看,这套实验最适合两类人:
- 正在上操作系统理论课、需要同步做课程设计的本科生,实验能帮你把“进程”“调度”“页表”这些抽象概念落到真实代码里;
- 想转行做底层开发、嵌入式或云原生基础设施方向,但一直没找过系统级动手机会的自学者。
核心价值不只是“过一门课”,而是借助实验题目的约束,迫使你站在内核视角去思考问题。做完这套实验,你对Linux系统调用、进程生命周期、同步原语、内存映射这些概念的理解深度,会完全不一样。
2. 环境准备与工具选型
2.1 为什么主流方案是虚拟机加Ubuntu
操作系统实验基本绕不开Linux环境。一方面,Linux内核源码完全开放,实验涉及的系统调用、进程调度、内存管理等模块都可以直接查看和修改;另一方面,大多数实验模板、参考代码、评测脚本都默认跑在Linux环境上,Windows的命令行生态虽然越来越完善,但涉及内核模块编译、系统调用跟踪这类操作时,依然不如Linux顺手。
具体的落地方式,我见过三种主流选择:
| 方案 | 优缺点 | 适合场景 |
|---|---|---|
| VMware Workstation + Ubuntu 22.04 | 环境隔离,快照方便,实验环境被搞挂了直接恢复 | 大多数课程实验,推荐首选 |
| WSL 2 | 启动快、资源占用低,与Windows文件互通 | 只需用系统调用的实验,编译内核模块会比较麻烦 |
| 本校实验室远程服务器 | 配置统一,但高峰期排队卡顿 | 有现成虚拟化平台的情况下 |
我自己用的是VMware方案。原因是课程实验里有一个环节需要修改Linux内核参数并重新编译模块,WSL 2虽然理论上也支持,但涉及内核头文件、模块编译工具链时坑很多。VMware的快照功能也是一大神器,实验前打一个快照,万一配置错了系统起不来,一键还原,比重新装虚拟机省太多时间。
提示:VMware安装完成后,下拉虚拟机系统镜像建议直接选Ubuntu Server版或Desktop版都会遇到的“客户机操作系统已禁用 CPU”报错,通常不是系统镜像问题,而是虚拟机CPU配置里的虚拟化引擎勾选不对。处理方式后文会专门说明。
2.2 中文资料这么多,该看谁
操作系统相关知识在网上多到爆炸,但质量参差不齐。做实验阶段,我建议按这个优先级选取资料:
- 教材:汤小丹《计算机操作系统》慕课版、哈工大操作系统理论课、电子工业出版社的《操作系统概念》第十版,这三类覆盖了主流教材体系,实验书上的知识点基本都能找到对应章节。
- 实验指导书:山东大学操作系统实验配套的实验手册是最高优先级,因为实验题目、评测标准、提交格式都以它为准。
- 在线博文:搜索“Linux 实验 fork 进程”“生产者消费者 PV 操作 C语言”“页面置换算法 LFU 实现”这类具体关键词,可以快速找到前人踩坑记录。
- 系统手册:
man 2 fork、man 2 mmap、man 2 semop这类系统调用手册页才是最终答案,不少网上代码其实错了,只有手册页是权威。
不建议一上来就看“操作系统八股”式的总结贴,这类资料适合期末复习,不适合做实验时查阅。做实验遇到API参数拿不准,直接终端里查man手册最快。
3. 核心细节解析与实操要点
3.1 进程管理实验:fork返回值到底怎么看
进程管理实验通常要求写一个程序,创建多个子进程并观察进程之间的关系。绝大多数同学在这里翻车,根源是对fork()的理解只停留在“复制了一个进程”这个层面。
fork()的精髓在于返回值:父进程中返回子进程PID,子进程中返回0。这不是随便设计的,而是为了让你区分两个进程的身份。我一开始写代码时用了这样的逻辑:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { pid_t pid = fork(); if (pid < 0) { perror("fork error"); return 1; } else if (pid == 0) { printf("子进程: PID=%d, 父进程PPID=%d\n", getpid(), getppid()); } else { printf("父进程: PID=%d, 创建的子进程PID=%d\n", getpid(), pid); } return 0; }运行后输出一般类似:
父进程: PID=1000, 创建的子进程PID=1001 子进程: PID=1001, 父进程PPID=1000看起来简单,但实验报告如果只写到这里,深度不够。我建议你再多做三件事:
第一,在fork前后各加一个全局变量并修改它,观察父子进程的全局变量是否互相影响。结果会是:各自持有一份副本,互不影响。这说明fork实现的是写时复制,而不是真复制。
第二,加入sleep(1),观察进程在子进程中运行时到底访问了哪些资源。这里可以引出进程控制块PCB(Linux中对应task_struct)的概念。
第三,运行ps -ef或pstree -p观察进程树,把实验中的父子关系对应到系统真实进程树中。这一步能帮你理解系统里所有进程都是通过父进程fork出来的层级结构。
注意:fork之后父子进程的执行顺序是不确定的,不要依赖哪个先执行。如果实验要求有序输出,必须使用同步原语(信号量、锁),不能靠
sleep硬等。
3.2 同步互斥实验:生产者消费者问题到底卡在哪
生产者和消费者问题是操作系统同步互斥实验的常客,也是期末考试题里的经典。核心知识点有三个:互斥锁(mutex)、信号量(semaphore)、条件变量(condition variable)。实验要求通常是用C语言在Linux下模拟多个线程,一个生产数据,多个消费数据,要求不能重复消费、不能丢失数据、不能死锁。
我最初的实现踩过一个典型坑:一开始只用一个信号量控制缓冲区,没有区分“空”和“满”,结果生产者和消费者同时操作缓冲区,数据重叠。代码逻辑跑出来不对,排查了半天才发现缓冲区满时生产者没有阻塞,消费者为空时又去读无效数据。
标准做法是使用三个信号量:一个互斥锁保护缓冲区访问,一个空格信号量表示缓冲区空余位置,一个数据信号量表示有数据可消费。代码如下:
#include <stdio.h> #include <pthread.h> #include <semaphore.h> #define BUFFER_SIZE 5 int buffer[BUFFER_SIZE]; sem_t empty; // 空位数量 sem_t full; // 数据数量 pthread_mutex_t mutex; void* producer(void* arg) { int item = 0; while (1) { item++; sem_wait(&empty); pthread_mutex_lock(&mutex); // 写入缓冲区 buffer[item % BUFFER_SIZE] = item; printf("生产: %d\n", item); pthread_mutex_unlock(&mutex); sem_post(&full); sleep(1); } return NULL; } void* consumer(void* arg) { int item; while (1) { sem_wait(&full); pthread_mutex_lock(&mutex); // 读取缓冲区 item = buffer[item % BUFFER_SIZE]; printf("消费: %d\n", item); pthread_mutex_unlock(&mutex); sem_post(&empty); sleep(2); } return NULL; } int main() { pthread_t tid1, tid2; sem_init(&empty, 0, BUFFER_SIZE); sem_init(&full, 0, 0); pthread_mutex_init(&mutex, NULL); pthread_create(&tid1, NULL, producer, NULL); pthread_create(&tid2, NULL, consumer, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; }这里有一个实验报告中容易忽略的点:为什么要用两个信号量而不是一个计数器?简单答案是,一个信号量无法同时表达“有空位”和“有数据”两个状态,生产者只关心空位,消费者只关心数据,分别用两个信号量才能让两边都阻塞在正确的事件上。
3.3 内存管理实验:页面置换算法要理解命中率
内存管理实验里最常见的就是模拟实现页面置换算法,比如FIFO、LRU、Clock等。输入是一个页面访问序列,输出是缺页次数和缺页率。这个实验看似是纯算法题,但实际上它检验的是你对局部性原理和页表结构的设计感。
有经验的实现会做这样的抽象:
typedef struct PageNode { int page_id; // 页面号 int access_time; // 上次访问时间,LRU用 int reference_bit; // 访问位,Clock用 struct PageNode* prev; struct PageNode* next; } PageNode;FIFO实现时用队列,LRU实现时用链表实现最近最久未使用排序,Clock算法则是在环形链表上扫描引用位。实验报告上要重点分析不同算法在同一输入序列上的表现差异。比如某个序列FIFO会缺页8次、LRU是5次、Clock是6次,你要解释为什么LRU比FIFO好,而不是只贴表格。
这个实验如果想拿高分,建议再做一些视角上的延伸:为什么现代操作系统很少直接用纯LRU?因为维护所有页面的访问时间成本太高,所以实际商用系统多用近似LRU,比如Clock算法。这个点写进实验报告,老师会看到你确实理解了设计背后的折衷。
提示:做内存管理实验时不要只把算法跑通就完事,建议手动构造几种页面访问序列,比如顺序访问、循环访问、局部性强的访问,对比这三种序列下算法的表现差异。这样能直观体会到不同算法的适用场景。
4. 实操过程与核心环节实现
4.1 两个月前我按这套流程跑通了实验环境
很多人卡在实验环境上,不是不会写代码,而是系统起不来、环境编译不过。这里分享一条稳妥的流程:
第一步,VMware Workstation 17 安装Ubuntu 22.04 Desktop版,磁盘给40GB,内存给4GB,CPU给2核。这里要特别注意:虚拟机设置里的“虚拟化引擎”三个选项全部勾选,尤其是“虚拟化 Intel VT-x/EPT 或 AMD-V/RVI”。如果不勾选,部分内核编译实验会提示CPU不支持虚拟化或直接报“客户机操作系统已禁用 CPU”。
第二步,装好系统后执行基础优化,切换到国内软件源,更新系统:
sudo sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list sudo apt update && sudo apt upgrade -y第三步,安装实验常用工具链:
sudo apt install -y build-essential gcc gdb make vim git manpages-dev第四步,验证环境是否可用。写一个最简单的系统调用程序,调用getpid()并打印。编译运行成功后,再开始正式的实验代码。
4.2 操作系统引导:BIOS自检和中断向量表谁先谁后
操作系统引导是理论课上容易讲,但实验里不容易直观验证的一个点。很多学操作系统的人对启动过程心里没底,甚至会问“BIOS通电自检和中断向量表的建立有没有先后顺序”。
答案是确定的:BIOS通电自检先执行,中断向量表后建立。原因很简单:中断向量表本质上是内存中的一张地址映射表,它需要操作系统或引导加载程序来填充。BIOS自检阶段,内存还没被操作系统接管,中断向量表还不存在。BIOS自身的运行时服务是通过固件级中断处理机制实现的,与操作系统的中断向量表是两套体系。
实验层面想验证这个流程,可以这么做:
先创建一个空的虚拟磁盘,再用dd写入一个自定义的主引导记录程序,程序里只写一行字符“Hello Boot”。启动虚拟机后,如果屏幕打印出这行字,说明BIOS成功把控制权交给了引导扇区。这一步虽然不会直接出现在操作系统实验的必做清单里,但做完之后你对“引导扇区→加载内核→建立中断向量表”整个链路会形成非常直观的认识。
4.3 无人值守安装与镜像定制:给批量实验机准备的技巧
如果课程是全年级一起做实验,老师通常会把实验环境做成虚拟机镜像统一发放,但某些自带实验器材的实验室也会要求你自己准备镜像。实际过程中,学会做无人值守安装非常有用。
Ubuntu的无人值守安装机制是通过cloud-init或预置的user-data文件实现的。最简单的做法是先用autoinstall参数生成一个配置文件:
# 构建包含autoinstall配置的ISO镜像 # 主要步骤:挂载原版ISO -> 写入user-data和meta-data -> 重新打包ISOuser-data文件里关键内容如下:
#cloud-config autoinstall: version: 1 identity: hostname: os-lab username: student password: c2hhMjU2JDEyMzQ1Njc4JjEyMzQ1Njc4 ssh: allow-pw-auth: true install-server: true packages: - build-essential - gdb - vim这样生成的ISO可以全自动安装,无需手动选择语言、键盘布局、磁盘分区等步骤。对于需要在一台机器上反复重装环境或批量部署实验机的场景,效率提升非常明显。虽然课程实验本身不强制要求掌握这个,但会了这个技能,后续做分布式实验、集群实验都会省很多事。
5. 常见问题与排查技巧实录
做实验过程中,我遇到的坑集中在这几个地方,整理成速查表,你可以直接抄作业。
5.1 虚拟机启动报错“客户机操作系统已禁用 CPU”
这个报错在虚拟机使用过程中相当常见。导致这个问题的原因通常是CPU虚拟化配置不对,具体排查步骤:
# 确保宿主机BIOS中已开启虚拟化技术(Intel VT-x/AMD-V) # 检查虚拟机设置 -> 处理器 -> 勾选“虚拟化 Intel VT-x/EPT 或 AMD-V/RVI” # 如果开启后仍报错,尝试关闭“加密”类选项,或降低CPU核数重试另外,如果用的是老旧系统镜像(比如Ubuntu 16.04)搭配新版VMware,也可能出现兼容性问题。建议直接使用Ubuntu 22.04 LTS镜像,兼容性最好。
注意:不要在虚拟机内再去开启虚拟化嵌套,除非你真的是要跑KVM等嵌套虚拟化方案,否则不必要的嵌套选项反而会引发性能下降和稳定性问题。
5.2 Linux系统莫名重启或直接关机
做实验时遇到过一次Ubuntu在编译内核模块过程中突然黑屏重启。排查过程让我长了不少经验,这里分享一下排查路径:
先查系统日志:
journalctl -k -b -1 | tail -50-b -1表示上一次启动的内核日志。如果是CPU过热或者内存不够导致的内核panic,日志里通常会记录关键信息。
我那次的情况是内存不够,编译时Linux内核模块把物理内存打满,触发了OOM killer。解决方案是给虚拟机加到6GB内存,并把swap空间从2GB扩到4GB。
如果遇到重启后无法进入系统,优先用Live CD启动,挂载磁盘并查看日志,而不是直接重装系统。
5.3 系统安装第三方软件时的签名校验失败
在国产操作系统或麒麟系统上安装第三方软件时,经常遇到签名校验失败的问题。Ubuntu上对应的场景是安装了非官方仓库的包后提示签名失效。解决思路是先确认包来源,再决定是否信任。
# 在Ubuntu上信任第三方仓库的GPG key wget -qO - http://example.com/key.asc | sudo apt-key add - # 或者使用新版方式 sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL http://example.com/key.asc | sudo tee /etc/apt/keyrings/custom.asc实验环境下建议优先使用系统自带软件源,第三方工具安装在单独的虚拟机里做测试,避免污染实验环境。信创环境(麒麟、统信UOS)的软件安装逻辑与Debian系类似,但有的操作需要用图形化“软件商店”,有的则直接支持apt。具体以实验指导书上要求的环境为准。
5.4 客户机操作系统保留用户名但无法登录
有段时间遇到VMware登录界面一直显示之前输入的旧用户名,密码怎么输都进不去。我查了一下,这其实是VMware的客户机操作系统“凭据记忆”功能在作怪。处理方法:
- 重启虚拟机,在登录界面选择“未列出”选项,手动输入新账户名;
- 在VMware菜单中关闭“客户机操作系统凭据同步”功能;
- 或者直接删除虚拟机的凭据缓存文件。
这类问题虽然不影响代码实验,但会浪费很多时间。遇到时心里有数即可。
6. 从实验到工程:进阶建议
6.1 读源码比跑实验更值钱
操作系统实验做到后期,你会发现单纯的“跑通实验”已经不能满足要求了。比如你要实现一个调度算法,但你连Linux默认的CFS调度器长什么样都没看过,写出来的代码大概率只是“听起来很合理”的玩具。
我看Linux源码的经验是:不要求全懂,抓住主线。比如fork()的实现,顺着kernel/fork.c里的copy_process()看,就能理解进程创建时复制了哪些资源、哪些是共享的。再到kernel/sched/core.c看调度主循环,配合实验里的生产者消费者模型,你对“进程为什么看起来像在同时运行”会有新的理解。
如果要看源码解析,推荐从这几个路径开始:
kernel/fork.c:进程创建核心逻辑kernel/sched/core.c:调度核心mm/memory.c:内存管理核心,页错误处理fs/*:文件系统实现
说实话,第一次读这些源码非常痛苦,一个函数跳来跳去,半天都看不完。但坚持下来会发现,后续做内核模块实验或者并发编程实验时,很多以前只能靠背的概念、结论变得顺理成章。
6.2 把实验报告写成作品集
很多同学把实验报告当成应付检查的作业,格式堆砌、内容空洞。换个思路想,如果你打算找Linux内核、云计算、嵌入式方向的工作,操作系统实验是最能直接证明你基础能力的材料。
我的做法是:每完成一个实验,把实验题目、设计思路、核心代码、运行结果、性能对比、遇到的问题和解决办法压缩成一个Markdown文档,再放到GitHub仓库里。后续面试聊到项目经历时,直接甩出链接,比口头说“我做过操作系统实验”有说服力得多。
另外,写实验报告时多问自己几个“为什么”:
- 为什么这个同步问题用信号量而不是用自旋锁?
- 为什么这个内存分配算法会有外碎片问题?
- 为什么页面置换算法评估要看缺页率而不是仅看执行时间?
把一个实验做到这个深度,就不只是“完成了课业要求”,而是真正的技术积累。这套思路放到任何学校、任何实验课程上都成立。
最后再分享一个小经验:做实验时做一份自己的速查命令清单,把实验中用过的gcc编译指令、gdb调试指令、进程查看指令、内存分析指令都记下来。到学期末复习或做课程设计时,这份清单比任何系统盘镜像都实用。
本文还有配套的精品资源,点击获取