Linux(10)—— 进程控制(等待)
本文主要讨论进程的等待。
进程等待
为什么要进行进程等待?
在前面进程状态中我们知道,子进程退出的时候,父进程如果不做处理,进程就会变为z状态,即僵尸进程,从而导致内存泄漏。 关于进程状态我们已经在前面的文章介绍过:进程状态
而进入僵尸状态的进程不会再被杀死,就算我们输入信号也无法处理这样一个死去的进程。
所以,父进程需要通过进程等待的方式来对进程进行回收,获取子进程的退出信息;也能解决内存泄漏问题。
什么是进程等待?
进程等待是一个通过系统调用 wait/waitpid 来进行对子进程的状态检测与回收的功能。
进程等待使用wait进行系统调用,访问进程的PCB,将僵尸进程释放掉。
下面我们利用fork创建子进程,再让子进程结束运行变为僵尸进程:
#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;
}
下面我们再打开一个窗口,输入如下命令:
while :; do ps ajx | head -1 && ps ajx | grep test |grep -v grep; sleep 1;echo "-----------------------------------------";done
来同步监视状态的变化。运行上面代码后可以在窗口中看到如下场景:

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

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

其说明为等待一个进程,直到该进程的状态改变。我们先不管函数的参数,在上面的代码例子中,我们修改parent部分的代码,让进程等待实现:
#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函数:
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进行等待,将创建的僵尸子进程回收。完整代码如下:
#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秒后所有进程结束。
阻塞等待
在上面的代码中,如果子进程始终不退出:
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;此时表示进程等待可以不关心进程的状态。
status
这部分的参数实际上是一段32位的二进制数,其存储的信息可以帮助父进程获取进程的运行情况:即程序运行是否异常?程序的退出码是多少?
如果程序是正常执行,那么就会返回退出码;如果程序异常,就会被信号终止。
在这段二进制数中,后八位表示信号;如果这八位为0,那么表示该进程跑完,没有中途停止。如果wield其他数,则会受到数字对应的信号。关于信号的内容在后面的文章介绍。
次八位就表示进程的退出码。所以,根据这段二进制数,父进程就能掌握子进程的运行情况。
阻塞非阻塞轮询
阻塞:我们用一个例子来理解传统阻塞方式;当你调用一个函数时,如果数据还没有准备好,程序就会挂起(卡住),直到数据准备完毕才返回。这样的方式会让程序一直等待返回结果,不能做自己的事情。
非阻塞轮询:和阻塞一直挂起不同,非阻塞轮询发出请求时,系统会立即返回结果;
如果数据好了,直接带走数据结果;
如果数据没好,返回一个错误码;
由于程序立即返回结果,只要没拿到结果,它就可以通过循环一直进行询问,这种方式就是轮询。
在实际操作中,阻塞和非阻塞轮询都会被使用;阻塞虽然有明显的缺点,但逻辑简单;非阻塞轮询可以让程序做自己的事情,每隔一段时间过来询问,降低等待成本。
option
option这个参数就是用于选择阻塞方式。
option=0:表示阻塞。
option=WNHANG:表示非阻塞方式。
如果想要实现轮询,那么套用在循环中即可。
更多推荐



所有评论(0)