Linux 进程等待与程序替换详解:僵尸进程防治及 exec 函数应用
Linux 进程管理中,进程等待用于回收子进程资源并避免僵尸进程产生,通过 wait 和 waitpid 系统调用实现阻塞或非阻塞等待,需正确解析退出状态位图。程序替换利用 exec 函数簇将新程序加载至当前进程地址空间,覆盖原有代码数据,支持多种参数传递方式及环境变量配置。掌握这两项技术是实现 Shell 及多任务服务器的基础,涉及 fork 创建、状态判断及权限检查等关键细节。

Linux 进程管理中,进程等待用于回收子进程资源并避免僵尸进程产生,通过 wait 和 waitpid 系统调用实现阻塞或非阻塞等待,需正确解析退出状态位图。程序替换利用 exec 函数簇将新程序加载至当前进程地址空间,覆盖原有代码数据,支持多种参数传递方式及环境变量配置。掌握这两项技术是实现 Shell 及多任务服务器的基础,涉及 fork 创建、状态判断及权限检查等关键细节。

在 Linux 进程管理中,进程等待和程序替换是衔接'进程创建'与'进程终止'的关键环节:进程等待解决了子进程退出后资源泄漏(僵尸进程)的问题,同时让父进程获取子进程的执行结果;程序替换则让子进程能脱离父进程代码,执行全新的程序(如 ls、ps 等系统命令),是 Shell、服务器等多任务程序的核心实现基础。
进程等待是父进程主动回收子进程资源、获取子进程退出状态的过程,是防治僵尸进程的唯一有效手段。
task_struct(PCB) 会一直保留在内存中,成为僵尸进程(Z 状态),占用系统资源;kill -9 也无法删除,只能通过父进程等待或父进程退出(子进程被 1 号进程领养回收)解决。
Linux 提供 wait 和 waitpid 两个系统调用实现进程等待,其中 waitpid 功能更灵活,是实际开发的首选。
(1)wait 函数(简单阻塞等待)
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
PID;失败返回 -1(如无子进程);status:输出型参数,存储子进程的退出状态,不关心则传 NULL;(2)waitpid 函数(灵活等待)
pid_t waitpid(pid_t pid, int* status, int options);
PID;0;-1;| 参数 | 取值与含义 |
|---|---|
pid | -1:等待任意子进程(同 wait) >0:等待 PID 等于该值的子进程 0:等待同进程组的子进程 |
status | 输出型参数,存储退出状态,解析方式同 wait |
options | 0:阻塞等待 WNOHANG:非阻塞等待(无退出子进程时立即返回 0) |

✅️ 图示理解:

图中所用到的示例为什么 status 是 256 怎么把他变成预期的 11:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
int main() {
printf("我是父进程:pid: %d, ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if (id < 0) {
perror("fork");
exit(1);
}
if (id == 0) {
// 子进程
int cnt = 5;
while (cnt) {
printf("我是子进程:pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
}
printf("子进程退出!\n");
exit(11);
}
// 父进程
// pid_t rid = wait(NULL);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) {
printf("等待子进程成功..., status: %d, exit code: %d\n", status, (status >> 8) & 0xFF);
}
return 0;
}
补充:

输出结果:
我是父进程:pid: 1234, ppid: 5678
我是子进程:pid: 1235, ppid: 1234, cnt: 5
我是子进程:pid: 1235, ppid: 1234, cnt: 4
我是子进程:pid: 1235, ppid: 1234, cnt: 3
我是子进程:pid: 1235, ppid: 1234, cnt: 2
我是子进程:pid: 1235, ppid: 1234, cnt: 1
子进程退出!
等待子进程成功..., status: 2816, exit code: 11
注意:这里的原理我们接着往下看就行了
status 并非普通整数,而是一个 16 位的位图,核心有效位为低 16 位,解析规则如下:


(1)核心宏函数解析(推荐使用,无需手动位运算)
WIFEXITED(status):判断子进程是否正常终止(返回非 0 为正常);**WEXITSTATUS(status)**:提取子进程的退出码(仅 WIFEXITED 为真时有效);WIFSIGNALED(status):判断子进程是否被信号终止(返回非 0 为信号终止);**WTERMSIG(status)**:提取终止子进程的信号编号(仅 WIFSIGNALED 为真时有效)。
(2)代码示例:解析退出状态
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程
sleep(5);
exit(10); // 正常退出,退出码 10
// kill(getpid(), 9); // 模拟被信号终止
} else {
// 父进程等待
int status;
pid_t ret = waitpid(pid, &status, 0); // 阻塞等待
if (ret > 0) {
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程被信号终止,信号编号:%d\n", WTERMSIG(status));
}
}
}
return 0;
}
109
(1)阻塞等待(options=0)
(2)非阻塞等待(options=WNOHANG)

