Linux操作系统——进程控制(二) 进程等待

1.什么是进程等待?

关于进程等待我们要回忆一下当时我们说到的进程状态中,子进程在终止的时候会出现一种状态叫僵尸状态,一旦僵尸了,那么子进程就需要等到我们的父进程或者系统对子进程进行回收,那么什么是进程等待呢?

其实是通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程。

2.进程为什么要进行等待?

a.解决子进程僵尸问题带来的内存泄漏问题 ---这个工作在目前来看是必须要做的。

b.父进程为什么要创建子进程呢? 要让子进程来完成任务。子进程将任务完成的如何要不要 知道?要知道----需要通过进程等待的方式,获取子进程退出信息----两个数字,一个叫信号编号,一个叫进程退出码。而这个理由不是必须的,但是系统需要提供这样的基础功能!

进程等待的必要性:

之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法
杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,
或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.进程如何进行等待?

wait方法

wait方法手册

Linux操作系统——进程控制(二) 进程等待_第1张图片

我们通过wait先验证两个问题:

下面我们用以下代码来进行演示该wait方法是如何实现进程等待的。

Linux操作系统——进程控制(二) 进程等待_第2张图片

这段代码一开始是父进程子进程一起跑,而子进程只会跑前五秒,所以前五秒的时候子进程一定是S状态或者R状态,5秒过后子进程退出,退出之后,父进程后面sleep5秒之后子进程会变成僵尸状态,我们对应的父进程10s后会打印出上述代码那句话,打印出来之后,那么子进程的僵尸状态就消失了。

运行结果如下:

Linux操作系统——进程控制(二) 进程等待_第3张图片

以上测试验证了如下结论:a.进程等待能回收子进程僵尸状态:Z->x。

关于wait需要验证的第二个问题:

在子进程运行期间,父进程有没有调用wait呢?在干什么呢?

下面我们用以下代码来进程验证:

Linux操作系统——进程控制(二) 进程等待_第4张图片

运行该段代码:

Linux操作系统——进程控制(二) 进程等待_第5张图片

监视窗口:

Linux操作系统——进程控制(二) 进程等待_第6张图片

一开始我们的子进程和父进程都是一起运行的,而当子进程在运行这5秒期间,父进程是通过调用了wait方法进程阻塞等待的,因为这5秒期间过后才打印出来wait after,说明这段时间父进程是在等子进程变成僵尸状态,wait自动回收,最后父进程sleep10秒钟。

这也就说明了第二个问题的答案:

b.如果子进程根本没有退出,父进程必须在wait上进行阻塞等待,直到子进程僵尸,wait自动回收,返回。

一般而言,父子进程谁先运行我们是不知道的,但是一般都是父进程最后退出。因为一般来说子进程都是由父进程创建,然后最后由父进程统一回收的。

#include
#include
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid方法

用了上述wait方法呢,我们大致了解了一下进程等待是如何等待的,那么下面呢我们通过man手册来看看waitpid方法的用法:

Linux操作系统——进程控制(二) 进程等待_第7张图片

waitpid需要传3个参数,与wait不同的是,该方法可以等待指定的某个子进程。下面我们将代码改成如下图中代码:

Linux操作系统——进程控制(二) 进程等待_第8张图片

运行结果:

Linux操作系统——进程控制(二) 进程等待_第9张图片

监视窗口:

Linux操作系统——进程控制(二) 进程等待_第10张图片

刚开始时子进程运行5秒,父进程休眠10秒,然后子进程退出,变成僵尸状态,父进程再休眠5秒后调用waitpid方法将子进程自动回收,子进程僵尸状态瞬间消失了,最后打印wait success,说明等待成功了。

下面我们再对waitpid的第二个参数int *  status进行进一步理解:

下面我们以如下这段代码来进行测试

Linux操作系统——进程控制(二) 进程等待_第11张图片

运行之后:

Linux操作系统——进程控制(二) 进程等待_第12张图片

我们发现status被改成了2560,那么为什么会是2560呢?这是怎么来的呢?

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
 

Linux操作系统——进程控制(二) 进程等待_第13张图片

status是一个int的整数,32bit位,但是我们只考虑低16位,而这16位里面又分为两种情况,一种是正常终止的情况,那么高8位就是进程的退出状态,低8位是终止信号,如果正常终止那么默认是0.

