【ONE·Linux || 地址空间与进程控制(二)】

总言

  主要内容:进程控制(进程等待、进程替换)
  
  

  
  
  
  
  
  
  
  前期回顾:地址空间与进程控制(一)

2、进程控制·续

2.3、进程等待

在这里插入图片描述

2.3.1、为什么需要进程等待

  1、子进程退出,父进程不管子进程,子进程就会进入僵尸状态。处于僵尸状态的子进程会导致内存泄露问题,那么,如何回收僵尸进程呢?此处就需要用到进程等待相关知识。
  2、我们学习父进程创建子进程,是为了让子进程办事。那么子进程把任务完成得怎样,父进程需要关系吗?如果要,它是如何得知的?如果不要,又该如何处理?此处也需要用到进程等待的知识。

  
  
  
  

2.3.2、阻塞式等待

2.3.2.1、使用wait

  1)、wait :基本验证,回收僵尸进程

在这里插入图片描述先对僵尸进程简单回顾(详细可以看进程概念那一篇博客)

  这是用于观察的脚本代码:

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

  下述为僵尸状态的演示代码:

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

int main(void)
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        exit(1);//表示进程运行完毕,结果不正确
    }
    else if(id==0)
    {
        int cnt=5;
        while(cnt)
        {
            printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
            cnt--;
            sleep(1);  
        }
			exit(0);//终止子进程  
    }
    else{
        while(1)
        {
            printf("I am parent, pid:%d ,ppid:%d \n",getpid(),getppid());
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述
  
  
  
  

在这里插入图片描述使用wait回收僵尸进程演示

  函数头文件:
在这里插入图片描述
  
  
  wait()函数功能基本描述: 父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

DESCRIPTION
       All  of  these  system calls are used to wait for state changes in a child of the calling process, and obtain information
       about the child whose state has changed.  A state change is considered to be: the child terminated; the child was stopped
       by  a signal; or the child was resumed by a signal.  In the case of a terminated child, performing a wait allows the sys‐
       tem to release the resources associated with the child; if a wait is not performed, then the terminated child remains  in
       a "zombie" state (see NOTES below).

       If  a  child  has  already changed state, then these calls return immediately.  Otherwise they block until either a child
       a "zombie" state (see NOTES below).

       If  a  child  has  already changed state, then these calls return immediately.  Otherwise they block until either a child
       changes state or a signal handler interrupts the call (assuming that system calls are not automatically  restarted  using
       the  SA_RESTART  flag of sigaction(2)).  In the remainder of this page, a child whose state has changed and which has not
       yet been waited upon by one of these system calls is termed waitable.

  
  
  参数说明:
  status:用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:

pid = wait(NULL);

在这里插入图片描述
  PS:如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作。

  

  函数的返回值: 如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
在这里插入图片描述

  使用演示:

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

int main(void)
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        exit(1);//表示进程运行完毕,结果不正确
    }
    else if(id==0)
    {
        int cnt=5;
        while(cnt)
        {
            printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
            cnt--;
            sleep(1);
        }
        exit(0);//终止子进程

    }
    else{

        printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
        sleep(7);
        pid_t ret=wait(NULL);//阻塞式等待
        if(ret>0)
        {
            printf("wait child process successfully!,ret:%d\n",ret);
        }
        //使用wait:可以不用让父进程做死循环等待了
        while(1)
        {
            printf("I am parent, pid:%d ,ppid:%d \n",getpid(),getppid());
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述
  往后我们编写多进程时,以便都是采用fork+wait/waitpid的方法。
  
  
  
  
  

2.3.2.2、使用waitpid

  1)、waitpid :获取子进程退出结果

在这里插入图片描述waitpid简单介绍

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

在这里插入图片描述

  返回值说明:
  如果成功,waitpid() 函数返回一个子进程的进程 ID。
  如果使用了 WNOHANG 选项,但没有子进程已经终止(等待成功但子进程尚未退出),waitpid() 函数返回 0。
  如果发生错误,waitpid() 函数返回 -1,并设置 errno。

RETURN VALUE
       waitpid(): on success, returns the process ID of the child whose state has changed; if  WNOHANG  was
       specified  and one or more child(ren) specified by pid exist, but have not yet changed state, then 0
       is returned.  On error, -1 is returned.

  
  
  2)、waitpid 演示一:阻塞式等待

在这里插入图片描述waitpid(pid, NULL , 0) ,等价于使用 wait(NULL)

  先简单演示一下waitpid的基本使用方法:

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

int main(void)
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        exit(1);//表示进程运行完毕,结果不正确
    }
    else if(id==0)
    {
        int cnt=5;
        while(cnt)
        {
            printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
            cnt--;
            sleep(1);
        }
        exit(0);//终止子进程

    }
    else{

        printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
        sleep(7);
        pid_t ret=waitpid(id,NULL,0);//注意此处父进程获取得的id即子进程id,详细请了解fork返回值。
        //pid_t ret=wait(NULL);//阻塞式等待
        if(ret>0)
        {
            printf("wait child process successfully!,ret:%d\n",ret);
        }
        //使用wait:可以不用让父进程做死循环等待了
        while(1)
        {
            printf("I am parent, pid:%d ,ppid:%d \n",getpid(),getppid());
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述
  
  
  

2.3.2.3、参数status基本介绍

  1)、问题引入

waitpid(pid, &status , 0)  //status为输出型参数,可用于获取子进程退出时的状态信息

  演示代码如下:我们将子进程退出码设置为自己意属的值,而后通过status获取该值来观测结果。

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

int main(void)
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        exit(1);//表示进程运行完毕,结果不正确
    }
    else if(id==0)
    {
        int cnt=5;
        while(cnt)
        {
            printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
            cnt--;
            sleep(1);
        }
        exit(111);//终止子进程,设置子进程退出码

    }
    else{

        printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
        int status=0;
        pid_t ret=waitpid(id,&status,0);//注意此处父进程获取得的id返回值,即子进程id。
        //pid_t ret=wait(NULL);//阻塞式等待
        if(ret>0)
        {
            printf("wait child process successfully!,ret:%d,status:%d\n",ret,status);
        }

    }
    return 0;
}

  如下述:程序执行完成获得status的值,可以发现该值与我们与其所设想的不一致。

