Linux进程控制(二)--进程等待(一)

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

Linux进程控制(二)--进程等待(一)_第1张图片

目录

1.进程退出的三种方式

exit函数和_exit函数

return函数

2.进程等待

为什么要进程等待

进程等待的两种方法

wait方法(系统调用)

waitpid方法(系统调用)

status的结构

宏函数WIFEXITED和WEXITSTATUS

options参数

非阻塞轮询等待状态

为何需要通过系统调用才能获取子进程的信息

父进程如何获得子进程的退出信息

多进程的进程等待


1.进程退出的三种方式

exit函数和_exit函数

       在Linux系统中, _exit 和 exit,这两个函数都用于终止程序的执行,但存在一些差异。更加详细的内容,我们在后续学习基础IO的时候还会继续深入学习。

  1. _exit 函数

    • 原型:void _exit(int status)
    • _exit 函数是系统调用(man手册中存在于2号手册),直接终止当前进程的执行,并返回一个表示终止状态的整数值。
    • 不会做任何清理工作,例如关闭文件描述符、刷新缓冲区等,直接进入内核的终止处理流程。
    • _exit 函数很常用,特别是在子进程中调用,以避免父子进程共享文件描述符、缓冲区等资源时引发的问题。
  2. exit 函数

    • 原型:void exit(int status)
    • exit 函数是C库函数(man手册中存在于3号手册),它调用了一些清理函数,例如执行 stdio 库的清理工作等,然后终止当前进程的执行。
    • exit 函数会先刷新缓冲区,并执行一些其他清理工作(例如 fclose 关闭所有打开的文件描述符),并通过参数 status 返回终止状态。
    • 由于 exit 函数会进行一些清理工作,因此它的执行时间较长。

man手册的常见分类

1:可执行的程式或是shell 指令。
2:系统调用(system calls,Linux 核心所提供的函数)。
3:一般函式库函数。
4:特殊档案(通常位于/dev)。
5:档案格式与协定,如 /etc/passwd
6:游戏。
7:杂项(巨集等,如man(7)、groff(7))。
8:系统管理者指令(通常是管理者 root 专用的)。
9:Kernel routines(非标准)。

return函数

      return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数。

2.进程等待

       进程等待,简单的说,就是通过wait/waitpid的方式,让父进程(一般)对子进程进行资源等待的过程。

为什么要进程等待

       首先,我们开篇提到的,我们需要解决子进程的僵尸问题造成的内存泄漏问题,其次,父进程创建子进程的目的,是要让子进程完成某些任务,所以父进程在需要的时候(虽然这个功能不是必须的,但是这是一种“我想要你就得有”式的基础类型的功能,所以系统需要提供),需要得到子进程的执行情况,所以就需要通过进程等待,获取子进程的退出信息,也就是我们在进程退出部分说过的退出码或者是错误码。

进程等待的两种方法

wait方法(系统调用)

Linux进程控制(二)--进程等待(一)_第2张图片

返回值: 成功返回被等待进程pid,失败返回-1。

参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

下面我们利用单进程代码来测试其功能:

Linux进程控制(二)--进程等待(一)_第3张图片

此时我们编译执行,同时开启另一个ssh渠道进行监视,输出监视语句:

while :; do ps -axj | head -1 && ps -axj | grep mycode | grep -v grep; sleep 1; echo "----------------------"; done;

运行代码的同时执行监视语句:

当我们开始运行后的前5秒,由于子进程正在执行,而父进程前10秒都在等待的状态,此时我们能看到父子进程都在监视窗口中,

当子进程运行结束并开始退出时,由于父进程还没有结束,所以无法回收子进程的资源,导致其变成了僵尸进程,

Linux进程控制(二)--进程等待(一)_第4张图片

接下来,父进程开始通过wait函数回收子进程的资源,并以子进程的id作为wait函数的返回值,此时子进程由僵尸状态进入终止状态至进程退出,最终,父进程运行结束,程序结束。

那现在我们产生了一个疑问?当子进程正在运行的时候,父进程难道一直在等待着子进程退出吗?换句话说,在子进程执行过程中,父进程在干什么?我们可以通过输出打印的方式来验证一下:

我们只需要在父进程执行wait函数之前输出一个标志,在wait函数之后输出一个标志,再运行代码判断父子进程执行过程即可,比如,我们可以采用如下的方式修改父进程代码:

Linux进程控制(二)--进程等待(一)_第5张图片

再次编译运行,我们发现,我们的父进程在等待的时候,子进程在结束之前,父进程都将保持一种阻塞式等待的状态,等到子进程退出之后才能继续执行父进程

Linux进程控制(二)--进程等待(一)_第6张图片

所以,一般而言,都是父进程最后退出,因为父进程需要回收子进程的运行结果和资源,就需要等待子进程运行结束才能结束。

waitpid方法(系统调用)