如果是被信号所杀,那么退出状态就不会使用到了,因为这属于异常终止,这时只有低8位有效,其中有一位时core dump标志,我们暂且不谈,还有七位使用了表示终止信号的。

我们上面设置的退出状态是exit(10),也就是10,而10用二进制来表示是1010,放在高8位上则是

0000 1010 0000 0000,而该二进程算出来就是我们上面打印出来的status的值:2560.

下面我们对status这16位通过如下的代码来进行测验

Linux操作系统——进程控制(二) 进程等待_第14张图片

通过位运算取到status各个位上的信息进行打印。

运行结果:

Linux操作系统——进程控制(二) 进程等待_第15张图片

exit sig: 0 代表的信号是0,而信号中并没有0信号,说明运行是成功的:

Linux操作系统——进程控制(二) 进程等待_第16张图片

但是退出码是1,代表结果不正确,运行正常和结果正确必须exit sig和exit code都为0.如果我们的exit sig不为0,也就是代表代码运行异常,此时exit code退出码则没有意义了。

下面我们给代码创建几个异常进行测验:

除0错误:

Linux操作系统——进程控制(二) 进程等待_第17张图片

运行结果:

Linux操作系统——进程控制(二) 进程等待_第18张图片

我们发现收到的信号是8号信号,对应的信号是SIGFPE,除0错误。

空指针异常:

Linux操作系统——进程控制(二) 进程等待_第19张图片

运行结果:

Linux操作系统——进程控制(二) 进程等待_第20张图片

我们发现变成了11号信号,对应的SIGSEGV.也就是段错误。

也就是父进程已经收到了子进程异常退出的信号。

下面我们针对刚刚的现象来提出几个问题:

1.当一个进程异常了(收到信号),exit code退出码还有意义吗? 没有意义。

2.有没有收到信号怎么判定? exit sig: 0

信号我们目前还不是很懂,但是我们可以通过kill -l 命令知道它是有一个数字对应一个信号的名称,这个名称是一个大写的宏,当我们查看信号列表的时候我们可以发现信号列表里是没有0号信号的,换句话说有没有收到信号就是我们的exit sig是不是0.如果是0那就是没有收到信号,如果不是0那就说明收到了信号。

3.我通过手动杀掉进程会有什么现象呢?

下面我们把子进程调用的Worker函数改成死循环通过手动杀掉这个死循环的子进程让其停下来

Linux操作系统——进程控制(二) 进程等待_第21张图片

运行结果:

Linux操作系统——进程控制(二) 进程等待_第22张图片

我们发现通过发送9号信号手动杀掉子进程6857让其进程终止。

那么父进程又是如何得知子进程的退出信息呢? wait   waitpid?

可是我们不想从系统调用的方面去谈了,而是从操作系统的层面上来谈:

Linux操作系统——进程控制(二) 进程等待_第23张图片

上述我们只是说了父进程等待一个子进程的案例,下面我们展示下父进程等待多个子进程的案例:

下面我们创建一个多进程的代码:

#include 
#include 
#include 
#include 
#include 

//int status = 0;
void Worker(int number)
{
    int *p = NULL;
    int cnt = 10;
    while(cnt)
    {
        printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--, number);
        sleep(1);

        //*p = 100;
        //int a = 10;
        //a /= 0;
    }
}
const int n = 10;


int main()
{
    for(int i = 0;i < n; i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            Worker(i);
            //status = i;
            exit(0);
        }
    }

    //等待多个子进程?
    for(int i = 0; i < n; i++)
    {
        int status = 0;
        pid_t rid = waitpid(-1, &status, 0); // pid>0, -1:任意一个退出的子进程
        if(rid > 0){
            printf("wait child %d success, exit code: %d\n", rid, WEXITSTATUS(status));
        }
    }

    return 0;
}

运行之后:

Linux操作系统——进程控制(二) 进程等待_第24张图片

监视窗口:

Linux操作系统——进程控制(二) 进程等待_第25张图片

最后代码的退出结果是:

Linux操作系统——进程控制(二) 进程等待_第26张图片

我们为什么不用全局变量获取子进程的退出信息?而用系统调用?

原因:进程具有独立性,父进程无法直接获得子进程的退出信息。

下面我们来谈waitpid的最后一个参数:int options

当options为0时:阻塞等待

WNOHANG:等待的时候,以非阻塞的方式等待。

下面我们用一个故事来进行理解一下

故事时刻:

