实验一:系统调用综合实验(进程管理)


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 格式,命名为 学号_姓名_实验一代码包,上传至头歌教学平台对应任务节点。