进程控制(退出&等待&程序替换&简单shell的实现)

1.进程退出的理论

进程退出有三个场景:
①代码运行完毕,结果正确
②代码运行完毕,结果不正确
③代码异常终止

main函数的返回值实际上是进程的退出码
echo $?指令可以输出最近一次程序退出时的退出码
进程控制(退出&等待&程序替换&简单shell的实现)_第1张图片

进程控制(退出&等待&程序替换&简单shell的实现)_第2张图片
后面的echo 为什么输出的是0?
以为最近的一次指令是上一次的echo,所以return 返回的是0
进程控制(退出&等待&程序替换&简单shell的实现)_第3张图片
退出码可以来判断程序执行的结果是正确还是不正确
我们一般用 0代表success , !0 代表failed
比如在这边错误的退出码就是2(2只是不正确情况中的一种,实际情况而定 )
进程控制(退出&等待&程序替换&简单shell的实现)_第4张图片我们可以打印一下,看一下各种错误码
进程控制(退出&等待&程序替换&简单shell的实现)_第5张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第6张图片
代码异常终止,在我们的VS中被称为程序崩溃,也就是说代码还没有跑完就终止了,在这种情况下,错误码是没有意义的了,因为是unknown的
进程控制(退出&等待&程序替换&简单shell的实现)_第7张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第8张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第9张图片

2 .进程退出的方式

有三种方式

方式一:return

main函数return, 代表进程退出,非main函数的return叫做函数返回

方式二:exit

exit在任意地方调用,都叫做终止进程,参数是退出码
进程控制(退出&等待&程序替换&简单shell的实现)_第10张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第11张图片
数据这边本身是会被放在缓冲区的,但是exit or main里面的return 本身就会要求系统进行,缓冲区刷新!
总结:return 和 exit 除了帮我们能够退出程序以外,还可以帮助我们刷新缓冲区
进程控制(退出&等待&程序替换&简单shell的实现)_第12张图片
在这里插入图片描述

方式三:_exit

_exit终止进程,强制终止进程,不要进行后续的收尾工作,比如刷新缓冲区
进程控制(退出&等待&程序替换&简单shell的实现)_第13张图片进程控制(退出&等待&程序替换&简单shell的实现)_第14张图片
一张图说明exit()和_exit的区别,_exit()直接终止,exit()要做以下的一系列工作
ps:不刷新缓冲区!=不释放系统资源
进程控制(退出&等待&程序替换&简单shell的实现)_第15张图片

3 .进程退出,OS做了什么?

系统层面上来看:少了一个进程:free PCB, free mm_struct , free 页表和各种映射结构,代码+数据,申请的空间也要被杀掉

4.进程等待

原理

①进程wait是什么

父进程fork出来的子进程是为了帮父进程完成某种任务,父进程进行fork的时候,需要通过wait/waitpid等待子进程的退出
进程控制(退出&等待&程序替换&简单shell的实现)_第16张图片

②为什么要让父进程等待

1 .通过获取子进程退出的信息,能够得知子进程执行结果
2 .可以保证:时序问题,子进程先退出,父进程再退出
3 .子进程退出的时候会进入僵尸状态,会造成内存泄漏的问题,需要通过父进程wait,释放该子进程占用的资源

③怎么进行进程等待(wait,waitpid)

进程控制(退出&等待&程序替换&简单shell的实现)_第17张图片

wait
不想让子进程完成之后还执行父进程的代码,子进程可能要执行五秒,父进程早就退出了,子进程会被系统领养,变成孤儿进程;然而,我们不想这么干,我们要等待子进程
进程控制(退出&等待&程序替换&简单shell的实现)_第18张图片

进程控制(退出&等待&程序替换&简单shell的实现)_第19张图片
在这里插入图片描述

进程控制(退出&等待&程序替换&简单shell的实现)_第20张图片
我们可以看到刚开始程序有两个进程,慢慢地子进程没有被父进程回收变成了僵尸进程
进程控制(退出&等待&程序替换&简单shell的实现)_第21张图片
接着父进程开始等待,回收子进程
进程控制(退出&等待&程序替换&简单shell的实现)_第22张图片
最后父进程也退出程序结束
说明:我们的wait是可以回收僵尸进程的

waitpid
这里第一个参数指定为id的话等待的就是id
进程控制(退出&等待&程序替换&简单shell的实现)_第23张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第24张图片
这里第一个参数如果是-1的话,表示的是等待任意 一个 子进程,等价于刚刚的wait
进程控制(退出&等待&程序替换&简单shell的实现)_第25张图片
这里的status是输出型参数
status的构成:有32个比特位,只使用低16位比特位,高16位比特位不用
进程控制(退出&等待&程序替换&简单shell的实现)_第26张图片