[wj@VM-4-3-centos t1113]$ ./test.out
I am parent, pid:22927, ppid:4925
cnt:5 , I am child, pid:22928, ppid:22927 
cnt:4 , I am child, pid:22928, ppid:22927 
cnt:3 , I am child, pid:22928, ppid:22927 
cnt:2 , I am child, pid:22928, ppid:22927 
cnt:1 , I am child, pid:22928, ppid:22927 
wait child process successfully!,ret:22928,status:28416//可看到此处status的返回值是一个很大的数值

  
  这是什么原因呢? (后续提及:status详细介绍)。
  
  
  一个补充说明·关于阻塞式等待时父子进程顺序问题: 文中提到的代码有一个特点,即只有子进程退出的时候,父进程才会使用waitpid/wait函数进行返回,即父进程在子进程之后仍旧活着。
  这说明 waitpid/wait目前情况下可以让进程具有一定的顺序性 ,将来我们也可以让父进程进行更多的收尾工作
  
  
  

  2)、status详细介绍

在这里插入图片描述0、综述:

在这里插入图片描述

  
  
  

在这里插入图片描述1、进程正常终止,status的次低8位表示进程退出码

  为了获取该码,我们可以做以下变化:

(status>>8)&0XFFF

  解释: 先将status得到的值右移8位,这样次低8位的值就到了最低8位上,然后我们让按位与上0XFF(0000 0000 1111 1111),这样status更高位的数值就都为0了。
  
  使用以下代码进行演示:

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

int main(void)
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        exit(1);//表示进程运行完毕,结果不正确
    }
    else if(id==0)
    {
        int cnt=5;
        while(cnt)
        {
            printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
            cnt--;
            sleep(1);
        }
        exit(111);//让子进程终止,并设置退出码为111.

    }
    else{

        printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
        int status=0;
        pid_t ret = waitpid(id,&status,0);
        if(ret>0)
        {
            printf("wait child process successfully!,ret:%d,status:%d\n",ret,(status>>8)&0XFF);
        }

    return 0;
}

  现在,我们来看一看结果:

[wj@VM-4-3-centos t1113]$ ./test.out
I am parent, pid:557, ppid:31111
cnt:5 , I am child, pid:558, ppid:557 
cnt:4 , I am child, pid:558, ppid:557 
cnt:3 , I am child, pid:558, ppid:557 
cnt:2 , I am child, pid:558, ppid:557 
cnt:1 , I am child, pid:558, ppid:557 
wait child process successfully!,ret:558,status:111 //status打印的值确实如我们设置一般
[wj@VM-4-3-centos t1113]$ 

  
  
  
  

在这里插入图片描述2、进程异常退出,status最低7位表示进程收到的信号

  一个前提认知:进程异常退出或者崩溃,本质上是操作系统杀掉了该进程。
  操作系统如何操作这个过程呢?本质是通过发送信号的方式。
  为了获取相关信号,我们对原先实验的代码做如下改动并验证:

(status&0X7F)
OX7F: 0000 0000 0111 1111

  
  验证一,进行正常退出: 此时发送的信号为0,说明进程正常终止。可通过status的返回值来判断它是正常跑完终止后的哪种状态。

[wj@VM-4-3-centos t1113]$ ./test.out
I am parent, pid:5662, ppid:31111
cnt:5 , I am child, pid:5663, ppid:5662 
cnt:4 , I am child, pid:5663, ppid:5662 
cnt:3 , I am child, pid:5663, ppid:5662 
cnt:2 , I am child, pid:5663, ppid:5662 
cnt:1 , I am child, pid:5663, ppid:5662 
wait child process successfully!,ret:5663,signal:0, status:111 //根据signal判断进程正常退出,根据status可获取退出码。
[wj@VM-4-3-centos t1113]$ 

  演示使用的代码:和之前一样,只是加入了(status&0X7F)

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

int main(void)
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        exit(1);//表示进程运行完毕,结果不正确
    }
    else if(id==0)
    {
        int cnt=5;
        while(cnt)
        {
            printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
            cnt--;
            sleep(1);
        }
        exit(111);//终止子进程

    }
    else{
        printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
        //sleep(7);
        int status=0;
        pid_t ret=waitpid(id,&status,0);
        if(ret>0)
        {
            printf("wait child process successfully!,ret:%d,signal:%d, status:%d\n",ret,(status&0X7F),(status>>8)&0XFF);
        }
    return 0;
}

  
  
  验证二:,子进程异常情况: 演示代码如下,只是在子进程中加入一个除0错误,来使得子进程报错终止退出。

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

