文章目录
- 进程创建
- fork函数进一步探讨
- 写时拷贝
- 进程终止
- 进程退出场景
- 进程终止时,操作系统做了什么?
- 三大终止进程函数
- 进程等待(阻塞)
- 进程等待的必要性
- 进程等待的两种函数
- 获取子进程参数status
- 如何通过status获取子进程的退出码。
- 为什么不用全局变量记录子进程的退出码和退出信号?
- 崩溃的本质
- 操作系统如何杀掉该进程的呢?
- 使用WAITSTATUS和WIFEXITED宏来获取子进程的退出码
- 进程等待(非阻塞)
- 进程替换
- 替换原理
- 进程替换函数
- 写时拷贝与进程替换
- 如何让一个程序去执行另一个程序?
- Mafile多文件编译
- exec.e程序执行mycmd程序
- 进程函数大总结
- 制定一个简易shell
- 一:打印提示符
- 二:获取用户的键盘输入
- 三:对连续的字符串进行解析为多个子字符串
- 四:父进程创建子进程并执行进程等待
- 五:简易shell的目录执行问题——内置命令
- 六:给ls命令显现的文件名添加颜色
- 简易shell完整代码
进程创建
fork函数进一步探讨
通过前篇我们了解到fork函数创建子进程,会给子进程分配对应的数据结构,可是一般而言子进程没有加载的过程,也就是说子进程没有属于自己的代码和数据,所以,子进程只能“使用”父进程的代码和数据。
可是当子进程要修改数据时,父进程的数据也被修改了,这不符合进程的独立性。所以,父子进程的代码必须分离。
写时拷贝
可是,如果在创建进程的时候,子进程就对父进程的代码直接拷贝一份储存在物理内存中时我们会碰到一下问题。
1:我们可能拷贝到子进程根本不会执行的代码,即使执行了那也只是读取而不是写入。
2:提前拷贝一份代码,子进程也有可能并不会立马使用,这会降低内存的使用效率。
所以我们操作系统采取了写时拷贝技术,当父子进程都只是读取代码和数据时,父子进程享用同一份数据和代码,当子进程想要修改数据时,操作系统会重新拷贝一份需要的数据在物理内存中,并将数据的物理内存写入页表的右侧,从而构建页表映射关系。
写时拷贝的作用:
1:写时拷贝实际上可以看作是一种延时申请策略,它能够再cpu进行访问时才申请物理内存进行写入需要的数据和代码,进而提高了内存的利用率。
2:父子进程读取的代码共用,要修改的数据重新再拷贝一份,父子进程互不干扰,进而父子进程得以彻底分离,这也是进程独立性的体现。
进程终止
进程退出场景
1:代码运行完毕,结果正确。
2:代码运行完毕,结果不正确。
3:代码异常终止。
进程终止时,操作系统做了什么?
当进程运行完毕,代码运行结果正确与不正确时:
我们知道一个进程的创建,它的父进程就是当前bash。
运行hello运行程序:
当我们不知道自己的代码运行结果是否正确,我们可以通过对运行结果进行逻辑判断来确定函数代码的运行结果是否正确,进而得以设置main函数的返回值。
echo $? 查看最近一次一个进程执行完毕的退出码。
此时的进程退出码为零表明代码运行完毕,结果正确。
通过strerror函数各种退出码的含义:
**
测试:
一:当进程代码运行完毕时的退出码。
1:ls这个命令进程代码跑完了结果不正确和退出码表达的含义相同。:
2:又比如:当我打开一个文件失败的时候,对代码运行结果做出判断,如果打开一个文件失败,就确定了退出码。
文件打开失败,文件退出码正好是一,用户就可以通过退出码来知道进程运行错误的原因。这便是退出码的意义。
二:当进程代码没有跑完,程序崩溃时:
如下文,当我们访问野指针时:
程序打印出三条字符串,然后出现段错误。后面的代码并没有被cpu执行。
程序崩溃的时候,退出码无意义。一般而言程序崩溃了,return语句并没有被执行。
总结:
main函数的返回意义是返回给父进程,用来评判该进程的运行结果是否正确,可以忽略。
其中表示进程运行代码结果正确,非零表示进程代码运行结果错误,非零值有无数个,不同的非零值可以表示不同进程运行结果错误的原因,从而方便定位错误结果原因。
三大终止进程函数
当运行一个程序时:
运行效果如下:
当执行到return语句时便不会执行后面代码,退出码作为main函数的返回值,被父进程获取后,进而充当该进程的退出码,并且,只有main函数的return语句才是终止进程的。
exit 与return的区别:
相同点:exit 和 return 都可以终止进程。
不同点:
1: exit可以在任何地方调用都可终止该进程。
2: main函数在函数体调用代表调用结束,获取返回值,在main函数内代表终止该进程。
exit与_exit的区别:
exit会执行用户定义的清理函数,冲刷缓冲,关闭流等。
例如:
以下代码:
当exit退出后,会自动冲刷缓冲区。
exit会直接结束进程,不会管理缓冲区。
进程直接退出。
特殊说明:
exit函数为应用层,缓冲区由应用层维护._exit函数为系统接口函数,不支持维护缓冲区。
进程等待(阻塞)
进程等待的必要性
父进程需要通过子进程的退出码进行分析子进程完成的效果,然后通过进程等待处理。
进程等待作用:
1:父进程通过进程等待获取子进程的退出码,进而获取子进程的退出结果,根据退出结果来做出不同的处理。
2:回收僵尸进程的资源。
进程等待的两种函数
一:wait函数(int*status)
作用: 1:只要子进程变成僵尸进程,父进程就会立马回收。
2:wait成功时返回子进程的PID,失败了就会返回-1. 3:输出型参数,获取子进程的退出码,不关心可设置NULL
例如:
以下代码为:子进程打印五次后退出变成僵尸进程,父进程sleep7秒后,执行wait语句后执行wait语句将子进程回收。
总结:
父进程调用wait函数时处于阻塞状态,此时父进程可能就在等待非CPU资源的某个等待队列中。当子进程退出时,由操作系统将父进程唤醒后放到运行队列中执行,调用wait取得返回值后继续执行剩下的代码。
二:waitpid函数(pid_t pid,int * status,int options)
说明:
返回值:
1:如果成功则返回被等待进程的pid。
2:如果设置了选项WNOHANG,而调用中发现没有已退出的子进程收集,则返回零。
例如:
复用wait代码,将wait(NULL)换成 waitpid( id,NULL,0) 效果等效。
运行结果如下:
总结:
1:如果子进程已经退出变成僵尸进程时,调用wait/wailpid 函数时,wait/waitpid:函数会立刻返回,并且释放资源,获得子进程退出信息。
2:如果在任意时刻调用wait和waitpid函数,子进程存在并且正常运行,则父进程可能会一直处于阻塞状态。
3:如果存在该子进程,则会立即出错返回。
获取子进程参数status
说明:
status : 输出型参数,获取子进程的退出码。在使用时为指针类型,则变量status的地址就等于子进程退出码的地址,status相当于退出码的别名,NULL代表不关心status。
status的构成:
status并不是按照整数来整体使用的,而是按照比特位的打方式,将32个比特位进行化分,我们只学习低16位。
如何通过status获取子进程的退出码。
当我们将status向右移动8位,即左边通过符号数补1,0xFF = 0000 0000 1111 1111 相&就取得了目标子进程的退出码。
运行结果如下:
此时的status输出参数正好也为1,符合预期。
为什么不用全局变量记录子进程的退出码和退出信号?
因为去安居变量是用户层面上的,父进程读取时会发生写时拷贝,有可能不会拷贝这些全局变量数据。
总结:
status能够拿到子进程的推出啊,父进程通过子进程的退出码>0或者==0判断子进程是否运行成功,当子进程运行结果错误时,父进程又能够通过退出码查找对应错误的原因,从而告知用户。
崩溃的本质
本质:
操作系统杀掉了该进程。
操作系统如何杀掉该进程的呢?
本质上是通过信号的方式。
当被信号所杀时,status的最低7个比特位就储存着子进程收到的终止信号。
获取kull信号编号:
status&0xff
运行结果如下:
可见子进程kill编号为零,代表子进程正常退出。
当我们选择让子进程崩溃时:
运行结果如下:
此时该进程崩溃了,此时的退出码也毫无意义。
使用WAITSTATUS和WIFEXITED宏来获取子进程的退出码
WIFEXITED:如果子进程为正常终止,则为真,否则为假。 (查看进程是否正常退出。)
WEXITEDSTATUS:如果WIFEXITED为非零,提取子进程退出码。 (查看进程的退出码)
此时如果使用kill - 9 命令去终止子进程时,通过脚本命令
while :; do ps axj | head -1 && ps axj | grep yzh | grep -v grep;sleep 1;done
可见如果子进程异常退出,那么它的退出码为零。(其实进程异常退出时的退出码也没什么意义)。
进程等待(非阻塞)
我们知道在waitpid( pid _t pid , int* status ,int options)中。
options: 默认为零,代表阻塞等待,WNOHANG宏默认为1,代表非阻塞等待。
阻塞等待:表明在父进程阻塞在队列中,等待子进程退出。
非阻塞等待:如果父进程检测子进程的退出状态,如果子进程还没有退出,则调用waitpid接口,立马返回,返回值为零。
测试:
我们让子进程执行5秒后退出,父进程时使用非阻塞等待子进程。
运行结果如下:
当子进程未退出时,即父进程未获取子进程的相关退出信息时,父进程会采用轮询检测方案,不断获取子进程获取信息,如果子进程依旧不退出,则父进程会以旧循环执行wait以后的代码,直到子进程退出。
进程替换
替换原理
我们当父进程用fork()函数创建子进程后,执行的适合父进程相同的代码和数据(也有可能执行不同的代码分支),如果我们想让子进程执行不同的程序,子进程往往要调用一种exec函数来执行另外一个程序。当进程调用一种exec等函数时,该进程的用户空间代码和数据完全被新程序所替换(此时旧的物理内存可能被释放),并从页表中重新构建映射关系,从新程序重新开始启动。调用exec中并不会创建新的内核数据结构,所以调用exec前后该进程的pid并未改变。
主要过程简图如下:
进程替换函数
一:execl函数
path: 找到程序: 路径 + 目标文件名。
char*arg,… :可变参数列表 :
可以传入多个不定个数的参数,这些字符串类型的参数一个个传入到可执行程序中,且最后一个参数必须以NULL结尾,表示参数传递完毕。
例如:
使用excel函数进行进程替换。
运行结果如下:
执行出一个上一个程序的printf。但是并没有执行出第二个printf。execl成功执行新的程序。
如果输入一个打不存在的命令地址,则进程替换失败:
运行结果如下:
execl进程替换失败后的返回值为1。
总结:
1:execl是程序替换,调用该函数成功之后,会将当前进程的所有代码和数据都进行替换,上一个程序的后续代码,全都不会执行。
2:不用对替换成功进行判断,因为替换成功后execl本身,包括他对应的返回值全部被新的程序替换了,并对这个新程序重新开始执行,所以exec函数调用成功后看起来是没有返回值的。只要执行了exit(1)就说明调用失败。
二:execv函数
char* argv[] :
指向一个个字符串的指针数组,将命令行参数的地址一个个传入到指针数组中,最后以NULL结尾,再将数组地址传入到接口之中。
例如:
我们复用一下execl测试代码:
运行结果如下:
execv与execl函数只有传参形式不同,本质上是相同的。
三:execlp函数
我们能通过环境变量PATH中,不带路径直接给程序名就能找到程序。
所以进程替换中,带p的含义指:操作系统会自己在环境变量PATH中进行查找相符合的搜索路径,我们不用告诉操作系统的程序在哪里,只需要告诉文件名。execlp函数中的其他函数就表示可执行程序传递给main函数命令行参数来实现目标程序的不同子功能。
**简单来说:**第一个参数决定用户执行谁,第二个参数决定了用户要如何去执行这个程序。
例如:
复用以上代码
运行结果如下;
我们所传的命令行参数会以字符串的形式传递给可执行程序的mian函数中的环境变量。
总结:
execlp()函数会从PATH 环境变量所指的目录中查找符合参数file的文件名, 找到后便执行该文件, 然后将第二个以后的参数当做该文件的argv[0]、argv[1]……, 最后一个参数必须用空指针(NULL)作结束
四:execvp函数
示例:
复用execv函数代码:
运行结果如下:
execv 和execvp两个函数传参形式是相同的,不同的是:一个通过程序地址查找程序,一个通过环境变量(只需要填写文件名)查找程序。
五:execle函数
execle函数相比较execl函数多了一个e,就说明其多了一个参数环境变量,这里我们会在两个程序调用中用到并具体讲解。
写时拷贝与进程替换
我们知道,加载新程序之前,父子进程的代码共享,数据写时拷贝,那么当子进程加载新程序的时候,也是一种“写入”,代码要进行写时拷贝,将新程序的代码和数据拷贝到物理内存中,这样父子进程代码和数据就彻底分开了,它们之间互不影响,更加体现了进程的独立性。
如何让一个程序去执行另一个程序?
Mafile多文件编译
当我们使用Makefile编译多文件时,我们可以使用ALL命令来指定需要生成的目标文件。通常我们也需要给ALL命令设置伪目标。
例如;
exec.e程序执行mycmd程序
我们只需要使用execle函数将mycmd程序的地址传入子进程就可以通过进程替换执行到这个程序。
并且此时我们也可以通过execle函数让子进程继承父进程的环境变量给新的程序。
一: exec.c程序:
2:mycmd程序:
根据子进程传递的命令行参数来实现mycmd程序的不同子功能。
并且调用getenv函数查看由子进程继承自父进程的环境变量。
使用make指令同时编译两个文件。
运行结果如下:
1:成功实现mycmd程序的输出a的功能。
2:输出环境变量yzh。
加粗样式总结:
环境变量是可以被子进程几次横的的,当子进程待用execle函数时,父进程的环境变量env也可以被子进程继承,继而在进程替换时传递给新的程序。
进程函数大总结
对于所有关于进程替换的函数中,多了字母便有了命令行参数传递的形式与功能不同。
1:带v将命令行参数以指针数组的方式传递,最后只用传入这个指针数组的地址。
2:带e的函数指可以将父进程的环境变量传给子进程。
3:带p的指不用传递目标程序地址,通过环境变量找到目标程序。
制定一个简易shell
原理:
通过让子进程执行传递参数执行程序,父进程等待&&解析命令。
创建子进程的原因:
因为进程的独立性,子进程出错而不会影响父进程的继续运行=。
一:打印提示符
打印时不能使用\n,应为不符合shell打印标准,但是如果步使用\n的话,打印的数据会储存在缓冲区中,所以我们可以尝试使用fflush函数快速将缓冲区里面的数据显现。
二:获取用户的键盘输入
1:创建一个数组作为缓冲区。
2:如果输入错误,那么则要终止次循环,重新输入命令行参数。
但是当我们输入命令行参数时,我们会将\n输入进去,此时命令行参数字符串便多了’\n",这样会造成程序错误。
对此我们可以再下一步步骤之前将\n去掉。可以访问该\n将它设为’\n’.
三:对连续的字符串进行解析为多个子字符串
我们可以尝试使用strtok函数,找到分割符" ",并用’\0’分开,有字符串分割时返回该子字符串的首地址,没有字符串分割时即返回NULL。
但是使用第二次时记得将目标字符串地址设为NULL。
四:父进程创建子进程并执行进程等待
父进程fork()创建子进程,并让子进程执行进程替换的新程序,让父进程等待子进程,并获取退出码。
五:简易shell的目录执行问题——内置命令
此时,我们便可以执行简单的shell命令了,但是当我们执行cd…关于目录问题时,我们发现当前进程路径却并没有改变!
原因:
当我们执行命令行参数cd…时,其实是让子进程通过进程替换执行,此时cd…便是让子进程回到上一层目录,但是子进程执行完便退出了,此时父进程的路径并没有发生改变,所以根本毫无意义。
所以如果是有关cd命令,我们不能去让子进程去执行,而是让父进程去执行该命令,我们也称之为——内置命令。
所以:我们可以在解析完参数命令时便检查第一个参数是否cd命令,如果是则通过chdir函数去执行。
六:给ls命令显现的文件名添加颜色
我们知道Linux中ls命令,命令行参数中其实包含了"–color == auto",其中命令行参数第一个位置被其本身占据了,所以我们可以在完全解析命令行参数字符串前便将"–color ==auto" 放入命令行参数数组第二个位置中。