# 第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 返回 -1，errno 为 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 的特点

- 没有控制终端（不与任何终端关联）
- 在后台运行
- 父进程是 init（PID=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章：进程的创建、终止、监控