int main(void)
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        exit(1);//表示进程运行完毕,结果不正确
    }
    else if(id==0)
    {
        int cnt=5;
        while(cnt)
        {
            printf("cnt:%d , I am child, pid:%d, ppid:%d \n",cnt,getpid(),getppid());
            cnt--;
            sleep(1);
            //以下为使子进程异常的演示代码:
            int a=10;
            a /=0;
        }
        exit(111);//终止子进程

    }
    else{

        printf("I am parent, pid:%d, ppid:%d\n",getpid(),getppid());
        //sleep(7);
        int status=0;
        pid_t ret=waitpid(id,&status,0);
        if(ret>0)
        {
            printf("wait child process successfully!,ret:%d,signal:%d, status:%d\n",ret,(status&0X7F),(status>>8)&0XFF);
        }
    }
    return 0;
}

  以下为演示结果: 进程异常退出,此时退出码无意义(过程就终止,进程执行不到退出的那条语句)

在这里插入图片描述
  
  需要注意的是,进程异常不仅仅只体现在内部代码有问题上,当我们外力人为直接杀掉进程, 也算作进程异常。
  比如我们之前执行kill -9 pid时:

在这里插入图片描述
  
  
  
  
  
  
  

2.3.3、一些细节与问题

2.3.3.1、进程独立性说明

在这里插入图片描述问题一: 父进程为什么要用wait/waitpid函数拿获取子进程的退出结果?直接定义一个全局变量来获取不是更加简洁吗?

  相关代码:

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

int code = 0; // 定义一个全局变量
int main(void)
{
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if (id == 0)
    {
        int cnt = 5;
        while (cnt)
        {
            printf("cnt:%d , I am child, pid:%d, ppid:%d \n", cnt, getpid(), getppid());
            cnt--;
            sleep(1);
        }
        code = 111; // 在子进程处直接设置全局变量
        exit(111);
    }
    else
    {
        printf("I am parent, pid:%d, ppid:%d\n", getpid(), getppid());
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if (ret > 0)
        {
            printf("code:%d\n", code); // 在父进程处读取该值
            printf("wait child process successfully!,ret:%d,signal:%d, status:%d\n", ret, (status & 0X7F), (status >> 8) & 0XFF);
        }
        return 0;
    }

  
  结果如下: 可以看到code读取失败直接为0,这是因为父子进程代码具有独立性,在子进程中修改code值时发生了写时拷贝。故不能用此法来获取子进程退出状态。

[wj@VM-4-3-centos t1113]$ ./test.out
I am parent, pid:11689, ppid:10835
cnt:5 , I am child, pid:11690, ppid:11689 
cnt:4 , I am child, pid:11690, ppid:11689 
cnt:3 , I am child, pid:11690, ppid:11689 
cnt:2 , I am child, pid:11690, ppid:11689 
cnt:1 , I am child, pid:11690, ppid:11689 
code:0
wait child process successfully!,ret:11690,signal:0, status:111
[wj@VM-4-3-centos t1113]$ 

  
  
  
  
  

2.3.3.1、父进程凭什么拿到子进程的数据

在这里插入图片描述问题二: 既然进程具有独立性,进程退出码不也是子进程独有的数据吗。那么父进程凭什么拿到子进程的数据?wait/waitpid在这期间扮演什么角色做了什么事?

  回答:
  1、首先,从僵尸进程谈起。僵尸进程退出后,至少会保留该进程的PCB信息。即task_struct里面保留了任何进程退出时的退出结果信息。
  2、wait、waitpid本质上是在读取子进程tast_struct结构体内的数据信息。
  3、wait、waitpid属于系统调用函数,本质上是操作系统本身进行处理,因此其有权限拿到task_struct该内核结构对象的数据

在这里插入图片描述
  
  
  
  

2.3.3.3、堆栈内存泄漏和操作系统层面的内存泄漏差异性

在这里插入图片描述问题三: 假如我们创建了一个进程,在该进程中使用了malloc或new一块空间,之后进程退出,请问在没有free或delete的情况下,该内存是否会泄露?和子进程的僵尸状态带来的内存泄露有什么区别?

  回答:对于前者,属于用户在堆区申请的空间,进程退出后不会造成内存泄漏,因为操作系统会自动回收该空间。对于后者,这属于操作系统层面的内存泄漏
  
  
  
  
  
  
  
  

2.3.4、如何等待2.0(进一步细节展示+非阻塞式等待)

2.3.4.1、参数说明2.0:pid、status相关宏设置

  1)、waitpid参数介绍一:pid
  在之前我们只是简单的介绍了waitpid的基本使用方法,对于其参数pid我们直接使用了fork返回的id值,此处需要注意:
  id > 0 时,我们等待的是指定进程。
  id== -1 时,我们等待的是任意一个子进程,等价于wait( )接口。

  

  pid<-1 等待进程组号为pid绝对值的任何子进程。
  pid=-1 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。
  pid=0 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
  pid>0 等待进程号为pid的子进程。
  
  

  2)、直接获取stauts:使用系统提供的宏
  WIFEXITED(status): (查看进程是否是正常退出),若为真,则表示正常终止子进程返回的状态。
  WEXITSTATUS(status): (查看进程的退出码),若WIFEXITED非零,提取子进程退出码。

  两个宏的记忆方法:W/IF/EXITEDW/EXIT/STATUS
  

  演示代码一:

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

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        exit(1);
    }
    else if(id==0)//子进程
    {
        int cen=5;
        while(cen)
        {
            printf("I am child:%d\n",cen--);
            sleep(1);
        }
        exit(111);
    }
    else //父进程
    {
        int status=0;
        pid_t result=waitpid(id,&status,0);//阻塞等待子进程状态变化
        if(result>0)
        {
            if(WIFEXITED(status))
            {
                //父进程等待成功
                printf("子进程执行完毕,子进程退出码:%d\n",WEXITSTATUS(status));
            }
            else 
            {
                printf("子进程异常退出:%d\n",WIFEXITED(status));//此处是在子进程异常退出时打印看看状态
            }
        }

    }
    return 0;
}

  执行结果:可看到WIFEXITED(status)执行为真,后续将退出码直接打印出来。
  这样子的一个好处是,我们可以不用了解具体的操作内核去做位移转换运算,直接通过使用系统提供的宏来获取子进程退出信息。

