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(推荐,简单稳定)

  1. 访问 VMware 官网下载页面:https://www.vmware.com/products/workstation-player.html
  2. 点击「Download Free」下载 Windows 或 Linux 版本
  3. 下载完成后双击安装,一路「Next」即可完成

方案B:VirtualBox(开源免费)

  1. 访问:https://www.virtualbox.org/wiki/Downloads
  2. 下载对应系统的安装包,双击安装

三、下载 Ubuntu 22.04 镜像

  1. 访问清华大学开源镜像站(国内下载快): https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/22.04/

  2. 选择下载:ubuntu-22.04.5-desktop-amd64.iso(约4.5GB)


四、创建 Ubuntu 虚拟机

以 VMware Workstation Player 为例:

  1. 打开 VMware,点击 「Create a New Virtual Machine」

  2. 选择 「Installer disc image file (iso)」,点击 Browse 选择刚下载的 ubuntu-22.04.5-desktop-amd64.iso

  3. 填写简易信息:

    • Full name: 学号姓名拼音
    • User name: 学号
    • Password: 设置一个易记密码(如 123456)
    • Confirm: 重复密码
  4. 虚拟机命名和位置:

    • Virtual machine name: Ubuntu22.04-OS-Lab
    • Location: 选择一个有 50GB 以上空闲空间的磁盘位置
  5. 磁盘容量设置:

    • Maximum disk size: 40 GB(建议)
    • 选择 「Store virtual disk as a single file」
  6. 点击 「Customize Hardware」 调整配置:

    • Memory: 4 GB(如果宿主机内存 16GB 以上,可设 8GB)
    • Number of processors: 2
    • 点击 Close,然后 Finish
  7. 虚拟机将自动启动并进入 Ubuntu 安装程序


五、安装 Ubuntu 系统

  1. 虚拟机启动后,在左侧语言列表选择 「中文(简体)」「English」(推荐用英文避免路径乱码)

  2. 点击 「Install Ubuntu」

  3. 键盘布局选择 「Chinese」「Chinese (Hanyu Pinyin)」

  4. 安装类型选择 「Normal installation」,勾选:

    • ☑ Download updates while installing Ubuntu
    • ☑ Install third-party software for graphics and Wi-Fi
  5. 磁盘分区选择 「Erase disk and install Ubuntu」(这是虚拟磁盘,不会影响宿主机)

  6. 设置时区:在地图上点击中国区域,或搜索 「Shanghai」

  7. 设置用户信息(若前面未设置):

    • Your name: 真实姓名或学号
    • Computer's name: oslab-vm
    • Username: 学号小写
    • Password: 设置并牢记
  8. 等待安装完成(约15-20分钟),点击 「Restart Now」

  9. 重启后看到登录界面,输入密码进入桌面


六、配置开发环境(重要)

打开终端: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程序

  1. 创建并打开文件:
