实验一:系统调用综合实验(进程管理)
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 格式,命名为 学号_姓名_实验一代码包,上传至头歌教学平台对应任务节点。