一、子进程与父进程的关系
1.基本概念
在 Linux 中,fork() 系统调用会创建一个新进程(子进程),并在父子进程中分别返回不同的值:
父进程中:返回新创建的子进程的 PID(一个大于 0 的整数)。
子进程中:返回值为 0。
创建失败时:返回值为 -1,不会创建任何进程。
因此,父进程中 fork() 的返回值是子进程的 PID。
#include <stdio.h> #include <unistd.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("这是子进程,fork返回值:%d,自身PID:%d\n", pid, getpid()); } else if (pid > 0) { printf("这是父进程,fork返回值:%d,自身PID:%d\n", pid, getpid()); } else { printf("fork创建失败!\n"); } return 0; }2.fork返回值
| 返回值 | 含义 | 对应进程 |
|---|---|---|
>0 | 新创建的子进程 PID | 父进程 |
0 | 代表 “当前是子进程” | 子进程 |
-1 | 创建失败(如进程数达到上限、内存不足) | 无进程创建 |
易错点:fork() 调用一次,返回两次(父子进程各返回一次),这是它最特殊的地方。
3.核心特质
1. 资源复制与独立性
子进程会拷贝父进程的地址空间(代码段、数据段、栈、堆、文件描述符等),父子进程的内存完全独立,修改互不影响。
优化机制:写时复制(Copy-On-Write, COW)
刚创建时,父子进程共享同一份物理内存。
只有当任意一方尝试修改内存时,才会真正复制一份副本,避免不必要的开销。
2. 执行顺序
父子进程谁先执行,完全由操作系统调度器决定,顺序不确定。
想要控制顺序,需要用 wait() / waitpid() 让父进程阻塞等待子进程退出。
3. 父子进程的 PID 关系
子进程的 getppid() = 父进程的 getpid()。
父进程退出后,子进程会变成孤儿进程,被 init 进程(PID=1)收养。
子进程先退出、父进程未调用 wait() 时,子进程会变成僵尸进程(PCB 未释放,占用 PID 资源)。
n 次 fork() 后,总进程数 = 2^n(前提是所有 fork() 都成功)
4.父子进程共享的资源
✅ 共享:文件描述符表、文件偏移量(比如父子进程同时写同一个文件,会互相覆盖)
❌ 不共享:变量、栈、堆、进程状态、PID
4.常见考点
1.为什么 fork() 要返回子进程的 PID 给父进程?
父进程需要通过 PID 管理子进程(如 wait() 回收、kill() 发送信号),所以必须知道子进程的 PID。
2.僵尸进程和孤儿进程的区别?
孤儿进程:父进程先退出,子进程被 init 收养,无危害。
僵尸进程:子进程先退出,父进程未调用 wait() 回收,PCB (process control block 进程控制块)残留,占用 PID 资源,大量僵尸进程会导致系统无法创建新进程。
3.如何避免僵尸进程?
父进程调用 wait() / waitpid() 阻塞等待子进程退出。
父进程捕获 SIGCHLD 信号,在信号处理函数中调用 waitpid() 回收所有子进程。
5.核心模板代码
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid == -1) { perror("fork failed"); return 1; } else if (pid == 0) { // 子进程逻辑 printf("Child process: PID=%d, PPID=%d\n", getpid(), getppid()); sleep(2); // 模拟子进程执行 printf("Child process exiting\n"); return 0; } else { // 父进程逻辑 printf("Parent process: PID=%d, Child PID=%d\n", getpid(), pid); // 等待子进程退出,避免僵尸进程 int status; waitpid(pid, &status, 0); printf("Parent process: Child exited with status %d\n", WEXITSTATUS(status)); } return 0; }写时复制:只有写操作才会触发复制,只读访问完全不需要拷贝,大幅提升 fork() 性能。
子进程刚创建时和父进程共享内存,修改时才会 “分家”,这就是 “写时复制” 的由来。
二、linux0/1/2 号进程 核心考点清单
| 进程 | PID | 名称 | 核心角色 | 父进程 | 主要职责 |
|---|---|---|---|---|---|
| 0 号进程 | 0 | swapper/idle | 内核根进程(所有进程的祖先) | 无(内核创建) | 1. 系统启动时创建,运行在内核态2. 初始化系统,创建 1 号和 2 号进程3. 系统空闲时作为 idle 进程调度 |
| 1 号进程 | 1 | init/systemd | 用户态进程的 “祖先” | 0 号进程 | 1. 接管用户空间初始化,启动系统服务2. 收养所有孤儿进程(父进程退出的进程)3. 负责管理和回收僵尸进程 |
| 2 号进程 | 2 | kthreadd | 内核线程管理器 | 0 号进程 | 1. 负责创建和管理所有内核线程(如kworker、kswapd)2. 内核线程的父进程都是 2 号进程3. 内核态线程不会进入用户空间 |
高频考点 & 易错点
1. 进程创建关系
0 号进程是唯一的父进程:1 号和 2 号进程都是由 0 号进程创建的,二者是兄弟关系,不是父子关系。
验证方式:用 ps -ef 查看 PPID(父进程号),PID=1 和 PID=2 的 PPID 都是 0。
2. 孤儿进程 & 僵尸进程的处理
孤儿进程:父进程先退出,子进程被 1 号进程收养,1 号进程会成为它的新父进程,避免子进程成为 “无主进程”。
僵尸进程:子进程退出后,父进程未调用wait()/waitpid()回收,子进程 PCB 残留。1 号进程会定期调用wait()回收孤儿进程的僵尸状态,但用户进程的僵尸进程需要父进程主动处理。
3. 内核线程 vs 用户进程
内核线程(由 2 号进程创建):
运行在内核态,没有独立的用户地址空间,共享内核地址空间。
不执行用户代码,只执行内核函数,不能被用户直接管理。
用户进程(由 1 号进程及其后代创建):
运行在用户态,有独立的地址空间,可通过系统调用切换到内核态。
可被用户创建、管理、终止。
4. 0 号进程的特殊性
它不是普通进程,是内核在启动阶段创建的内核线程,没有用户态上下文。
当 CPU 没有任务可调度时,会切换回 0 号进程运行,也就是 “idle 状态”。
Linux 进程完整生命周期
整条链路:
创建 → 运行 → 阻塞 / 就绪 → 终止 → 资源回收
一、进程创建:fork /vfork/clone
0 号进程 最先存在,创建 1 号 (systemd)、2 号 (kthreadd)
普通用户进程:
用户进程调用 fork()
复制父进程页表、栈、数据、缓冲区
写时复制 COW,只读共享,写才拷贝
子进程:从fork 返回处继续执行,不重跑前面代码
fork 一次,两次返回
父:返回子进程 PID
子:返回 0
失败:返回 -1
补充:
vfork:子进程共享父进程地址空间,父进程阻塞
clone:Linux 底层,可定制共享资源,线程也靠它
二、进程替换:exec 系列
fork 只是复制代码,子进程和父进程跑一样的程序。
想要跑新程序 → 调用 exec
覆盖当前进程代码段、数据段、堆
保留:PID、PPID、文件描述符、进程属性
执行成功无返回,失败才返回 - 1
典型搭配:
fork() 创建子进程 → 子进程调用 exec 跑新程序三、进程三种基本状态(必考)
就绪态
一切准备好,等 CPU 调度
运行态
正在 CPU 上执行代码
阻塞态 (等待)
等资源、等 IO、等信号、sleep
不占用 CPU,唤醒后回到就绪
状态切换:
就绪 ↔ 运行
运行 → 阻塞
阻塞 → 就绪
四、进程终止 3 种方式
正常退出
return
exit() // 库函数,刷新缓冲区、调用退出钩子
异常退出
段错误、除 0、非法指令 → 内核发信号杀死
人为终止
kill 命令 / 信号终止
五、退出后:僵尸进程 & 孤儿进程
1. 僵尸进程
子进程先退出,父进程没调用 wait/waitpid
子进程:代码、资源全部释放
只剩 PCB 进程控制块 残留,记录退出状态
危害:占用 PID,大量僵尸会导致无法新建进程
2. 孤儿进程
父进程先退出,子进程没人管
自动被 1 号进程 (systemd) 收养
无害,1 号进程会自动回收它
六、资源回收(收尾)
父进程通过:
wait()
waitpid()
获取子进程退出状态,释放 PCB,彻底消灭僵尸进程。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid; int status; // 1. 创建子进程:fork() pid = fork(); if (pid < 0) { perror("fork failed"); exit(EXIT_FAILURE); } if (pid == 0) { // ---------------------- // 子进程:替换程序 + 执行 + 退出 // ---------------------- printf("Child: PID=%d, Parent PID=%d\n", getpid(), getppid()); // 2. exec() 替换为新程序(这里用 ls 命令举例) // execlp(程序名, 命令, 参数, NULL); execlp("ls", "ls", "-l", NULL); // 只有 exec 失败才会执行到这里 perror("execlp failed"); // 3. exit() 终止进程 exit(EXIT_FAILURE); } else { // ---------------------- // 父进程:等待子进程 + 回收资源 // ---------------------- printf("Parent: PID=%d, Child PID=%d\n", getpid(), pid); // 4. wait() 阻塞等待子进程退出 wait(&status); // 解析子进程退出状态 if (WIFEXITED(status)) { printf("Parent: Child exited normally, exit code=%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("Parent: Child killed by signal %d\n", WTERMSIG(status)); } } return 0; }三、进程状态宏 总结表
| 场景 | 判断宏(先判断) | 取值宏(再取值) | 说明 |
|---|---|---|---|
| 子进程正常退出 | WIFEXITED(wstatus)返回:非 0 表示真 | WEXITSTATUS(wstatus)返回:子进程退出码 (0~255) | 子进程调用exit()/return正常结束 |
| 子进程被信号杀死 | WIFSIGNALED(wstatus)返回:非 0 表示真 | WTERMSIG(wstatus)返回:终止信号编号 | 被kill、段错误等信号强制终止 |
| 子进程被暂停 / 停止 | WIFSTOPPED(wstatus)返回:非 0 表示真 | WSTOPSIG(wstatus)返回:暂停信号编号 | 收到SIGSTOP/SIGTSTP暂停运行 |
| 子进程暂停后恢复 | WIFCONTINUED(wstatus)返回:非 0 表示真 | 无 | 收到SIGCONT信号恢复运行 |
| 子进程生成 core dump | WIFSIGNALED(wstatus)返回:非 0 表示真 | WCOREDUMP(wstatus)返回:非 0 表示生成 | 被信号杀死且生成 core 文件(系统支持) |
正常退出:WIFEXITED → WEXITSTATUS(退出码)
被信号杀死:WIFSIGNALED → WTERMSIG(信号号)
被暂停:WIFSTOPPED → WSTOPSIG(信号号)
恢复运行:WIFCONTINUED(无取值)
生成 core:WIFSIGNALED → WCOREDUMP
最重要规则(考试必考)
必须先判断,再取值
判断宏返回非 0 = 条件成立
取值宏只有在对应判断成立时才有意义
四、waitpid 选项常量表
| 选项常量 | 作用 | 具体行为 | 典型应用场景 |
|---|---|---|---|
0 | 实现阻塞等待 | 父进程暂定直到子进程状态变化(退出 / 暂停) | 常规子进程同步等待(如批处理) |
WNOHANG | 实现非阻塞等待 | 调用waitpid时,若指定子进程未退出,函数立即返回0,父进程无需阻塞等待 | 父进程需轮询监控子进程状态(如后台任务管理) |
WUNTRACED | 捕获子进程暂停状态 | 当子进程因信号(如SIGSTOP)停止运行时,waitpid返回该子进程 PID | 调试场景、需处理子进程暂停逻辑的程序 |
WCONTINUED | 捕获子进程恢复运行状态 | 已停止的子进程通过SIGCONT信号恢复运行时,waitpid返回该子进程 PID | 需跟踪子进程完整生命周期(暂停 - 恢复)场景 |
注:这些选项可以用 | 组合使用,比如 WNOHANG | WUNTRACED
1. waitpid(pid, &status, 0) — 阻塞等待(等价于 wait)
#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("子进程:开始执行任务\n"); sleep(3); // 模拟耗时任务 printf("子进程:任务完成,退出\n"); exit(0); } else { printf("父进程:等待子进程...\n"); int status; // 阻塞等待子进程退出 waitpid(pid, &status, 0); printf("父进程:子进程已退出,状态码:%d\n", WEXITSTATUS(status)); } return 0; }2. WNOHANG — 非阻塞等待(父进程不卡住,可做其他事)
#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("子进程:开始执行任务\n"); sleep(3); printf("子进程:任务完成,退出\n"); exit(0); } else { printf("父进程:轮询监控子进程...\n"); int status; while (1) { // 非阻塞等待,子进程未结束时立即返回 0 pid_t ret = waitpid(pid, &status, WNOHANG); if (ret == 0) { printf("父进程:子进程还在运行,我先做别的事...\n"); sleep(1); } else if (ret > 0) { printf("父进程:子进程已退出,状态码:%d\n", WEXITSTATUS(status)); break; } else { perror("waitpid error"); exit(1); } } } return 0; }3. WUNTRACED — 捕获子进程暂停状态
#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("子进程:运行中,等待被暂停...\n"); while (1) sleep(1); // 子进程一直运行 } else { printf("父进程:发送 SIGSTOP 暂停子进程\n"); kill(pid, SIGSTOP); // 暂停子进程 int status; // 等待子进程退出 或 被暂停 waitpid(pid, &status, WUNTRACED); if (WIFSTOPPED(status)) { printf("父进程:子进程被信号 %d 暂停\n", WSTOPSIG(status)); } } return 0; }4. WCONTINUED — 捕获子进程恢复运行状态
#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("子进程:运行中\n"); while (1) sleep(1); } else { printf("父进程:发送 SIGSTOP 暂停子进程\n"); kill(pid, SIGSTOP); sleep(2); printf("父进程:发送 SIGCONT 恢复子进程\n"); kill(pid, SIGCONT); int status; // 等待子进程退出/暂停/恢复 waitpid(pid, &status, WUNTRACED | WCONTINUED); if (WIFCONTINUED(status)) { printf("父进程:子进程已恢复运行\n"); } } return 0; }五、回收僵尸进程方法
回收僵尸子进程是系统编程中常见的问题。僵尸进程本身不占用内存或CPU,但会占用进程ID(PID)表项,若大量存在会导致系统无法创建新进程。
以下是回收僵尸子进程的所有有效方法,按使用场景分类:
1. 父进程主动回收(最推荐)
这是最标准、最可控的方式。父进程在子进程退出后,应主动调用wait()或waitpid()系统调用,以获取子进程的退出状态并释放其进程表项。
- 阻塞式回收:
wait(NULL)会阻塞父进程,直到任意一个子进程结束。 - 非阻塞式回收:
waitpid(-1, NULL, WNOHANG)会立即返回,若没有子进程结束则返回0,适合在父进程主循环中轮询使用。 - 信号驱动回收:在父进程中注册
SIGCHLD信号处理函数。当子进程退出时,内核会向父进程发送SIGCHLD信号,处理函数中调用waitpid回收所有已退出的子进程。这是异步、高效的处理方式。
2. 父进程退出,由 init 进程接管
如果父进程意外退出或设计缺陷未回收子进程,子进程会成为“孤儿进程”,并被 PID 为 1 的init进程(现代系统中通常是systemd)收养。init进程会定期调用wait回收其所有子进程,包括这些被接管的僵尸进程。这是一种“兜底”机制,但不应作为常规手段,因为它依赖于父进程的异常退出。
3. 系统级处理(极端情况)
当僵尸进程的父进程是init(即PPID=1),但仍未被回收时,可能是内核或系统层面的异常。此时可尝试:
- 检查系统日志(如
dmesg或/var/log/syslog),排查硬件或驱动问题。 - 在极端情况下,重启系统是最终解决方案。
简易背诵版:
父进程主动回收:通过wait()或waitpid()阻塞等待子进程结束,获取其退出状态并释放资源。其中waitpid()可通过WNOHANG参数实现非阻塞回收。
信号处理机制:父进程注册SIGCHLD信号处理函数,当子进程退出时自动触发wait()回收,避免阻塞主线程。
init 进程收养:若父进程先于子进程退出,子进程会被 init 进程(PID=1)收养,init 会定期回收其僵尸子进程。
重要提醒
不要尝试用kill -9命令杀死僵尸进程。僵尸进程已经“死亡”,它不响应任何信号,包括SIGKILL。发送信号对僵尸进程无效,只会徒劳无功。
综上,回收僵尸进程的核心在于父进程的主动管理,通过wait系列系统调用或信号处理机制,是健壮程序设计的基本要求。
守护进程