本文主要讨论进程的等待。
进程等待
为什么要进行进程等待?
在前面进程状态中我们知道,子进程退出的时候,父进程如果不做处理,进程就会变为z状态,即僵尸进程,从而导致内存泄漏。 关于进程状态我们已经在前面的文章介绍过:进程状态
而进入僵尸状态的进程不会再被杀死,就算我们输入信号也无法处理这样一个死去的进程。
所以,父进程需要通过进程等待的方式来对进程进行回收,获取子进程的退出信息;也能解决内存泄漏问题。
什么是进程等待?
进程等待是一个通过系统调用 wait/waitpid 来进行对子进程的状态检测与回收的功能。
下面我们利用fork创建子进程,再让子进程结束运行变为僵尸进程:
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id<0)//创建子进程失败
{
perror("fork");
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("child, pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
exit(0);//子进程退出
}
else
{
//parent
while(1)
{
printf("father,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
下面我们再打开一个窗口,输入如下命令:
cpp
while :; do ps ajx | head -1 && ps ajx | grep test |grep -v grep; sleep 1;echo "-----------------------------------------";done
来同步监视状态的变化。运行上面代码后可以在窗口中看到如下场景:

可以看到在五次循环后,即五秒之后子进程变为僵尸进程状态。
且代码的打印结果为:

wait/waitpid
下面我们介绍如何进行进程等待。
进程等待需要用到wait。我们用man命令查看一下:
bash
man 2 wait

其说明为等待一个进程,直到该进程的状态改变。我们先不管函数的参数,在上面的代码例子中,我们修改parent部分的代码,让进程等待实现:
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
//...
else
{
//parent
int val = 10;
while(val)
{
printf("father,pid:%d,ppid:%d,val:%d\n",getpid(),getppid(),val);
sleep(1);
val--;
}
pid_t ret = wait(NULL);
if(ret == id)//判断等待的子进程
{
printf("wait success,ret:%d\n",ret);
}
sleep(5);
}
//...
之后重复上面的操作,运行之后可以看到子进程在五秒后进入僵尸状态,再五秒后被回收。从打印结果中可以看到,pid为ret == 13079 的子进程被成功回收。

多个进程
我们也可以通过循环来一次性创建多个子进程。比如如下书写main函数:
cpp
int main()
{
int i = 0;
for(i=0;i<N;i++)
{
pid_t id = fork();//创建子进程
if(id == 0)
{
TestChild();
exit(0);
}
}
}
由于父子进程的代码是共享的,子进程会进入if的判断并通过exit退出;而父进程则会继续进行循环并创建子进程,从而实现一次性创建多个子进程的功能。
下面我们在让子进程打印一下自己的pid信息,并通过sleep等待缓慢打印结果。
最后使用wait进行等待,将创建的僵尸子进程回收。完整代码如下:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define N 10//准备创建十个进程
void TestChild()
{
int cnt = 5;
while(cnt)
{
printf("I am child process,pid:%d,ppid:%d\n",getpid(),getppid());
cnt--;
sleep(1);
}
}
int main()
{
int i = 0;
for(i=0;i<N;i++)
{
pid_t id = fork();//创建子进程
if(id == 0)
{
TestChild();
exit(0);
}
printf("create child process: %d success\n",id);
}
sleep(10);
//等待
for(i = 0;i < N;i++)
{
pid_t id = wait(NULL);
if(id>0)
{
printf("wait %d success\n",id);
}
}
sleep(5);
return 0;
}
运行之后可以看到一瞬间创建出了十个僵尸子进程,在10秒后者十个子进程被全部回收,只剩下父进程仍在运行;再5秒后所有进程结束。
阻塞等待
在上面的代码中,如果子进程始终不退出:
cpp
for(i=0;i<N;i++)
{
pid_t id = fork();//创建子进程
if(id == 0)
{
TestChild();
//进程始终不退出
//exit(0);
}
}
并且我们中途也不进行等待sleep(10),直接让父进程进入等待,那么:
父进程默认在wait的时候调用这个系统调用的时候,也就不返回;该状态就为阻塞状态。所以一个进程在阻塞态的时候,不是一定在等待硬件资源,也可以等待软件资源,也就是这个子进程不退出,不进入僵尸状态的情况。
waitpid
下面我们来介绍waitpid。同样使用man命令查看一下:

我们可以发现waitpid的参数明显毕wait更多,不过二者都有一个 int *status 参数。这些参数表示什么呢?
参数:
- pid: Pid=-1,等待任一个子进程。与wait等效。
- pid>0:等待其进程ID与pid相等的子进程。
- status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
在上面的代码中,int *status 参数部分可以设置为NULL;此时表示进程等待可以不关心进程的状态。