# 第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 #include #include 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 #include #include 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["当前进程
PID=1234
执行 myprogram"] -->|execvp("ps", args)| B["同一进程
PID=1234
执行 ps 命令"] B --> C["原来的代码段
被完全替换"] 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)` | `` | 执行清理工作(刷新缓冲区、调用 atexit 注册的函数),然后终止 | | `_exit(status)` | `` | 立即终止,不执行任何清理 | ### 5.2 exit 的清理过程 ```mermaid graph TD A["调用 exit(status)"] --> B["执行 atexit() 注册的清理函数"] B --> C["刷新 stdio 缓冲区
(fclose 所有打开的流)"] C --> D["调用 _exit(status)"] D --> E["内核回收进程资源"] style A fill:#e1f5fe style E fill:#ffcdd2 ``` ### 5.3 退出状态 参考 `实例源代码/chap5/exitstatus.c`: ```c #include int main() { exit(100); } ``` 父进程通过 `waitpid` 的 `WEXITSTATUS(status)` 宏获取子进程的退出状态值(0-255)。 --- ## 六、僵尸进程 ### 6.1 什么是僵尸进程 当子进程终止后,如果父进程没有调用 `wait()` 或 `waitpid()` 回收子进程的退出状态,子进程的进程控制块(PCB)仍然保留在系统中,成为**僵尸进程(Zombie Process)**。 ### 6.2 僵尸进程的产生 参考 `实例源代码/chap5/zombie.c`: ```c #include #include #include #include #include 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["子进程成为孤儿进程
被 init 收养"] B --> C["第2步: setsid()"] C --> D["创建新会话
成为会话首进程
脱离控制终端"] D --> E["第3步: fork() + 父进程 exit()"] E --> F["确保不会重新获得
控制终端"] F --> G["第4步: chdir('/')"] G --> H["避免占用可卸载的
文件系统"] H --> I["第5步: 关闭 0/1/2
重定向到 /dev/null"] I --> J["Daemon 创建完成
在后台运行"] 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章:进程的创建、终止、监控