2026春季医学班操作系统实验环境搭建指南
一、实验环境总览
本实验涉及Linux系统调用(如 fork, wait, exec),因此必须在Linux环境下进行。Windows的CMD或PowerShell原生不支持这些系统调用。
| 项目 | 说明 |
|---|---|
| 宿主机系统 | Windows 10/11 |
| 虚拟机软件 | VMware Workstation Player(免费)或 VirtualBox(免费) |
| 虚拟机系统 | Ubuntu 22.04 LTS |
| 编程语言 | C语言 |
| 编译器 | gcc |
| 编辑器 | Vim / VS Code / gedit |
二、安装虚拟机软件
方案A:VMware Workstation Player(推荐,简单稳定)
- 访问 VMware 官网下载页面:https://www.vmware.com/products/workstation-player.html
- 点击「Download Free」下载 Windows 或 Linux 版本
- 下载完成后双击安装,一路「Next」即可完成
方案B:VirtualBox(开源免费)
- 访问:https://www.virtualbox.org/wiki/Downloads
- 下载对应系统的安装包,双击安装
三、下载 Ubuntu 22.04 镜像
-
访问清华大学开源镜像站(国内下载快): https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/22.04/
-
选择下载:
ubuntu-22.04.5-desktop-amd64.iso(约4.5GB)
四、创建 Ubuntu 虚拟机
以 VMware Workstation Player 为例:
-
打开 VMware,点击 「Create a New Virtual Machine」
-
选择 「Installer disc image file (iso)」,点击 Browse 选择刚下载的 ubuntu-22.04.5-desktop-amd64.iso
-
填写简易信息:
- Full name: 学号姓名拼音
- User name: 学号
- Password: 设置一个易记密码(如 123456)
- Confirm: 重复密码
-
虚拟机命名和位置:
- Virtual machine name:
Ubuntu22.04-OS-Lab - Location: 选择一个有 50GB 以上空闲空间的磁盘位置
- Virtual machine name:
-
磁盘容量设置:
- Maximum disk size: 40 GB(建议)
- 选择 「Store virtual disk as a single file」
-
点击 「Customize Hardware」 调整配置:
- Memory: 4 GB(如果宿主机内存 16GB 以上,可设 8GB)
- Number of processors: 2
- 点击 Close,然后 Finish
-
虚拟机将自动启动并进入 Ubuntu 安装程序
五、安装 Ubuntu 系统
-
虚拟机启动后,在左侧语言列表选择 「中文(简体)」 或 「English」(推荐用英文避免路径乱码)
-
点击 「Install Ubuntu」
-
键盘布局选择 「Chinese」 → 「Chinese (Hanyu Pinyin)」
-
安装类型选择 「Normal installation」,勾选:
- ☑ Download updates while installing Ubuntu
- ☑ Install third-party software for graphics and Wi-Fi
-
磁盘分区选择 「Erase disk and install Ubuntu」(这是虚拟磁盘,不会影响宿主机)
-
设置时区:在地图上点击中国区域,或搜索 「Shanghai」
-
设置用户信息(若前面未设置):
- Your name: 真实姓名或学号
- Computer's name: oslab-vm
- Username: 学号小写
- Password: 设置并牢记
-
等待安装完成(约15-20分钟),点击 「Restart Now」
-
重启后看到登录界面,输入密码进入桌面
六、配置开发环境(重要)
打开终端:Ctrl + Alt + T,按顺序执行以下命令。
1. 更换国内软件源(加快下载速度)
sudo sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list
sudo apt update
2. 安装基础开发工具
sudo apt install -y build-essential gdb git vim
验证安装:
gcc --version # 应显示 gcc 11.x.x
3. 安装 VS Code(可选,图形化编辑器更友好)
sudo snap install code --classic
或从官网下载 .deb 包安装:https://code.visualstudio.com/
4. 安装中文输入法(可选)
sudo apt install -y ibus-libpinyin
安装后在「Settings → Region & Language → Input Sources」中添加「汉语(智能拼音)」
七、创建实验目录结构
在终端中执行:
mkdir -p ~/os-lab/{exp1-process-fork,exp2-process-memory,exp3-multitask,exp4-filesystem}
这会在主目录下创建 os-lab 文件夹,内含四个实验子目录。
八、测试环境——第一个C程序
- 创建并打开文件:
cd ~/os-lab/exp1-process-fork
vim hello.c
- 按
i进入编辑模式,输入:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Hello from PID: %d\n", getpid());
return 0;
}
-
按
Esc,输入:wq保存退出 -
编译运行:
gcc hello.c -o hello
./hello
如果看到类似 Hello from PID: 12345 的输出,说明环境配置成功!
九、常用操作速查
| 操作 | 命令/快捷键 |
|---|---|
| 打开终端 | Ctrl + Alt + T |
| 复制文本 | Ctrl + Shift + C(终端内) |
| 粘贴文本 | Ctrl + Shift + V(终端内) |
| 强制退出程序 | Ctrl + C |
| 查看帮助 | man 命令名(如 man fork) |
| 虚拟机全屏切换 | Ctrl + Alt + Enter |
| 从虚拟机释放鼠标 | Ctrl + Alt |
十、常见问题处理
Q1:虚拟机内文字太小怎么办?
- Ubuntu桌面右键 → Display Settings → Scale → 选择 200%
Q2:编译时报错 fork: undefined reference?
- 检查代码是否包含
#include <unistd.h>
Q3:无法从宿主机复制粘贴到虚拟机?
- VMware:虚拟机菜单 → VM → Install VMware Tools(如已安装则重启)
- VirtualBox:设备 → 安装增强功能 → 重启
Q4:虚拟机开机黑屏?
- 关闭虚拟机 → 虚拟机设置 → Display → 取消勾选「Accelerate 3D graphics」
实验一:系统调用综合实验(进程管理)
1. 实验背景
在现代智慧医院信息系统(HIS)中,高并发与稳定性是核心需求。当新患者入院时,医院前台接待系统(主进程)需要为每一位患者创建一个独立的电子病历建档进程(子进程)。
本实验模拟该场景,具体要求如下:
- 隔离性:每个患者的建档过程相互独立,单个患者的信息录入错误不应影响其他患者。
- 资源管理:建档完成后,系统必须及时回收进程资源,防止“僵尸进程”占用系统表项。
- 异常处理:若患者信息非法(如姓名为空或包含敏感字符),建档进程应异常终止,接待处需捕获该状态并记录错误日志。
通过 C 语言在 Ubuntu 环境下模拟这一过程,旨在帮助你深入理解操作系统中的进程创建、状态管理及资源回收机制。
2. 实验目的
- 掌握
fork()系统调用:理解父子进程的代码执行流差异及返回值含义。 - 理解进程状态:直观观察进程的运行、终止及僵尸(Zombie)状态。
- 掌握进程同步与回收:熟练使用
wait()或waitpid()回收子进程资源,避免资源泄漏。 - 了解进程映像替换:初步掌握
execlp()的作用及执行流程。 - 提升错误处理能力:学会通过子进程退出码(Exit Status)判断任务执行结果。
3. 实验环境
| 项目 | 要求/版本 |
|---|---|
| 操作系统 | Ubuntu 22.04 LTS |
| 编译器 | GCC (GNU Compiler Collection) |
| 编辑器 | Vim / VS Code / Gedit |
| 终端工具 | Bash Shell |
4. 预备知识
fork():创建一个新进程。- 父进程:返回子进程 PID (>0)。
- 子进程:返回 0。
- 失败:返回 -1。
exit(status):终止进程。status为退出码(0 表示成功,非 0 表示失败)。wait(&status):父进程阻塞,直到任意一个子进程结束。- 状态宏定义:
WIFEXITED(status):判断子进程是否正常终止。WEXITSTATUS(status):获取子进程的退出码。
ps命令:查看进程状态(推荐参数:ps -p 进程ID -o pid,ppid,stat,cmd)。
5. 实验内容与步骤
任务一:基础进程创建与建档
任务描述: 医院接待处刚刚启动,第一位患者到达前台。为了确保数据录入的安全性与隔离性,系统不能直接在主程序中处理病历,而是需要“分身”出一个专门的子进程来负责该患者的建档工作。你需要实现这个最基础的“分身”功能,并确认父子进程的身份关系。
操作指引:
- 请在提供的代码框架
admit_patient.c中,找到子进程和父进程的逻辑区域,补全缺失的代码。 - 让子进程在控制台打印出自己的进程号(PID)以及父进程的进程号(PPID),以此来验证它们之间的父子关系是否正确。
- 随后,子进程调用
generate_medical_record()函数,模拟耗时的病历生成过程,并打印患者信息。 - 最后,编译并运行程序,输入一位正常患者的信息(例如姓名:
ZhangSan),观察控制台输出的进程 ID 信息及病历内容。
实验要求:
- 按步骤实现功能,确保父子进程 PID 关系正确。
- 在实验报告中粘贴关键代码段并说明思路。
- 对运行输出结果进行截图。
任务二:异常处理与退出码捕获
任务描述: 在实际工作中,患者可能会输入非法信息(例如姓名中包含敏感字符)。如果子进程遇到错误直接崩溃,父进程可能无从知晓。因此,我们需要建立一种“汇报机制”:子进程遇到错误时通过退出码告诉父进程,父进程根据退出码决定是记录成功还是记录错误日志。
操作指引:
- 基于任务一的代码,新建文件
admit_patient_task2.c,准备增加异常判断逻辑。 - 在子进程区域增加判断:检查输入的姓名是否包含
"Error"字符串。如果包含,说明信息非法,子进程调用exit(1)模拟录入失败;否则正常调用exit(0)。 - 回到父进程区域,使用
wait()函数等待子进程结束,并获取其状态值。通过宏判断退出码,如果非 0 则打印 “建档失败,已记录错误日志”,否则打印 “建档成功”。 - 运行程序,故意输入姓名
ErrorUser,验证父进程是否能正确捕获到子进程的异常状态并给出相应提示。
实验要求:
- 保存代码为
admit_patient_task2.c。- 在报告中粘贴关键代码段(特别是
wait和exit部分)。- 对正常和异常两种情况的测试结果进行截图。
任务三:僵尸进程观察
任务描述: 系统运行久了之后,如果父进程只管创建子进程却不关心子进程的“后事”(资源回收),子进程结束后就会变成“僵尸进程”,长期占用系统进程表项。我们需要通过实验亲眼看看僵尸进程是什么样子的,以及如何让它们消失。
操作指引:
- 暂时注释掉父进程中的
wait()代码,并在子进程结束后让父进程执行sleep(30),保持运行状态一段时间。 - 编译运行程序,输入正常信息。然后在另一个终端窗口使用
ps命令查看进程状态。观察子进程结束后的状态栏(STAT)显示什么字符,思考如果父进程一直不调用wait()会发生什么。 - 取消注释
wait()代码,重新编译运行,再次观察进程状态的变化,对比两次实验结果的差异。
实验要求:
- 对子进程在“有 wait"和“无 wait"两种情况下的状态栏显示结果进行截图。
- 在报告中回答思考题(关于僵尸进程的现象及原因)。
任务四:进程映像替换
任务描述: 为了提升安全性,医院决定不再使用内部函数处理病历备份,而是调用一个独立的、受信任的外部系统命令来完成“云端备份”。这意味着子进程不再执行原来的 C 代码,而是完全替换成另一个程序的执行流。
操作指引:
- 基于任务二代码,新建文件
admit_patient_task4.c。在子进程区域,注释掉原有的generate_medical_record(p)函数调用。 - 调用
execlp()函数执行 Linux 系统命令(如echo)来模拟“云端备份”。参数设置为命令名echo,消息内容"病历已备份至云端",并以NULL结尾。 - 在
execlp()代码行的后面,添加一行printf语句(例如:“错误:备份工具启动失败”)。运行程序思考:如果execlp执行成功,这条打印语句会执行吗?为什么? - 尝试将命令改为一个不存在的命令(如
fake_command),观察程序的反应,验证错误捕获机制。
实验要求:
- 保存代码为
admit_patient_task4.c。- 在报告中粘贴关键代码段。
- 对执行结果进行截图。
- 回答关于
execlp执行流程的思考题。
任务五(扩展):并发检查功能
任务描述: 患者入院后往往需要同时进行多项医学检查(如血常规、尿常规、心电图)。如果串行处理会浪费大量时间,系统需要利用多进程技术,让这三项检查同时开始进行,以提升就诊效率。
操作指引:
- 基于任务一代码,新建文件
admit_patient_task5.c。在建档完成后,你需要创建一个循环,利用fork()连续创建 3 个子进程。 - 每个子进程负责模拟一项检查,打印项目名称和 PID,睡眠片刻模拟耗时,然后退出。
- 父进程需要等待所有检查完成,使用循环调用
wait()回收资源,最后输出汇总信息。 - 为了方便你实现,可以参考下方的代码提示结构,将其融入你的程序中。
💡 代码实现提示:
// 在子进程完成建档后,父进程继续执行以下逻辑
pid_t check_pids[3];
const char *check_names[] = {"血常规", "尿常规", "心电图"};
// 1. 循环创建 3 个子进程
for (int i = 0; i < 3; i++) {
check_pids[i] = fork();
if (check_pids[i] == 0) {
// --- 子进程逻辑 ---
printf("开始检查:%s (PID: %d)\n", check_names[i], getpid());
sleep(2); // 模拟检查耗时
printf("检查完成:%s\n", check_names[i]);
_exit(0); // 注意子进程退出使用 _exit 或 exit
}
}
// 2. 父进程等待所有子进程结束
int finished = 0;
int status;
while (finished < 3) {
wait(&status);
finished++;
}
printf("患者所有检查完毕。\n");
- 观察输出结果,思考这种多进程并发实现相比串行处理有什么优缺点。
实验要求:
- 保存代码为
admit_patient_task5.c。- 在报告中粘贴关键代码段。
- 对执行结果进行截图(需体现并发效果)。
- 回答关于多进程并发优缺点的思考题。
6. 代码框架 (admit_patient.c)
请在以下框架基础上完成任务一,后续任务基于此文件进行修改。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <sys/types.h>
typedef struct {
char name[50];
int age;
char symptom[100];
} Patient;
void generate_medical_record(Patient p);
int main() {
Patient p;
int status;
pid_t pid;
int count = 0;
printf("=== 医院接待处系统启动 ===\n");
printf("提示:输入'quit' 或 'exit' 可关闭系统。\n\n");
while (1) {
count++;
printf("--- 第 %d 位患者建档 ---\n", count);
// 1. 读取姓名(前置空格跳过残留换行符)
printf("姓名:");
if (scanf(" %49s", p.name) != 1) break;
// 2. 安全退出条件
if (strcmp(p.name, "quit") == 0 || strcmp(p.name, "exit") == 0) {
printf("系统安全退出。\n");
break;
}
// 3. 读取年龄与症状
printf("年龄:");
if (scanf("%d", &p.age) != 1) {
printf("年龄格式错误,已清空输入缓冲。\n");
while (getchar() != '\n'); // 清空错误输入
continue;
}
printf("主要症状:");
if (scanf(" %99s", p.symptom) != 1) break;
// 4. 创建子进程处理建档
pid = fork();
if (pid < 0) {
perror("fork 失败");
return 1;
}
else if (pid == 0) {
// ================= 子进程区域 =================
// 1. 打印子进程 PID 和父进程 PPID
// 2. 检查姓名是否包含 "Error",若是则 exit(1)
// 3. 调用 generate_medical_record(p)
// 4. 正常退出 exit(0)
// [TODO] 请在此处填写子进程逻辑
// 提示:使用 strstr 检查字符串
exit(0);
}
else {
// ================= 父进程区域 =================
// 1. 打印等待提示
// 2. 调用 wait(&status) 等待子进程
// 3. 判断 WIFEXITED 和 WEXITSTATUS
// 4. 根据退出码打印成功或失败信息
// [TODO] 请在此处填写父进程逻辑
printf("=== 入院流程结束 ===\n");
}
}
return 0;
}
void generate_medical_record(Patient p) {
sleep(1); // 模拟检验科耗时操作
printf(" [病历] 姓名:%s | 年龄:%d | 症状:%s\n", p.name, p.age, p.symptom);
}
7. 编译、运行与测试
# 1. 编译 (以任务 2 为例)
gcc -o admit_patient_task2 admit_patient_task2.c
# 2. 运行测试 (正常流程)
./admit_patient_task2
# 输入示例:ZhangSan, 20, Fever
# 3. 运行测试 (异常流程)
./admit_patient_task2
# 输入示例:ErrorUser, 30, Cough
# 4. 查看进程状态 (任务 3 专用)
ps -p 进程ID -o pid,ppid,stat,cmd
8. 实验报告及提交要求
8.1 实验报告
请提交一份 PDF 格式 的实验报告,命名格式:学号_姓名_实验一.pdf。报告需包含以下内容:
- 核心代码:粘贴每个任务的关键代码段,并简要说明实现思路。
- 运行截图:按每个任务的实验要求截图,并配以文字说明(如:“图 1:任务二异常捕获测试”)。
- 问题与思考:
- 任务三中僵尸进程的状态及原因分析。
- 任务四中
execlp后语句执行情况的分析。 - 任务五中多进程并发的优缺点分析。
- 心得体会:实验过程中遇到的问题及解决方案(例如:
wait返回值含义的理解、输入缓冲区的处理等)。
8.2 实验代码
请提交以下源码文件(确保代码中有必要的注释):
admit_patient_task2.cadmit_patient_task4.cadmit_patient_task5.c(注:任务一和任务三的代码可包含在报告或任务二/四的代码版本中,无需单独提交,但需确保功能完整)
8.3 提交方式
请将实验报告(PDF)和代码文件打包为 .zip 或 .rar 格式,命名为 学号_姓名_实验一代码包,上传至头歌教学平台对应任务节点。
实验二:系统调用综合实验(内存管理与异步并发)
1. 实验背景
在实验一中,我们实现了多进程并发检查,但存在一个明显的瓶颈:主进程必须等待所有子进程结束(阻塞式 wait())才能接待下一位患者。这在现实医院中是不可接受的——接待员不可能站在原地等化验结果出来才叫下一个号。
此外,实验一中的进程间数据传递受限。为了解决这两个问题,本实验将引入操作系统的共享内存(Shared Memory)机制来实现高效数据交换,并利用非阻塞系统调用实现异步处理流程。
本实验模拟医院内部的高速数据总线与异步接待流程:
- 数据共享:多个进程直接读写同一块内存区域,实现“零拷贝”数据共享。
- 异步非阻塞:接待进程发送检查任务后,立即接待下一位患者,无需等待检查结果返回。
- 资源回收:通过轮询或信号机制,在后台清理已结束的子进程,防止僵尸进程。
2. 实验目的
- 理解虚拟内存隔离:验证普通变量无法在进程间共享,理解共享内存的必要性。
- 掌握内存映射(mmap):学会使用
mmap()创建进程间共享内存区域。 - 掌握非阻塞等待:熟练使用
waitpid(..., WNOHANG)实现非阻塞式的子进程状态检查。 - 异步编程思维:理解“发送即忘”(Fire-and-Forget)与“状态轮询”的异步处理模式。
- 内存安全意识:学习在共享内存中进行边界检查,防止缓冲区溢出。
3. 实验环境
| 项目 | 要求/版本 |
|---|---|
| 操作系统 | Ubuntu 22.04 LTS |
| 编译器 | GCC (GNU Compiler Collection) |
| 编辑器 | Vim / VS Code / Gedit |
| 终端工具 | Bash Shell |
4. 预备知识
mmap():将文件映射到内存。MAP_SHARED:共享映射,修改对其他进程可见。MAP_PRIVATE:私有映射,修改不影响其他进程。
waitpid(pid, &status, WNOHANG):- 普通
wait():如果没有子进程结束,父进程会阻塞(停在这里不动)。 WNOHANG:如果没有子进程结束,函数立即返回 0,父进程可以继续做其他事。
- 普通
- 共享数据结构:所有进程必须对共享内存中的结构体定义保持一致。
unlink():删除文件系统中的一个名字(用于清理共享内存文件)。
5. 实验内容与步骤
任务一:验证内存隔离性
任务描述:
在开发初期,有实习生试图在父进程中定义一个全局变量 int result,希望子进程修改它后,父进程能直接读到。我们需要通过实验证明为什么这种做法在普通多进程中是无效的,从而理解引入共享内存的必要性。
操作指引:
- 创建一个简单的 C 程序
medical_record_task1.c。 - 定义一个全局整型变量
shared_var = 0。 - 使用
fork()创建子进程。 - 在子进程中将
shared_var修改为 100,并打印子进程中的值。 - 在父进程中等待子进程结束后,打印父进程中的
shared_var值。 - 观察并记录:父进程看到的值变了吗?为什么?
💡 代码实现提示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int shared_var = 0; // 全局变量
int main() {
pid_t pid = fork();
if (pid == 0) {
// === 子进程 ===
// [TODO] 将 shared_var 修改为 100
// [TODO] 打印子进程中的 shared_var 值
exit(0);
} else if (pid > 0) {
// === 父进程 ===
wait(NULL); // 等待子进程结束
// [TODO] 打印父进程中的 shared_var 值
}
return 0;
}
实验要求:
- 保存代码为
medical_record_task1.c。- 截图运行结果,证明父子进程内存是隔离的。
- 在报告中简述“虚拟地址空间”的概念。
任务二:创建共享内存区域
任务描述:
为了解决任务一的问题,医院信息科决定建立一块“公共白板”(共享内存)。所有接诊进程和检查进程都可以在这块白板上写字,且彼此可见。我们将使用 mmap 基于文件创建这块共享区域。
操作指引:
- 补全框架文件
medical_record.c。 - 创建一个临时文件(如
patient_shm.dat),并扩展其大小(例如 4096 字节)。 - 调用
mmap()将该文件映射到内存,标志位使用MAP_SHARED。 - 将映射返回的指针强制转换为一个结构体指针(例如
PatientData *)。 - 在父进程中向该结构体写入患者姓名,然后
fork()创建子进程。 - 子进程直接读取该指针指向的姓名并打印,验证数据是否共享成功。
- 程序结束前,调用
munmap()解除映射,并删除临时文件。
实验要求:
- 截图证明子进程读取到了父进程写入的数据。
- 解释
MAP_SHARED参数的作用。
任务三:多进程并发写入共享数据
任务描述:
回到实验一的场景,现在我们有 3 个检查项目(血常规、尿常规、心电图)。我们希望这 3 个子进程将检查结果直接写入共享内存中的不同区域,而不是通过 exit 状态码返回简单的成功/失败。
操作指引:
- 基于任务二代码,扩展
PatientData结构体,增加一个字符数组results[3][100]用于存储 3 项检查的文本结果。 - 父进程映射内存后,循环
fork()创建 3 个子进程。 - 每个子进程根据索引
i(0, 1, 2),将模拟的检查结果(如“血常规:正常”)写入results[i]。 - 子进程写入完成后,更新
check_status字段(例如使用原子操作或简单标记)。 - 父进程等待所有子进程结束后,一次性从共享内存中读取并打印所有检查结果。
实验要求:
- 截图展示父进程读取到的 3 项详细检查结果。
- 对比实验一,说明这种方式在数据传输量上的优势。
任务四:内存安全与边界检查
任务描述: 医疗数据的安全性至关重要。如果某个检查设备发生故障,向共享内存写入了超长的错误日志,可能会导致缓冲区溢出,覆盖掉其他患者的数据甚至导致程序崩溃。我们需要在代码中加入防护机制。
操作指引:
- 在任务三的基础上,模拟一个“故障子进程”。
- 尝试使用
strcpy()将一个长度超过 100 字符的字符串写入results[i]。 - 观察程序是否崩溃或数据是否被破坏。
- 改进代码,使用
strncpy()或手动检查长度,确保写入数据不超过缓冲区大小。 - 在写入前增加判断:
if (len >= buffer_size) { 处理错误 }。
实验要求:
- 保存代码为
medical_record_task4.c。- 截图展示未加保护时的异常现象(如有)和加保护后的正常提示。
- 在报告中简述“缓冲区溢出”对医疗系统的潜在风险。
任务五(扩展):异步非阻塞处理与状态轮询
任务描述: 在现实医院中,接待员(父进程)将化验单交给检验科(子进程)后,会立即叫号下一位患者,而不会站在原地等结果。我们需要修改程序,使父进程不再阻塞等待子进程结束,而是通过“轮询”方式在后台检查任务状态。
操作指引:
- 基于任务四代码,在父进程创建完检查子进程后,不要立即调用
wait()或waitpid()阻塞等待。 - 父进程打印“请下一位患者就诊”,并继续循环接受输入(模拟接待新患者)。
- 非阻塞状态检查:在接待新患者的间隙,使用
waitpid(pid, &status, WNOHANG)检查之前的子进程是否结束。- 如果返回 0:说明子进程还在运行,父进程继续接待工作。
- 如果返回 PID:说明子进程已结束,父进程记录结果并清理资源。
- 运行程序,连续输入两位患者信息。观察是否能在第一位患者的检查结果出来之前,就开始处理第二位患者的信息。
💡 代码实现提示:
// 父进程主循环中
pid_t check_pids[3];
// ... fork 创建子进程后 ...
// 不要在这里 wait()!直接继续循环
printf(">> 接待员:检查单已送出,请下一位患者!\n");
// 在循环的空闲时间或下次循环开始时检查状态
for (int i = 0; i < 3; i++) {
// WNOHANG 表示非阻塞:如果没有结束,立即返回 0
int ret = waitpid(check_pids[i], &status, WNOHANG);
if (ret > 0) {
printf(">> 系统通知:第 %d 项检查已完成\n", i);
// 从共享内存读取结果...
}
}
实验要求:
- 保存代码为
medical_record_task5.c。- 截图展示“接待下一位”提示出现在“检查完成”提示之前的现象。
- 思考:如果父进程一直不调用
wait系列函数,子进程结束后会变成什么状态?本实验中的waitpid(..., WNOHANG)是如何避免这个问题的?
6. 代码框架 (medical_record.c)
请在以下框架基础上完成任务二。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>
// 定义共享数据结构
typedef struct {
char name[50];
} PatientData;
int main() {
const char *filename = "patient_shm.dat";
int fd;
PatientData *ptr;
// [TODO 1] 使用 open 创建或打开文件,权限设为 0666
// 提示:标志位使用 O_RDWR | O_CREAT
// 请在此处编写 open 代码,并将返回值赋给 fd
// fd = open(...);
if (fd == -1) {
perror("open failed");
return 1;
}
// [TODO 2] 使用 ftruncate 设置文件大小为 4096 字节
// 提示:ftruncate(fd, length)
// 请在此处编写 ftruncate 代码
// ftruncate(...);
// [TODO 3] 使用 mmap 创建共享内存映射
// 提示:参数依次为 (NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
// 请在此处编写 mmap 代码,并将返回值赋给 ptr
// ptr = mmap(...);
if (ptr == MAP_FAILED) {
perror("mmap failed");
return 1;
}
// 父进程写入数据
strcpy(ptr->name, "ZhangSan");
printf("父进程:已写入姓名 %s\n", ptr->name);
pid_t pid = fork();
if (pid == 0) {
// === 子进程 ===
// [TODO 4] 读取并打印共享内存中的姓名
// 提示:直接访问 ptr->name
// 请在此处编写 printf 代码
// printf(...);
_exit(0);
} else if (pid > 0) {
// === 父进程 ===
wait(NULL); // 等待子进程结束
// [TODO 5] 清理资源 (munmap, close, unlink)
// 提示:顺序解除映射 -> 关闭文件 -> 删除文件
// 请在此处编写清理代码
// munmap(...);
// close(...);
// unlink(...);
} else {
perror("fork failed");
return 1;
}
return 0;
}
7. 编译、运行与测试
# 1. 编译 (以任务五为例)
gcc -o medical_record_task5 medical_record_task5.c
# 2. 运行测试
./medical_record_task5
# 3. 测试流程
# 输入患者 1 姓名 -> 观察是否立即提示"请下一位"
# 输入患者 2 姓名 -> 观察期间是否有患者 1 的检查结果弹出
8. 实验报告及提交要求
8.1 实验报告
请提交一份 PDF 格式 的实验报告,命名格式:学号_姓名_实验二.pdf。报告需包含以下内容:
- 核心代码:粘贴每个任务的关键代码段(特别是
mmap和waitpid部分)。 - 运行截图:
- 任务一:内存隔离验证结果。
- 任务二/三:共享内存读写成功截图。
- 任务五(可选):关键截图,展示“请下一位患者”提示出现在“检查完成”提示之前。
- 问题与思考:
- 对比实验一,共享内存在大数据传输场景下有什么优势?
- 任务五中,
WNOHANG参数起到了什么作用?如果没有它,系统会变成什么样? - 思考:如果多个进程同时写入共享内存的同一位置会发生什么?(提示:竞态条件,简述即可)。
- 心得体会:对操作系统“虚拟内存”与“异步并发”关系的理解。
8.2 实验代码
请提交以下源码文件(确保代码中有必要的注释):
medical_record_task4.c- (可选)
medical_record_task5.c
8.3 提交方式
请将实验报告(PDF)和代码文件打包为 .zip 或 .rar 格式,命名为 学号_姓名_实验二代码包,上传至头歌教学平台对应任务节点。