【Linux】进程等待

目录

一、进程终止

1.1 程序正常终止

1.2 程序异常终止

1.3 exit和_exit

二、进程等待

2.1 进程等待的方法

(1)wait

(2)waitpid

2.2 获取子进程退出状态


一、进程终止

一个进程在退出的时候,有以下三种情况:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

1.1 程序正常终止

我们可以通过在main函数中return、调用exit或_exit函数让一个进程正常终止,进程正常结束后会返回一个退出码。不同的退出码可以代表不同的错误原因,用于告诉用户程序是否发生错误、发生了什么错误。

例如return 0就是返回一个为0的退出码,表示程序成功运行

我们可以通过 echo $? 命令来查看上个进程的退出码,例如:

#include <stdio.h>

int main()
{
    return 12;
}     

这个程序将会返回退出码12,我们运行该程序后输入上面的命令

问题来了:我怎么知道有哪些退出码,什么退出码又对应哪些错误原因呢?

我们可以用strerror函数来观察,向该函数传入退出码后会返回该退出码对应的错误信息

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    for(int i = 0;i < 200;i++)
    {
        printf("%d: %s\n", i, strerror(i));
    }
    return 0;                                                                                                                                                                                                 
}                       

1.2 程序异常终止

有时候运行代码时会遇到一些严重错误导致程序异常终止,此时我们就不用关心退出码了

就好比你考试考差了,原因是身体不舒服。但是如果你作弊,直接无法完成考试,别人也不会关心其他的原因了。所以如果程序异常终止或运行结果不正确,我们都要先检查是否出现异常,再检查退出码

程序发生异常后,对应的异常会被系统转化为信号的形式发送给父进程

在程序中将一个变量除以0,或者使用野指针都会导致异常。我们可以尝试一下:

可以看到除0错误在编译时就已经出现了警告,运行程序后直接发生Floating point exception即浮点数溢出的异常

而野指针错误在编译时虽然不会发出警告,但是运行程序后也会发生Segmentation fault即段错误的异常

实际上,每种异常也都有自己的编号,我们可以使用命令 kill -l 来查看

其中Floating point exception对应的编号为8,Segmentation fault对应的编号为11

我们甚至还可以通过 kill -编号 pid 的方式给一个进程发送信号将其终止,例如:

所以确认有没有发生异常,只需要检测是否收到信号即可。如果没收到信号则说明代码运行完毕,此时再检测退出码

1.3 exit和_exit

前面提到,除了遇到return让程序正常终止,我们还可以在程序中调用exit或_exit函数来让程序正常终止。这三者之间有什么区别呢?

return就像函数的出口,一个函数遇到return只会结束该函数,如果是main函数的话程序才会终止

exit和_exit就像程序的出口,无论在哪个函数中遇到它都会让程序直接终止。这两个函数的参数就是程序的退出码。

那么,这两个函数之间又有什么区别呢?

首先,_exit属于一个系统调用接口,而exit是C语言库中的函数。其次,exit在退出时会先对缓冲区进行刷新,然后才结束进程;而_exit只会直接结束进程

问题又来了,何为缓冲区?

实际上我们在对一个文件进行写入时并不是直接写入到文件中的,而是先写入到缓冲区,缓冲区再根据不同的方式将其内容刷新到文件中

缓冲区有三种刷新方式:

  • 无缓冲,即一进行写入就对缓冲区进行刷新,一个一个字符写入
  • 行缓冲,遇到\n才对缓冲区进行刷新,一行一行写入
  • 全缓冲,缓冲区满了才进行刷新,一堆一堆写入

一般我们在向显示器文件进行写入时采用的策略是行缓冲,即遇到\n就打印一行;而对于普通文件系统则采用全缓冲策略。缓冲区的存在能够减少消耗,提高效率。

知道缓冲区是什么后,我们如何证明exit在退出时会刷新缓冲区,而_exit不会呢?

前面提到,我们在程序中打印内容(向显示器文件中写入)是行缓冲,那么我们可以多打印一些不带\n的内容,这些内容都会堆积在缓冲区中。然后再用exit和_exit终止程序,看看是否会进行打印

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    for(int i = 0;i < 10;i++)                                                                                                                                                                                 
    {
        printf("hello Linux");
    }
    //exit(0);
    _exit(0);
}

运行代码后可以看到,如果用_exit函数终止程序,缓冲区不会被刷新;而用exit函数终止程序时缓冲区被刷新,显示器上也能够显示内容

实际上我们在前面说的缓冲区是语言层面的缓冲区,内核中还存在着其他的缓冲区,这些是后面才会提到的内容


二、进程等待

之前提到过,子进程退出如果没有被父进程回收,就会变成僵尸进程造成内存泄漏。而且我们需要知道子进程是否完成了我们给的任务,所以进程等待就显得很有必要了

进程等待即父进程通过wait或waitpid函数回收子进程的资源,并且获取子进程的退出信息

2.1 进程等待的方法

(1)wait

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int* status);

