Linux 进程终止 进程等待 进程替换 简易shell

进程终止

进程退出的三种场景:
1.代码运行完毕,结果正确
2.结果运行完毕,结果不正确
3.代码异常终止,程序跑了一部分终止了,在vs中对应程序崩溃

首先,我们需要知道,main函数的return值是其实是进程的退出码,会返回给父进程或者系统,父进程可以根据退出码进行原因分析。
一般来说,退出码为0表示进程运行完毕,结果正确。退出码为非0表示进程运行完毕,结果错误。
对此,我们可以进行验证:
格式:echo $?
功能:输出最近一次进程退出时的退出码
当我们使用正确命令的时候,退出码为0,使用错误命令的时候,退出码为非0
在这里插入图片描述
一个进程正常运行结束,结果正确,对于这种情况不用太多的分析。
但是一个进程正常运行结束,结果错误,这时就需要分析进程是因为什么原因导致结果错误。
所以,每个进程的退出码其实都表示了一个错误原因。我们可以通过strerror函数来获取退出码对应的错误原因。
头文件:#include<string.h>
格式:strerror(退出码)
功能:返回退出码对应的退出原因字符串
在这里插入图片描述
注:
程序正常运行结束,说明程序的主体代码已经执行完成,exit或者return等代码已经执行,这时退出码就具有意义。如果程序异常终止结束,这时退出码也就没有参考意义。

进程正常退出的方式

   ( 1 ) (1) (1)return
关于return有两种场景:
1.非主函数进行return
这种非主函数内的return叫做函数返回,其并不是终止进程。

2.主函数进行return
主函数的return是终止进程。

   ( 2 ) (2) (2) exit

头文件:#include<stdlib.h>
格式:exit(退出码)
功能:终止进程

注:exit函数不管在哪使用其含义都是终止进程。

在明白了exitreturn以后,我们需要重新看向以前的一个知识点:
以前我们说了printf打印内容的时候,如果不带\n,那么只有等进程退出以后系统才会刷新缓冲区,将内容打印到显示器上,其实这背后是exitreturn在要求系统刷新缓冲区。
   ( 3 ) (3) (3)_exit

头文件:#include<unistd.h>
格式:_exit(退出码)
功能:终止进程

_exitexit的功能基本一致,我们着重学习两者的区别:
一个正常运行的程序,在运行exit函数以后会执行用户定义的清理函数,冲刷缓冲区,关闭流等等,接着再将控制权交给内核操作系统。

_exit在程序运行完以后,不会执行清理,冲刷缓冲区这些操作,而是将控制器直接交给内核操作系统。

注:这里的缓冲区是用户级缓冲区
在这里插入图片描述
对此我们通过代码进行验证:

#include<stdio.h>      
#include<unistd.h>      
      
int main()      
{      
  printf("_exit验证");                                                                 
  _exit(0);                               
}       

在这里插入图片描述
操作系统层面做了什么?
进程的退出,说明在操作系统层面,少了一个进程,那么PCB,进程地址空间,页表和各种映射关系,代码,数据和申请的空间都要被释放掉。

进程等待

在了解进程等待之前,我们重新看向fork函数的作用:

fork创建子进程,子进程的创建是为了帮助父进程完成某种任务,那么既然是帮助父进程完成某种任务,父进程就需要知道子进程执行任务是否成功?完成了多少。
在讲进程状态的时候,我们说过进程处于僵尸状态时,会将退出信息写入PCB中,供父进程进行读取,但是父子进程的调度顺序不是固定的,可能存在这样一种情况,子进程还在运行,父进程已经退出了,这时父进程就无法读取到子进程的退出信息,所以进程等待出现了,通过一些系统调用函数可以使得父进程等待子进程运行完再退出。

总结下进程等待的作用:
1.父进程通过等待获取子进程退出的信息,能够得知子进程的执行结果。
2.可以保证时序问题:子进程先退出,父进程后退出。
3.进程退出的时候会先进入僵尸状态,会造成内存泄露的问题,通过进程等待,父进程可以释放子进程占用的资源。(这里父进程释放子进程的资源实际上是不准确的,父进程只会读取子进程的退出信息,释放资源的操作最终还是由操作系统来完成)。

父进程是如何获取到子进程的退出信息的?