在这里插入图片描述
父进程拿到什么status结果,一定和子进程如何退出强相关!!!
子进程退出的话题,不就是我们刚刚将的进程退出吗?

进程控制(退出&等待&程序替换&简单shell的实现)_第27张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第28张图片
最终一定要让父进程通过status得到子进程执行的结果
执行的结果就是之前讲的三种
进程控制(退出&等待&程序替换&简单shell的实现)_第29张图片
结果正确和不正确,也就是看退出码,也就是return or exit的结果
如何判断代码异常终止?本质是这个进程因为异常问题,导致自己收到了某种信号

实操

①获取子进程的退出结果(右移方式)

进程控制(退出&等待&程序替换&简单shell的实现)_第30张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第31张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第32张图片
再稍微修改一下,就是我们代码正确的情况
进程控制(退出&等待&程序替换&简单shell的实现)_第33张图片

进程控制(退出&等待&程序替换&简单shell的实现)_第34张图片
bash是命令行启动的所有进程的父进程,bash一定是通过wait方式得到子进程的退出结果,所以我们能看到echo $?能够查到子进程的退出码!

②接口方式

status:
WIEXITED(status):查看进程是否是否正常退出
WEXITSTATUS(status):若WIFEXITED非0,提取子进程退出码(查看进程退出码)
进程控制(退出&等待&程序替换&简单shell的实现)_第35张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第36张图片

③WNOHANG

第三个参数OPTIONS:
WNOHANG:设置等待非阻塞
首先理解阻塞和非阻塞:

阻塞等待:现实生活中,等一个朋友的期间什么都没有干,没等到就不回家(不挂电话,朋友一直在电话中等)

非阻塞等待:现实生活中,不停的打电话挂电话询问,检测朋友的运行状态
可能需要多次检测:基于非阻塞状态等待的轮询方案

OS层面上看
阻塞的本质:其实是进程的PCB被放入了等待队列,并将进程的状态改为S状态
返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度
进程控制(退出&等待&程序替换&简单shell的实现)_第37张图片

无论是阻塞非阻塞,都是等待的一种。谁等?等谁?等什么?
现在这种情况是父进程在等子进程退出
进程控制(退出&等待&程序替换&简单shell的实现)_第38张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第39张图片

5.进程的程序替换

①什么是程序替换

进程不变,仅仅替换当前进程的代码和数据的技术,叫做,进程的程序替换

进程控制(退出&等待&程序替换&简单shell的实现)_第40张图片

相当于我们用了一个老的进程的壳子,执行了新的代码和数据
有没有创建新的进程?没有
进程控制(退出&等待&程序替换&简单shell的实现)_第41张图片
我们发现后面打印的语句竟然被替换了
为什么后面的代码没有执行??因为程序替换,后面的代码和数据全都被替换掉了
进程控制(退出&等待&程序替换&简单shell的实现)_第42张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第43张图片
程序替换的本质就是 把指定的代码+数据,加载进特定进程的上下文中!!
进程控制(退出&等待&程序替换&简单shell的实现)_第44张图片
子进程进行代码替换,父进程间断持续打印,最后阻塞回收
进程控制(退出&等待&程序替换&简单shell的实现)_第45张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第46张图片
但是为什么子进程在执行的时候,父进程也在执行呢???
答案是进程具有独立性
之前所说的父子进程代码共享是建立在没有进程程序替换的前提下的,
也就是说,
进程程序替换会更改代码区的代码,也要发生写时拷贝

联想前面,我们创建子进程的目的,是为了让子进程执行父进程代码的一部分!那么如果想让子进程执行一个“全新的程序”呢?
程序替换!

只要exec*的函数返回了,就一定是因为调用失败了,不然一定不会使用后续的代码的?

②为什么要执行程序替换

目的也就是让子进程去执行一个全新的程序!

③各个程序替换函数的基本使用(execl,p,v,ve,le)

使用man execl查看使用
进程控制(退出&等待&程序替换&简单shell的实现)_第47张图片
先写一个测试代码
进程控制(退出&等待&程序替换&简单shell的实现)_第48张图片
我们发现程序替换之后的代码end<<也没有了
也可以知道execl返回的话函数必定出错了
进程控制(退出&等待&程序替换&简单shell的实现)_第49张图片
为什么出来有execl , 还有execlv , execlv这些呢????
命名理解
进程控制(退出&等待&程序替换&简单shell的实现)_第50张图片

l ( list ) : 表示参数采用列表
这样一个一个传入就叫列表
在这里插入图片描述

v ( vector ) : 参数用数组
进程控制(退出&等待&程序替换&简单shell的实现)_第51张图片