wait函数成功执行会返回被等待进程的pid,如果失败返回-1

其参数是一个输出型参数,我们可以通过这个参数拿到子进程的退出码等信息,如果不需要获取退出状态则设置为NULL即可

我们可以通过下面这段代码测试一下wait函数:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork error");
        return 1;
    }
    else if(id == 0) //子进程
    {
        int cnt = 3;
        while(cnt) //循环3次
        {
            printf("I am child, pid:%d, ppid:%d, cnt=%d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(0); //子进程先退出
    }
    else //父进程
    {
        int cnt = 5;
        while(cnt) //循环五次
        {
            printf("I am parent, pid:%d, cnt=%d\n", getpid(), cnt);
            cnt--;
            sleep(1);
        }
        pid_t ret = wait(NULL); //等待子进程
        if(ret == id)
        {
            printf("wait success, ret=%d\n", ret); //等待成功
        }
        sleep(5);                                                                                                                                                                                             
    }                                                                      
    return 0;                                                              
}     

运行代码,子进程先退出,然后被父进程回收

wait函数采用阻塞式等待,即如果父进程在执行到wait的时候子进程还没结束,那么父进程停止继续运行,wait函数一直不返回,直到子进程退出

(2)waitpid

#include<sys/types.h>
#include<sys/wait.h>

pid_t waitpid(pid_t pid, int* status, int options);

waitpid函数比wait又多了两个参数,其中pid参数用于设置要等待的进程,设置为-1则等待任意一个进程,如果传入子进程ID则只会等待该进程结束

options参数可以设置为WNOHANG,即非阻塞等待。此时若指定的进程未结束则返回0,否则返回该子进程ID。设置为0则默认阻塞式等待

waitpid成功调用后返回被等待子进程ID,如果设置了非阻塞等待且调用时子进程未结束则返回0,如果调用中出错则返回-1(如等待了一个不属于自己的子进程)

我们可以用下面这段代码测试一下waitpid:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork error");
        return 1;
    }
    else if(id == 0) //子进程
    {
        while(1) //死循环 
        {
            printf("I am child, pid:%d, ppid:%d\n", getpid(), getppid());
            sleep(100);
        }
    }
    else //父进程
    {
        int cnt = 5;
        while(cnt) //循环五次
        {
            printf("I am parent, pid:%d, cnt=%d\n", getpid(), cnt);
            cnt--;
            sleep(1);
        }
        pid_t ret = waitpid(id, NULL, WNOHANG); //非阻塞等待子进程 
        if(ret == id)
        {
            printf("wait success, ret=%d\n", ret); //等待成功
        }
        else if(ret == 0)  
        {                      
            printf("子进程未退出");
        }                                                                                                                                                                                              
    }                                                                      
    return 0;                                                              
}     

运行代码,子进程一直不退出,而waitpid中options设置为WNOHANG即非阻塞等待

此时父进程不再阻塞式等待,如果调用waitpid时子进程还没退出则直接返回0

2.2 获取子进程退出状态

前面提到,wait和waitpid中都有个输出型参数status,我们可以通过它获取子进程的退出状态

用下面这段代码测试是否真的能获取子进程的退出码等信息

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork error");
        return 1;
    }
    else if(id == 0) //子进程
    {
        int cnt = 3;
        while(cnt) //循环3次
        {
            printf("I am child, pid:%d, ppid:%d, cnt=%d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(0); //子进程先退出,这里将退出码设置为1
    }
    else //父进程
    {
        int cnt = 5;
        while(cnt) //循环五次
        {
            printf("I am parent, pid:%d, cnt=%d\n", getpid(), cnt);
            cnt--;
            sleep(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0); //阻塞式等待子进程
        if(ret == id)
        {
            printf("wait success, ret=%d, status=%d\n", ret, status); //等待成功,打印status
        }                                                                                                                                                                                      
    }                                                                      
    return 0;                                                              
}     

运行代码

可以看到子进程的退出码明明是1,但是这里打印status的值竟然是256,为什么?

前面提到,一个进程在退出的时候有三种情况,即正常终止结果正确、正常终止结果不正确和异常终止,所以status不仅要获取进程的退出码,还要获取进程的异常信息。因此status实际上并不是以整型为单位来存储信息的,而是将int整型的前16位分成几个区,一个区包含多个位

其中,16位中的低7位存储异常的终止信号,高8位存储程序的退出状态码

我们可以通过位运算来打印出信号和退出码,如:

 

除了位运算,还有两个宏也可以帮助我们解析status

  • WIFEXITED(status):若子进程正常终止则返回真,用于检测进程是否正常退出
  • WEXITSTATUS(status):提取子进程退出码

除了阻塞式等待和非阻塞等待,还有一种方式叫做非阻塞轮询

非阻塞轮询:非阻塞等待+循环式询问

例如你要找一个人,给他打电话时他在忙,于是你过一段时间就打一次电话问他忙完没,这是非阻塞轮询;如果你和他打电话并一直不挂断,直到他忙完,这是阻塞式等待

完.

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值