进程等待
前面我们了解了如果父进程没有回收子进程, 那么当子进程接收后, 就会一直处于僵尸状态, 导致内存泄漏, 那么我们如何让父进程来回收子进程的资源.
waitpid
我们可以通过 Linux 提供的系统调用函数 wait 系列函数来等待子进程死亡, 并回收资源.
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
wait
函数用于等待任何一个子进程结束, 并回收其资源.
status
:指向整数的指针, 用于存储子进程的退出状态. 如果不需要这个信息, 可以传递NULL.
- 成功时返回被等待的子进程的PID, 失败时返回 -1, 并设置 errno.
waitpid
函数允许父进程等待特定的子进程结束
pid
:子进程的PID. 如果为 -1, 则等待任何一个子进程。status
:同wait
函数.options
:等待选项, 常用的有 WNOHANG (非阻塞等待).- 成功时返回被等待的子进程的PID. 失败时返回
-1
,并设置errno
。
一般来说, 用 waitpid 多一点, 因为 waitpid 提供的更为细致的操作.
int main()
{
pid_t id = fork();
if(id<0)
{
perror("fork");
exit(1);
}
if(id==0)//子进程代码
{
int count = 5;
while(count)
{
printf("[%d]我是子进程,我的pid是: %d\n",count,getpid());
sleep(1);
count--;
}
exit(0);//子进程执行完代码后退出, exit 会直接终止本进程
}
//父进程代码
waitpid(id,NULL,0);
printf("等待子进程成功!\n");
return 0;
}
可以观察到, 在等待子进程结束之前, 父进程卡在了 waitpid(), 直到子进程都被等待成功, 父进程才会继续向后执行.
status 参数
在 wait 和 waitpid 函数中都存在一个 status 的参数.
在 status 中存储着子进程的退出码和退出信号.
如果父进程想要了解子进程的退出信息, 可以通过 status 来了解.
status 是一个 int 类型的变量, 一共有 32 个bit, 我们主要看它的低 16 位bit.
那么如何获取退出状态 (退出码) 和 信号
退出状态 (退出码):
(status >> 8) & 0xFF
退出信号:
status & 0x7F
int main()
{
pid_t id = fork();
if(id<0)
{
perror("fork");
exit(1);
}
if(id==0)//子进程代码
{
int count = 5;
while(count)
{
printf("[%d]我是子进程,我的pid是: %d\n",count,getpid());
sleep(1);
count--;
}
exit(55);//子进程执行完代码后退出
}
//父进程代码
int status = 0;
waitpid(id,&status,0);
printf("等待子进程成功!\n");
printf("进程退出码: %d,进程退出信号: %d\n",(status >> 8) & 0xFF,status & 0x7F);
return 0;
}
当子进程在运行时, 使用 kill -9 617714命令, 来终止子进程, 那么也就能观察到, 退出信号为 9.
option
在前面的参数解释中说到, 这是一个等待选项.
父进程可以选择一直阻塞下去, 直到等到子进程的死亡,
或者当子进程还没死亡时, 父进程去执行其他的代码. 等一会再来查看子进程是否死亡.
waitpid(pid,&status,WNOHANG); // 非阻塞等待
waitpid(pid,&status,0); // 阻塞等待
int main()
{
pid_t id = fork();
if(id<0)
{
perror("fork");
exit(1);
}
if(id==0)//子进程代码
{
int count = 5;
while(count)
{
printf("[%d]我是子进程,我的pid是: %d\n",count,getpid());
sleep(1);
count--;
}
exit(55);//子进程执行完代码后退出
}
//父进程代码
while(1)//循环访问子进程退出情况
{
int wait = waitpid(id,NULL,WNOHANG);
if(wait>0)//子进程退出成功
{
printf("子进程退出成功,子进程pid: %d\n",wait);
break;
}
else if(wait==0)//子进程还没退出,父进程干自己的事情
{
//此处简单模拟父进程干的事情
printf("我是父进程\n");
}
else //等待子进程退出失败
{
perror("waitpid");
exit(1);
}
sleep(1);
}
return 0;
}
执行上面的代码就能观察到, 在子进程结束前, 父进程还在向频幕上打印文字.
进程替换
创建子进程是为了完成一些工作, 但是子进程的代码和父进程是一样的,
有可能子进程并不需要执行父进程的代码, 而是执行一些其他代码.
这种场景下, 就可以使用进程替换.
我们为什么不直接将子进程要执行的代码写在父进程中呢, 还要去使用进程替换?
1. 如果所有的代码都放在父进程中, 那么父进程的代码会有多么的庞大,
这会提高代码编写和维护的成本.
2. 进程所执行的一定是我们的C/C++程序吗? 进程也可以执行其他的非 C/C++ 程序,
那对于那些非 C/C++ 程序 (java程序), 我们无法将他们和我们所写的 C/C++ 代码整合在一起, java 和 C/C++ 的运行环境都不同.
exec 系列函数
如果想要创建出来的子进程执行全新的程序, 可以使用 exec 系列函数进行程序替换.
一共有6个函数, 其中主要分为两类
1. execl 系列
2. execv 系列
execl
int main()
{
printf("进行程序替换了\n");
int n = execl("/usr/bin/ls","ls","-a","-l",NULL);
if(n==-1)
{
perror("execl");
}
printf("程序替换完毕!\n");
return 0;
}
execl参数: 第一个是要执行程序的路径 (/usr/bin/ls),
第二个参数是要执行的程序的名称 (ls),后面的参数到 NULL 之前, 都是要替换的程序参数 (-a, -l, 都是 ls 程序的参数).
execl 中 l, 表示如何将参数传递要替换的程序. l 表示通过一个列表的方式,
即向上面的 "-l", "-a"..., 一个列表的形式.
execlp 和 execle 两个函数则分别多了 p 和 e.
p 则代表要执行的程序可以从环境变量 PATH 中找到, 所以不用写执行程序的路径.
e 则表示, 可以传入用户自己定义的环境变量 (_env[]) 给程序使用.
int main()
{
printf("我要进行程序替换了...\n");
int n = execlp("ls","-l",NULL);
if(n==-1)
{
perror("execl");
}
printf("程序替换完毕!\n");
return 0;
}
int main()
{
const char* _env[]={"MY_ENV=666",NULL};
printf("我要进行程序替换了...\n");
int n = execle("/usr/bin/ls","ls","-l",NULL,_env);//自己定义一个环境变量MY_ENV=666传递给要去执行的程序
if(n==-1)
{
perror("execl");
}
printf("程序替换完毕!\n");
return 0;
}
execv
上面的 execl 中的 l, 代表传参使用列表的形式.
那么 v 很容易就想到了是vector.
所以 execv 函数在给替换的程序传参时, 是通过一个 vector 来传参的.
int main()
{
char* const set[]={"ls","-a","-l",NULL};
printf("我要进行程序替换了...\n");
int n = execv("/usr/bin/ls",set);
if(n==-1)
{
perror("execl");
}
printf("程序替换完毕!\n");
return 0;
}
那么剩下的 execvp 和 execvpe 和之前的 execl 系列中的一样.
p 代表在环境变量 PATH 中查找, e 可以传入自己定义的环境变量.
- l (list): 传参的方式为使用列表来传递
- v (vector): 使用数组来传递参数
- p (path): 会在环境变量 PATH 中查找程序
- e (env): 可以传递自己定义的环境变量