[wj@VM-4-3-centos t1113]$ ./test.out
I am child:5
I am child:4
I am child:3
I am child:2
I am child:1
子进程执行完毕,子进程退出码:111

  
  
  
  
  

2.3.4.2、waitpid第三参数options:非阻塞式等待设置处

  1)、options参数简单介绍
  根据上述几个演示代码,可以看到阻塞式等待中,父进程只在做一件事,即处于阻塞状态中,等待子进程状态发生改变。这样使用父进程有些大材小用,因此延伸出了让父进程既能等待子进程,同时也能处理其它任务的操作方法:
  这就需要学习waitpid的第三参数:options
  1、options== 0,默认为阻塞等待
  2、options== WNOHANG,代表父进程为非阻塞等待。相关记忆方式:W/NO/HANG

  PS:WNOHANG是系统提供的一个宏(定义成宏的原因:魔鬼数字)

在这里插入图片描述
  
  
  
  2)、关于阻塞式等待、非阻塞式等待相关理解说明
在这里插入图片描述
  如上述,简单举例了系统调用函数waitpid相关实现框架,根据第三参数options来判断进程等待方式。
  对于阻塞式等待,父进程不会执行后续代码操作,这是由于父进程在系统内部被挂起,即父进程的pcb(进程控制块)被放入等待队列中,其结果为进程阻塞在系统函数内部,只有当条件满足的时候,父进程被唤醒(相关进程控制块重新加载到CPU中被执行),而后接着执行。注意:这里waitpid重新调用时,是通过EIP寄存器来判断上此切换出的相关代码位置,并非重新执行所有代码。
  
  对于非阻塞等待,使用waitpid后,waitpid判断子进程没有退出后会直接返回,换句话说,非阻塞式等待是通过轮询检测的方案来实现一次次调用的
  
  
  
  3)、相关意义说明
  比如scanf、cin等,虽然上层看是通过语言实现的,但其内部使用了相关的系统调用接口,实际上,网络代码中大部分为IO类别,它们会不断面临阻塞与非阻塞接口。
  
  
  
  

2.3.4.3、实操演示

  1)、基础演示一:
  以下把如何用非阻塞式等待进行轮询检测的相关框架列举出来,代码如下:

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

int main()
{
    pid_t id = fork();
    if (id < 0)
    {
        return -1;
    }
    else if (id == 0) // 子进程
    {
        int count = 5;
        while (count)
        {
            printf("I am child:%d\n", count--);
            sleep(1);
        }
        exit(9); // 测试:此处数字无具体含义
    }
    else // 父进程:演示非阻塞式等待,轮循检测
    {
        int quit = 0; // 类似flag,用于标记何时退出
        while (!quit)
        {
            int status = 0;
            pid_t ret = waitpid(-1, &status, WNOHANG);
            if (ret > 0) // 等待成功+子进程退出
            {
                printf("等待子进程退出成功,退出码为:%d\n", WEXITSTATUS(status));
                quit = 1;
            }
            else if (ret == 0) // 等待成功+子进程尚未退出
            {
                printf("子进程尚在运行中暂时未退出,此时父进程可处理其它事件\n");
                // ……
                // 这里可以写父进程要处理的内容
            }
            else // waitpid等待失败
            {
                printf("wait失败。\n");
                quit = 1;
            }
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述

  
  
  2)、基础演示二:
  我们将上述演示一中,父进程待处理事件完善一下,做一个简单示范:

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

typedef void (*Handler_t)();//函数指针类型
std::vector<Handler_t> handlers;//函数指针数组

//待处理的临时任务:这是在非阻塞等待时,交给父进程的任务
void fun1()
{
    printf("待处理临时任务1\n");
}

void fun2()
{
    printf("待处理临时任务2\n");
}

//Load函数:可用于注入待处理任务,将其设置为Load可做到切块处理,便于更改
void Load()
{
    handlers.push_back(fun1);
    handlers.push_back(fun2);
}

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        return -1;
    }
    else if(id==0)//子进程
    {
        int count=5;
        while(count)
        {
            printf("I am child:%d\n",count--);
            sleep(1);
        }
        exit(9);//测试:此处数字无具体含义
    }
    else //父进程:演示非阻塞式等待,轮循检测
    {
        int quit=0;//类似flag,用于标记何时退出
        while(!quit)
        {
            int status=0;
            pid_t ret=waitpid(-1,&status,WNOHANG);
            if(ret>0)//等待成功+子进程退出
            {
                printf("等待子进程退出成功,退出码为:%d\n",WEXITSTATUS(status));
                quit=1;
            }
            else if(ret==0)//等待成功+子进程尚未退出
            {
                printf("子进程尚在运行中暂时未退出,此时父进程可处理其它事件\n");
                //…
                if(handlers.empty())
                    Load();
                for(auto iter:handlers)
                {
                    //执行相关任务
                    iter();
                }
            }
            else//waitpid等待失败 
            {
                printf("wait失败。\n");
                quit=1;
            }
            sleep(1);
        }
    }
    return 0;
}

  
  
  
  
  