waitpid的三个参数分别有不同的含义:

  1. pid:用来指定要等待的子进程的 ID。参数值可以有以下几种情况:

    • 正整数:等待指定进程 ID 的子进程。
    • -1:等待任意子进程。相当于 wait 函数。
    • 0:等待与调用进程属于同一进程组的任意子进程。
    • 负整数:等待进程组 ID 等于参数绝对值的任意子进程。
  2. status:输出型参数,指向一个整型变量的指针。status 用于存储子进程的退出状态和其他相关信息,例如导致子进程终止的信号编号等。如果不关心子进程的退出状态,则可以将 status 设置为 NULL

  3. options:可以用来指定额外的选项来控制 waitpid 函数的行为。常用的选项有以下几种:

    • WNOHANG:如果没有子进程终止,不会阻塞等待,立即返回。
    • WUNTRACED:除了等待子进程终止,还会等待暂停的子进程。
    • WCONTINUED:等待已暂停的子进程继续执行。
    • 0:阻塞等待。
  4. 函数返回值:       1. >0 :进程等待成功;   2. ==0 :进程等待成功,但是被等待的进程仍然在运行并没有退出;     3. <0 :进程等待失败。
status的结构

       对于第二个参数status,我们来重点说一下,status实际上是一个整形指针,其实也是一个int型的整数,占32个比特位,其中,我们只看低16位作为有效的退出信号,这16位之中,其中高8位是退出码,低7位是退出信号。

Linux进程控制(二)--进程等待(一)_第7张图片

进程正常退出代码演示:

Linux进程控制(二)--进程等待(一)_第8张图片

Linux进程控制(二)--进程等待(一)_第9张图片

进程异常退出代码演示:

我们在上面代码的基础上,添加一个空指针的解引用,这种行为会导致异常报错,我们来观察此时的status的状态信息:

Linux进程控制(二)--进程等待(一)_第10张图片

Linux进程控制(二)--进程等待(一)_第11张图片

       那么,我们该如何判断进程是否异常(收到了信号呢),由上面运行的程序来看,我们能够得出结论,进程在成功正常退出时返回的退出信号是0,像我们常用的return 0,当然,我们也可以直接手动通过另外的窗口杀死进程,此时对应的退出信号也会变成我们杀死进程时所使用的信号。 

宏函数WIFEXITED和WEXITSTATUS

      如果,用户不熟悉位操作的话,那不是干瞪眼吗?不不不,在Linux中,专门为用户提供了判断是否是正常退出,和正常退出情况下获取进程的退出码的宏函数,如下:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

      至于进程异常退出,我们就没有了关心该进程退出码的必要,虽然其依旧能正常产生退出码,但是我们此时更关注的是它的错误码,就好像一个同学考试作弊了,那他返回的结果(考试的成绩)还重要吗?答案显而易见,此时我们只是关心他的惩罚结果罢了。

Linux进程控制(二)--进程等待(一)_第12张图片

options参数

 options 参数可以控制 waitpid() 的行为,其参数值可以是以下的一个或几个值的位或:

  • WNOHANG:如果子进程还没有运行结束,那么 waitpid() 不会阻塞,而是立即返回,这种方式称之为非阻塞方式的等待,举个例子,我们如果和朋友约好在某个时间和地点见面,那么在和朋友见面之前,如果我们选择约定好时间之后就马上到指定的地点等着朋友而不再处理其他的事务,那么这种等待方式就叫做阻塞等待,反之则叫做非阻塞等待。
  • WUNTRACED:除了等待子进程结束,如果子进程被暂停(例如,由于接收到 SIGSTOP 或 SIGTTIN 信号),waitpid() 也会返回。这样,调用者可以决定是否要继续等待或者采取其他行动。
  • WCONTINUED:这个选项用于在子进程被暂停后继续等待其结束。在某些系统中,如果没有这个选项,那么当子进程被暂停时,waitpid() 将返回一个错误。
  • WNOWAIT:这个选项使得 waitpid() 不在等待子进程结束时收集其退出状态。相反,它立即返回,调用者可使用 waitid() 或 wait4() 来收集退出状态。
  • 0:表示进程正在阻塞等待一个子进程。
非阻塞轮询等待状态

        非阻塞等待的和阻塞等待的区别,其一在于wait函数的返回值不同(阻塞等待返回的值>0,而非阻塞等待返回的值==0),其二就是在于在父进程采用非阻塞等待的方式等待子进程时,子进程和父进程各自都有自己的工作可以干,也就是说,父进程可以在等待子进程的过程中,先干着其他的事情,我们可以通过下面的代码来进行验证:

#include
#include
#include
#include
#include
void worker(int cnt)
{
    printf("i am child ,pid= %d, cnt= %d\n",getpid(),cnt);
}
int main()
{
   pid_t id=fork();//创建一个子进程
   if(id==0)//子进程返回值
   {
       int cnt=10;
       while(cnt)
       {
           worker(cnt);
           sleep(1); //如果程序执行过快,那么监视窗口容易监视不到
           cnt--;
       }

       exit(0);//直接退出子进程
   }

   //走到这一步的一定是父进程了
   int status=0;
   pid_t rid=waitpid(id,&status,WNOHANG);//以非阻塞的方式等待
   
   if(rid>0)//等待成功,且子进程退出
   {
       // printf("wait success ,child exit now\n");                         
       printf("exit code =%d. exit signal =%d\n",(status>>8)&0xFF,status&0x7F);//分别取出进程的退出码额退出信号        

   }    
   else if(rid==0)//等待子进程成功,但是子进程还没有退出,也就是非阻塞等待
   {
        printf("do else things\n");
   }
   
   //接下去就是子进程退出失败的情况
    return 0;
}