父进程是通过系统调用这类方式让操作系统来访问子进程的PCB,间接获取到子进程的退出信息的。

父进程一般在什么时候获取子进程的退出信息

父进程获取子进程退出信息的工作一般被安排在了最后来执行,但是这并不是绝对的,例如使用wait进程等待这类调用以后,父进程会将回收子进程退出信息的工作提前完成。

如何做到进程等待?
在Linux中,通过wait,waitpid这样的系统调用函数可以让父进程做到等待子进程。

   ( 1 ) (1) (1)wait
头文件 #include<sys/types.h>
#include<sys/wait.h>
函数原型:pid_t wait(int* status)
返回值:类型pid_t 等待成功返回子进程pid 失败返回-1
参数类型 int * status:输出型参数,可用于获取子进程的退出信息
功能:等待子进程,等待成功以后回收子进程的退出信息

对于这里的参数我们需要进行详细的介绍:

关于输出型参数status,`其类型是一个整形指针,也就是说用户只需要在外部定义一个变量status,在参数部分传递进变量的地址,在wait函数内部,系统会自动为status进行赋值。

status参数由32个比特位组成,但是目前在这里我们只介绍低16位。
在这里插入图片描述
进程的终止就三种可能性:前两种可能我们已经介绍过了,但是对于进程的异常终止,我们还没有过多的介绍,这里大家需要知道进程异常终止的本质是因为这个进程因为异常问题,导致自己收到了某种信号。所以异常终止的进程其退出码没有借鉴意义。

注:若进程正常运行,那么进程信号就为0
所以可以通过if语句进行判断,当进程信号为0时,获取进程的退出码,反之获取进程信号。

如何通过status获取到子进程的退出码和退出信号呢?
退出码 = (status >> 8) & 0xFF;
退出信号 = status & 0x7F;

注:如果用户不关心子进程的退出信息和退出码,参数status可以传递空指针。

注:除了可以使用按位与的操作得到退出码,Linux中还有两个宏可以获得退出信号和退出码。
WIFEXITED(status):若子进程正常退出,则返回一个大于0的数字。(判断进程是否正常退出)
WEXITSTATUS(status): 若返回值非零,则可以提取子进程退出码。(查看进程的退出码)
通过代码我们来验证status参数的组成。

#include<iostream>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
using namespace std;


int main()
{
  pid_t id=fork();

  if(id==0)
  {
    int cnt=5;
    while(cnt--)
    {
      cout<<"I am a son proc"<<endl;
      sleep(1);
    }
    exit(13);
  }
  else
  {
    int status=0;
    sleep(10);
    cout<<"father begin"<<endl;
    pid_t ret= wait(&status);
    sleep(1);
    if(ret>0)
    {
    cout<<"father wait sucess"<<endl;
    if((status&0x7f)==0)
    {
      cout<<"son pron exit normal"<<endl;
      cout<<"son exitcode="<<((status>>8)&0xFF)<<endl;
    }
    else
    cout<<(status&0x7f)<<endl;  
    }
    else 
    cout<<"father wait failed"<<endl;
  }   

}

结果发现,子进程的退出码和我们设置的一样,确实为13
在这里插入图片描述
也可以通过给子进程发送退出信号,来让其异常终止。
在这里插入图片描述
在得知了父进程如何获取子进程的退出信息以后,我们也就能理解一个现象:
为何echo$?能够获取到最近一个进程的退出码,通过一串代码我们进行解释:

  #include<iostream>    
  #include<stdlib.h>    
  using namespace std;    
      
  int main()    
  {    
	 cout<<"son proc id="<<geipid()<<endl;    
	 cout<<"father proc id="<<getppid()<<endl;                                          
                                    
                                           
  } 

在这里插入图片描述
在前面我们提到过,在命令行上启动的进程的父进程都为bashecho $?命令的作用是输出最近一个进程的退出码,而最近一个进程的退出信息父进程bash通过进程等待接收了,这就是echo $?的原理。

通过进程等待,我们也可以验证父进程会清理子进程的资源(只是读取子进程的退出信息)

#include<iostream>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;

int main()
{
  pid_t id =fork();
  if(id==0)
  {
    int cnt=5;
    while(cnt--)
    {
      cout<<"I am a son proc"<<endl;
      sleep(1);
    }
    exit(13);
  }
 else 
 {
   sleep(10);
   cout<<"father begin"<<endl;
   pid_t ret=wait(NULL);
   if(ret>0)
     cout<<"father wait sucess"<<endl;
   else 
     cout<<"father wait failed"<<endl;
 }
}

子进程运行5秒后退出,父进程休眠10秒,开始等待子进程,在父进程等待结束可以看到僵尸状态消失了。
在这里插入图片描述

在这里插入图片描述

   ( 2 ) (2) (2)waitpid
头文件 #include<sys/types.h>
#include<sys/wait.h>
函数原型:pid_t watipid(pid_t pid,int* status,int options);
返回值:类型pid_t 等待成功返回子进程pid 失败返回-1
功能:等待子进程,等待成功以后回收子进程的退出信息

waitpid的功能和wait大致一样,其第二个参数的作用和wait一样,这里就不多解释了。
我们着重解释参数一和参数三。

参数一:
1.传入具体要等待的子进程的pid
2.传入-1,父进程会选择任意一个子进程进行等待。

参数二:父进程的等待方式
1.输入0表示以阻塞等待的方式等待子进程
2.输入WNOHANG表示以非阻塞的方式等待子进程。

注:
WNOHANG表示非阻塞等待,看到某些应用或者操作系统本身,卡住了长时间不动,一般称其HANG住了。
W :wait
NO:没有

阻塞等待:如果父进程以阻塞等待的方式等待子进程,那么父进程在等待的过程中什么也不会做,会一直等待子进程直到等待成功or失败。
阻塞了是不是意味着父进程不被调度执行了?
是的,阻塞的本质:其实是进程的PCB从运行队列被放入了等待队列并将进程的R状态改为S状态
返回的本质:进程的PCB从等待队列被放入了运行队列被CPU调度,并获取子进程的退出信息。

非阻塞等待:父进程以非阻塞等待的方式等待子进程,那么父进程在等待的过程中不是完全卡住的,其会间隔一段时间查看子进程的情况,如果子进程退出,那么父进程返回,如果子进程还在运行,那么父进程在一段时间过后会继续查看,直到等待成功or失败,这种方式也叫基于非阻塞的轮询等待方式
非阻塞等待由多次的询问/查看组成,父进程不是一直卡着不动等待子进程,而是隔一段时间等待一次,在这些等待的时间间隔内,父进程依旧可以被CPU调度,执行自己的代码。

非阻塞等待和阻塞等待的异同:
同:两种等待的本质都是PCB和状态的切换。
异:阻塞等待时,父进程啥也不干。非阻塞等待时,父进程在等待间隔的时间内可以被CPU调度,父进程的PCB不断在运行队列和等待队列中进行切换。

基于非阻塞轮询的等待方式(代码实现)

代码实现:

注:通过代码实现非阻塞轮询等待,waitpid返回0表示子进程还在运行,父进程需要重复等待。返回值大于0表示等待成功,等待结束。返回值小于0表示等待失败。

代码主体大致不变,因为是轮询等待,所以需要将等待包在一个while循环内。

#include<iostream>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
using namespace std;


int main()
{
  pid_t id=fork();
  if(id==0)
  {
    int cnt=5;
    while(cnt--)
    {
      cout<<"I am a son proc"<<endl;
      sleep(1);
    }
    exit(12);
  }
  else 
  {
    
    int status=0;
    cout<<"father wait begin"<<endl;
    while(true)
    {
        pid_t ret=waitpid(id,&status,WNOHANG);
        if(ret==0)//子进程还在运行,父进程继续等待 
        {
          cout<<"father proc do my things"<<endl;//父进程在等待时间间隔内可以做自己的事情 
        }
        else if(ret>0)//父进程等待成功 
        {
          cout<<"father wait sucess"<<endl; 
          cout<<"exitsignal:"<< (status & 0x7F)<<endl;
          cout<<"exitcode:"<<((status >> 8)&0xFF)<<endl;
         
          break;
        }
        else if(ret<0)//父进程等待失败 
        {
          cout<<"father wait failed"<<endl;
          break;
        }
        sleep(1);
    }
  }
}

在这里插入图片描述

进程替换

父进程通过fork函数创建子进程,子进程的出现可以是帮助父进程完成某种任务,能否让子进程运行一个新的程序呢?
事实上,现有技术是可以做到的,通过调用exec*系列替换函数可以达到进程替换的目的。

进程替换:进程不变,仅仅替换当前进程的代码和数据的技术,叫做进程的程序替换。
进程替换只有替换物理内存中的代码和数据,进程原本的页表,进程地址空间,PCB等数据结构都没有发生变化。

进程替换会创建新进程吗?

不会,进程替换技术主要是替换物理内存中的代码和数据。

进程替换听起来很玄乎,所以我们需要先看这个技术会造成怎样的现象,再从现象进行步步的剖析。
这里我们开始介绍进程替换函数

   ( 1 ) (1) (1)execl
头文件:#include<unistd.h>
函数原型:int execl(const char*path,const char *arg,.....)
功能:实现进程的替换,运行一个全新的程序

参数一:要替换程序的路径+程序名

参数二:如何运行这个程序,如ls-l(那么参数二就需要传入"ls","-l",NULL)
注:参数二使用了可变参数列表,所以参数二必须以NULL结尾这样系统才知道参数结束。

注:这里的参数二其实也就是命令行参数

#include<iostream>    
#include<unistd.h>    
using namespace std;                                                                   
    
    
    
int main()    
{    
      cout<<"I am a proc"<<endl;    
      execl("/usr/bin/ls","ls","-l",NULL);    
      cout<<"Hello execl!"<<endl;    
      cout<<"Hello execl!"<<endl;    
      cout<<"Hello execl!"<<endl;    
} 

在这里插入图片描述
一个程序变成进程,在这个过程中,加载器会将程序的代码和数据加载到内存中,加载器的底层原理使用的就是exec*系列的程序替换函数
注:程序替换的本质就是把程序的代码和数据加载进特定进程的上下文中,这两句话很不好理解,在后面的学习中我们逐渐加深对其的理解。

父进程通过fork创建子进程,父进程和子进程的代码是共享的,但是如果父进程or子进程发送进程替换,不是会影响代码和数据吗?

替换数据很好理解,会发生写时拷贝,事实上在替换代码的时候也会发生写时拷贝。

替换函数的命名规则

l(list) : 采用可变参数列表的形式传递
v(vector) : 参数用数组的方式进传递
p(path) : 系统会根据程序名在path内进行匹配
e(env) : 进程单独维护自己自己的环境变量

   ( 2 ) (2) (2)execv
头文件:#include<unistd.h>
函数原型:int execv(const char *path,char *const argv[])

参数一:要替换程序的路径+程序名
参数二:将程序的运行方式写入数组中,传递数组的地址。(数组最后需要以NULL结尾)

  char *argv[]={"ls","-a","-l",NULL};
  execv("/usr/bin/ls","ls","-a","-l");	      

   ( 3 ) (3) (3)execlp
头文件:#include<unistd.h>
函数原型:int execlp(const char *file,const char* arg,...)

参数一:只需要传递进程序的名字,系统会自动在path里进行路径匹配。
参数二:如何运行这个程序,如ls-l(那么参数二就需要传入"ls","-a",NULL)

 execlp("ls","ls","-a","-l",NULL);

   ( 4 ) (4) (4)execvp
头文件:#include<unistd.h>
函数原型:int execvp(const char* file,char * const argv[]);

参数一:只需要传递进程序的名字,系统会自动在path里进行路径匹配。
参数二:和execl```函数的参数二一样,告诉系统如何运行这个程序