2.4、进程替换

在这里插入图片描述

2.4.1、是什么

  1)、问题引入:fork的两种用法说明
  在前文,我们介绍了fork的一种用法,实际上fork通常有两种常见用法:
  ①一个父进程希望复制自己,使父子进程同时执行不同的代码段。此时,父子进程代码相同,数据写时拷贝各自一份。
  ②一个进程要执行一个不同的程序。此时父子进程代码不同,数据也不同。
  那么有没有相关实现方法?这里就要用到进程替换。
  
  
  
  2)、进程替换介绍
  进程替换: 通过特定接口,加载磁盘上的一个权限的程序(包括代码和数据),加载到调用进程的地址空间中,让子进程执行相关程序。

在这里插入图片描述
  说明:事实上,操作系统为我们提供了这类函数,当进程调用一种exec*函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行
  
  
  
  
  3)、细节理解
  问题一:进程替换,有没有创建新的进程?
  回答:调用 exec*并不创建新进程,所以调用exec*前后该进程的id并未改变
  
  
  
  问题二:如何理解将程序放入内存中?
  回答:实际指将程序加载到内存中,和当前进程页表建立映射关系,可通过操作系统相关接口调用完成,即接下来要介绍的exec系列函数。
  
  
  
  
  

2.4.2、怎么办1.0:execl函数演示

2.4.2.1、execl程序替换:不创建子进程

  1)、函数介绍
  man execl可查询进程替换相关函数。

在这里插入图片描述

  path为对应程序所在路径。
  arg, ...是可变参数列表,可以传入多个不定个数参数,但其最后一个参数必须传递NULL,表示参数传递完毕。

       int execl(const char *path, const char *arg, ...);
       int execlp(const char *file, const char *arg, ...);
       int execle(const char *path, const char *arg,..., char * const envp[]);
       int execv(const char *path, char *const argv[]);
       int execvp(const char *file, char *const argv[]);
       int execvpe(const char *file, char *const argv[],char *const envp[]);

  
  
  2)、基础演示
  这里以execl函数为例,演示不创建子进程时,如何进行程序替换: 需要注意,这里path的参数为const char *,我们传入的是字符串。

在这里插入图片描述
  
  上述三个演示的相关代码:

在这里插入图片描述演示一:

[wj@VM-4-3-centos T0714]$ ll
total 40
-rw-rw-r-- 1 wj wj  155 Jul 14 19:25 makefile
-rw-rw-r-- 1 wj wj 1236 Jul 14 12:27 proc01.c
-rwxrwxr-x 1 wj wj 8616 Jul 14 12:28 proc01.out
-rw-rw-r-- 1 wj wj  166 Jul 14 19:23 proc02.c
-rw-rw-r-- 1 wj wj 1962 Jul 14 13:02 proc02.cpp
-rwxrwxr-x 1 wj wj 8360 Jul 14 19:25 proc02.out

[wj@VM-4-3-centos T0714]$ ./proc02.out
当前进程开始:
......
当前进程结束
[wj@VM-4-3-centos T0714]$ cat proc02.c
#include<stdio.h>
#include<unistd.h>

int main()
{
    printf("当前进程开始:\n");
    printf("......\n");
    printf("当前进程结束\n");
    return 0;
}

  
  

在这里插入图片描述演示二:

[wj@VM-4-3-centos T0714]$ ll
total 40
-rw-rw-r-- 1 wj wj  155 Jul 14 19:25 makefile
-rw-rw-r-- 1 wj wj 1236 Jul 14 12:27 proc01.c
-rwxrwxr-x 1 wj wj 8616 Jul 14 12:28 proc01.out
-rw-rw-r-- 1 wj wj  201 Jul 14 19:31 proc02.c
-rw-rw-r-- 1 wj wj 1962 Jul 14 13:02 proc02.cpp
-rwxrwxr-x 1 wj wj 8408 Jul 14 19:31 proc02.out

[wj@VM-4-3-centos T0714]$ ./proc02.out
当前进程开始:
makefile  proc01.c  proc01.out	proc02.c  proc02.cpp  proc02.out
[wj@VM-4-3-centos T0714]$ cat proc02.c
#include<stdio.h>
#include<unistd.h>

int main()
{
    printf("当前进程开始:\n");
    execl("/usr/bin/ls","ls",NULL);
    printf(".....\n");
    printf("当前进程结束\n");
    return 0;
}
[wj@VM-4-3-centos T0714]$ which ls
alias ls='ls --color=auto'
	/usr/bin/ls

  
  

在这里插入图片描述演示三:

