进程创建,进程终止,进程等待,进程替换(Linux进程的生命周期)

1. 进程创建

第一次认识fork是为了验证父子进程,今天在来深入理解他。

1.1 回忆fork

fork创建子进程是以父进程为模板,继承父进程的信息。

为何给父进程返回pid,给子进程返回0?
在之前的博客有深入讨论,点这里

  1 #include<iostream>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 using namespace std;
  5 int main()
  6 {
  7   cout<< "i am a process:"<<getpid()<<endl;
  8   pid_t id=fork();
  9   if(id<0)
 10   {
 11     cerr<<"fork err"<<endl;
 12   }
 13   else if(id==0)
 14   {
 15        cout<<"i am son:"<<getpid()<<endl;
 16        sleep(2);
 17   }
 18   else{
 19   cout <<"i am father: "<<getpid()<<" myson pid :"<<id<<endl;               
 20   sleep(5);
 21   }
 22   return 0;
 23 }

执行程序,输出没有问题
在这里插入图片描述
在我们./proc运行的时候进程创建,fork之后子进程被创建。
在程序中我们使用的getpid(),以及getppid(),是系统调用接口。
那么内核在这中间又干了什么呢?

  • 分配心得内存块和内核数据结构给子进程。
  • 将父进程部分数据结构拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度
    虽然子进程复制了全部代码,但是同时程序计数器也被复制,所以fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器调度。

1.2 写时拷贝

我们知道父子进程共享代码,数据各自私有。
代码共享:全部代码共享(代码是只读的,没有必要私有节省空间),不过由于程序计数器,从fork之后开始运行。
数据私有:由于进程的独立性,一个进程修改数据,是不能修改另一个进程的。
独立性:体现在拥有两个不同的地址空间,数据私有

假如数据很多,不是所有数据都要立马使用,不是所有的数据都需要修改。假如有一个数据要修改,我难道要直接全部拷贝吗,os肯定不会做这样的事情,遇见哪个数据拷贝在拷贝那个对应的数据,就叫做写时拷贝。
那不写呢,只进行读取操作,那就没必要将数据私有,这时候数据也是共享的。
直接对内存进行操作的

1.3 fork常规用法

  1. 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  2. 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1.4 fork调用失败的原因

  1. 系统中有太多的进程
  2. 实际用户的进程数超过了限制

2. 进程终止

2.1 进程退出的三个场景

注意是代码退出的情况:

  • 代码运行完,返回正确结果。
  • 代码运行完,返回错误结果
  • 代码异常终止
    在main函数我们在最后总是要写 return 0;这个返回的数字叫做进程的退出码。
    使用echo $?可以查看最近一次进程的退出码。
    首先看代码运行完,返回错误情况,我们ls并不存在-xyz,所以肯定是再其中执行了差错处理的代码,退出码为2.
    在这里插入图片描述
    然后我们在运行一次echo $?,这次查看的是上一次echo命令成功
    在这里插入图片描述
    这就是执行成功的情况。
    那么异常终止的情况呢,我们可以故意在代码里进行除0操作,结果运行到那一行就会报错,直接终止进程
    在这里插入图片描述
    操作系统使用的是8号信号,来终止他,我们也可以另起一个ssh渠道,使用信号来模拟终止
    在这里插入图片描述
    代码运行完,退出码才是有意义的。
    当代码异常终止,退出码无意义,需要获取退出原因。

2.2 exit与return

exit:终止整个进程,任何地方调用,都会终止,参数就是退出码
return:会终止函数,返回。只有在main函数才会终止。在main函数里return 的也是退出码

假如执行到exit(argc)这条语句,里面的参数就是退出码.

  1 #include<iostream>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5                                                                                
  6 using namespace std;
  7 
  8 int exe()
  9 {
 10     exit(12);
 11     }
 12 int main()
 13 {
 14   exe();
 15   cout<<"hello"<<endl;
 16 
 17 return 0;
 18 }

2.3 _exit与exit

  1 #include<iostream>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 
  6 using namespace std;
  7 
  8 int exe()
  9 {
 10     exit(12);                                                                                                                                                                          
 11     }
 12 int main()
 13 {
 14 
 15 
 16   cout<<"hello";
 17   sleep(2);
 18   exe();
 19 return 0;
 20 }