char * argv[]={"ls","-a","-l",NULL};
execvp("ls",argv);

   ( 5 ) (5) (5)execle
头文件:#include<unistd.h>
函数原型:int execle(const char*path,const char* arg,.....,char* const envp[]);

参数一:要替换程序的路径+程序名
参数三:传递环境变量数组,替换的程序会维护这个数组作为自己的环境变量。
关于这里的参数三,我们通过一串代码进行解释:
生成testprocess可执行程序,在process可执行程序中调用execle替换函数执行test可执行程序。

test可执行程序:
作用:打印环境变量

int main()
{
  extern char** environ;
  for(int i=0;environ[i];i++)
    printf("%s\n",environ[i]);

}

process可执行程序
作用:使用execl替换函数执行test可执行程序。

int main()
{
  char* env[]={"MYENV=myenvenvenv"};
  execle("./test","./testt",NULL,env);

}

在这里插入图片描述

   ( 6 ) (6) (6)execve
头文件:#include<unistd.h>
int execve(const char* path,char* const argv[],char* const envp[])

	char * argv[]={"./class",NULL};
	char* env[]={"MYENV=myenvenvenv"};
 	execve("./class",argv,env);

注:替换函数失败会返回1,成功没有返回值,通过替换函数,还可以调用其他语言的程序