[wj@VM-4-3-centos T0714]$ make
g++ -o proc02.out proc02.c
[wj@VM-4-3-centos T0714]$ ./proc02.out
当前进程开始:
total 48
drwxrwxr-x 2 wj wj 4096 Jul 14 19:37 .
drwxrwxr-x 4 wj wj 4096 Jul 14 12:06 ..
-rw-rw-r-- 1 wj wj  155 Jul 14 19:25 makefile
-rw-rw-r-- 1 wj wj 1236 Jul 14 12:27 proc01.c
-rwxrwxr-x 1 wj wj 8616 Jul 14 12:28 proc01.out
-rw-rw-r-- 1 wj wj  245 Jul 14 19:37 proc02.c
-rw-rw-r-- 1 wj wj 1962 Jul 14 13:02 proc02.cpp
-rwxrwxr-x 1 wj wj 8408 Jul 14 19:37 proc02.out

[wj@VM-4-3-centos T0714]$ cat proc02.c
#include<stdio.h>
#include<unistd.h>

int main()
{
    printf("当前进程开始:\n");
    //execl("/usr/bin/ls","ls",NULL);
    execl("/usr/bin/ls","ls","-al",NULL);
    printf(".....\n");
    printf("当前进程结束\n");
    return 0;
}

  
  
  
  3)、相关说明
  问题一:如何理解使用execl后不再执行后续代码?
  回答: exec系列函数如果调用成功,则会将当前进程中的所有代码和数据统统替换,包括已执行的和未执行的
  以下述代码为例做解释: 使用execl后,进程内部代码数据在这里替换为ls,后续printf(".....\n");printf("当前进程结束\n");将不会被执行,而execl前的printf("当前进程开始:\n");也会被一并替换,只是它在替换前已经被执行。

int main()
{
    printf("当前进程开始:\n");
    execl("/usr/bin/ls","ls",NULL);
    printf(".....\n");
    printf("当前进程结束\n");
    return 0;
}

  
  
  问题二:关于execl返回值。为什么调用成功没有返回值?
  回答: 原因同上。该系列函数,若调用成功,则加载新的程序并从启动代码开始执行,不再返回。如果调用出错则返回-1。所以 exec*系列的函数只有出错的返回值,而没有成功的返回值
在这里插入图片描述

  举例: 实际使用时,可根据需要在execl后加上exit,一旦程序在此处退出,意味着进程替换执行失败。

int main()
{
    printf("当前进程开始:\n");
    execl("/usr/bin/ls","ls",NULL);
    exit(-1);//表示进程替换失败。
    printf(".....\n");
    printf("当前进程结束\n");
    return 0;
}

  
  
  
  
  

2.4.2.2、execl程序替换:创建子进程

   1)、为什么要有创建子进程的替换方式?
  问题说明: 在上述中,我们直接使用了execl函数可实现将当前进程进行替换,那么为什么还需要额外创建子进程来完成此项任务?
  1、从需求角度考虑: 若原先父进程原先代码为刚需,要求不能更动(例如后续还需要执行、用到),那么创建子进程用于进程替换可以保证不影响父进程的同时,替换上新的代码数据。(PS:进程具有独立性,因此只会替换掉子进程的代码数据段,不会把父进程的一并替换)
  2、从分工角度考虑: 让父进程聚焦在读取数据、解析数据上,指派进程执行代码功能。
  
  
   2)、如何操作?

  以下为一个演示案例: 这里我们以ls -a -l为例。

[wj@VM-4-3-centos T0715]$ ls -a -l
total 32
drwxrwxr-x 2 wj wj 4096 Jul 15 14:57 .
drwxrwxr-x 5 wj wj 4096 Jul 15 11:22 ..
-rw-rw-r-- 1 wj wj   73 Jul 15 12:20 makefile
-rwxrwxr-x 1 wj wj 8616 Jul 15 14:57 test01
-rw-rw-r-- 1 wj wj  982 Jul 15 14:57 test01.c
-rw-rw-r-- 1 wj wj 1130 Jul 15 14:54 test02.c
[wj@VM-4-3-centos T0715]$ which ls
alias ls='ls --color=auto'
	/usr/bin/ls

  
  演示结果如下:

execl("/usr/bin/ls","ls","-a","-l",NULL);

在这里插入图片描述
  
  总览:

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

#define NUM 16

int main(int argc, char *argv[], char *env[])
{
    pid_t id=fork();
    if(id<0)
    {
        return -1;
    }
    else if(id==0)//子进程
    {
        printf("子进程开始运行,pid:%d\n",getpid());
        //在子进程中使用进程替换
        //ls -a -l
        execl("/usr/bin/ls","ls","-a","-l",NULL);
        exit(-2);//进程替换失败
    }
    else//父进程 
    {
        printf("父进程开始运行,pid:%d\n",getpid());
        //让父进程阻塞式等待子进程:子进程运行完毕,父进程获取后才退出,保证运行时的顺序(PS:单独fork,父子进程顺序具有不确定性)
        int status=0;
        pid_t ret=waitpid(-1,&status,0);//参数:等待任意一个子进程、获取进程状态、阻塞式等待
        if(ret)
            printf("wait success, exit code:%d\n",WEXITSTATUS(status));

    }
    return 0;
}

  关于为什么需要进程等待说明: 实际上这里的顺序性涉及到后续实际运用。我们可以结合上述1)中内容来理解,假父进程为总指挥官,旗下有各子进程作为执行者,则父进程需要不断下达子任务,委派子进程执行并将结果反馈。那么,单独使用fork时,由于CPU调度,无法确保谁先执行完成退出,加入进程等待,可以保证父进程在子进程之后退出。
  故而上述演示案例,我们也可以嵌套一层while(1)循环,保证父进程时刻运行。