p ( path ): 有p自动搜索环境变量PATH
只要传file名帮我们自动寻找
进程控制(退出&等待&程序替换&简单shell的实现)_第52张图片
在这里插入图片描述
vp也就是v+p的功能同理
进程控制(退出&等待&程序替换&简单shell的实现)_第53张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第54张图片

e (env) : 表示自己维护环境变量

我们想要通过执行myexe.c然后把我们的myload.c给跑起来
进程控制(退出&等待&程序替换&简单shell的实现)_第55张图片
把myexec.c全部替换成myload.c的过程

进程控制(退出&等待&程序替换&简单shell的实现)_第56张图片
Makefile默认只会执行第一个依赖关系,所以如果我们想要同时编译两个文件,就需要写成下面的这种方式
想执行多个执行文件只需要在all的后面往后跟就可以了
进程控制(退出&等待&程序替换&简单shell的实现)_第57张图片
我们成功地通过运行myload将myexe的内容执行了
进程控制(退出&等待&程序替换&简单shell的实现)_第58张图片
在这里插入图片描述
验证execle
进程控制(退出&等待&程序替换&简单shell的实现)_第59张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第60张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第61张图片
也就是说,带e的可以自定义环境变量
这样来看
execve也很简单的
适当修改代码,检验同样也是可以的
进程控制(退出&等待&程序替换&简单shell的实现)_第62张图片
在这里插入图片描述
总结:以后写程序如果有其他语言写的代码,我们可以通过exec进行接口调用
这里用c++程序调用python代码举个例子
进程控制(退出&等待&程序替换&简单shell的实现)_第63张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第64张图片
进程控制(退出&等待&程序替换&简单shell的实现)_第65张图片
为什么会有这么多的接口?
是为了满足不同的应用场景的,但是万变不离其宗
进程控制(退出&等待&程序替换&简单shell的实现)_第66张图片

④制作一个简单的shell

什么是解释器?
解释器的本质不就是给你一个命令窗口,然后不断的获取你的输入吗?

如何获得你的输入?
进程控制(退出&等待&程序替换&简单shell的实现)_第67张图片
C语言中的strtok()?
进程控制(退出&等待&程序替换&简单shell的实现)_第68张图片
我们的“破烂版”mini_shell

    1 #include
    2 #include
    3 #include
    4 #include
    5 #include
    6 
    7 #define NUM 128
    8 #define CMD_NUM 64//最多可以定义64个字符
    9 
   10 int main()
   11 {
   12   char command[NUM];
   13   for(;;)
   14   {
   15     //步骤一:打印提示符
   16     char *argv[CMD_NUM] = {NULL};//这是一个指针数组,可以用来拆开 ls -l -a 的每个选项
   17     command[0]=0;//这种方式可以做到O(1)的复杂度清空字符串                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         
   18     printf("[super_zkx@myhostname minishell]#");
   19     fflush(stdout);
   20 
   21     //步骤二:获取字符串
   22     fgets(command,NUM,stdin);
   23     //输入的时候会多一个回车
   24     command[strlen(command)-1] ='\0';//"ls\n\0" "\n"算的是一个字符,strlen算出来是3 - 1 = 2  \n对应下标刚好是2 
   25 
   26     //步骤三:解析字符串
   27    // printf("echo: %s\n",command);
   28     const char* sep = " ";
   29     argv[0] = strtok(command,sep);//传入字符串,以sep为分隔符
   30     int i = 1;
W> 31     while(argv[i] = strtok(NULL,sep))
   32     {
   33       i++;
   34     }
   35 
   36     //步骤四:检测命令是否需要shell本身执行的内建命令
   37     if(strcmp("cd",argv[0]) == 0){
   38       if(argv[1] != NULL) chdir(argv[1]);
   39       continue;
   40     }
   41 
   42 
   43     //步骤五:执行第三方命令
   44     if(fork() == 0)
   45     {
   46       //child
   47       execvp(argv[0],argv);
   48       //如果返回了就说明失败了,那么我们就终止进程
   49       exit(1);
   50     }
   51 
   52     int status = 0;
   53     waitpid(-1,&status,0);
   54     printf("exit code: %d\n",(status >> 8)&0xFF);
   55 //  while()
   56 //   {
   57 //     //如果是老串的话,第一个变量就传NULL
   58 //     argv[i] = strtok(NULL,sep);
   59 //     if(argv[i] == NULL)
   60 //       break;
   61 //     i++;
   62 //   }
   63 //    for(i = 0; argv[i];i++)
   64 //    {
   65 //      printf("argv[%d]:%s\n",i,argv[i]);
   66 //    }
   67 
   68   }
   69   return 0;
   70 }
 

你可能感兴趣的:(Linux操作系统,linux,centos)