~
~
~

会打出hello,而且退出码也是12
在这里插入图片描述

当exit换成_exit时,虽然退出码也是12但是什么都不打印
在这里插入图片描述

总结
_exit是系统调用,不会刷新缓冲区的数据。直接关掉进程。
exit是库函数,他会做一些清理工作关闭执行流,刷新缓冲区。

一般都是在多进程中,不想让进程继续执行下去,使用exit进行退出。
一般来说都是子进程先退出,假如父进程先退出,那子进程就成了孤儿进程,由操作系统回收了,而且父进程很容易的对子进程进行管理。

3. 进程等待

子进程是为了给父进程处理一些子业务,需要让子进程给父进程一些结果。
所以父进程是需要等待子进程的。假如父进程对子进程不管不顾,子进程就成了僵尸进程,内存泄漏。

子进程运行完,有三种情况。父进程需要知道你的情况。
总的来说那么进程等待有两个作用:

  1. 回收系统资源
  2. 获取子进程退出信息

模拟一个僵尸进程。

 #include<iostream>  
  2 #include<stdlib.h>  
  3 #include<unistd.h>  
  4 #include<sys/types.h>  
  5 #include<sys/wait.h>                                                                                                                                                                   
  6 using namespace std;                   
  7 int main()                             
  8 {                                      
  9   pid_t id=fork();                     
 10   if(id==0)                            
 11   {                                    
 12     int count=0;                       
 13     while(1)                           
 14     {                                  
 15       sleep(1);                        
 16                                        
 17       cout<<"i am child"<<endl;        
 18       count++;                         
 19       if(count>=15)                    
 20      {                                 
 21       break;                           
 22      }                                 
 23      }                                 
 24     exit(0);                           
 25   }                                    
 26   else{                                
 27                       
 28                                        
 29     while(1)                           
 30     {                                  
 31       sleep(1);                        
 32       cout<<"i am father"<<endl;       
 33     }
        }
        return 0;
       }                               
              

使用while :; do ps axj | head -1 &&ps axj |grep proc |grep -v grep; sleep 1;echo "##############"; done脚本来监测他们两个进程的状态

当子进程退出后,父进程还在运行
在这里插入图片描述
子进程就成了僵尸状态
在这里插入图片描述

3.1 wait

在这里插入图片描述

在父进程的代码中加入wait,让他在20s的时候wait,NULL表示不关心状态

else{
 27     int count=0;
 28 
 29     while(1)
 30     {
 31       sleep(1);
 32       cout<<"i am father"<<endl;
 33       if(count==20)
 34       {
 35         wait(NULL);
 36       }
 37       count++;
 38     }
 39   }
 40 return 0;

我们可以使用wait进行阻塞等待(一直看着子进程,等待他死亡),将它回收(父进程触发回收动作,操作系统回收资源)。
在这里插入图片描述
假如不关心他的状态可以将参数设置为NULL

3.2 waitpid方法

这份代码表示,子进程执行10次退出,父进程回收他,当waitpid方法,参数为id,NULL,0时和wait方法无差异。

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 #include<stdlib.h>
    4 #include<sys/types.h>
    5 #include<sys/wait.h>
    6 int main()
    7 {
    8   pid_t id =fork();
    9   if(id<0)
   10   {
   11     perror("fork err\n");
   12     return 1;
   13   }
   14   else if(id==0)
   15   {
   16    int count=0;
   17    while(count<10)
   18    {
   19      printf("child:%d is running\n",getpid());
   20      sleep(1);
   21      count++;                                                                                                                                                                        
   22    }
   23    exit(0);
   24   }
   25   else{
   26     printf("father wait  before\n");
   27     int st=0;
   28     pid_t ret= waitpid(id,NULL,0);
   29     if(ret>0)
   30     {
   31       printf("wait successed\n");
   32     }
   33     else{
   34       printf("wait failed\n");
   35     }
   36     printf("father wait after\n");
   37   }
   38   return 0;
   39 }

3.2.1 第二个参数 int *status