int main(int argc, char*argv[], char *env[])
{
    while(1)
    {
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            //……进程替换
        }
        else 
        {
            //父进程
            //……
        }
    } //end while(1)
    return 0;
}

  
  
  
  
  3)、进程替换中,父子进程代码和数据的关系解释
  根据先前所学,fork之后,父子进程代码共享,数据写时拷贝
  这里引入进程替换,当新程序加载之前,fork后父子进程仍旧是代码共享,数据写时拷贝。但当子进程加载新的程序时,相当于一种写入,此时子进程的代码也要进行写时拷贝。
  由此说明,进程替换后,父子进程代码、数据都进行写时拷贝
  
  
  
  
  

2.4.3、怎么办2.0:其它exec系列函数演示

   1)、exec系列函数再展示

       #include <unistd.h>

       extern char **environ;

       int execl(const char *path, const char *arg, ...);
       int execlp(const char *file, const char *arg, ...);
       int execle(const char *path, const char *arg,
                  ..., char * const envp[]);
       int execv(const char *path, char *const argv[]);
       int execvp(const char *file, char *const argv[]);
       int execvpe(const char *file, char *const argv[],
                   char *const envp[]);

  关于上述几个函数的记忆与理解:

l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p则自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

在这里插入图片描述
  
  
  
  

2.4.3.1、execv函数演示

  函数声明如下:

int execv(const char *path, char *const argv[]);

  基本说明: 这些系列函数的使用大体无区别,只是在细节上做一定修改。比如这里的execv,根据第二参数char *const argv[]需要传递的是指针数组,因此这里我们建立一个数组用于传递相关指令:

#define NUM 16

char* const _argv[NUM]={(char*)"ls",(char*)"-a",(char*)"-l",NULL};
execv("/usr/bin/ls",_argv);//注意路径写法

  
  
  演示结果如下:
在这里插入图片描述

  
  相关代码如下:

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

#define NUM 16


//int main(int argc, char *argv[], char *env[])
int main(void)
{
    pid_t id=fork();
    if(id<0)
    {
        return -1;
    }
    else if(id==0)//子进程
    {
        printf("子进程开始运行,pid:%d\n",getpid());
        //在子进程中使用进程替换
        //ls -a -l
       
        char *const _argv[NUM] = {
            (char*)"ls",(char*)"-a",(char*)"-l",(char*)"-i", NULL};
        execv("/usr/bin/ls", _argv);
        //char *const argv[] = {"ps", "-ef", NULL};
        //execv("/bin/ps",argv);

        //execl("/usr/bin/ls","ls","-a","-l",NULL);
        exit(-2);//进程替换失败
    }
    else//父进程 
    {
        printf("父进程开始运行,pid:%d\n",getpid());
        //让父进程阻塞式等待子进程:子进程运行完毕,父进程获取后才退出,保证运行时的顺序(PS:单独fork,父子进程顺序具有不确定性)
        int status=0;
        pid_t ret=waitpid(-1,&status,0);//参数:等待任意一个子进程、获取进程状态、阻塞式等待
        if(ret)
            printf("wait success, exit code:%d\n",WEXITSTATUS(status));

    }
    return 0;
}

  
  补充: 也可以加上颜色编辑:
在这里插入图片描述

  
  
  
  

2.4.3.2、execlp\ececvp函数演示

  1)、使用execlp\ececvp执行系统环境变量中的程序

  函数声明如下:

int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);

  p(path) : 有p自动搜索环境变量PATH。因此这里的const char *file若是环境变量PATH中的程序,可不用写绝对路径,能够直接搜索到。
  
  
  相关演示:

	execlp("ls","ls","-a","-l",NULL);
    char *const _argv[NUM] = {
         (char*)"ls",(char*)"--color=auto",(char*)"-a",(char*)"-l",(char*)"-i", NULL};
    execvp("ls",_argv);

  
  
  演示结果如下:
在这里插入图片描述

  
  
  相关代码如下:

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

#define NUM 16


//int main(int argc, char *argv[], char *env[])
int main(void)
{
    pid_t id=fork();
    if(id<0)
    {
        return -1;
    }
    else if(id==0)//子进程
    {
        printf("子进程开始运行,pid:%d\n",getpid());
        //在子进程中使用进程替换
        //ls -a -l
       
        //execlp("ls","ls","-a","-l",NULL);

        char *const _argv[NUM] = {
            (char*)"ls",(char*)"--color=auto",(char*)"-a",(char*)"-l",(char*)"-i", NULL};
        execvp("ls",_argv);
        //execv("/usr/bin/ls", _argv);
    
        //char *const argv[] = {"ps", "-ef", NULL};
        //execv("/bin/ps",argv);

        //execl("/usr/bin/ls","ls","-a","-l",NULL);
        exit(-2);//进程替换失败
    }
    else//父进程 
    {
        printf("父进程开始运行,pid:%d\n",getpid());
        //让父进程阻塞式等待子进程:子进程运行完毕,父进程获取后才退出,保证运行时的顺序(PS:单独fork,父子进程顺序具有不确定性)
        int status=0;
        pid_t ret=waitpid(-1,&status,0);//参数:等待任意一个子进程、获取进程状态、阻塞式等待
        if(ret)
            printf("wait success, exit code:%d\n",WEXITSTATUS(status));

    }
    return 0;
}

  
  
  
  

  2)、如何执行我们自己写的C、C++二进制程序、其它语言的程序?