execlp,execl,execle,execvp,execv都是库函数,本质都是execve系统调用函数的上层封装。

实现简易shell解释器
#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<unistd.h>
#include<string.h>

char command[128];
int main()
{

  
  while(1)
  {
  char* argv[64]={NULL};//(6)
  command[0]=0;//(3)
  printf("[fengli@iZ8vbhcpwdmnwq6ejjs26zZ ~]$");//(1)
  fflush(stdout);//(2)
  fgets(command,128,stdin);//(4)
  command[strlen(command)-1]=0;//(5)
  argv[0]=strtok(command," ");//(7)
  int i=1;
  while(argv[i]=strtok(NULL," "))//(8)
  {
    i++; 
  }
  if(strcmp(argv[0],"cd")==0)//(12)
  {
      if(argv[1])
        chdir(argv[1]);
      continue;

  }
  if(fork()==0)//(9)
  {
    execvp(argv[0],argv);//(10)
    exit(1);
  }
  waitpid(-1,NULL,0);//(11)

  } 
}

   ( 1 ) (1) (1) ( 2 ) (2) (2)打印提示符
使用fflush()进行刷新,不能使用\n\n会进行换行。
在这里插入图片描述
   ( 3 ) ( 4 ) ( 5 ) (3)(4)(5) (3)(4)(5)接收命令行输入的命令
