Files
obsidian/操作系统/06_进程控制/06_进程控制.md

869 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第06讲进程控制
> **本节目标**:理解进程的概念与生命周期,掌握 `fork()`、`exec()`、`wait()`、`exit()` 等进程控制系统调用,理解信号机制与僵尸进程,最终能实现一个简易 shell 和 daemon 进程。
## 前置知识
- [[03_C语言编程基础]] -- C 语言指针、数组、字符串操作
- [[07_多线程编程]] -- 线程与进程的对比
---
## 一、进程概念
### 1.1 什么是进程
**进程Process** 是程序的一次执行实例。程序是静态的代码文件,进程是动态的执行过程。当操作系统将程序加载到内存并开始执行时,就创建了一个进程。
每个进程拥有独立的地址空间,包含以下组成部分:
```mermaid
graph TD
P[进程] --> T[代码段 Text]
P --> D[数据段 Data]
P --> H[堆 Heap]
P --> S[栈 Stack]
P --> PCB[PCB 进程控制块]
T -->|存放| T1[程序指令]
D -->|存放| D1[全局变量/静态变量]
H -->|动态分配| H1[malloc/free]
S -->|自动管理| S1[局部变量/函数调用]
PCB -->|记录| PCB1[PID/状态/寄存器/文件描述符]
style P fill:#fff3e0
style PCB fill:#e1f5fe
```
### 1.2 进程 vs 程序
| 对比维度 | 程序Program | 进程Process |
|----------|-----------------|-----------------|
| 本质 | 静态的代码文件 | 动态的执行过程 |
| 存储位置 | 硬盘 | 内存 |
| 生命周期 | 长期保存 | 有创建、运行、终止 |
| 对应关系 | 一个程序可对应多个进程 | 一个进程只对应一个程序 |
| 包含内容 | 代码 + 数据 | 代码 + 数据 + 堆栈 + PCB |
### 1.3 进程状态
```mermaid
stateDiagram-v2
[*] --> 创建: fork()
创建 --> 就绪: 分配资源
就绪 --> 运行: 调度器选中
运行 --> 就绪: 时间片用完
运行 --> 阻塞: 等待I/O
阻塞 --> 就绪: I/O完成
运行 --> 终止: exit()
终止 --> [*]
就绪: Ready
运行: Running
阻塞: Blocked/Waiting
终止: Terminated/Zombie
```
### 1.4 进程标识
每个进程都有唯一的 **PID**Process ID。常用函数
```c
pid_t getpid(); // 获取当前进程的 PID
pid_t getppid(); // 获取父进程的 PID
```
---
## 二、fork() -- 创建子进程
### 2.1 基本原理
`fork()` 是 Linux 中创建新进程的唯一方式。调用 `fork()` 后,操作系统会创建一个与父进程几乎完全相同的子进程。子进程获得父进程的代码段、数据段、堆栈的**副本**。
**关键特性**`fork()` 被调用一次,但**返回两次**
- 父进程中返回子进程的 PID正整数
- 子进程中返回 0
- 如果出错返回 -1
```mermaid
graph TD
A["父进程调用 fork()"] --> B{操作系统创建子进程}
B --> C["父进程:返回子进程 PID>0"]
B --> D["子进程:返回 0"]
C --> E["父子进程各自独立执行"]
D --> E
style A fill:#e1f5fe
style C fill:#fff3e0
style D fill:#e8f5e9
```
### 2.2 fork 基础示例
参考 `实例源代码/chap5/fork1.c`
```c
#include "wrapper.h"
int main()
{
pid_t pid;
int x = 1;
pid = fork();
if (pid == 0) { // 子进程执行这段代码
x = x + 1;
printf("child: x=%d\n", x); // 输出 child: x=2
}
if (pid > 0) { // 父进程执行这段代码
x = x - 1;
printf("parent: x=%d\n", x); // 输出 parent: x=0
}
sleep(10); // 让父子进程都执行完代码
}
```
**关键理解**
- `fork()` 后,父子进程拥有**独立的变量副本**
- 子进程修改 `x` 不影响父进程的 `x`,反之亦然
- 父子进程的执行顺序是不确定的,取决于调度器
### 2.3 多次 fork 的进程数计算
参考 `实例源代码/chap5/fork3.c`
```c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
fork(); // 第1次2个进程
fork(); // 第2次4个进程
fork(); // 第3次8个进程
printf("hello \n");
sleep(10);
return;
}
```
**进程树分析**n 次 `fork()` 会产生 2^n 个进程。上面的代码执行 3 次 `fork()`,最终产生 2^3 = 8 个进程,每个进程打印一次 "hello"。
```mermaid
graph TD
P0["P0 (原始进程)"] -->|第1次fork| P1["P1"]
P0 -->|继续| P0a["P0"]
P0a -->|第2次fork| P2["P2"]
P0a -->|继续| P0b["P0"]
P1 -->|第2次fork| P3["P3"]
P1 -->|继续| P1a["P1"]
P0b -->|第3次fork| P4["P4"]
P1a -->|第3次fork| P5["P5"]
P2 -->|第3次fork| P6["P6"]
P3 -->|第3次fork| P7["P7"]
style P0 fill:#ffcdd2
style P1 fill:#e1f5fe
style P2 fill:#e1f5fe
style P3 fill:#e1f5fe
style P4 fill:#e8f5e9
style P5 fill:#e8f5e9
style P6 fill:#e8f5e9
style P7 fill:#e8f5e9
```
### 2.4 fork 的复杂示例
参考 `实例源代码/chap5/fork2.c`
```c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
int pid;
pid = fork(); // 创建 P1
pid = fork(); // P0 和 P1 各创建一个子进程
if (pid > 0) fork(); // pid>0 的进程再创建一个子进程
printf("hello \n");
exit(0);
}
```
**分析**:需要追踪每个进程的 `pid` 值来确定哪些进程会执行第三次 `fork()`。最终打印 6 个 "hello"。
### 2.5 fork 的注意事项
| 要点 | 说明 |
|------|------|
| 返回值判断 | 必须用 `if-else` 分别处理父子进程 |
| 资源复制 | 子进程获得父进程的副本(写时复制 COW 优化) |
| 执行顺序 | 父子进程执行顺序不确定 |
| 文件描述符 | 子进程继承父进程打开的文件描述符 |
| 必须 exit | 子进程处理完后必须调用 `exit()` 终止 |
---
## 三、exec 族函数 -- 替换进程映像
### 3.1 基本原理
`exec` 族函数用一个新的程序**替换**当前进程的代码段、数据段和堆栈。进程的 PID 不变,但执行的代码完全改变。`exec` 调用成功后**不会返回**,只有出错时才返回 -1。
```mermaid
graph LR
A["当前进程<br/>PID=1234<br/>执行 myprogram"] -->|execvp("ps", args)| B["同一进程<br/>PID=1234<br/>执行 ps 命令"]
B --> C["原来的代码段<br/>被完全替换"]
style A fill:#e1f5fe
style B fill:#e8f5e9
```
### 3.2 exec 族函数对比
| 函数 | 参数形式 | 路径搜索 | 环境变量 |
|------|----------|----------|----------|
| `execl(path, arg0, ..., NULL)` | 列表 | 完整路径 | 继承父进程 |
| `execlp(file, arg0, ..., NULL)` | 列表 | 搜索 PATH | 继承父进程 |
| `execle(path, arg0, ..., NULL, envp)` | 列表 | 完整路径 | 自定义 |
| `execv(path, argv)` | 数组 | 完整路径 | 继承父进程 |
| `execvp(file, argv)` | 数组 | 搜索 PATH | 继承父进程 |
**记忆技巧**
- `l` = list参数列表`v` = vector参数数组
- `p` = path搜索 PATH 环境变量),`e` = environment自定义环境变量
### 3.3 exec 示例
参考 `实例源代码/chap5/exec1.c`
```c
#include "wrapper.h"
int main(void)
{
char *arg[] = {"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};
execvp("ps", arg); // 用 ps 命令替换当前进程
perror("exec ps"); // 如果 execvp 返回,说明出错
exit(1);
}
```
**运行结果**:程序执行后变成了 `ps` 命令,显示当前系统的进程信息。
### 3.4 fork + exec 组合
在实际应用中,`fork()``exec()` 通常配合使用:
```mermaid
sequenceDiagram
participant 父进程
participant 子进程
participant 新程序
父进程->>子进程: fork()
Note over 子进程: 子进程是父进程的副本
子进程->>新程序: execvp("ls", args)
Note over 新程序: 子进程的代码被 ls 替换
新程序-->>父进程: 执行完毕
父进程->>父进程: wait() 回收子进程
```
这种模式是 shell 执行命令的核心机制:先 `fork` 创建子进程,再在子进程中 `exec` 执行新程序。
---
## 四、wait/waitpid -- 等待子进程
### 4.1 为什么需要 wait
父进程创建子进程后,需要等待子进程结束并回收其资源。如果不调用 `wait`,子进程终止后会变成**僵尸进程**(详见第六节)。
### 4.2 wait 与 waitpid
| 函数 | 说明 |
|------|------|
| `wait(&status)` | 等待任意一个子进程结束 |
| `waitpid(pid, &status, options)` | 可指定等待的子进程 |
| `WIFEXITED(status)` | 判断子进程是否正常退出 |
| `WEXITSTATUS(status)` | 获取子进程的退出状态 |
**waitpid 的 pid 参数**
- `pid > 0`:等待指定 PID 的子进程
- `pid = -1`:等待任意子进程(等同于 `wait`
- `pid = 0`:等待同一进程组的任意子进程
- `pid < -1`:等待进程组 ID 为 |pid| 的任意子进程
### 4.3 waitpid 示例
参考 `实例源代码/chap5/waitpid1.c`
```c
#include "wrapper.h"
#define N 2
int main()
{
int status, i;
pid_t pid;
// 父进程创建 N 个子进程
for (i = 0; i < 2; i++)
if ((pid = fork()) == 0) // 子进程
exit(100 + i); // 以不同状态退出
// 父进程按任意顺序等待所有子进程
while ((pid = waitpid(-1, &status, 0)) > 0) {
if (WIFEXITED(status))
printf("child %d terminated normally with exit status=%d\n",
pid, WEXITSTATUS(status));
else
printf("child %d terminated abnormally\n", pid);
}
// 所有子进程已结束waitpid 返回 -1errno 为 ECHILD
if (errno != ECHILD)
perror("waitpid error");
exit(0);
}
```
**输出示例**
```
child 1235 terminated normally with exit status=100
child 1236 terminated normally with exit status=101
```
---
## 五、exit/_exit -- 进程终止
### 5.1 两种终止方式
| 函数 | 头文件 | 行为 |
|------|--------|------|
| `exit(status)` | `<stdlib.h>` | 执行清理工作(刷新缓冲区、调用 atexit 注册的函数),然后终止 |
| `_exit(status)` | `<unistd.h>` | 立即终止,不执行任何清理 |
### 5.2 exit 的清理过程
```mermaid
graph TD
A["调用 exit(status)"] --> B["执行 atexit() 注册的清理函数"]
B --> C["刷新 stdio 缓冲区<br/>fclose 所有打开的流)"]
C --> D["调用 _exit(status)"]
D --> E["内核回收进程资源"]
style A fill:#e1f5fe
style E fill:#ffcdd2
```
### 5.3 退出状态
参考 `实例源代码/chap5/exitstatus.c`
```c
#include <stdlib.h>
int main() { exit(100); }
```
父进程通过 `waitpid``WEXITSTATUS(status)` 宏获取子进程的退出状态值0-255
---
## 六、僵尸进程
### 6.1 什么是僵尸进程
当子进程终止后,如果父进程没有调用 `wait()``waitpid()` 回收子进程的退出状态子进程的进程控制块PCB仍然保留在系统中成为**僵尸进程Zombie Process**。
### 6.2 僵尸进程的产生
参考 `实例源代码/chap5/zombie.c`
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork failed");
exit(1);
} else if (pid == 0) {
printf("child...\n"); // 子进程打印后退出
} else {
printf("parent...\n");
while (1); // 父进程不调用 wait(),子进程成为僵尸
}
return 0;
}
```
### 6.3 僵尸进程的生命周期
```mermaid
sequenceDiagram
participant 父进程
participant 子进程
participant 操作系统内核
父进程->>子进程: fork() 创建子进程
子进程->>子进程: 执行任务
子进程->>操作系统内核: exit() 终止
操作系统内核->>父进程: 发送 SIGCHLD 信号
Note over 父进程: 父进程没有调用 wait()
Note over 操作系统内核: 子进程的 PCB 无法释放
Note over 操作系统内核: 子进程成为僵尸进程
Note over 操作系统内核: 僵尸进程占用 PID 资源
```
### 6.4 僵尸进程的危害与解决
| 危害 | 说明 |
|------|------|
| 占用 PID | 僵尸进程的 PID 无法被复用 |
| 资源泄漏 | 虽然不占内存,但 PCB 信息占用内核资源 |
| 累积效应 | 大量僵尸进程可耗尽 PID 资源 |
**解决方法**
1. 父进程调用 `wait()``waitpid()` 回收子进程
2. 注册 `SIGCHLD` 信号处理函数,在其中调用 `waitpid()`
3. 父进程先退出,让 init 进程PID=1自动回收孤儿进程
---
## 七、信号机制
### 7.1 信号的概念
信号是操作系统通知进程发生了某种事件的一种**异步通信机制**。进程可以注册信号处理函数来响应特定信号。
```mermaid
graph LR
SRC["信号来源"] --> SIG["信号"]
SIG --> PROC["目标进程"]
SRC --> S1["用户输入 Ctrl+C"]
SRC --> S2["kill 命令"]
SRC --> S3["子进程终止"]
SRC --> S4["定时器超时"]
SRC --> S5["非法内存访问"]
PROC --> H1["执行信号处理函数"]
PROC --> H2["忽略信号"]
PROC --> H3["执行默认动作"]
style SIG fill:#fff3e0
style SRC fill:#e1f5fe
style PROC fill:#e8f5e9
```
### 7.2 常见信号
| 信号 | 编号 | 默认动作 | 说明 |
|------|:----:|----------|------|
| `SIGINT` | 2 | 终止 | 用户按 Ctrl+C |
| `SIGKILL` | 9 | 终止 | 强制终止(**不可捕获** |
| `SIGTERM` | 15 | 终止 | 请求终止(可捕获) |
| `SIGCHLD` | 17 | 忽略 | 子进程状态改变 |
| `SIGALRM` | 14 | 终止 | 定时器超时 |
| `SIGSEGV` | 11 | 终止+core | 段错误 |
| `SIGSTOP` | 19 | 停止 | 暂停进程(**不可捕获** |
| `SIGCONT` | 18 | 继续 | 恢复被暂停的进程 |
### 7.3 signal() 注册信号处理函数
参考 `实例源代码/chap5/signal1.c`
```c
#include "wrapper.h"
void handler1(int sig)
{
pid_t pid;
if ((pid = waitpid(-1, NULL, 0)) < 0)
perror("waitpid error");
printf("Handler reaped child %d\n", (int)pid);
sleep(2);
return;
}
int main()
{
int i, n;
char buf[MAXBUF];
// 注册 SIGCHLD 信号处理函数
if (signal(SIGCHLD, handler1) == SIG_ERR)
perror("signal error");
// 父进程创建 3 个子进程
for (i = 0; i < 3; i++) {
if (fork() == 0) {
printf("Hello from child %d\n", (int)getpid());
sleep(1);
exit(0);
}
}
// 父进程等待终端输入
if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
perror("read");
printf("Parent processing input\n");
while (1);
exit(0);
}
```
**信号处理流程**
```mermaid
sequenceDiagram
participant 父进程
participant 子进程1
participant 子进程2
participant 子进程3
participant 信号处理函数
父进程->>信号处理函数: signal(SIGCHLD, handler1)
父进程->>子进程1: fork()
父进程->>子进程2: fork()
父进程->>子进程3: fork()
子进程1->>父进程: exit() 触发 SIGCHLD
父进程->>信号处理函数: 调用 handler1()
信号处理函数->>信号处理函数: waitpid() 回收子进程
子进程2->>父进程: exit() 触发 SIGCHLD
父进程->>信号处理函数: 调用 handler1()
信号处理函数->>信号处理函数: waitpid() 回收子进程
子进程3->>父进程: exit() 触发 SIGCHLD
父进程->>信号处理函数: 调用 handler1()
信号处理函数->>信号处理函数: waitpid() 回收子进程
```
### 7.4 SIGALRM 定时器
参考 `实例源代码/chap5/alarm.c`
```c
#include "wrapper.h"
void handler(int sig)
{
static int beeps = 0;
printf("BEEP\n");
if (++beeps < 5)
alarm(1); // 1秒后再次触发 SIGALRM
else {
printf("BOOM!\n");
exit(0);
}
}
int main()
{
signal(SIGALRM, handler); // 注册 SIGALRM 处理函数
alarm(1); // 1秒后触发第一次 SIGALRM
while (1); // 等待信号
exit(0);
}
```
**输出**:每隔 1 秒打印一次 "BEEP",第 5 次后打印 "BOOM!" 并退出。
---
## 八、Shell 的实现
### 8.1 Shell 的工作原理
Shell 是一个命令解释器,其核心逻辑可以用一个循环概括:
```mermaid
graph TD
A["打印提示符 %"] --> B["读取用户输入命令"]
B --> C{"命令是否为空?"}
C -->|是| A
C -->|否| D{"是否为内置命令?"}
D -->|是 exit| E["退出 shell"]
D -->|是其他| F["执行内置命令"]
F --> A
D -->|否| G["fork() 创建子进程"]
G --> H["子进程: execvp() 执行命令"]
G --> I["父进程: waitpid() 等待子进程"]
H --> A
I --> A
style A fill:#e1f5fe
style E fill:#ffcdd2
style G fill:#e8f5e9
```
### 8.2 shellex.c 核心实现
参考 `实例源代码/chap5/shellex.c`,这是一个完整的简易 shell 实现:
```c
#include "wrapper.h"
#define MAXARGS 128
// 解析命令行,返回是否为后台作业
int parseline(char *buf, char **argv)
{
char *delim;
int argc;
int bg;
buf[strlen(buf)-1] = ' '; // 用空格替换末尾换行
while (*buf && (*buf == ' ')) // 跳过前导空格
buf++;
argc = 0;
while ((delim = strchr(buf, ' '))) {
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' '))
buf++;
}
argv[argc] = NULL;
if (argc == 0) return 1;
// 检查是否应在后台执行(最后一个参数为 &
if ((bg = (*argv[argc-1] == '&')) != 0)
argv[--argc] = NULL;
return bg;
}
// 执行命令
void execute(char *cmdline)
{
char *argv[MAXARGS];
char buf[MAXLINE];
int bg;
pid_t pid;
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL) return;
if (!builtin_command(argv)) {
if ((pid = fork()) == 0) { // 子进程
if (execvp(argv[0], argv) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
if (!bg) { // 前台执行
int status;
if (waitpid(pid, &status, 0) < 0)
perror("waitpid error");
}
else // 后台执行
printf("%d %s", pid, cmdline);
}
return;
}
// 判断内置命令(如 exit
int builtin_command(char **argv)
{
if (!strcmp(argv[0], "exit"))
exit(0);
if (!strcmp(argv[0], "&"))
return 1;
return 0;
}
int main()
{
char cmdline[MAXLINE];
while (1) {
printf("%% "); // 打印提示符
fgets(cmdline, MAXLINE, stdin); // 读取命令
if (feof(stdin)) exit(0);
execute(cmdline); // 执行命令
}
}
```
### 8.3 Shell 执行流程图
```mermaid
sequenceDiagram
participant 用户
participant Shell主进程
participant 子进程
participant 操作系统
loop Shell 主循环
用户->>Shell主进程: 输入 "ls -l"
Shell主进程->>Shell主进程: parseline() 解析命令
Shell主进程->>子进程: fork() 创建子进程
子进程->>操作系统: execvp("ls", ["ls", "-l", NULL])
Note over 操作系统: ls 程序替换子进程
操作系统-->>子进程: ls 执行完毕
子进程->>Shell主进程: exit()
Shell主进程->>Shell主进程: waitpid() 回收子进程
Shell主进程-->>用户: 显示结果,打印提示符
end
```
### 8.4 前台进程与后台进程
- **前台进程**Shell 调用 `waitpid()` 等待子进程结束后才继续接受输入
- **后台进程**Shell 不等待,直接打印子进程 PID 并继续接受输入(命令末尾加 `&`
### 8.5 管道与重定向
实验中的 `task52.c` 实现了管道和重定向功能:
**输出重定向**:使用 `dup2()` 将标准输出重定向到文件
```c
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); // 将 STDOUT 重定向到文件
close(fd);
execvp(args[0], args); // 执行命令,输出写入文件
```
**管道**:使用 `pipe()` 创建管道,连接两个进程的输入输出
```c
int pipefd[2];
pipe(pipefd); // pipefd[0]=读端, pipefd[1]=写端
// 子进程1: 写入管道
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[0]);
execvp(cmd1_args[0], cmd1_args);
// 子进程2: 从管道读取
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[1]);
execvp(cmd2_args[0], cmd2_args);
```
---
## 九、Daemon 进程
### 9.1 什么是 Daemon
Daemon守护进程是在后台运行的特殊进程通常在系统启动时创建一直运行到系统关闭。常见的 daemon 有:`sshd`SSH 服务)、`httpd`Web 服务)、`crond`(定时任务)等。
### 9.2 Daemon 的特点
- 没有控制终端(不与任何终端关联)
- 在后台运行
- 父进程是 initPID=1
- 通常以 `d` 结尾命名
### 9.3 创建 Daemon 的五步法
参考 `实例源代码/chap5/daemon.c`
```c
int init_daemon(void)
{
pid_t pid;
int i;
// 第1步fork() + 父进程退出,脱离终端控制
pid = fork();
if (pid == -1) return -1;
else if (pid != 0) exit(EXIT_SUCCESS);
// 第2步setsid() 创建新会话,成为会话首进程
if (setsid() == -1) return -1;
// 第3步(可选) 再次 fork(),确保不会重新获得控制终端
// daemon.c 中省略了这一步,但 task53.c 中包含
// 第4步chdir("/") 避免占用可卸载的文件系统
if (chdir("/") == -1) return -1;
// 第5步关闭所有打开的文件描述符重定向 0/1/2 到 /dev/null
for (i = 0; i < NR_OPEN; i++) close(i);
open("/dev/null", O_RDWR); // stdin -> /dev/null
dup(0); // stdout -> /dev/null
dup(0); // stderr -> /dev/null
return 0;
}
```
**五步法流程图**
```mermaid
graph TD
A["第1步: fork() + 父进程 exit()"] --> B["子进程成为孤儿进程<br/>被 init 收养"]
B --> C["第2步: setsid()"]
C --> D["创建新会话<br/>成为会话首进程<br/>脱离控制终端"]
D --> E["第3步: fork() + 父进程 exit()"]
E --> F["确保不会重新获得<br/>控制终端"]
F --> G["第4步: chdir('/')"]
G --> H["避免占用可卸载的<br/>文件系统"]
H --> I["第5步: 关闭 0/1/2<br/>重定向到 /dev/null"]
I --> J["Daemon 创建完成<br/>在后台运行"]
style A fill:#e1f5fe
style J fill:#e8f5e9
```
### 9.4 Daemon 文件监控实例
实验中的 `task53.c` 实现了一个 daemon 文件监控程序:
1. 创建守护进程
2. 每隔 5 分钟读取目标文件内容,计算 hash 值
3. 与上一次的 hash 值对比
4. 如果文件被篡改,将事件记录到日志文件
---
## 十、实验任务概览
本讲对应 [[实验02_进程控制]],包含以下任务:
| 任务 | 文件 | 内容 | 核心知识点 |
|------|------|------|------------|
| 任务一 | task51.c | 创建进程族亲结构p1 -> p11, p12 -> p121, p122 | fork() + wait() + 进程树 |
| 任务二 | task52.c | 简单 shell 实现(命令解析、重定向、管道) | fork() + exec() + dup2() + pipe() |
| 任务三 | task53.c | daemon 文件监控hash 值检测篡改) | daemon 五步法 + 文件 I/O |
| 任务四 | task54.c | 信号机制管理子进程create/kill/ps/exit | signal() + kill() + SIGCHLD |
---
## 知识关联
- 进程状态转换在处理机调度中有更详细的讨论,参见相关课件
- 信号机制与进程间通信密切相关,参见 [[08_进程间通信]]
- Shell 的并发版本(多进程并发服务器)在网络编程中会深入讨论
- 线程可以看作轻量级进程,参见 [[07_多线程编程]]
---
## 思考题
1. **fork 返回值的设计**:为什么 `fork()` 要返回两次而不是只返回一次?如果子进程不知道自己的 PID可以用什么方式获取
2. **exec 的不可逆性**:为什么 `exec()` 成功后不返回?如果 `exec()` 执行失败会怎样?
3. **僵尸进程的危害**:如果一个服务器程序不断创建子进程但从不 `wait`,最终会怎样?如何用 `SIGCHLD` 信号处理函数避免僵尸进程?
4. **shell 的本质**shell 执行 `ls -l` 命令时,为什么需要 `fork()` + `exec()` 两个步骤?只用 `exec()` 行不行?
5. **daemon 的双重 fork**:为什么 daemon 创建通常要 `fork()` 两次?只 `fork()` 一次有什么问题?
---
## 扩展阅读
- 《UNIX环境高级编程》第8章进程控制
- 《深入理解计算机系统》第8章异常控制流
- 《Linux/UNIX系统编程手册》第24-27章进程的创建、终止、监控