Linux 进程控制深度解析:创建、终止与等待
一、进程创建
1-1 fork 函数初识
fork是 Linux 中创建子进程的核心系统调用,作用是基于当前父进程复制生成一个全新的子进程。
- 调用前:系统中只有一个独立的执行流
- 调用后:内核为子进程创建独立的
task_struct进程控制块,复制地址空间、文件描述符表、信号处理表等资源,父子进程从 fork 调用点开始并行执行- 继承属性:子进程几乎完整继承父进程的代码段、数据段、堆、栈、环境变量、已打开的文件、工作目录、用户ID/组ID等
- 独有属性:PID、PPID、资源统计信息、挂起信号、内存锁等不继承
1-2 fork 函数返回值
fork 最核心的特性:一次调用,两次返回。
同一个调用点,父子进程各自返回一次,返回值不同:
- 父进程中:返回值 > 0,值为新创建子进程的 PID
- 子进程中:返回值 = 0
- 调用失败:返回 -1,同时设置
errno错误码为什么会有两次返回?
fork 执行时,内核不仅创建子进程的内核结构,还会初始化子进程的栈帧与执行上下文,让子进程的执行起点也落在 fork 调用的返回位置。父子进程各自独立返回,因此可以通过返回值区分身份,执行不同的代码分支。
基础使用示例:
#include <unistd.h> #include <stdio.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("我是子进程,PID=%d\n", getpid()); } else if (pid > 0) { printf("我是父进程,子进程PID=%d\n", pid); } else { perror("fork失败"); } return 0; }
1-3 写时拷贝(COW)
fork 创建子进程时,并不会立刻全量拷贝所有物理内存,而是采用写时复制(Copy On Write) 机制,这是 Linux 内存管理的经典优化。
- fork 时内核只拷贝父进程的页表,将所有可写页的页表项标记为只读
- 父子进程共享同一块物理内存,代码段、只读数据段永久共享
- 当任意一方尝试写入只读页时,触发缺页异常,内核才分配新的物理页、拷贝数据、更新页表,父子进程各自拥有独立副本
核心价值:
大幅提升 fork 效率,避免无意义的全量内存拷贝。 绝大多数场景下,fork 之后子进程会立刻调用 exec 加载新程序,全量拷贝内存会完全浪费,写时拷贝完美解决了这个问题。
1-4 fork 常规用法
- 多进程并发服务:父进程监听网络连接,每来一个请求就 fork 一个子进程处理,是经典的并发服务器模型
- 执行新程序:fork 创建子进程后,立刻调用 exec 系列函数,加载并运行全新的可执行文件
- 守护进程创建:fork 子进程后父进程退出,子进程脱离终端,成为后台独立运行的守护进程
- 故障隔离:将不稳定、有风险的任务放在子进程执行,子进程崩溃不会影响主进程
1-5 fork 调用失败的原因
- 进程数达到系统上限:系统总进程数或用户进程数达到限制,可通过
ulimit -u查看普通用户进程数上限- 内存不足:创建新的 task_struct、页表等内核结构需要消耗内存,内存紧张时调用失败
- 资源限制:cgroup、容器等资源控制器限制了进程数量
二、进程终止
2-1 进程退出场景
分为正常退出和异常终止两大类:
- 正常退出:程序执行完成,主动发起退出
- main 函数执行结束 return 返回
- 调用 exit 标准库函数
- 调用 _exit 系统调用
- 异常终止:程序运行出错或收到外部信号,被动退出
- 收到终止信号(如 SIGTERM、SIGKILL)
- 段错误、总线错误等内存访问违规
- 除零错误、栈溢出等运行时异常
2-2 进程常见退出方法
2-2-1 退出码
进程退出时会携带一个退出状态码,标识进程的退出原因,父进程可以通过 wait 系列函数获取。
- 约定:
0表示正常退出,非 0 表示异常退出- 取值范围:0 ~ 255,仅低 8 位有效
- 标准宏:
EXIT_SUCCESS(0)、EXIT_FAILURE(1)
2-2-2 _exit 函数
#include <unistd.h> void _exit(int status);
- 本质:系统调用,直接进入内核层终止进程
- 行为:直接释放进程内核资源,将退出码提交给内核,立刻终止进程
- 特点:不执行任何用户层清理,不刷新 stdio 缓冲区,不执行
atexit注册的函数- 适用场景:子进程异常快速退出、fork 后子进程退出,避免污染父进程的缓冲区与资源
说明:
虽然status是int,但是仅有低8位可以被⽗进程所⽤。
所以_exit(-1)时,在终端执⾏$?发现,返回值是255。
2-2-3 exit 函数
#include <stdlib.h> void exit(int status);
- 本质:C 标准库函数,底层最终调用
_exit系统调用- 执行流程:
- 执行所有通过
atexit/on_exit注册的清理函数- 刷新所有标准 IO 缓冲区**,将缓存数据写入文件**
- 调用
_exit系统调用,进入内核终止进程- 特点:完整、安全的退出方式,保证用户层资源全部清理
- 适用场景:普通程序正常退出,是开发中最常用的退出方式
2-2-4 return 退出
- return 是 C 语言的函数返回语句,本身不是系统调用
- main 函数中的
return n,等价于调用exit(n):main 函数返回后,程序启动代码会取出返回值作为参数调用 exit- 普通函数中的 return 只是函数返回,不会终止整个进程
- 特点:最自然的退出方式,程序执行完 main 逻辑后自然返回
三种退出方式对比
方式 层级 清理行为 本质 适用场景 _exit系统调用 无用户层清理,直接进入内核 内核层终止进程 子进程快速退出、异常终止 exit标准库函数 执行atexit、刷新IO缓冲区,再调用_exit 用户层清理+内核终止 普通程序正常退出 return语言语句 main中return等价于调用exit 函数返回 main函数执行结束自然退出 三、进程等待
3-1 进程等待的必要性
- 回收子进程资源,避免僵尸进程 子进程退出后,内核不会立刻释放全部资源,会保留
task_struct、退出状态等信息,等待父进程读取确认。 如果父进程不等待、不回收,子进程会成为僵尸进程(Zombie),持续占用 PID 与内核资源。- 获取子进程退出状态 父进程需要获知子进程的执行结果:是正常退出还是异常终止、退出码是多少、被哪个信号杀死。
- 同步执行时序 父进程等待子进程完成任务后再继续后续逻辑,实现父子进程间的执行同步。
3-2 进程等待的方法
3-2-1 wait 方法
#include <sys/wait.h> pid_t wait(int *status);
- 功能:阻塞等待任意一个子进程退出,回收其内核资源
- 参数
status:输出型参数,保存子进程的退出状态信息;传NULL表示不关心退出状态- 返回值:成功返回被回收子进程的 PID;失败返回 -1(如没有任何子进程)
- 特点:阻塞调用,只要有一个子进程退出就返回;没有子进程时立即出错返回
3-2-2 waitpid 方法
#include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
- 功能:更灵活的等待函数,支持指定目标进程、非阻塞模式
- 参数详解:
- pid:指定等待对象
pid > 0:等待 PID 为指定值的特定子进程pid = -1:等待任意子进程,功能与 wait 完全等价pid = 0:等待同一个进程组内的任意子进程pid < -1:等待进程组 ID 为 pid 绝对值的任意子进程- status:同 wait,输出子进程的退出状态编码
- options:功能选项
0:阻塞等待,行为与 wait 一致WNOHANG:非阻塞模式,没有子进程退出时立即返回 0,不挂起进程- 返回值:
> 0:成功回收的子进程 PID= 0:非阻塞模式下,当前没有子进程退出= -1:调用失败(如不存在对应子进程)
waitvswaitpid核心区别:wait 只能阻塞等待任意子进程;
waitpid 可以指定特定 PID,支持非阻塞模式,功能完全覆盖 wait,实际开发中更常用。
3-2-3 获取子进程 status
当进程被外部信号(如 SIGKILL、SIGSEGV、SIGTERM)杀死时,状态字按如下格式编码:
位段 15 ~ 8 位(高 8 位) 第 7 位 6 ~ 0 位(低 7 位) 含义 未使用(保留位) core dump 标志 终止信号编号 说明
- 低 7 位(终止信号):存放杀死进程的信号编号,例如 SIGSEGV = 11、SIGKILL = 9。
- 低 7 位非全 0,是信号终止的识别标志。
- 第 7 位(core dump 标志):
- 为 1:进程终止时生成了 core dump 核心转储文件,保存了进程崩溃时的内存快照,可用于 gdb 调试崩溃原因。
- 为 0:终止时未生成 core 文件。
- 高 8 位:保留未使用
status 不是简单的整数退出码,而是按位编码的状态信息,必须通过系统提供的宏解析:
终止类型 解析宏 作用 通用判断 WIFEXITED(status)返回非零 → 进程正常终止(return/exit/_exit) 正常终止 WEXITSTATUS(status)提取高 8 位,得到进程的退出码(0~255) 通用判断 WIFSIGNALED(status)返回非零 → 进程被信号终止 信号终止 WTERMSIG(status)提取低 7 位,得到终止进程的信号编号 信号终止 WCOREDUMP(status)返回非零 → 进程终止时生成了 core dump 文件 通用判断 WIFSTOPPED(status)返回非零 → 进程被暂停(如收到 SIGSTOP) 进程暂停 WSTOPSIG(status)提取导致暂停的信号编号 状态解析示例:
int status; waitpid(child_pid, &status, 0); if (WIFEXITED(status)) { printf("正常退出,退出码:%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("被信号终止,信号编号:%d\n", WTERMSIG(status)); }为什么要按位编码,而不是直接返回退出码?
内核需要在一个整数中同时传递「终止原因 + 退出码 / 信号 + core dump 标志」三类信息,按位域编码是最紧凑高效的方式,父进程只需读取一个参数即可解析全部退出信息。
如何判断进程是正常退出还是被信号杀死?
看低 8 位:低 8 位全 0 是正常终止;低 7 位非 0 是信号终止。 工程中永远不要直接判断 status == 0,必须使用
WIFEXITED+WEXITSTATUS组合判断。
exit (256) 退出码是多少?
退出码只保留低 8 位,256 会被截断为 0,等价于 exit (0)。因此工程上退出码建议控制在 0~255 范围内。
core dump 什么时候会生成?
进程收到默认行为为 "终止 + core" 的信号时生成,如 SIGSEGV(段错误)、SIGABRT(异常终止);SIGKILL、SIGTERM 默认不生成 core。 可通过
ulimit -c控制 core 文件大小限制,设为 0 则关闭 core dump。
WIFSTOPPED 对应什么?
对应进程被暂停(如收到 SIGSTOP),此时状态字编码格式不同:高 8 位是暂停信号,低 8 位固定为
0x7F,通过WIFSTOPPED+WSTOPSIG解析
3-2-4 阻塞与非阻塞等待
- 阻塞等待(默认)
- 调用 wait/waitpid 时,如果没有子进程退出,进程挂起阻塞,直到子进程退出才返回
- 优点:不占用 CPU,响应实时
- 适用场景:父进程没有其他任务,专门等待子进程执行结果
- 非阻塞等待(WNOHANG)
- 调用时如果没有子进程退出,立即返回 0,不阻塞当前进程
- 优点:父进程可以同时处理其他任务,通过轮询检查子进程状态
- 缺点:需要循环轮询,有一定延迟,会占用 CPU
- 适用场景:父进程有其他任务要处理,不能一直阻塞等待
注意:
非阻塞模式下必须循环轮询,直到 waitpid 返回大于 0 的值,否则会漏掉子进程回收,产生僵尸进程。
**🚩**高频面试题汇总
fork 为什么一次调用两次返回?返回值分别是什么?
fork 在内核中创建子进程的执行上下文,让父子进程都从 fork 调用点返回。
父进程返回子进程 PID(>0),子进程返回 0;调用失败返回 -1。
什么是写时拷贝?fork 为什么不用全量拷贝?
fork 时只拷贝页表,标记可写页为只读,父子共享物理内存;
只有写入时触发缺页才分配新物理页。
避免无意义的全量内存拷贝,大幅提升 fork 效率;
尤其是 fork 后立刻 exec 的场景,全量拷贝完全浪费。
exit 和 _exit 有什么本质区别?
exit 是 C 标准库函数,会执行 atexit 清理函数、刷新 IO 缓冲区,再调用 _exit; _exit 是系统调用,直接进入内核终止进程,不做任何用户层清理。
什么是僵尸进程?如何产生?如何回收?
子进程退出后,父进程没有调用 wait/waitpid 回收资源,子进程保留 PCB 与退出状态,成为僵尸进程。
父进程调用 wait/waitpid 主动回收;父进程退出后,僵尸子进程会由 init 进程领养回收。
wait 和 waitpid 的核心区别?
wait 只能阻塞等待任意子进程;
waitpid 可以指定特定 PID 的子进程,支持 WNOHANG 非阻塞模式,功能更灵活。
进程等待的必要性是什么?
回收子进程内核资源,避免僵尸进程;获取子进程退出状态与终止原因;同步父子进程执行时序。
WNOHANG 选项的作用?
将 waitpid 设置为非阻塞模式,没有子进程退出时立即返回 0,不阻塞;父进程可以同时处理其他任务,通过轮询等待子进程。
子进程继承父进程哪些资源?哪些不继承?
继承:代码、数据、地址空间、打开的文件描述符、环境变量、信号处理方式、用户ID/组ID、工作目录。
不继承:PID、PPID、挂起信号、内存锁、定时器、资源统计信息。