设置一个command数组,用于搭配接收命令行的命令输入。
通过fgets函数将命令行的命令输入输出到command数组中。
回车键也属于字符串,在接收命令行命令时也会输出到command数组中,需要将其去掉。
   ( 6 ) ( 7 ) ( 8 ) (6)(7)(8) (6)(7)(8)分割字符串
command数组接收的字符串格式为:"ls -a -b",需要将其分解成"ls","-a","-b"的形式保存到argv数组中。
注:strtok函数用法
   ( 9 ) ( 10 ) ( 11 ) (9)(10)(11) (9)(10)(11)使用替换函数
创建子进程,使用execvp替换函数,让子进程执行shell命令解析。
注意:不能让父进程执行替换函数,这样会使得父进程的代码和数据被更改。
这时我们也就能理解:为什么shell作为媒婆,需要让实习生子进程去替自己办事情,因为子进程死了父进程不会受到影响。
当子进程完成任务以后,父进程进行清理回收工作。
   ( 12 ) (12) (12)内建命令
Linux中有很多内建命令是需要父进程自己执行,如cd,export,所以对于内建命令需要进行特判。
cd命令也是一个内建命令,子进程修改自己的路径不会影响到父进程,所以需要特判,让父进程修改自己的路径。

实现mi_ni shell一定需要父进程等待子进程吗?

父进程最好是等待子进程的,因为这样保证了子进程在执行完自己的程序替换代码以后,父进程才开始新一轮的询问。
如果不等待就可能存在一种情况:CPU不断调度父进程,而子进程一直在队列等待

关于进程资源的回收

一个进程的资源最终是由操作系统进行回收的,父进程只会读取子进程的退出信息,不会对子进程资源进行回收。

关于进程的退出

进程无论是使用exit函数退出,或者是return退出,又或者是因为异常。当退出以后进程都需要先进入僵尸状态,其PCB资源依旧保存在内存中,内存中代码和数据会被操作系统回收,而pcb资源,只有等父进程接收了退出信息,操作系统才会进行进程资源的回收。

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



int main()
{
 if(fork()==0)
 {
   int cnt=0;
   while(1)
   {
     printf("%d\n",cnt);
     cnt++;
     sleep(1);
     if(cnt==5)
     {
       exit(0);
     }
   }
 }
 while(1)
 {
   printf("I am father\n");
   sleep(2);
 }
}

这里我们验证下exit()退出,结果的确和我们说的一样。
在这里插入图片描述

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

凤梨罐头@

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

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

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

打赏作者

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

抵扣说明:

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

余额充值