(3)非阻塞等待代码示例
示例一:简单演示
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程
sleep(5);
exit(1);
} else {
// 父进程非阻塞等待
int status;
pid_t ret;
do {
ret = waitpid(pid, &status, WNOHANG); // 非阻塞
if (ret == 0) {
printf("子进程仍在运行,父进程可处理其他任务...\n");
sleep(1); // 模拟父进程其他工作
}
} while (ret == 0); // 直到回收成功或失败
if (WIFEXITED(status)) {
printf("子进程退出,退出码:%d\n", WEXITSTATUS(status));
}
}
return 0;
}

示例 2:模拟工作
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void PrintLog() {
printf("我要打印日志\n");
}
void SyncMySQL() {
printf("我要访问数据库!\n");
}
void Download() {
printf("我要下载核心数据\n");
}
typedef void (*task_t)();
task_t tasks[3] = {PrintLog, SyncMySQL, Download};
int main() {
printf("我是父进程,pid: %d, ppid: %d", getpid(), getppid());
pid_t id = fork();
if (id == 0) {
// child
int cnt = 5;
while (cnt) {
printf("我是子进程,pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
}
exit(13);
}
while (1) {
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG);
if (rid > 0) {
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
} else {
printf("进程异常退出,请注意!\n");
}
break;
} else if (rid == 0) {
sleep(1);
printf("子进程还没有退出,父进程轮询!\n");
for (int i = 0; i < 3; i++) {
tasks[i]();
}
} else {
printf("wait failed, who: %d, status: %d\n", rid, status);
break;
}
}
return 0;
}

(4)多进程模拟 (C/C++ 混编,利用 vector 来管理)

#include <iostream> // 替代 stdio.h
#include <cstdlib> // 替代 stdlib.h
// unistd.h 在 C++ 中通常保留(但更推荐使用 C++ 标准库)
#include <unistd.h> // Unix 系统调用,C++ 中没有直接替代
// sys/types.h 在 C++ 中通常保留
#include <sys/types.h> // 系统类型定义
#include <sys/wait.h> // 进程等待函数
#include <vector>
const int gnum = 5;
void Work() {
int cnt = 5;
while (cnt) {
printf("%d work..., cnt: %d\n", getpid(), cnt--);
sleep(1);
}
}
int main() {
std::vector<pid_t> subs;
for (int idx = 0; idx < gnum; idx++) {
pid_t id = fork();
if (id < 0) exit(1);
else if (id == 0) {
// child
Work();
exit(0);
} else {
subs.push_back(id);
}
}
for (auto& sub : subs) {
int status = 0;
pid_t rid = waitpid(sub, &status, 0);
if (rid > 0) {
if (WIFEXITED(status)) {
printf("child quit normal, exit code: %d\n", WEXITSTATUS(status));
} else {
printf("%d child quit error!\n", sub);
}
}
}
return 0;
}
进程程序替换是通过 exec 函数簇,将磁盘上的全新程序(代码 + 数据)加载到当前进程的地址空间,覆盖原有代码和数据,从新程序的入口开始执行。
PID 不变,仅用户空间的代码和数据被替换;exec 函数调用成功,新程序会立即执行,不会回到原进程的代码;通过图示加深理解:

fork() 之后,父子进程各自执行父进程代码的一部分,如果子进程想要执行一个全新的程序我们就可以使用程序替换来实现。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
printf("我是父进程:pid: %d, ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0) {
printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
execl("usr/bin/ls", "ls", "-a", "-l", NULL);
exit(1);
}
// father
pid_t rid = waitpid(id, NULL, 0);
if (rid > 0) {
printf("wait child process success\n");
}
return 0;
}

exec 函数簇的核心差异在于参数传递方式、是否自动搜索 PATH、是否自定义环境变量,掌握命名规律即可快速区分:
l(list):参数以列表形式传递,末尾必须以 NULL 结尾;v(vector):参数以字符串数组形式传递,数组末尾必须以 NULL 结尾;p(path):自动搜索环境变量 PATH,无需写程序全路径;e(env):自定义环境变量,需传递环境变量数组。

