目录
前言
本篇是我们的进程控制相关内容专题,等我们学完了这些进程控制内容再结合前面的内容,我们就可以制作一个简单的命令行解释器啦!
fork函数
先来了解一个函数fork(),其实我们前面也有用过,我们在这就深入去了解一下
是在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程
进程调⽤fork,当控制转移到内核中的fork代码后,内核做:
• 分配新的内存块和内核数据结构给⼦进程
• 将⽗进程部分数据结构内容拷⻉⾄⼦进程
• 添加⼦进程到系统进程列表当中
• fork返回,开始调度器调度
1.进程创建:fork返回值问题
fork函数返回值
• ⼦进程返回0
• ⽗进程返回的是⼦进程的pid
1.如何理解fork函数有两个返回值?
fork函数在库中实现的主要步骤:
a.创建子进程的PCB 赋值
b.创建子进程的地址空间 赋值
c.创建并设置页表
d.子进程放入进程list
e. ......
return pid;
当一个函数准备return时,它内部的核心代码已经执行完了,说明子进程早已经被创建好了,并且可能在操作系统的运行队列中准备被调度了;所以在内部没返回时就已经有两个执行流了,父子进程共享代码,return pid就被共享了,所以会有两个返回值(父进程和子进程各自执行return)
2.如何理解fork返回之后,给父进程返回子进程pid,给子进程返回0?
答:因为子进程的父进程只有一个,不需要知道父进程的id,而父进程的子进程可能有多个,所以需要知道子进程的id
3.如何理解同一个id值,怎么可能会保存两个不同的值,让if else if同时执行
答:pid_r id = fork();返回的本质就是写入,所以谁先返回,谁就先写入id,而后来的进程因为进程具有独立性,会触发写时拷贝,所以同一个id,地址是一样的,但是内容却不一样
再次认识写时拷贝:通常,父子进程代码共享,父子进程再不写入时数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本
fork常规用法:
1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
2.一个进程要执行一个不同的程序,例如子进程从fork返回后,调用exec函数
fork调用失败的原因:
1.系统中有太多的进程
2.实际用户的进程数超过了限制
2.进程终止
./mytest —— 运行一个进程
echo $? —— $?:永远记录最近一个进程在命令行中执行完毕时对应得退出码
(main -> return ? ;)
写代码是为了完成某件事情,我如何得知我的任务跑的如何呢?
答:通过进程退出码来判断
进程退出时,对应的退出码来标定进程执行的结果是否正确
退出码:
1.意义:0:success,!0:标识失败,!0具体是几,来标识不同的错误
但是数字对人不友好,对计算机友好,我们没法单通过数字来判断到底问题出在哪里,所以一般而言,退出码都必须要有对应的文字描述:
1.可以自定义
2.可以使用系统的映射关系(不常用)
系统给的部分错误码含义
2.如何设定main函数返回值呢?如果不关心进程退出码,return 0就行(一般用0表示成功,用非0表示错误),如果未来我们是要关心进程退出码得时候,要返回特定的数据表明特定的错误
进程退出的情况
1.代码跑完了,结果正确 ——return 0;
2.代码跑完了,结果不正确 ——return !0; 退出码在这个时候起效果
3.代码没跑完,程序异常,退出码无意义
正常进程如何退出:
a.main函数return返回
b.任意地方调用 exit(code) C语言库函数,在系统调用接口之上
c. _exit();——了解 系统调用
exit终止进程,主动刷新缓冲区;_exit终止进程,不会刷新缓冲区;缓冲区位于用户级
都是终止当前进程
3.进程等待
前面我们在进程状态那里知道了Z僵尸状态是一个问题,需要我们通过进程等待的方式来解决该问题
进程等待的必要性:
需要回收子进程资源,获取子进程退出信息(为什么要进程等待的原因)
进程等待相关头文件
#include<sys/types.h>
#include<sys/wait.h>
用wait(进程等待函数)回收子进程资源:
用waitpid通过等待拿到子进程的退出结果,其中pid为对应子进程的pid,status获取对应子进程退出时的退出结果(注意:这里的status不是当成整型来看待,而是有自己的位图结构,来设置不同的值,我们是想通过status知道进程退出时是哪一种情况),option传入0表示阻塞时等待
• 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息
• 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
• 如果不存在该子进程,则立即出错返回
关于waitpid的第二个参数status:
• wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
• 如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。
• 否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。
• status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16⽐特位)
[^] 退出状态来表示结果是否正确,终止信号表示是否正常退出
子进程正常退出:
子进程异常退出
让子进程发生除0错误
此时拿到的信号是8号(8号代表浮点数错误,因为除0了)
等待的本质:检测子进程退出信息(在子进程的pcb中),并将子进程退出信息通过status拿回父进程的上下文中
再谈进程退出:
1.进程退出会变成僵尸进程——会把自己的退出结果写入到自己的task_struct中
2.wait/waitpid是一个系统调用,表示操作系统也有资格、能力去读取子进程的task_struct
综上,wait/waitpid是通过操作系统从退出子进程的task_struct中获取退出码(信息)的
阻塞和非阻塞:
非阻塞轮旋等待——父进程一直在询问子进程退出没有
非阻塞有什么好处?
答:不会占用父进程的所有精力,可以在轮询期间干干别的事情
4.进程程序替换(重要)
1.创建子进程的目的
a.想让子进程执行父进程代码的一部分(执行父进程对应磁盘代码的一部分)
b.想让子进程执行一个全新的程序(让子进程想办法加载到磁盘上指定的程序,执行新程序的代码和数据)—— 进程的程序替换
替换原理:⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变
替换函数:
int execl(const char* path,const char* arg,...); ——将指定的程序加载到内存中,让指定进程执行;第一个参数是用于找到该程序,第二个参数是表示如何执行(你在命令行中怎么执行,就怎么传参),第三个...为可变参数列表,可传多种参数
执行ls
可以让我们的程序去用c把别人的程序调用起来——程序替换
2.进程程序替换的原理
a.程序替换的本质,就是将指定的代码和数据加载到指定的位置(覆盖自己的代码和数据)
b.进程替换的时候没有创建新的进程
所以在exec*类函数成功执行后,后面的代码已经被覆盖了,不会执行
exec*类函数成功执行返回不会有返回值(不需要,因为成功执行就代表和接下来的原来的代码无关了,判断返回值没有意义),执行失败会返回-1,只要返回,一定是错误了
多进程时:
[^] 在程序替换时,代码也可能发生写时拷贝
3. 其他的exec*函数
前面的execl函数中l的意思是list:将参数一个一个的传入exec*
1.
p:path:如何找到程序的功能;带p字符的函数,不需要告诉我程序的路径,只需要告诉我是谁,我会自动在环境变量PATH,进行可执行程序的查找
[^] 两个ls并不重复,第一个ls是告诉系统我要执行谁,第二个ls是告诉系统我想怎么执行
2.
v:vector:可以将所有的执行参数放入数组中,统一传递,而不用进行使用可变参数方案
前两个结合一下就是3:
可以使用程序替换,调用任意后端语言对应的可执行程序
4.
e:自定义环境变量
是先执行main函数,还是先加载exec*函数?
由于我们需要先把程序加载到内存中,所以理应是exec*函数先加载,main函数的参数数据来源就是从exec *函数中来的
1、2、4结合起来就是5:
上面都是基于系统调用做的封装,为了让我们有更多的选择性
真正的执行程序替换的系统调用接口为:
5.手撕简单命令行解释器
相关源代码可看:myshell
这里会有一个问题,就是运行之后,如果我们改我们的所处路径之后,输入pwd,显示的还是我们命令行解释器所处的路径:
要弄清这个问题,我们先需要彻底理解到底什么是当前路径
当前路径指的是当前进程的工作目录
可以更改工作目录:
如何修改:
所以可以解释为什么我们自己写的shell,cd的时候,路径没有变化
答:是因为在fork()之后,是子进程执行的cd,子进程也有自己的工作目录,cd其实更改的是子进程的目录,而子进程在执行cd完毕就没有了,继续用pwd命令时用的是父进程(即是shell),父进程的工作目录可没有变
那么我们上面的命令行解释器可以在读取输入的命令后加上:
那么就可以成功使用cd命令,pwd指向的是cd之后的路径了
像这种不需要让我们子进程来执行,而是让shell自己来执行的命令叫 ——内建/内置命令
结尾:
以上就是我们进程控制专题的相关内容啦,怎么样,是不是内容还是很多的,没关系,慢慢消化就行啦( •̀ ω •́ )