Linux 系统编程 05:进程控制
前言承接上一篇进程创建与 fork 核心机制本篇深入讲解进程生命周期的后半段进程退出、资源回收、程序替换以及面试高频的孤儿 / 僵尸进程问题。掌握这些内容才能完整管控进程的全生命周期写出稳定的多进程程序同时也是排查进程泄漏、资源残留等工程问题的核心理论基础。一、进程的退出方式进程的退出分为正常退出和异常退出两大类不同退出方式的清理行为不同对系统资源的影响也有差异。1. 正常退出的三种方式正常退出是进程主动结束按约定返回退出状态码内核会有序释放资源。main 函数 return 返回main 函数执行结束 return等价于调用 exit 函数会自动执行清理工作。调用 exit () 函数C 标准库函数执行完用户注册的清理函数、刷新所有缓冲区后进入内核终止进程。调用_exit () /_Exit () 函数系统调用直接进入内核终止进程不执行用户层清理不刷新缓冲区。2. 异常退出的两种场景异常退出是进程被外部事件强制终止无法执行自身的收尾逻辑。收到终止信号如CtrlC触发的 SIGINT、kill 命令发送的 SIGKILL 等进程被内核强制终止。调用 abort () 函数主动发送 SIGABRT 信号终止自己属于异常退出会触发核心转储。3. exit 与_exit 的核心区别面试高频这是进程退出最核心的考点两者的本质差异在于是否执行用户态的清理工作对比维度exit库函数_exit系统调用层级C 标准库函数用户态系统调用直接进入内核缓冲区处理刷新所有标准 IO 缓冲区写入数据不处理缓冲区直接丢弃终止处理函数执行 atexit 注册的回调函数不执行任何用户回调清理程度执行完整的用户态清理再进入内核直接终止进程只做内核级清理头文件stdlib.hunistd.h工程规范普通业务逻辑退出统一用 exit子进程 fork 后出错、需要立刻终止且不污染父进程缓冲区时用_exit。4. atexit 注册终止处理函数#include stdlib.h int atexit(void (*function)(void));功能注册进程正常退出时自动执行的回调函数支持注册多个执行顺序与注册顺序相反。限制只有调用 exit 或 main 函数 return 时才会触发_exit、信号终止、abort 均不会触发。二、进程资源回收wait 与 waitpid子进程退出后内核不会立刻释放全部资源会保留 PCB 等少量信息等待父进程读取退出状态。如果父进程不回收子进程就会变成僵尸进程占用系统资源。1. 为什么必须回收子进程子进程退出时内核释放其内存、文件等大部分资源但保留进程 PID、退出状态、运行时间等信息在 PCB 中目的是让父进程获取子进程的结束情况。父进程调用 wait/waitpid内核清理残留 PCB彻底释放资源父进程不回收子进程成为僵尸进程PID 一直被占用大量僵尸会耗尽系统 PID无法创建新进程2. wait 函数阻塞回收任意子进程#include sys/types.h #include sys/wait.h pid_t wait(int *wstatus);功能阻塞当前进程直到任意一个子进程退出回收其资源参数wstatus传出参数保存子进程的退出状态信息可通过宏解析具体状态返回值成功返回回收的子进程 PID失败返回 - 1如没有子进程3. waitpid 函数灵活可控的回收wait 只能阻塞等待任意子进程waitpid 支持指定进程、非阻塞等待是工程中更常用的版本。pid_t waitpid(pid_t pid, int *wstatus, int options);参数详解pid指定回收的目标 0回收指定 PID 的子进程-1回收任意一个子进程等价于 wait0回收和当前进程同组的任意子进程options控制选项常用WNOHANG表示非阻塞没有已退出的子进程则立刻返回 0wstatus同 wait存储退出状态4. 退出状态解析宏wstatus不能直接当整数读取必须通过系统提供的宏解析WIFEXITED(status)子进程正常退出则为真WEXITSTATUS(status)获取子进程正常退出的返回码仅 WIFEXITED 为真时有效WIFSIGNALED(status)子进程被信号终止则为真WTERMSIG(status)获取终止子进程的信号编号仅 WIFSIGNALED 为真时有效5. 实战回收子进程并解析状态#include stdio.h #include unistd.h #include sys/wait.h #include stdlib.h int main(void) { pid_t pid fork(); if (pid 0) { printf(子进程运行PID%d\n, getpid()); sleep(2); exit(66); // 子进程正常退出返回码66 } // 父进程阻塞回收 int status; pid_t ret wait(status); if (ret -1) { perror(wait failed); return 1; } if (WIFEXITED(status)) { printf(子进程%d正常退出退出码%d\n, ret, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf(子进程%d被信号终止信号编号%d\n, ret, WTERMSIG(status)); } return 0; }三、exec 函数族程序替换fork 创建的子进程和父进程执行相同的代码而实际开发中子进程往往需要执行全新的独立程序这就需要用到 exec 函数族完成程序替换。1. exec 的本质exec 不是创建新进程而是用磁盘上的新可执行文件替换当前进程的代码段、数据段、堆、栈等全部用户空间内容进程的 PID 保持不变相当于给进程 “换了灵魂”。调用成功后原进程中 exec 之后的所有代码都不会再执行因为代码已经被替换调用失败才会返回 - 1继续执行后续代码2. 六个函数的命名规律与分类exec 一共有 6 个函数后缀字母对应不同的传参和查找规则llist参数以列表形式逐个传入以 NULL 结尾vvector参数以指针数组形式传入ppath自动在 PATH 环境变量中查找可执行文件eenvironment自定义传入环境变量数组函数名传参方式查找路径环境变量execl列表需写完整路径继承原进程execlp列表自动搜 PATH继承原进程execle列表需写完整路径自定义传入execv数组需写完整路径继承原进程execvp数组自动搜 PATH继承原进程execve数组需写完整路径自定义传入底层本质前 5 个都是库函数最终都调用系统调用 execve 实现。3. 实战execlp 执行系统命令#include stdio.h #include unistd.h #include stdlib.h int main(void) { pid_t pid fork(); if (pid 0) { // 子进程替换为 ls -l 命令 execlp(ls, ls, -l, NULL); // 只有执行失败才会走到这里 perror(execlp failed); _exit(1); } wait(NULL); printf(子进程执行完毕父进程结束\n); return 0; }4. 核心注意事项exec 成功无返回值失败返回 - 1因此不需要判断成功分支只处理错误即可参数列表第一个参数必须是可执行文件名本身最后必须以 NULL 结尾程序替换后原进程的文件描述符默认保持打开状态除非设置了 FD_CLOEXEC 标志真实的 PID 不变只是用户空间内容被全部替换四、孤儿进程与僵尸进程这是进程模块最经典的面试题两者都是进程生命周期异常的产物但成因、危害、解决方案完全不同。1. 孤儿进程定义父进程先于子进程退出子进程失去父进程就成为孤儿进程。收养机制Linux 内核会自动将孤儿进程收养给 PID 为 1 的 init/systemd 进程由 init 负责后续的资源回收危害无实际危害孤儿进程会正常运行退出时由 init 回收资源不会残留2. 僵尸进程定义子进程先退出父进程没有调用 wait/waitpid 回收资源子进程的 PCB 残留在系统中状态为 Zzombie就是僵尸进程。成因子进程退出后内核保留 PCB 等待父进程读取状态父进程不回收就会一直残留危害僵尸进程已经释放了大部分资源只占用 PID 和少量 PCB 内存但大量僵尸进程会耗尽系统 PID 号导致无法创建新进程3. 僵尸进程的解决方案父进程主动回收父进程调用 wait/waitpid 等待子进程退出主动回收资源这是最规范的做法信号异步回收子进程退出时会给父进程发送 SIGCHLD 信号在信号处理函数中调用 waitpid 批量回收不阻塞主业务父进程退出让 init 收养让父进程先退出子进程变成孤儿进程由 init 负责回收适合父进程无需等待子进程的场景两次 fork父进程 fork 一次子进程再 fork 出孙子进程执行业务子进程立刻退出孙子进程变成孤儿由 init 收养父进程回收子进程即可五、拓展守护进程实现守护进程Daemon是运行在后台的特殊进程脱离终端控制生命周期长常用于服务器、日志服务等后台常驻场景是嵌入式与服务端开发的常用技术。1. 标准实现步骤fork 子进程父进程退出让子进程在后台运行脱离终端控制setsid 创建新会话子进程成为新会话组长彻底脱离原终端修改工作目录切换到根目录避免占用挂载点导致无法卸载重设 umask重置文件权限掩码避免继承父进程的限制关闭文件描述符关闭从父进程继承的所有文件描述符将标准输入输出重定向到 /dev/null2. 完整实现代码#include stdio.h #include unistd.h #include stdlib.h #include sys/stat.h #include fcntl.h void daemon_create(void) { // 1. fork子进程父进程退出 pid_t pid fork(); if (pid 0) exit(0); // 2. 创建新会话脱离终端 setsid(); // 3. 改变工作目录到根目录 chdir(/); // 4. 重置权限掩码 umask(0); // 5. 关闭所有文件描述符重定向标准流到/dev/null int fd open(/dev/null, O_RDWR); dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); close(fd); } int main(void) { daemon_create(); // 守护进程主逻辑后台循环运行 while (1) { sleep(1); // 业务逻辑 } return 0; }六、面试高频考点与易错坑点1. 经典面试问答Q1exit 和_exit 有什么核心区别答 exit 是 C 标准库函数_exit 是系统调用。 exit 会执行 atexit 注册的回调函数、刷新所有标准 IO 缓冲区完成用户态清理后再进入内核终止进程 _exit 直接进入内核终止进程不执行用户态清理不处理缓冲区。 普通场景用 exitfork 后子进程出错需要立刻退出、避免刷新父进程缓冲区时用_exit。Q2什么是僵尸进程成因是什么有什么危害怎么解决答子进程先退出父进程没有调用 wait/waitpid 回收子进程的 PCB 残留在系统中就是僵尸进程。成因子进程退出后内核会保留退出状态等待父进程读取父进程不回收就会一直残留。危害占用 PID 资源大量僵尸会耗尽系统 PID无法创建新进程。 解决方案父进程主动调用 wait/waitpid 回收用 SIGCHLD 信号异步回收让子进程变成孤儿由 init 回收。Q3exec 函数族的作用是什么调用成功后有返回值吗答 exec 用于程序替换用磁盘上的新可执行文件替换当前进程的全部用户空间内容PID 保持不变。 调用成功后不会返回因为原代码已经被全部替换后续代码不会执行只有调用失败才会返回 - 1。Q4wait 和 waitpid 有什么区别答wait 只能阻塞等待任意一个子进程waitpid 可以指定回收某个 PID 的子进程也可以回收任意子进程。wait 只能阻塞waitpid 支持 WNOHANG 选项实现非阻塞回收没有已退出子进程时立刻返回。waitpid 支持作业控制可以等待进程组。 工程中优先使用 waitpid更灵活可控。Q5孤儿进程和僵尸进程有什么区别哪个有危害答 孤儿进程是父进程先退出子进程被 init 进程收养会正常运行退出时由 init 回收没有危害。 僵尸进程是子进程先退出父进程没回收PCB 残留系统会占用 PID 资源大量僵尸有危害。2. 常见易错坑点子进程中用 exit 退出导致刷新了从父进程复制的缓冲区出现重复输出误以为 exec 会创建新进程忽略 PID 不变的特性exec 传参时忘记最后加 NULL导致参数解析错误只调用一次 wait 就认为回收了所有子进程多个子进程时仍有僵尸残留waitpid 不加 WNOHANG 在主循环里调用导致主业务被阻塞认为杀死僵尸进程的父进程没用实际父进程退出后僵尸会被 init 回收守护进程创建时忘记 setsid无法彻底脱离终端控制以上就是进程控制的全部核心内容完整覆盖了进程从退出、回收到程序替换的全生命周期管理。下一篇我们将进入信号机制模块讲解信号的本质、处理方式与可重入函数这是 Linux 系统编程中异步事件处理的核心。制作不易如果对你有用希望能点赞收藏支持一下。

相关新闻