我们重点来研究第二个参数。这个参数是一个输出型参数,作为形参我们要传他的地址,因为函数里面会改变它。将退出信息保存在st中。
在这里插入图片描述

若参数为NULL,则表示我们不关心。
若有参数,则需要关注两方面,进程终止无非三种情况,执行完的两种情况属于第一方面,异常退出属于第二方面。

  1. 退出两种情况
    结果正确:
    结果错误:
  2. 异常退出
    在这里插入图片描述
    返回之后,st中就保存的是我们进程退出的信息,正常运行,结果不同的两种情况退出码是多少,异常退出的一种情况退出信号是多少?
    int 是32位,在这里只用了低16位就够了。

第一种正常终止,所以终止信号是0,去后8位查看他的退出状态。来查看他结果是否正确

第二种被信号所杀,所以就是异常退出。我们需要查看退出信号。
在这里插入图片描述
将参数放到waitpid里,ret>0,代表waitpid成功,ret就是pid,输出st看一看。

//父进程
else{
 26     printf("father wait  before\n");
 27     int st=0;
 28     pid_t ret= waitpid(id,&st,0); 
         //wait成功                                                                                                                                                     
 29     if(ret>0)                                                                                                                                           
 30     {                                                                                                                                                   
 31       printf("%d \n",st);                                                                                                                               
 32       printf("wait successed\n");                                                                                                                       
 33     }

在这里插入图片描述
st为0。说明低8位和高八位都为0,也就说明这个是正常退出,而且结果正确。

我们看看正常退出,结果不正确的时候(exit(1))
在这里插入图片描述
其实也不难想,1在第9位,就是28,当然就是256了。
在这里插入图片描述

那么我们想要看看它的退出码,即检测他的高八位。

 int st=0;
     pid_t ret= waitpid(id,&st,0);
    if(ret>0)
    {
      printf("st:%d \n",st);
      int exit_code=(st>>8)&(0xff);
      printf("exit_code:%d\n",exit_code);
      printf("wait successed\n");
    }

在这里插入图片描述
这样就检测到了,还在位操作这一步愣了一会。由于你是高8位存储退出码,那我就右移8位,然后和0xff进行按位与操作,就能检测他哪一位是1,然后将它输出就得到了退出码。

再验证一下
在这里插入图片描述
然后向右移8位,和0xff,按位与,就得到了高8位也就是,23+22=12
在这里插入图片描述

其实理论我们要先检测他的低七位(第8位core dump先不用管),来查看进程是否异常终止,不是异常终止,才能看他是正常终止两种情况的哪一种。
我们只需要让st按位于0x7f,因为退出信号在低七位存储,所以我们只需要低七位按位与7个1即0x7f就可以拿到1的个数。从而打印出退出信号。

 int st=0;
     pid_t ret= waitpid(id,&st,0);
    if(ret>0)
    {
      printf("st:%d \n",st);
      printf("exit_singal:%d\n",st&(0x7f)); 
      int exit_code=(st>>8)&(0xff);
      printf("exit_code:%d\n",exit_code);
      printf("wait successed\n");
    }

在这里插入图片描述
这个就表示,正常退出,但结果不正确。

我们在子进程执行的模块加上一个显著的错误除0,很显然没执行到exit他就会异常退出,随后父进程等待会获取到退出信号,放在低7位当中

 int main()
   {
    pid_t id =fork();
     if(id<0)
    {
      perror("fork err\n");
     return 1;
    }
    else if(id==0)
    {
     int count=0;
     while(count<3)
     {
       printf("child:%d is running\n",getpid());
       sleep(1);
       count++;
    }
     int i=1/0;                                                                                                                                                       
     exit(12);                                                                                                                                     
    }                         

在这里插入图片描述
可以看到退出信号为8,因为8代表的就是精度异常
在这里插入图片描述
对程序做最终修改,所以我们就可以用if进行检测,来输出。

 1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 int main()                                                                                                                                                          
  7 {
  8   pid_t id =fork();
  9   if(id<0)
 10   {
 11     perror("fork err\n");
 12     return 1;
 13   }
 14   else if(id==0)
 15   {
       //子进程
 16    int count=0;
 17    while(count<3)
 18    {
 19      printf("child:%d is running\n",getpid());
 20      sleep(1);
 21      count++;
 22    }
 23    
 24    exit(0);
 25   }
 26   else{
          //父进程
 27     printf("father wait  before\n");
 28     int st=0;
 29     pid_t ret= waitpid(id,&st,0);
 30     if(ret>0)
 31     {
           //waitpid成功
 32       printf("st:%d \n",st);
 33       int exit_singal=st & 0x7f;
 34       int exit_code=(st>>8)&(0xff);
 35       
 36       if(exit_singal)
 37       {
            //进程异常终止
 38         printf("child error terminal");
 39       }
 40       else{
 41         if(exit_code)
 42         {      //进程正常执行,结果异常
 43                printf("child successed but result is not  right, exit_code: %d \n",exit_code);
 44         }
 45         else{
                   //进程正常执行,结果正常
 46           printf("child successed  result is  right ,exit_code: %d\n ",exit_code);
 47         }
 48       }
 49     }
 50     else{
          //waitpid失败
 51       printf("wait failed\n");
 52     }
 53     printf("father wait after\n");
 54   }                                                                                                                                                                 
 55   return 0;
 56 }


进程异常退出
在这里插入图片描述

进程正常,结果正常
在这里插入图片描述
进程正常,结果错误
在这里插入图片描述

其实在这里我们只是为了深入了解他的原理,其实已经有两个宏已经帮我们实现了,不需要我们自己做。
在这里插入图片描述
第一个就对应我们刚才实现的 status&ox7f只不过要和这个宏一样就得这样表达!(status&0x7f),因为st&ox7f为真表示进程异常退出。表示正常就需要取反。

第二个对应,若进程没有异常终止,(status>>8) & 0xff取到并查看退出码,来判断结果是否正确。

3.2.2 第三个参数WNOHANG

假如设置了第三个参数 wonohang,现在没有退出的子进程时,waipid就会返回0,即非阻塞式等待,一会再来看看有没有需要等待的。

3.3 waitpid总结

在这里插入图片描述
灵魂三问:

  1. 什么是进程等待:
    父进程通过wait/waitpid等系统调用,用来等待子进程,这是必须的。
  2. 为什么需要?
    防止子进程发生僵尸问题,会产生内存泄漏。读取子进程的退出状态。
  3. 怎么使用?
    wait/waitpid,使用waitpid时,可以通过第二个参数status获取退出状态,异常信号。通过第三个参数实现阻塞和非阻塞式等待

4. 进程替换

fork创建子进程的目的:

  1. 想让子进程执行父进程代码的一部分
  2. 想让子进程执行与父进程无关的事情

用fork创建子进程之后,子进程会执行与父进程相同的代码(可能是不同的模块),但是当我们想要让子进程执行与子进程完全无关的代码的时候,我们就需要进程替换。
需要注意的是,在我们之前讲到,数据是写时拷贝,代码是共享的,但是在这里代码也是需要写时拷贝的。进程替换是不会产生新进程的,它相当于狸猫换太子,进程的id不会改变。

进程替换是使用exec函数来进行操作的。

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 execve(const char *path, char *const argv[], char *const envp[]);

命名看起来很混乱,其实可以通过函数名字带有的参数来理解

  • l:函数形参采用列表进行输入
  • p:自动搜索环境变量
  • v:传递数组
  • e:表示自己维护环境变量

4.1 execl

需要自己传递路径,然后后面参数argc依次代表,执行程序和他对应的命令。

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 
  4 int main()
  5 {
  6   printf("i can execute");
  7   execl("/usr/bin/ps","ps","-e","-l","-f",NULL);
  8   printf("i also can execute ");                                    
  9   return 0;                    
 10 }   

于是这个我们写的./myexe程序就调用了ps这条系统命令,而且就在execl执行之后第二条打印语句是不会被打印的因为进程已经全部被替换,之前的代码也就不在了
在这里插入图片描述
可以看到执行结果。换句话说,像exec这一系列函数是没有返回值的(不会再返回来执行第二个printf语句,假如返回了,那就代表进程替换失败)

4.2 execlp

p代表自动搜索环境变量,但是需要我们传程序的名字,l代表执行命令的列表

    #include<stdio.h>
    #include<unistd.h>
    int main()
    {
       printf("i can execute\n");
      //execl("/usr/bin/ls","ls","-a","-l",NULL);
      execlp("ls","ls","-a","-l",NULL);                                                                                                                               
    
     printf("i also cam execute\n");
    return 0;
  }
  ~

可以执行
在这里插入图片描述

4.3 execv

v代表vector,以数组形式传参

  1 #include<stdio.h>
    2 #include<unistd.h>
    3 int main()
    4 {
    5   printf("i can execute\n");
    6   //execl("/usr/bin/ls","ls","-a","-l",NULL);
    7   char* const arg[]={
    8      "ls",
    9      "-i",
   10      "-a",
   11      "-l",
   12      NULL,
   13   };
   14   //execlp("ls","ls","-a","-l",NULL);
   15   execv("/usr/bin/ls",arg);                                                                                                                                       
   16   printf("i also cam execute\n");
   17   return 0;            
   18 }  

4.4 execvp

v代表以数组形式传参,p代表自动搜索环境变量只需要传程序名称。

 execvp("ls",arg);  

4.5 另外两个函数之前的补充

在演示之前我们先makefile创建两个可执行文件

  1 .PHONY:all
  2 all:myexe mycmd
  3 
  4 myexe:myexe.c                                                       
  5   gcc  $^ -o $@
  6 mycmd:mycmd.c
  7   gcc  $^ -o $@
  8 .PHONY:clean
  9 clean:
 10   rm -f myexe mycmd
~
~
~
~

假如不在生成可执行文件的依赖关系中添加伪目标,默认生成第一个可执行。

进程是可以使用exec系列函数替换自己的,只不过会成为死循环。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5   printf("myexe can execute\n");
  6   execl("./myexe","./myexe",NULL);                                  
  7   //execlp("ls","ls","-a","-l",NULL);
  8   //execv("/usr/bin/ls",arg);
  9   // execvp("ls",arg);
 10   printf("i also cam execute\n");
 11   return 0;
 12 }

在这里插入图片描述
我们再写一个本目录下的进程替换。直接带上他的相对路径,传参形式无差别

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5   printf("myexe can execute\n");
  6   execl("./mycmd","./mycmd",NULL);                                  
  7   //execlp("ls","ls","-a","-l",NULL);
  8   //execv("/usr/bin/ls",arg);
  9   // execvp("ls",arg);
 10   printf("i also cam execute\n");
 11   return 0;
 12 }