配合下面的监控语句,我们可以掌握程序运行期间进程的情况:

while :; do ps -axj | head -1 && ps -axj | grep mycode | grep -v grep; sleep 1; echo "#############################"; done;

然后我们运行监视窗口中的语句,接着运行我们的代码,我们可以发现以下的结果:

Linux进程控制(二)--进程等待(一)_第13张图片

       这个运行结果看起来很奇怪,首先,我们知道父子进程是同时并行执行的,当子进程开始执行到打印的语句时,父进程恰好执行等待语句,此时由于我们设置的是非阻塞的等待模式,所以,此时的返回值是0,父进程也就执行rid==0的代码部分了且只能执行一次,此后父进程退出,子进程变为了孤儿进程被bash进程所接受,直到子进程退出,但是这只是父进程检测子进程退出状态的第一轮的结果,在非阻塞等待中,父进程需要进行轮询等待,父进程需要以一定的频率重复访问到子进程的退出状态,才能达到子进程的退出信息能够被父进程所掌握的目的,因此,父进程等待子进程的代码部分,我们需要用循环来实现,修改后父进程实现的代码如下:

#include                                                              
#include    
#include    
#include    
#include    
void worker(int cnt)    
{    
    printf("i am child ,pid= %d, cnt= %d\n",getpid(),cnt);    
}    
int main()    
{    
    
    
   pid_t id=fork();//创建一个子进程    
   if(id==0)//子进程返回值    
   {    
       int cnt=5;    
       while(cnt)    
       {    
           worker(cnt);    
           sleep(2); //为了便于观察,我们将子进程的执行时间设置的长于父进程,那么父进程的等待时间就要多一些 
           cnt--;    
       }    
    
       exit(0);//直接退出子进程    
   }    
    
   //走到这一步的一定是父进程了    
   while(1) //循环来实现父进程的非阻塞轮询等待    
   {    
         int status=0;    
         pid_t rid=waitpid(id,&status,WNOHANG);//以非阻塞的方式等待    
    
         if(rid>0)//等待成功,且子进程退出    
         {    
       // printf("wait success ,child exit now\n");    
             printf("exit code =%d. exit signal =%d\n",(status>>8)&0xFF,status&0x7F);//分别取出进程的退出码额退出信号    
              
              break;    
         }
         else if(rid==0)//等待子进程成功,但是子进程还没有退出,也就是非阻塞等>待
         {
                printf("do else things\n");
         }
         else //进程退出失败
         {
               break;
         }
         sleep(1);//等待一段时间,否则循环速度太快
   }
    return 0;
}

Linux进程控制(二)--进程等待(一)_第14张图片

为何需要通过系统调用才能获取子进程的信息

       在Linux 系统中,进程是独立运行的,每个进程有自己独立的内存空间、文件描述符等资源,并且与其他进程独立运行。因此,在一个进程中想要获取另一个进程的信息,必须通过系统提供的特定接口来实现。

       获取子进程信息的过程需要涉及内核态和用户态的切换,因为进程的状态信息(如退出状态)等都保存在内核中,因此需要通过系统调用来从内核中获取这些信息。进程等待的过程中会进入阻塞状态,也需要操作系统内核来协调多个进程之间的执行,并发挥到优秀的调度策略来合理地分配CPU资源。

      像全局变量这类的变量,并不能用来保存子进程的退出信息,我们在前面讲写时拷贝技术的时候也曾经谈过,全局变量看似是一份,实则当父子进程中的任意一个进程想要进行写操作时,就会发生写时拷贝,原来父子进程共享的全局变量就会各自变成独立的两份,也就成了两个全局变量的值,所以父进程通过自己的全局变量无法直接读到子进程的全局变量信息。

父进程如何获得子进程的退出信息

   Linux进程控制(二)--进程等待(一)_第15张图片

       总之,通过上述的解释,我们不能形成固定思维,只有外设中才能有自己的阻塞等待队列,进程中也可以有自己的等待队列。

多进程的进程等待

     我们前面的代码都是单进程等待的代码,其实多进程等待的代码和单进程高的代码并无特别的不同之处,只是多进程需要多个进程等待处理而已,前面我们已经学过了多进程的创建,我们只需要使用进程等待函数,使得父进程等待它的多个子进程退出即可,下面是一段演示代码及其结果:

Linux进程控制(二)--进程等待(一)_第16张图片

Linux进程控制(二)--进程等待(一)_第17张图片

       这里我们使用了waitpid函数进行等待,有一个好处,就是waitpid可以等待任意一个子进程的退出,不需要考虑顺序,我们从运行结果中也不难看出,进程的退出是没有顺序的,这样我们无需为了一个比较慢的进程而过多的等待,可以转而先执行较快退出的子进程的等待回收,提高了效率。

你可能感兴趣的:(Linux,linux)