📝 函数原型与对比:
| 函数名 | 原型 | 核心特性 |
|---|---|---|
| execl | int execl(const char *path, const char *arg, ..., NULL); | 列表传参,需全路径,使用当前环境变量 |
| execlp | int execlp(const char *file, const char *arg, ..., NULL); | 列表传参,自动搜 PATH,使用当前环境变量 |
| execle | int execle(const char *path, const char *arg, ..., char *const envp[]); | 列表传参,需全路径,自定义环境变量 |
| execv | int execv(const char *path, char *const argv[]); | 数组传参,需全路径,使用当前环境变量 |
| execvp | int execvp(const char *file, char *const argv[]); | 数组传参,自动搜 PATH,使用当前环境变量 |
| execve | int execve(const char *path, char *const argv[], char *const envp[]); | 数组传参,需全路径,自定义环境变量(系统调用,其他函数最终调用它) |


综合示例 (每个函数大家都可以单独去试试,myexe.c + myproc.cc):
myexe.c#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
int main() {
printf("我是父进程:pid: %d, ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0) {
printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
char* const argv[] = {(char*)"top", (char*)"-d", (char*)"1", (char*)"-n", (char*)"3", NULL};
char* const env[] = {(char*)"haha = HAHA", (char*)"PATHd = xxx", (char*)"kid = miney ", (char*)"moveud = normal", (char*)"most = object"};
// execvp(argv[0],argv);
// execlp("ls", "ls", "-a", "-l",NULL);
// execv("usr/bin/top", argv);
// execl("usr/bin/ls", "ls", "-a", "-l",NULL);
// execl("usr/bin/top", "top", "-d", "1", "-n", "3",NULL);
// execl("./myproc", "myproc", "-a", "-b", "-c", NULL);
// execle("./myproc", "myproc", "-a", "-b", "-c", NULL, env);
extern char** environ;
putenv((char*)"haha=hehe");
putenv((char*)"class=118");
execle("./myproc", "myproc", "-a", "-b", "-c", NULL, environ);
//我们没有传递环境变量!
//execl("/usr/bin/bash", "bash", "shell.sh", NULL); //?????
//execl("/usr/bin/python3", "python3", "test.py", NULL); //?????
exit(1);
}
// father
pid_t rid = waitpid(id, NULL, 0);
if (rid > 0) {
printf("wait child process success\n");
}
return 0;
}
myproc.cc#include <iostream>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[], char* env[]) {
printf("===========================================\n");
std::cout << "我是一个 C++ 程序,我变成了一个进程:" << getpid() << std::endl;
for (int i = 0; i < argc; i++) {
printf("argv[%d]: %s\n", i, argv[i]);
}
printf("===========================================\n");
for (int i = 0; env[i]; i++) {
printf("env[%d] : %s\n", i, env[i]);
}
printf("===========================================\n");
return 0;
}
常用函数实战示例 (单独拿出来几个再看看,剩下的大家自己举一反三即可):
示例 1:execlp 执行系统命令(自动搜 PATH)#include <unistd.h>
#include <stdio.h>
int main() {
// 执行 ls -l 命令,execlp 自动从 PATH 中查找 ls 程序
execlp("ls", "ls", "-l", NULL);
// 末尾必须传 NULL
// 若替换成功,以下代码不会执行
perror("execlp failed");
return 1;
}
示例 2:execvp 执行自定义程序(数组传参)#include <unistd.h>
#include <stdio.h>
int main() {
char* const argv[] = {"ls", "-a", "-l", NULL};
// 数组末尾必须为 NULL
execvp("ls", argv);
// 自动搜 PATH
perror("execvp failed");
return 1;
}
示例 3:execve 自定义环境变量#include <unistd.h>
#include <stdio.h>
int main() {
char* const argv[] = {"echo", "PATH", NULL};
char* const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
// 自定义环境变量
execve("/bin/echo", argv, envp);
// 需全路径
perror("execve failed");
return 1;
}

NULL:exec 函数通过 NULL 判断参数结束,否则会导致参数解析错误;chmod +x 程序名);e 的 exec 函数使用当前进程的环境变量,带 e 的需手动传递环境变量数组;exec 成功后,当前进程的原有代码和数据被覆盖,后续代码不会执行(除错误处理)。常见误区澄清:
程序替换会创建新进程:错误!替换后 PID 不变,仅用户空间代码和数据被覆盖;waitpid 只能等待指定 PID 的子进程:错误!pid=-1 时可等待任意子进程,功能同 wait;exec 函数可以返回成功:错误!替换成功后不会返回,只有失败时返回 -1;非阻塞等待不需要循环:错误!需通过循环持续检测,直到回收子进程或失败;僵尸进程可以通过 kill 删除:错误!僵尸进程已退出,只能通过父进程等待或父进程退出回收。
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online