在这里插入图片描述

4.6 execle与execve

没有带p,说明都需要传递路径,l和v说明传参形式不同,e说明自定义环境变量。

在这里插入图片描述

单独运行mycmd,没有环境变量
在这里插入图片描述
使用myexe调用mycmd就可以将我们的env传入进去
在这里插入图片描述
还记得main函数里是有环境变量env的,当我们什么都不干,两者环境变量都为NULL
在这里插入图片描述

当我们在外export env时,调用myexe环境变量就会作为参数传递下去
在这里插入图片描述

那么execve就是传数组。
其实系统调用只有一个就是execve,其他都是为了适用各种场景的再一次封装,调用的都是execve。
而且这也说明了c/c++是可以调用其他语言所编写的程序。

4.7 多进程实现进程替换

exec这一系列函数实际上是多进程场景下使用的,即父进程运行,创建子进程,让子进程替换别的进程取运行。

子进程去做命令,父进程只需要等待关心结果就好。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<sys/wait.h>
  5 int main()
  6 {
  7   pid_t id=fork();
  8   if(id<0)
  9   {
 10     perror("fork err");
 11     return 1;
 12   }
 13   else if(id==0)
 14   {
 15     sleep(3);
 16     execl("/usr/bin/ls","ls","-a","-l",NULL);                                                                                                                       
 17     exit(1);
 18   }
 19   pid_t ret =waitpid(id,NULL,0);
 20   if(ret>0)
 21   {
 22         printf("wait successed\n ");
 23   }
 24   return 0;
 25 }

在这里插入图片描述
这样即使子进程执行失败,他也会exit退出,然后被回收,不会影响父进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

楠c

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值