故事一:假设你的名字叫小明,在学校里呢是一个放荡不羁的公子哥,反正就是在学校就是上课经常不去,考试能不能过呢完全是看你努不努力,或者看运气,你呢有你自己的努力方式。小明你呢有一个同学叫做小张,小张是你们班的努力型学霸,经常稳定在班上排前十名,然后上课笔记也做得特别详细。然后呢,突然有一天要考c语言了,你呢一开始并不知道,突然在宿舍听到你室友说过两天要考c语言,你心想:"这下坏了,啥也不知道咋过呢"。然后你突然想到你班上有个跟你玩的还可以的努力型学霸小张,知道他做笔记做的好,肯定做了老师划重点的笔记,然后就打算找他带你好好复习一下,然后你走到他的宿舍楼下,就跟小张打电话:"小张啊,在不在?过两天是不是要考c语言了啊?" 小张说:"是啊" 然后你说:"那你等会有没有时间啊?" 小张就说:"有时间啊,不过得再等我30来分钟把这个知识点复习完才有空"  那么此时,你听到说有空,那你就又说:"行,那你一会把你的c语言笔记带上,咱们一会去食堂吃个饭,我请你,我想借你的c语言复习笔记拿来复习一下。" 然后小张听说有人请吃饭,给他复习,顺便也可以给我再复习一遍,然后就说:"行吧,那你等一等我"。然后你呢就把电话挂了,准备在下面刷了会短视频等着。然后你等了一会呢,就有点着急了,这怎么还没好啊,然后呢你又给小张来了个电话:"小张,你好了没?"小张说:"我不跟你说了吗,我还得 一会呢,你在等我把这个知识点弄清楚了。" 然后呢你说:"好吧。"然后你把电话一挂,挂了之后呢,你又闲着没事干,然后又玩了玩手机,给家里面打了个电话,聊了会天,打了会游戏,你发现小张还没下来,又继续打电话:"小张,你还没好呢嘛?" 小张说:"快了,快了。"   然后你挂电话继续玩你的手机,又过了好一会,你发现还没下来,又打了个电话:"小张,你好了没?" 小张说:"好了,在下楼了。" 然后过了好一会儿,终于小张看到你,你两愉快的去食堂吃个饭顺便复习去了,然后过了两天你以60分的c语言成绩冒险过了。

故事二:

又过了几天突然又说要考数据结构了,发现比c语言更难了,然后你又再次想到了小张,照样走到小张宿舍楼下跟他打电话说:"过两天又要考试了,跟上次一样,我请你吃饭你给我划重点复习呗!"小张听到就回复说:"好"。 然后你又说:"这次电话别挂了,你就跟我保持着吧,搞完了早点下来啊,到时候你弄好了直接跟我说一声再挂电话"  小张说:"行吧" 然后你等了十来二十分钟也不知道小张在干嘛,最后下来了。然后看到你之后在电话里跟你说了句话,然后挂了。

Linux操作系统——进程控制(二) 进程等待_第27张图片

进程的阻塞式等待方式:

int main()
{
    pid_t pid;
    pid = fork();
    if(pid < 0){
    printf("%s fork error\n",__FUNCTION__);
    return 1;
} else if( pid == 0 ){ //child
    printf("child is run, pid is : %d\n",getpid());
    sleep(5);
    exit(257);
} else{
    int status = 0;
    pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
    printf("this is test for wait\n");
    if( WIFEXITED(status) && ret == pid ){
    printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
    }else{
    printf("wait child failed, return.\n");
    return 1;
}

}
return 0;
}
运行结果:
[root@localhost linux]# ./a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.

进程的非阻塞式等待:

#include 
#include 
#include 
#include 
int main()
{
    pid_t pid;
    pid = fork();
    if(pid < 0){
    printf("%s fork error\n",__FUNCTION__);
    return 1;
    }else if( pid == 0 ){ //child
    printf("child is run, pid is : %d\n",getpid());
    sleep(5);
    exit(1);
    } else{
    int status = 0;
    pid_t ret = 0;
    do
    {
    ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
    if( ret == 0 ){
    printf("child is running\n");
    }
    sleep(1);
    }while(ret == 0);
    if( WIFEXITED(status) && ret == pid ){
    printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
    }else{
    printf("wait child failed, return.\n");
    return 1;
        }
    }
    return 0;
}

你可能感兴趣的:(linux,运维,服务器)