在这里插入图片描述使用execlp\ececvp执行我们自己写的C、C++程序

  说明:此时需要写明程序所在路径。 绝地路径、相对路径都行。

  演示结果如下:
在这里插入图片描述
  
  相关代码:
  test02.c中:

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

#define NUM 16
//const char *filepath="/home/wj/one.-studybylinux/study2023/T2307/T0715/proc02.out";
const char *filepath="./proc02.out";

int main(int argc, char *argv[], char *env[])
{
    pid_t id=fork();
    if(id<0)
    {
        return -1;
    }
    else if(id==0)//子进程
    {
        printf("子进程开始运行,pid:%d\n",getpid());
        execlp(filepath,"proc02.out","-b",NULL);
        exit(-2);//进程替换失败
    }
    else//父进程 
    {
        printf("父进程开始运行,pid:%d\n",getpid());
        //让父进程阻塞式等待子进程:子进程运行完毕,父进程获取后才退出,保证运行时的顺序(PS:单独fork,父子进程顺序具有不确定性)
        int status=0;
        pid_t ret=waitpid(-1,&status,0);//参数:等待任意一个子进程、获取进程状态、阻塞式等待
        if(ret)
            printf("wait success, exit code:%d\n",WEXITSTATUS(status));

    }
    return 0;
}

  proc02.c中:

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

int main(int argc,char*argv[])
{
    if(argc !=2)
    {
        printf("can not exectue!\n");
        exit(1);
    }

    if(strcmp(argv[1],"-a")==0)
    {
        printf("hello a!\n");
    }
    else if(strcmp(argv[1],"-b")==0)
    {
        printf("hello b!\n");
    }
    else 
    {
        printf("default!\n");
    }
    return 0;
}

  makefile中:

.PHONY:all
all:test02.out proc02.out
	
test02.out:test02.c
	gcc -o $@ $^
proc02.out:proc02.c
	gcc -o $@ $^

.PHONY:clean
clean:
	rm -rf *.out

  
  
  

在这里插入图片描述对其它语言的程序

  方法同上,只要有相关运行脚本即可。实际上exec系列函数功能类似于加载器。

execlp("./test.py", "test.py", NULL);
execlp("bash", "bash", "test.sh", NULL);
execlp("python", "python", "test.py", NULL);

  
  
  
  

2.4.3.3、execle函数演示

  1)、带e的exec系列函数,需要自己组装环境变量

       int execle(const char *path, const char *arg,..., char * const envp[]);

       int execvpe(const char *file, char *const argv[], char *const envp[]);

  相关演示如下:关于环境变量章节,相关链接
在这里插入图片描述
  
  相关代码:

  在test02.c中设置一个环境变量,char *const _env[NUM],使用execle进程替换。

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

#define NUM 16
//const char *filepath="/home/wj/one.-studybylinux/study2023/T2307/T0715/proc02.out";
const char *filepath="./proc02.out";

int main(int argc, char *argv[], char *env[])
{   
    //设置一个环境变量:fork后子进程能继承父进程的环境变量
    char *const _env[NUM]={(char*)"MY_ENV=2233445566",NULL};

    pid_t id=fork();
    if(id<0)
    {
        return -1;
    }
    else if(id==0)//子进程
    {
        printf("子进程开始运行,pid:%d\n",getpid());
        execle(filepath,"proc02.out","-a",NULL,env);
        //execle(filepath,"proc02.out","-a",NULL,_env);
        //execlp(filepath,"proc02.out","-b",NULL);
        exit(-2);//进程替换失败
    }
    else//父进程 
    {
        printf("父进程开始运行,pid:%d\n",getpid());
        //让父进程阻塞式等待子进程:子进程运行完毕,父进程获取后才退出,保证运行时的顺序(PS:单独fork,父子进程顺序具有不确定性)
        int status=0;
        pid_t ret=waitpid(-1,&status,0);//参数:等待任意一个子进程、获取进程状态、阻塞式等待
        if(ret)
            printf("wait success, exit code:%d\n",WEXITSTATUS(status));

    }
    return 0;
}

  proc02.c,形成proc02.out文件,让子进程替换上:

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

int main(int argc,char*argv[])
{
    if(argc !=2)
    {
        printf("can not exectue!\n");
        exit(1);
    }
	
    printf("获取环境变量为:MY_ENV:%s\n",getenv("MY_ENV"));

    if(strcmp(argv[1],"-a")==0)
    {
        printf("hello a!\n");
    }
    else if(strcmp(argv[1],"-b")==0)
    {
        printf("hello b!\n");
    }
    else 
    {
        printf("default!\n");
    }
    return 0;
}

  
  意义说明:

execle(myfile, "mycmd", "-a", NULL, env);

  我们知道main函数有环境变量参数char *env[]。假设该main函数是父进程的,使用上述进程替换函数,就可以直接通过父进程的env,在子进程中获取到相应的环境变量。

int main(int argc, char *argv[], char *env[])
{
	//子进程
	
	//父进程   
}

  
  
  
  

2.4.3.4、ececve 系统调用接口
int execve(const char *path, char *const argv[], char *const envp[]);

  事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。
在这里插入图片描述

  
  
  
  
  

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值