目录
status是一个输出型参数。我如何通过给一个函数传参拿到函数内部的值?
无论是阻塞还是非阻塞都是等待的一种方式。需要有两个要素,谁等?等谁,等什么?
一、进程创建
fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
创建进程是有成本的:体现在时间+空间。
fork函数返回值
- 子进程返回0,
- 父进程返回的是子进程的pid
写时拷贝
![](https://img-blog.csdnimg.cn/f8cb6f89860b466fbbf00f913fd08451.png)
二、进程终止
进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
为什么main函数最终会return 0?意义在哪里呢?
return值是进程退出码,衡量进程退出情况,实际上通过一个进程在return的时候根据return值判断进程执行的结果,本质是通过退出码衡量进程执行的对或者不对,一般用0代表success !0表示failed。main函数开始return就代表代码已经执行完了,结果对或者不对我们用0或者非0表示。
echo $? :输出最近一次进程退出时的退出码
代码跑完如果结果不正确为什么用非0表示?
很显然非0值有很多,当代码跑完结果不正确我们更想知道的是为什么不正确。为什么不正确一定是有多种可能的,就可以用具体的一种数字代表一种可能性。这个值就是错误的退出码,所以这个值不能随便编写,要根据具体的原因来写,退出码1,2,3,4...都对应一种退出结果不正确的可能性。比如你考试的时候,你考好了,你爸是不会问你原因的,你考差了你爸会问你为什么你考的不好的原因。原因1:你感冒了 原因2:你没睡好.....因为你的结果不符合预期,所以你爸才问你 为什么。程序就相当于你的儿子,当程序跑完后结果不正确,你应该关心的是为什么不正确,所以程序为了给你一些基本答案,就会在main函数当中通过返回不同的错误数字代表不同的可能性。当你说1,2,3的时候你爸就以编号对应上错误的原因。
我们的程序就以非0值对应上错误原因的错误码字符串。实际上在C语言中,系统已将提供好了各种错误码对应的错误原因。
用到这样一个接口,strerror
#include<stdio.h> #include<string.h> int main() { for(int i=0;i<140;i++) { printf("%d: %s\n",i,strerror(i)); } }
运行结果如下:
这是C基于系统提供的一些错误列表,linux中有多达133中,vs下只有30多种。当然你自己也可以设计一张,用字符指针数组。
代码异常终止
当我们假如一个除0操作时,代码就会异常退出,这就叫做你的程序跑了一半就被终止了。vs中就叫做程序崩溃。它的错误编号是136在系统中也是未知的,所以在程序崩溃的时候退出码也变的没有意义了,就好比你的程序崩溃,main函数return 的特定值也不会进行执行,所以最终退出码是多少已经不用关心了。就好比你考试时作弊被抓了,之后你的成绩也就没有意义了。
进程退出的方式
- 1. 从main返回
- 2. 调用exit
- 3. _exit
- ctrl + c,信号终止
1.return
main函数return,代表进程退出!非main函数呢?
可见尽管fun函数return了但是还会继续执行下面的代码,这就叫做函数调用,所以非main函数进行return代表的叫做函数返回,main函数返回就叫做进程退出。
2.exit
exit调用,实现进程退出
![]()
exit在任意地方调用,都代表终止进程,参数是退出码!
eg2:这段代码如果带了\n,就会立马把我们打印的消息立即刷新出来,不带\n这条消息会保存在缓冲区中,不会立即刷新出来。只有程序退出时,刷新缓冲区才会把我们的消息显示出来。
我们用exit(EXIT_SUCCESS)代替return 0;也是一样的效果, EXIT_SUCCESS是个宏值也就是0
没带\n,此时的数据暂时被保存到了输出缓冲区中,所以在sleep期间并不会显示出来,exit或者是main函数return,本身就会要求系统进行缓冲区刷新。return 0或者exit除了保证进程退出之外,还能帮助刷新缓冲区。
3. _exit
_exit终止进程,强制终止进程,不要进行进程的后序收尾工作,比如刷新缓冲区。这里的缓冲区是用户级缓冲区。
最常用的还是前两种。
exit与_exit的异同
- 1. 执行用户通过 atexit或on_exit定义的清理函数。
- 2. 关闭所有打开的流,所有的缓存数据均被写入
- 3. 调用_exit
进程退出,OS层面做了什么?
系统层面少了一个进程:释放 PCB,释放 mm_struct,释放页表和各种映射关系,代码和数据申请的空间也要给释放掉 。
三、进程等待
进程等待是什么?
fork()之后子进程和父进程一起运行,但是不能确定谁先退出谁后退出的问题,子进程创建出来就是为了帮助父进程完成某种任务,父进程就需要某种方式知道子进程完成任务时完成的怎么样,让父进程fork()之后,需要通过wait/waitpid等待子进程退出。这种现象就叫做进程等待。
为什么要让父进程等待呢?
- 1.通过获取子进程退出的信息,得知子进程执行结果。
- 2.可以保证:时序问题,子进程先退出,父进程后退出。这样才能保证父进程获得子进程的信息。
- 3.进程退出的时候,会先进入僵尸状态,如果父进程不等,会造成内存泄露的问题,需要通过父进程wait释放子进程占用的资源。
如何进行进程等待,从而解决僵尸问题呢?
(1)wait等待方法
#include<sys/types.h>#include<sys/wait.h>pid_t wait(int*status);返回值:成功返回被等待进程 pid ,失败返回 -1 。参数:输出型参数,获取子进程退出状态 , 不关心则可以设置成为 NULL
以下面这段代码展开讨论
因为父子进程后序代码都是共享的,我不想让子进程执行完之后if判断之后,继续向下执行父进程的代码。所以可以exit终止进程,所以子进程只会执行if区间里的代码。
如果只是这样,子进程跑5秒钟,父进程从第一行直接跑到23行,直接就退出来,此时子进程要执行5秒钟,但是父进程一瞬间就结束啦,所以子进程就立即会变成一个被OS领养的孤儿进程,但是我不想这么干,我想让我的父进程去等待子进程。也就是在这5秒期间,父进程就算啥也不干,也必须在这等着。
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//child
int cnt=5;
while(cnt)
{
printf("child[%d] is running : cnt is : %d\n",getpid(),cnt);
cnt--;
sleep(1);
}
exit(0);
}
sleep(10);
printf("father wait begin\n");
//parent
pid_t ret=wait(NULL);
if(ret>0)
{
printf("father wait:%d\n success",ret); //等待成功
}
else
{
printf("father wait faield!\n");
}
sleep(10);
}
子进程执行5秒后自己退出,父进程先不让它进行等待,因为等待的作用本身就有回收僵尸进程,所以让父进程sleep10秒,在这期间前5秒 子进程正常运行,后5秒子进程终止,终止的时候没有父进程读取他,此时子进程当前就属于Z状态。10秒后父进程就开始等了,等待完后,再让父进程sleep 10秒,也就是父进程把子进程回收完后自己再活上10秒,所以我们会看到进程由两个变一个,僵尸进程被回收后,就剩个父进程在运行了。
执行结果:
利用脚本命令监视进程:
while :; do ps axj | head -1 && ps ajx | grep myproc | grep -v grep; sleep 1; echo "###########################"; done
至此看到了一个完整是生命周期,所以wait可以做到回收子进程。
(2)waitpid等待方法
pid_ t waitpid(pid_t pid, int *status, int options);返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:pid :Pid=-1, 等待任一个子进程。与 wait 等效。Pid>0. 等待其进程 ID 与 pid 相等的子进程。status:WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)options:WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID 。
使用的注意事项:
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
//pid_t ret=wait(NULL);
//pid_t ret=waitpid(id,NULL,0);//等待指定为id的子进程。
//pid_t ret=waitpid(-1,NULL,0);//设置成-1代表等待任意一个子进程,可能我们有10个子进程,这里只等待任意一个,等价于wait
这里我们用waitpid替换wait结果都是不变的。
status是一个输出型参数。我如何通过给一个函数传参拿到函数内部的值?
函数参数必须是传指针或者引用,这里因为系统是C语言写的只能传指针。所以我们可以通过一个status获得退出结果。
以下面这份代码为例:
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//child
int cnt=3;
while(cnt)
{
printf("child[%d] is running : cnt is : %d\n",getpid(),cnt);
cnt--;
sleep(1);
}
exit(0);
}
printf("father wait begin\n");
//parent
int status=0;
pid_t ret=waitpid(id,&status,0);//
if(ret>0)
{
printf("father wait:%d success, status: %d\n",ret, status); //等待成功
}
else
{
printf("father wait faield!\n");
}
}
(1)当eixt退出码是0,执行结果如下 :
(2)当我们将子进程exit的退出码改为10后,执行结果如下:
如何理解status?
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
我们发现这里的status不是10,而成了2560,这里又该如何理解呢?
父进程拿到的status结果一定和子进程如何退出强相关!!!子进程退出就有三种情况,代码跑完结果对或不对,代码异常。所以一定要通过status帮我们反馈出子进程的这三种情况,所以status最终一定要让父进程通过status得到子进程执行的这三种结果。所以这里的整形status就不能简单的把它理解成一个整数。
status的构成
而这里的status的构成有如下的构成,这里是status构成一共是一个整数,32个比特位,我们为了反映出代码运行结果只使用低16个比特位,高16比特位不用。这里代码退出结果对或者不对是由退出码决定的,也就是return或者exit传入的参数。我可以通过进程的退出码查看对不对。
如何证明我的代码是跑完的呢?
如果一个进程出现异常终止,本质是这个进程因为异常问题,导致自己收到了某种信号!我们可以通过判定进程是否有信号来判定它是不是异常终止的。所以这里就有两组信息被我们所获取到,一个叫做退出码一个叫做进程是否收到信号, 如果没有收到信号就证明它的代码是正常跑完的,然后才关心退出码,否则不关心退出码。所以我们使用的status的低16位,其中次低8位叫做进程退出时的退出状态也就是退出码,其中低7个比特位代表进程终止时所对应收到的信号,如果是正常情况下大部分都是0.还有一个比特位叫做core dump。
所以我将来识别status的时候,如果低七位是0就代表没有收到信号,就证明程序是正确运行完的,结果正确不正确通过次低8位来获得。
如何得到status的次低8位和低7位呢?
次低8位: (status>>8) & 0xFF
解释:status右移8位后按位与上1111 1111.
低7位:status & 0x7F
解释:status按位与上111 1111;
eg1:
这说明当前进程是正常退出的没收到任何信号,只不过结果不正确,错误原因是因为11.
eg2:将exit的值改为0;
这就叫做代码跑完且结果正确。
eg3:对我们的进程发送一个2号信号
这就代表你的代码执行到一部分,还没跑完就直接异常了,因为你收到了2号信号。
eg4:这里我们故意写一个除0操作。
执行结果:
这也是代码异常退出,退出信号为8,代表浮点数错误。
如上就是父进程等待时获取子进程的退出结果。
bash是命令行启动的所有进程的父进程!bash一定是通过wait方式得到子进程的退出结果,所以我们能看到echo $?能够查到子进程的退出码。
获取status的次低8位和低7位的简单方法
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出,也就是查看进程的退出信号是否为0,为0则继续执行 )WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
实际在进行进程等待操作的时候每一次都要进行大量的位操作这样是不太好的,所以OS提供了一组不用进行为操作的宏
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//child
int cnt=3;
while(cnt)
{
printf("child[%d] is running : cnt is : %d\n",getpid(),cnt);
cnt--;
sleep(1);
}
//int a=10;
//a/=0;
exit(0);
}
printf("father wait begin\n");
//parent
int status=0;
pid_t ret=waitpid(id,&status,0);//
if(ret>0)
{
if(WIFEXITED(status)) //没有收到任何退出信号的
{
//正常结束的,获取相应的退出码
printf("exit code: %d\n", WEXITSTATUS(status));
}
else
{
printf("error, get a signal!\n");
}
}
}
同样可以实现之前的操作。
option参数
第三个参数optiton,这里的0代表默认行为,阻塞等待。就是我在调用wait是父进程什么也没有做,它只是卡在这里等待子进程退出。相当于父子进程都在运行,但是父进程啥也不干,就在看子进程什么时候退出,子进程不退出父进程不返回。这就叫做阻塞等待。我们可以将0改为WNOHANG设置等待方式为非阻塞。
什么是阻塞和非阻塞?
如果我在楼下等一个人,我给他带你电话说你好了没,他说他没好,你说没关系,电话别挂,我可以随时沟通获取你的状态,你不下来我不挂电话。这叫做阻塞状态。相当于调用电话,电话那头的资源没就绪,你自己没准备好,就不挂电话,我一直在电话这头等你。还有一种情况是:我给你打电话检测你的状态,你没好,直接把电话一挂,过一会再给你打一个,不断去给你打,不会因为你没好而让我卡在这里。这就是非阻塞。每一次都是一次非阻塞,通常完成一次等待过程可能需要多次检测我们把他叫做基于非阻塞等待的轮询方案。
无论是阻塞还是非阻塞都是等待的一种方式。需要有两个要素,谁等?等谁,等什么?
父进程在等,在等子进程,在等子进程退出。其中父子进程是一种角色,子进程退出时一种事件或者条件。
如果此时是父进程等子进程此时相当于是:在OS层面,有俩进程,子进程不退出,父进程不返回,直到子进程结束退出,父进程再返回,这种等待方式就叫做阻塞。
阻塞了是不是意味着父进程不被调度执行了?
实际上在阻塞的时候是没有跑父进程代码的,也就是父进程并不会被CPU运行,刚开始父进程是R状态,父进程等待的本质,就是将父进程的PCB链接到了等待队列中,将父进程的状态由R改为S,其中父进程就啥也不干了,代码也不执行也不被调度,就在等待队列中等待。等到OS识别到子进程结束,进而发现父进程是在等的,就把父进程从等待队列中拿到运行队列中,然后执行后序等待方式,来进行获取子进程的退出结果。
阻塞的本质其实是进程的PCB被放入到了等待队列,并将进程的状态改为S状态。返回的本质其实就是进程的PCB从等待队列拿到运行队列,从而被CPU调度,拿到子进程的退出结果。所以这就是为什么一旦阻塞等待的时候,上层应用是卡住不动,因为CPU不在调度它了,所以用户看来就是卡住了。
非阻塞父进程还会执行吗?
非阻塞就意味着我们调一个接口,调用完后立马返回,CPU可以正常去调用它,CPU可以不断重复去调度这个父进程,就是重复的去执行这个waitpid的过程,叫做不会被阻塞。
非阻塞等待
看到某些应用或者OS本身,卡住了长时间不动,称之为应用或程序hang住了。hang(悬挂)
WNOHAGN:非阻塞
- 1.子进程根本就没有退出。
- 2.子进程退出。此时你才可以真正读取它。调用waitpid可能调用成功或失败。
我们以以下代码为例:
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//child
int cnt=10;
while(cnt)
{
printf("child[%d] is running : cnt is : %d\n",getpid(),cnt);
cnt--;
sleep(1);
}
//int a=10;
//a/=0;
exit(0);
}
int status=0;
while(1) //基于阻塞的轮询方案
{
pid_t ret=waitpid(id,&status,WNOHANG);
if(ret==0)
{
//子进程没有退出,但是waitpid是等待成功的,需要父进程重复进行等待
printf("Do father things\n");
}
else if(ret > 0)
{
//子进程退出了,waitpid也成功了,获取到了对应的结果
printf("father wait:%d success, status exit code: %d. status exit signal:%d\n",ret, (status>>8)&0xFF, status&0x7F); //等待成功
break;
}
else
{
//等待失败
perror("waitpid");
break;
}
sleep(1);
}
ret等于0,就好比你给张三打电话问好了没,张三说对不起在等一等吧,此时你检测张三的状态成功了,此时你之后还要在进行打电话。
ret>0,就好比你给张三打电话问好了没,张三说好了,立马下来
ret<0,就好比你给张三打电话问好了没,张三直接将电话关机。
PS:这里可能存在一种情况,我们在等的时候子进程没有退出,但是wait成功了,所以我们需要重复进行等待。
四、进程的程序替换
程序替换是什么呢?
通过if,else让子进程执行父进程代码的一部分,如果我想让子进程执行一个“全新的程序”呢?也就意味着子进程不想执行父进程的一部分,就想执行一个全新的程序。
进程不变,仅仅替换当前进程的代码和数据的技术叫做,进程的程序替换。
程序替换的原理
程序本质就是一个文件,文件=程序代码+程序数据。一个进程执行的时候执行的是当前的代码和数据,如果想要这个进程执行全新的代码和数据时,就可以把这个文件的代码和数据加载到当前进程的代码和数据段,左侧空间是没有任何变化的,相当于用一个老的进程的壳子,去执行了一个新的程序的代码和数据,这就叫做进程的程序替换。进程在程序替换时,是没有创建任何新的进程。
如何将磁盘上的代码和数据加载到内存中呢?
利用execl
eg1:
让程序不再执行自己的代码,后面的haha没有被执行就是因为程序替换,代码和数据全被替换调了,所以后序的printf代码和数据不会被打印出来了,因为已被替换掉了。第一句话可以打印出来原因在于执行它的时候程序替换还没有被执行。
程序替换的本质就是把程序的进程代码+数据,加载到特定进程的上下文中。C/C++程序要运行必须得先加载到内存中!如何加载呢?通过加载器(类似exec系列的程序替换函数)。
磁盘上的文件是在磁盘上的,数据加载到内存一定是把磁盘上的数据加载到内存,一定会涉及到系统调用,一定是只能OS完成,只不过exec系列的函数给我们提供了方法。
程序替换会把当前执行execl函数的进程的所有进程代码和数据全替换调
eg:
为什么父进程没有受到影响呢?
因为进程具有独立性。所以进行子进程替换的时候是不会影响父进程的。父子代码不是共享的吗?所以子进程替换代码的时候父进程为啥没收到影响呢?原因就是:进程程序替换会更改代码区的代码,也要发生写时拷贝!
程序替换失败的改怎么办呢?
只要进程的程序替换成功,就不会执行后续代码,意味着exec系列的函数,成功的时候,不需要返回值检测,只要exec系列的函数返回了,就一定是因为调用失败了。
eg:这里随便填了个路径,这个路径下是没有ls命令的随意一定会程序替换失败
所以一般替换失败,直接终止进程。
总结:
为什么要进行程序替换?想让一个子进程执行一个全新的程序。
什么是程序替换?原理就是新进程的代码和数据替换调原进程的代码和数据。
程序替换函数的介绍
#include <unistd.h>`
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[]);
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
命名理解
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
execl
int execl(const char *path, const char *arg, ...);
path:你要执行的目标文件的全路径(所在路径+文件名)
arg:代表要执行的目标程序在命令行上怎么执行,这里参数就怎么一个一个的传递进去。
...:是可变参数列表,意味着传参的传递数量可变的若干参数,必须以NULL作为参数传递的结束。
eg:
这就叫做传递的若干命令行参数。
这个函数的底层一定是把这些命令行参数的若干参数组合成一个argv,去调用ls的main函数,第一个ls代表你要执行谁,第二个ls代表你想如何执行。
execv
int execv(const char *path, char *const argv[]);
1.execv和execl之间没有任何差别,只是在传参形式上有差别。
2.传可变参数列表和传数组本质是一样的。
3.execv执行时会把argv这个参数传给了ls的main函数。
执行结果:
execlp
int execlp(const char *file, const char *arg, ...);
传参是以列表方式,但是第一个参数是file,意味着执行特定程序时只要告诉我这个程序的名字就可以了,不用告诉我路径,因为我会自动搜索环境变量PATH去找到路径
执行结果:
execvp
int execvp(const char *file, char *const argv[]);
执行结果:
execle
int execle(const char *path, const char *arg, ...,char *const envp[]);
我不想用默认环境变量,我想自己传入或维护环境变量,传给envp这个指针数组。
首先我们需要明白如何形成两个可执行文件呢?
这样的方法显然是不行的,因为Makefile默认形成它一个个碰到的依赖文件,也就是myload.所以我们在这里搞一个伪目标all,它的依赖文件是myexe和myload,all不需要依赖方法。因为是伪目标,所以他总是被执行,因为没有依赖方法所以根本不会形成all。所以Makefile在执行的时候一定会先形成all,形成all的时候一定会形成myexe和myload,所以分别执行myexe和myload的执行方法。所以把他俩形成后也不会形成all,因为all没依赖方法,所以这样就可以实现多个可执行。
然后我们需要明白如何用exec去执行我自己的程序?
myexe程序
myload程序
execle的使用
myexe.c
myload.c
执行结果:
execve
myexe程序,这里打印下我们自己传入的环境变量
myload程序
执行结果:
exec函数执行其他语言的程序
以后我们用其他语言写的程序都可以用exec系列的函数去执行。
eg:我们写个python的程序,
在我们的myload.c借助execl执行python程序
也就是说我们可以借助exec随便调用其他程序。
注意:这里如果用execlp和execvp执行自己的可执行程序时,第一个参数也必须指明路径,除非你把你自己的可执行程序添加到你的环境变量中去。
为什么会有这么多接口呢?
原因就是为了满足不同的应用场景。
OS只给你提供了一个接口execve,剩下的都叫做库函数,是系统调用的简易封装。
所有的接口看起来时没有太大差别的,只有一个就是参数的不同。
总结:程序替换类似于加载器可以将磁盘程序加载到内存让指定进程去运行它,原理就是替换进程进程中地址空间所映射的代码和数据。有六种方法去执行
五、模拟实现shell
命令行上输入的命令不就是一些我们所看到的传入的字符串,我们可以将它拆出来,就是命令和选项,然后我们把参数和选项依次传给程序替换,就可以实现一个自己的shell。解释器的本质就相当于先给你输出一个提示符,然后不断获取你的输入。
(1) 打印提示符
printf不带\n,不会立即显示出来,所以我们利用fflush刷新缓冲区。
(2) 获取命令行字符串
(3) 解析命令字符串
按照空格为分隔符进行解析 利用strtok()函数
strtok()
第一个参数传要解析的字符串,第二个参数传分隔符,返回的就是截取的第一个子串。
第二次调用strtok()如果还想再次截取之前的字符串第一个参数就只能传NULL。
(4)执行第三方命令
接下来就要进行执行命令,执行命令可以替换我们自己的这些个命令呢?
不能,因为我们只有一个进程,一旦调用了exec函数就会把上面所有的代码全部替换,所以不能让当前进程全部替换,而应该创建个子进程,让子进程替换。
这时候我们自己的shell就完成了。 我们的shell是一个简陋的版本,这也就是shell的运行原理。shell是一个命令行解释器,它就是一个进程 。
将我们自己的mini_shell名字换成bash 就是一个进程了。这个进程在内部会做命令行解析获取用户输入,然后对命令行解析把我们的命令输入制作成argv的样子,然后在依次把他们用exec系列的函数替换,然后让fork创建子进程,让子进程去执行exec。我们实际上用的命令行就是这么干的。
我们自己的shell的功能展示:
模拟实现shell的改进
但是我们自己的shell并不完整
eg:该条命令直接报错
原因是当前程序不认识竖画线。因为我们当前的代码没有进行组合的一个设置。
我们发现我们在cd的时候路径并没有回退,这是为什么呢?
原因就是现阶段我们的执行原则是无脑fork的,也就是说执行cd..的是子进程!换句话说执行cd..回退的时候并非是你的shell,实际上我们想让父进程(bash)去执行cd.. ,这样的话我们才能进行切换,因为fork切换是子进程的,pwd打印的是当前进程所处的路径。
查看当前进程所有的属性信息
cwd表示当前我所处的工作路径,它就是在这里的,
所以你在这里执行cd..改变的是子进程的路径并非改变父进程路径,所以它没变,
凡是需要fork()执行的是第三方命令,所谓的第三方就是系统在/usr/bin/ 目录下的命令,他们是独立的程序,你自己写一个程序让他执行也可以,这就是第三方。
我们在执行像cd这样的命令,就叫做以内建命令的方式进行运行。所谓的内建命令就是不创建子进程,让我们的父进程shell自己执行,相当于调用了自己的一个函数。所以在OS中更改当前进程路径有一个调用接口chdir(),你想把你改成那个命令,你就直接把你要改的路径填进去就可以了。所以我们的shell少了一个基本的操作,叫做当我解析完比之后,需要检测命令是否是需要shell本身执行的内建命令。
更改之后,就可以实现cd ..
同时我们也可以明白为什么命令失败了,父进程可以拿到退出码,因为子进程退出结果我们是能wait到的。
实现shell的完整代码
#include<string.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#define NUM 128
#define CMD_NUM 64
int main()
{
char command[NUM]; //获取命令及命令行参数
for( ; ;)
{
char *argv[CMD_NUM]={NULL};
//1.打印提示符
command[0]=0; //用这种方式,可以做到O(1)的时间复杂度,清空字符串
printf("[who@myhostname mydir]# ");
fflush(stdout);
//2.获取命令字符串
fgets(command, NUM, stdin);
command[strlen(command)-1]='\0'; //清理调空格,strlen统计的是除了\0的字符长度,所以\n在倒数第二个位置
//printf("echo: %s\n",command);
//3.解析命令字符串,char *argv[] eg: ls -a -l -c\0
//按照空格为分隔符进行解析 利用strtok()
//strtok() //第一个参数
//传要解析的字符串,第二个参数传分隔符,返回的就是截取的第一个子串。
const char *sep=" ";
argv[0]=strtok(command, sep); //把第一个子串ls就提取出来了
int i=1;
while(argv[i] = strtok(NULL, sep)) //把解析出来的子串一次传给argv, 当它返回为NULL的时候结束
{
//argv[i]=strtok(NULL, sep ); //这里在第二次调用strtok()如果还想再次截取之前的字符串就传NULL,如果还是传之前的command是不对的
i++;
}
//打印测试下
// for(i=0; argv[i]; i++)
// {
// printf("argv[%d]: %s\n",i, argv[i]);
// }
//接下来就要进行执行命令,执行命令可以替换我们自己的这些个命令呢?不能,因为我们只有一个进程一旦调用了就会把上面所有的代码全部
//替换,所以不能让当前进程全部替换,而应该创建个子进程让子进程替换
//4. 检测命令是否是需要shell本身执行的,内建命令
if(strcmp(argv[0], "cd")==0)
{
if(argv[1] != NULL)
{
chdir(argv[1]);
continue; //一旦内建了就不用执行后续的代码了
}
}
//5.执行第三方命令
if(fork()==0)
{
//child
execvp(argv[0], argv);
exit(1);
}
int status=0;
waitpid(-1, &status, 0);
printf("exit code: %d\n", (status >> 8)&0xFF);
//这样的话如果我们自己执行错误的命令此时就可以看到退出码了。
}
}