cd ~/os-lab/exp1-process-fork
vim hello.c
  1. i 进入编辑模式,输入:
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("Hello from PID: %d\n", getpid());
    return 0;
}
  1. Esc,输入 :wq 保存退出

  2. 编译运行:

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. 实验目的

  1. 掌握 fork() 系统调用:理解父子进程的代码执行流差异及返回值含义。
  2. 理解进程状态:直观观察进程的运行、终止及僵尸(Zombie)状态。
  3. 掌握进程同步与回收:熟练使用 wait()waitpid() 回收子进程资源,避免资源泄漏。
  4. 了解进程映像替换:初步掌握 execlp() 的作用及执行流程。
  5. 提升错误处理能力:学会通过子进程退出码(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. 实验内容与步骤

任务一:基础进程创建与建档

任务描述: 医院接待处刚刚启动,第一位患者到达前台。为了确保数据录入的安全性与隔离性,系统不能直接在主程序中处理病历,而是需要“分身”出一个专门的子进程来负责该患者的建档工作。你需要实现这个最基础的“分身”功能,并确认父子进程的身份关系。

操作指引

  1. 请在提供的代码框架 admit_patient.c 中,找到子进程和父进程的逻辑区域,补全缺失的代码。
  2. 让子进程在控制台打印出自己的进程号(PID)以及父进程的进程号(PPID),以此来验证它们之间的父子关系是否正确。
  3. 随后,子进程调用 generate_medical_record() 函数,模拟耗时的病历生成过程,并打印患者信息。
  4. 最后,编译并运行程序,输入一位正常患者的信息(例如姓名:ZhangSan),观察控制台输出的进程 ID 信息及病历内容。

实验要求

  • 按步骤实现功能,确保父子进程 PID 关系正确。
  • 在实验报告中粘贴关键代码段并说明思路。
  • 对运行输出结果进行截图。

任务二:异常处理与退出码捕获

任务描述: 在实际工作中,患者可能会输入非法信息(例如姓名中包含敏感字符)。如果子进程遇到错误直接崩溃,父进程可能无从知晓。因此,我们需要建立一种“汇报机制”:子进程遇到错误时通过退出码告诉父进程,父进程根据退出码决定是记录成功还是记录错误日志。

操作指引

  1. 基于任务一的代码,新建文件 admit_patient_task2.c,准备增加异常判断逻辑。
  2. 在子进程区域增加判断:检查输入的姓名是否包含 "Error" 字符串。如果包含,说明信息非法,子进程调用 exit(1) 模拟录入失败;否则正常调用 exit(0)
  3. 回到父进程区域,使用 wait() 函数等待子进程结束,并获取其状态值。通过宏判断退出码,如果非 0 则打印 “建档失败,已记录错误日志”,否则打印 “建档成功”
  4. 运行程序,故意输入姓名 ErrorUser,验证父进程是否能正确捕获到子进程的异常状态并给出相应提示。

实验要求

  • 保存代码为 admit_patient_task2.c
  • 在报告中粘贴关键代码段(特别是 waitexit 部分)。
  • 对正常和异常两种情况的测试结果进行截图。

任务三:僵尸进程观察

任务描述: 系统运行久了之后,如果父进程只管创建子进程却不关心子进程的“后事”(资源回收),子进程结束后就会变成“僵尸进程”,长期占用系统进程表项。我们需要通过实验亲眼看看僵尸进程是什么样子的,以及如何让它们消失。

操作指引

  1. 暂时注释掉父进程中的 wait() 代码,并在子进程结束后让父进程执行 sleep(30),保持运行状态一段时间。
  2. 编译运行程序,输入正常信息。然后在另一个终端窗口使用 ps 命令查看进程状态。观察子进程结束后的状态栏(STAT)显示什么字符,思考如果父进程一直不调用 wait() 会发生什么。
  3. 取消注释 wait() 代码,重新编译运行,再次观察进程状态的变化,对比两次实验结果的差异。

实验要求

  • 对子进程在“有 wait"和“无 wait"两种情况下的状态栏显示结果进行截图。
  • 在报告中回答思考题(关于僵尸进程的现象及原因)。

任务四:进程映像替换

任务描述: 为了提升安全性,医院决定不再使用内部函数处理病历备份,而是调用一个独立的、受信任的外部系统命令来完成“云端备份”。这意味着子进程不再执行原来的 C 代码,而是完全替换成另一个程序的执行流。

操作指引

  1. 基于任务二代码,新建文件 admit_patient_task4.c。在子进程区域,注释掉原有的 generate_medical_record(p) 函数调用。
  2. 调用 execlp() 函数执行 Linux 系统命令(如 echo)来模拟“云端备份”。参数设置为命令名 echo,消息内容 "病历已备份至云端",并以 NULL 结尾。
  3. execlp() 代码行的后面,添加一行 printf 语句(例如:“错误:备份工具启动失败”)。运行程序思考:如果 execlp 执行成功,这条打印语句会执行吗?为什么?
  4. 尝试将命令改为一个不存在的命令(如 fake_command),观察程序的反应,验证错误捕获机制。

实验要求

  • 保存代码为 admit_patient_task4.c
  • 在报告中粘贴关键代码段。
  • 对执行结果进行截图。
  • 回答关于 execlp 执行流程的思考题。

任务五(扩展):并发检查功能

任务描述: 患者入院后往往需要同时进行多项医学检查(如血常规、尿常规、心电图)。如果串行处理会浪费大量时间,系统需要利用多进程技术,让这三项检查同时开始进行,以提升就诊效率。

操作指引

  1. 基于任务一代码,新建文件 admit_patient_task5.c。在建档完成后,你需要创建一个循环,利用 fork() 连续创建 3 个子进程。
  2. 每个子进程负责模拟一项检查,打印项目名称和 PID,睡眠片刻模拟耗时,然后退出。
  3. 父进程需要等待所有检查完成,使用循环调用 wait() 回收资源,最后输出汇总信息。
  4. 为了方便你实现,可以参考下方的代码提示结构,将其融入你的程序中。

💡 代码实现提示

// 在子进程完成建档后,父进程继续执行以下逻辑

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");
  1. 观察输出结果,思考这种多进程并发实现相比串行处理有什么优缺点。

实验要求

  • 保存代码为 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. 核心代码:粘贴每个任务的关键代码段,并简要说明实现思路。
  2. 运行截图:按每个任务的实验要求截图,并配以文字说明(如:“图 1:任务二异常捕获测试”)。
  3. 问题与思考
    • 任务三中僵尸进程的状态及原因分析。
    • 任务四中 execlp 后语句执行情况的分析。
    • 任务五中多进程并发的优缺点分析。
  4. 心得体会:实验过程中遇到的问题及解决方案(例如:wait 返回值含义的理解、输入缓冲区的处理等)。

8.2 实验代码

请提交以下源码文件(确保代码中有必要的注释):

  • admit_patient_task2.c
  • admit_patient_task4.c
  • admit_patient_task5.c (注:任务一和任务三的代码可包含在报告或任务二/四的代码版本中,无需单独提交,但需确保功能完整)

8.3 提交方式

请将实验报告(PDF)和代码文件打包为 .zip.rar 格式,命名为 学号_姓名_实验一代码包,上传至头歌教学平台对应任务节点。


实验二:系统调用综合实验(内存管理与异步并发)


1. 实验背景

实验一中,我们实现了多进程并发检查,但存在一个明显的瓶颈:主进程必须等待所有子进程结束(阻塞式 wait())才能接待下一位患者。这在现实医院中是不可接受的——接待员不可能站在原地等化验结果出来才叫下一个号。

此外,实验一中的进程间数据传递受限。为了解决这两个问题,本实验将引入操作系统的共享内存(Shared Memory)机制来实现高效数据交换,并利用非阻塞系统调用实现异步处理流程。

本实验模拟医院内部的高速数据总线与异步接待流程:

  • 数据共享:多个进程直接读写同一块内存区域,实现“零拷贝”数据共享。
  • 异步非阻塞:接待进程发送检查任务后,立即接待下一位患者,无需等待检查结果返回。
  • 资源回收:通过轮询或信号机制,在后台清理已结束的子进程,防止僵尸进程。

2. 实验目的

  1. 理解虚拟内存隔离:验证普通变量无法在进程间共享,理解共享内存的必要性。
  2. 掌握内存映射(mmap):学会使用 mmap() 创建进程间共享内存区域。
  3. 掌握非阻塞等待:熟练使用 waitpid(..., WNOHANG) 实现非阻塞式的子进程状态检查。
  4. 异步编程思维:理解“发送即忘”(Fire-and-Forget)与“状态轮询”的异步处理模式。
  5. 内存安全意识:学习在共享内存中进行边界检查,防止缓冲区溢出。

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,希望子进程修改它后,父进程能直接读到。我们需要通过实验证明为什么这种做法在普通多进程中是无效的,从而理解引入共享内存的必要性。

操作指引

  1. 创建一个简单的 C 程序 medical_record_task1.c
  2. 定义一个全局整型变量 shared_var = 0
  3. 使用 fork() 创建子进程。
  4. 在子进程中将 shared_var 修改为 100,并打印子进程中的值。
  5. 在父进程中等待子进程结束后,打印父进程中的 shared_var 值。
  6. 观察并记录:父进程看到的值变了吗?为什么?

💡 代码实现提示

#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 基于文件创建这块共享区域。

操作指引

  1. 补全框架文件 medical_record.c
  2. 创建一个临时文件(如 patient_shm.dat),并扩展其大小(例如 4096 字节)。
  3. 调用 mmap() 将该文件映射到内存,标志位使用 MAP_SHARED
  4. 将映射返回的指针强制转换为一个结构体指针(例如 PatientData *)。
  5. 在父进程中向该结构体写入患者姓名,然后 fork() 创建子进程。
  6. 子进程直接读取该指针指向的姓名并打印,验证数据是否共享成功。
  7. 程序结束前,调用 munmap() 解除映射,并删除临时文件。

实验要求

  • 截图证明子进程读取到了父进程写入的数据。
  • 解释 MAP_SHARED 参数的作用。

任务三:多进程并发写入共享数据

任务描述: 回到实验一的场景,现在我们有 3 个检查项目(血常规、尿常规、心电图)。我们希望这 3 个子进程将检查结果直接写入共享内存中的不同区域,而不是通过 exit 状态码返回简单的成功/失败。

操作指引

  1. 基于任务二代码,扩展 PatientData 结构体,增加一个字符数组 results[3][100] 用于存储 3 项检查的文本结果。
  2. 父进程映射内存后,循环 fork() 创建 3 个子进程。
  3. 每个子进程根据索引 i (0, 1, 2),将模拟的检查结果(如“血常规:正常”)写入 results[i]
  4. 子进程写入完成后,更新 check_status 字段(例如使用原子操作或简单标记)。
  5. 父进程等待所有子进程结束后,一次性从共享内存中读取并打印所有检查结果。

实验要求

  • 截图展示父进程读取到的 3 项详细检查结果。
  • 对比实验一,说明这种方式在数据传输量上的优势。

任务四:内存安全与边界检查

任务描述: 医疗数据的安全性至关重要。如果某个检查设备发生故障,向共享内存写入了超长的错误日志,可能会导致缓冲区溢出,覆盖掉其他患者的数据甚至导致程序崩溃。我们需要在代码中加入防护机制。

操作指引

  1. 在任务三的基础上,模拟一个“故障子进程”。
  2. 尝试使用 strcpy() 将一个长度超过 100 字符的字符串写入 results[i]
  3. 观察程序是否崩溃或数据是否被破坏。
  4. 改进代码,使用 strncpy() 或手动检查长度,确保写入数据不超过缓冲区大小。
  5. 在写入前增加判断:if (len >= buffer_size) { 处理错误 }

实验要求

  • 保存代码为 medical_record_task4.c
  • 截图展示未加保护时的异常现象(如有)和加保护后的正常提示。
  • 在报告中简述“缓冲区溢出”对医疗系统的潜在风险。

任务五(扩展):异步非阻塞处理与状态轮询

任务描述: 在现实医院中,接待员(父进程)将化验单交给检验科(子进程)后,会立即叫号下一位患者,而不会站在原地等结果。我们需要修改程序,使父进程不再阻塞等待子进程结束,而是通过“轮询”方式在后台检查任务状态。

操作指引

  1. 基于任务四代码,在父进程创建完检查子进程后,不要立即调用 wait()waitpid() 阻塞等待。
  2. 父进程打印“请下一位患者就诊”,并继续循环接受输入(模拟接待新患者)。
  3. 非阻塞状态检查:在接待新患者的间隙,使用 waitpid(pid, &status, WNOHANG) 检查之前的子进程是否结束。
    • 如果返回 0:说明子进程还在运行,父进程继续接待工作。
    • 如果返回 PID:说明子进程已结束,父进程记录结果并清理资源。
  4. 运行程序,连续输入两位患者信息。观察是否能在第一位患者的检查结果出来之前,就开始处理第二位患者的信息。

💡 代码实现提示

// 父进程主循环中
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。报告需包含以下内容:

  1. 核心代码:粘贴每个任务的关键代码段(特别是 mmapwaitpid 部分)。
  2. 运行截图
    • 任务一:内存隔离验证结果。
    • 任务二/三:共享内存读写成功截图。
    • 任务五(可选):关键截图,展示“请下一位患者”提示出现在“检查完成”提示之前。
  3. 问题与思考
    • 对比实验一,共享内存在大数据传输场景下有什么优势?
    • 任务五中,WNOHANG 参数起到了什么作用?如果没有它,系统会变成什么样?
    • 思考:如果多个进程同时写入共享内存的同一位置会发生什么?(提示:竞态条件,简述即可)。
  4. 心得体会:对操作系统“虚拟内存”与“异步并发”关系的理解。

8.2 实验代码

请提交以下源码文件(确保代码中有必要的注释):

  • medical_record_task4.c
  • (可选)medical_record_task5.c

8.3 提交方式

请将实验报告(PDF)和代码文件打包为 .zip.rar 格式,命名为 学号_姓名_实验二代码包,上传至头歌教学平台对应任务节点。