24 KiB
第06讲:进程控制
本节目标:理解进程的概念与生命周期,掌握
fork()、exec()、wait()、exit()等进程控制系统调用,理解信号机制与僵尸进程,最终能实现一个简易 shell 和 daemon 进程。
前置知识
- 03_C语言编程基础 -- C 语言指针、数组、字符串操作
- 07_多线程编程 -- 线程与进程的对比
一、进程概念
1.1 什么是进程
进程(Process) 是程序的一次执行实例。程序是静态的代码文件,进程是动态的执行过程。当操作系统将程序加载到内存并开始执行时,就创建了一个进程。
每个进程拥有独立的地址空间,包含以下组成部分:
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 进程状态
stateDiagram-v2
[*] --> 创建: fork()
创建 --> 就绪: 分配资源
就绪 --> 运行: 调度器选中
运行 --> 就绪: 时间片用完
运行 --> 阻塞: 等待I/O
阻塞 --> 就绪: I/O完成
运行 --> 终止: exit()
终止 --> [*]
就绪: Ready
运行: Running
阻塞: Blocked/Waiting
终止: Terminated/Zombie
1.4 进程标识
每个进程都有唯一的 PID(Process ID)。常用函数:
pid_t getpid(); // 获取当前进程的 PID
pid_t getppid(); // 获取父进程的 PID
二、fork() -- 创建子进程
2.1 基本原理
fork() 是 Linux 中创建新进程的唯一方式。调用 fork() 后,操作系统会创建一个与父进程几乎完全相同的子进程。子进程获得父进程的代码段、数据段、堆栈的副本。
关键特性:fork() 被调用一次,但返回两次:
- 父进程中返回子进程的 PID(正整数)
- 子进程中返回 0
- 如果出错返回 -1
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:
#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:
#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"。
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:
#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。
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:
#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() 通常配合使用:
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:
#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 的清理过程
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:
#include <stdlib.h>
int main() { exit(100); }
父进程通过 waitpid 的 WEXITSTATUS(status) 宏获取子进程的退出状态值(0-255)。
六、僵尸进程
6.1 什么是僵尸进程
当子进程终止后,如果父进程没有调用 wait() 或 waitpid() 回收子进程的退出状态,子进程的进程控制块(PCB)仍然保留在系统中,成为僵尸进程(Zombie Process)。
6.2 僵尸进程的产生
参考 实例源代码/chap5/zombie.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 僵尸进程的生命周期
sequenceDiagram
participant 父进程
participant 子进程
participant 操作系统内核
父进程->>子进程: fork() 创建子进程
子进程->>子进程: 执行任务
子进程->>操作系统内核: exit() 终止
操作系统内核->>父进程: 发送 SIGCHLD 信号
Note over 父进程: 父进程没有调用 wait()
Note over 操作系统内核: 子进程的 PCB 无法释放
Note over 操作系统内核: 子进程成为僵尸进程
Note over 操作系统内核: 僵尸进程占用 PID 资源
6.4 僵尸进程的危害与解决
| 危害 | 说明 |
|---|---|
| 占用 PID | 僵尸进程的 PID 无法被复用 |
| 资源泄漏 | 虽然不占内存,但 PCB 信息占用内核资源 |
| 累积效应 | 大量僵尸进程可耗尽 PID 资源 |
解决方法:
- 父进程调用
wait()或waitpid()回收子进程 - 注册
SIGCHLD信号处理函数,在其中调用waitpid() - 父进程先退出,让 init 进程(PID=1)自动回收孤儿进程
七、信号机制
7.1 信号的概念
信号是操作系统通知进程发生了某种事件的一种异步通信机制。进程可以注册信号处理函数来响应特定信号。
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:
#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);
}
信号处理流程:
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:
#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 是一个命令解释器,其核心逻辑可以用一个循环概括:
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 实现:
#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 执行流程图
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() 将标准输出重定向到文件
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); // 将 STDOUT 重定向到文件
close(fd);
execvp(args[0], args); // 执行命令,输出写入文件
管道:使用 pipe() 创建管道,连接两个进程的输入输出
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:
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;
}
五步法流程图:
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 文件监控程序:
- 创建守护进程
- 每隔 5 分钟读取目标文件内容,计算 hash 值
- 与上一次的 hash 值对比
- 如果文件被篡改,将事件记录到日志文件
十、实验任务概览
本讲对应 实验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_多线程编程
思考题
- fork 返回值的设计:为什么
fork()要返回两次而不是只返回一次?如果子进程不知道自己的 PID,可以用什么方式获取? - exec 的不可逆性:为什么
exec()成功后不返回?如果exec()执行失败会怎样? - 僵尸进程的危害:如果一个服务器程序不断创建子进程但从不
wait,最终会怎样?如何用SIGCHLD信号处理函数避免僵尸进程? - shell 的本质:shell 执行
ls -l命令时,为什么需要fork()+exec()两个步骤?只用exec()行不行? - daemon 的双重 fork:为什么 daemon 创建通常要
fork()两次?只fork()一次有什么问题?
扩展阅读
- 《UNIX环境高级编程》第8章:进程控制
- 《深入理解计算机系统》第8章:异常控制流
- 《Linux/UNIX系统编程手册》第24-27章:进程的创建、终止、监控