跳到主要内容 Linux 进程等待与程序替换详解:僵尸进程防治及 exec 实战 | 极客日志
C++
Linux 进程等待与程序替换详解:僵尸进程防治及 exec 实战 Linux 进程管理中,进程等待用于回收子进程资源并避免僵尸进程,通过 wait 和 waitpid 系统调用实现阻塞或非阻塞等待,可获取子进程退出状态。程序替换利用 exec 函数簇将新程序加载至当前进程地址空间,覆盖原有代码,PID 保持不变。掌握这两项技术是理解 Shell、服务器等多任务程序的基础,涉及 fork、exit、信号处理及环境变量传递等关键细节。
游戏玩家 发布于 2026/2/5 更新于 2026/4/19 1.7K 浏览前言
在 Linux 进程管理中,进程等待和程序替换 是衔接'进程创建'与'进程终止'的关键环节:进程等待解决了子进程退出后资源泄漏(僵尸进程)的问题,同时让父进程获取子进程的执行结果;程序替换则让子进程能脱离父进程代码,执行全新的程序(如 ls、ps 等系统命令),是 Shell、服务器等多任务程序的核心实现基础。本文从进程等待的必要性、两种等待方式(阻塞/非阻塞),到程序替换的原理、exec 函数簇的用法,再到实战案例,层层递进拆解核心逻辑。
一。进程等待:回收子进程资源,避免僵尸进程
进程等待是父进程主动回收子进程资源、获取子进程退出状态的过程,是防治僵尸进程的唯一有效手段。
1.1 进程等待的必要性
避免僵尸进程 :子进程退出后,若父进程不回收,其 task_struct(PCB) 会一直保留在内存中,成为僵尸进程(Z 状态),占用系统资源;
获取执行结果 :父进程通过等待可获取子进程的退出码(正常终止)或终止信号(异常终止),判断任务执行是否成功;
僵尸进程不可杀 :一旦子进程变成僵尸状态,即使 kill -9 也无法删除,只能通过父进程等待或父进程退出(子进程被 1 号进程领养回收)解决。
1.2 进程等待的两种核心方法
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;
:
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
核心参数解析
参数 取值与含义 pid-1 :等待任意子进程(同 wait)>0 :等待 PID 等于该值的子进程0 :等待同进程组的子进程status输出型参数,存储退出状态,解析方式同 wait options0 :阻塞等待WNOHANG :非阻塞等待(无退出子进程时立即返回 0)
图中展示了父子进程的状态流转,父进程调用 waitpid 阻塞直到子进程结束并回收资源。
#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 );
}
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 ;
}
status 的值经过位运算提取,例如 exit(11) 对应的 status 值需右移 8 位取低 8 位才能得到 11。
我是父进程:pid: 1234 , ppid: 5678
我是子进程:pid: 1235 , ppid: 1234 , cnt: 5
...
子进程退出!
等待子进程成功..., status: 2816 , exit code: 11
1.3 解析子进程退出状态(status 参数) status 并非普通整数,而是一个 16 位的位图,核心有效位为低 16 位,解析规则如下:
低 7 位 :存储子进程的终止信号(若为非 0,说明子进程异常终止);
高 8 位 :存储子进程的退出码(仅当低 7 位为 0 时有效,即正常终止);
WIFEXITED(status) :判断子进程是否正常终止(返回非 0 为正常);
WEXITSTATUS(status) :提取子进程的退出码(仅 WIFEXITED 为真时有效);
WIFSIGNALED(status) :判断子进程是否被信号终止(返回非 0 为信号终止);
WTERMSIG(status) :提取终止子进程的信号编号(仅 WIFSIGNALED 为真时有效)。
#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 );
}
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 ;
}
输出结果(正常退出) :子进程正常退出,退出码:10
输出结果(信号终止) :子进程被信号终止,信号编号:9
1.4 阻塞等待 vs 非阻塞等待
父进程暂停执行,直到有子进程退出,适合不需要并发处理其他任务的场景;
代码简单,无需循环检测。
(2)非阻塞等待(options=WNOHANG)
父进程发起等待后立即返回,若无子进程退出则返回 0,不会阻塞;
适合父进程需要并发处理其他任务的场景(如服务器同时处理多个客户端请求);
需通过循环持续检测,直到回收子进程。
#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 ;
}
#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 )
{
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>
#include <cstdlib>
#include <unistd.h>
#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 )
{
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 函数簇,将磁盘上的全新程序(代码 + 数据)加载到当前进程的地址空间,覆盖原有代码和数据,从新程序的入口开始执行。
2.1 程序替换的核心原理
不创建新进程 :替换后进程的 PID 不变,仅用户空间的代码和数据被替换;
替换成功不返回 :若 exec 函数调用成功,新程序会立即执行,不会回到原进程的代码;
替换失败返回 -1 :只有当程序路径错误、权限不足等情况时才会返回错误。
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 );
}
pid_t rid = waitpid(id, NULL , 0 );
if (rid > 0 )
{
printf ("wait child process success\n" );
}
return 0 ;
}
2.2 exec 函数簇(6 个核心函数,含实战示例) 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) :
#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" };
extern char **environ;
putenv((char *)"haha=hehe" );
putenv((char *)"class=118" );
execle("./myproc" , "myproc" , "-a" , "-b" , "-c" , NULL , environ);
exit (1 );
}
pid_t rid = waitpid(id, NULL , 0 );
if (rid > 0 )
{
printf ("wait child process success\n" );
}
return 0 ;
}
#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 ()
{
execlp("ls" , "ls" , "-l" , NULL );
perror("execlp failed" );
return 1 ;
}
示例 2:execvp 执行自定义程序(数组传参)
#include <unistd.h>
#include <stdio.h>
int main ()
{
char *const argv[] = {"ls" , "-a" , "-l" , NULL };
execvp("ls" , argv);
perror("execvp failed" );
return 1 ;
}
#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 ;
}
2.3 程序替换的关键注意事项
参数末尾必须加 NULL :exec 函数通过 NULL 判断参数结束,否则会导致参数解析错误;
权限检查 :执行的程序必须有可执行权限(chmod +x 程序名);
环境变量传递 :不带 e 的 exec 函数使用当前进程的环境变量,带 e 的需手动传递环境变量数组;
替换后原代码失效 :exec 成功后,当前进程的原有代码和数据被覆盖,后续代码不会执行(除错误处理)。
程序替换会创建新进程 :错误!替换后 PID 不变,仅用户空间代码和数据被覆盖;
waitpid 只能等待指定 PID 的子进程 :错误!pid=-1 时可等待任意子进程,功能同 wait;
exec 函数可以返回成功 :错误!替换成功后不会返回,只有失败时返回 -1;
非阻塞等待不需要循环 :错误!需通过循环持续检测,直到回收子进程或失败;
僵尸进程可以通过 kill 删除 :错误!僵尸进程已退出,只能通过父进程等待或父进程退出回收。
结语 进程等待和程序替换是 Linux 多任务编程的核心技术:进程等待解决了资源泄漏和结果获取问题,程序替换让子进程能灵活执行全新程序。两者结合是实现 Shell、服务器等复杂程序的基础,掌握后能大幅提升你对 Linux 进程管理的理解深度。本文覆盖了等待方式、状态解析、exec 函数簇用法和实战案例,代码可直接编译运行。如果需要深入学习 Shell 的内建命令(如 cd、export)实现,或进程间通信与程序替换的结合场景,可以进一步扩展。