目录
- 进程控制
- 进程终止
- 进程退出的方式
- 进程等待
- 进程等待的方法
- wait使用方法
- waitpid使用方法
- 进程程序替换
- 替换函数
- execl函数
- execv函数
- execlp函数
- execvp函数
- execle函数
- execve函数---->只有这一个是系统调用,其他都是库函数
- execvpe函数
补充几个知识:
- %s/被替换的文件名/替换文件名/g 就可以实现替换替换之后所有的test都被替换成了tt
- 如何在Makefile中一次形成两个可执行?
形成多个可执行同理,只需要把想生成的可执行跟在all后面即可
进程控制
一、fork函数初识(见之前)
-
写实拷贝
二、fork常规用法 -
一个父进程希望复制自己,使父子进程同时执行不同的代码段,例如:父进程来等待客户端的请求,生成子进程来处理请求
-
一个进程要执行不同的程序,例如:子进程从fork返回后,调用exec函数
三、fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程超过了限制
创建进程是需要很大成本的
进程终止
一、进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果错误
- 代码异常终止(一般程序崩溃后退出码也就没有意义了)
为什么main总会return 0?意义在哪?
- main函数的return值是进程的退出码
- 查看退出码:echo $?(输出最近一次进程退出时的退出码)
- 一般用0表示:success
- !0:failed(错误)之所以用!0表示,是因为相比于程序错误,我们更想知道的是为什么程序会错误,所以我们可以通过结束时的退出码,来查看进程退出的原因,所以可以用具体的数字来代表一种退出原因
- strerror() :可以查看退出码所对应的内容头文件string.h
进程退出的方式
-
main函数return,代表进程退出,非main函数return叫做函数返回
-
exit():进程终止,头文件stdlib,在任意地方调用都代表终止进程,参数是退出码
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main() { printf("hello word!"); sleep(4);//数据被暂存到输出缓冲区中 exit(EXIT_SUCCESS); //return 0; //都能够看到hello word被打印,说明刷新了缓冲区 //其原因就是main return or exit 本身就会要求系统,进行缓冲区刷新! }
-
_exit():终止进程,头文件unistd,强制终止进程,不要进行进程的后续收尾工作,比如刷新缓冲区(指的是用户级缓冲区)
进程退出,OS层面做了什么?系统层面,少了一个进程:free PCB,free mm_struct, free页表和各种映射关系
进程等待
-
进程为什么要等待?
父进程fork之后,可能有这样一种情况:父进程需要子进程完成某种任务,这样父进程就需要知道子进程完成的情况,所以一般需要父进程通过wait/waitpid等待子进程退出,这种现象就叫做进程等待
-
为什么要父进程等待?
- 通过获取子进程退出信息,能够得知子进程执行结果
- 可以保证:时序问题,子进程先退出,父进程后退出
- 进程退出的时候,会先进入僵尸状态,会造成内存泄露的问题,需要通过父进程wait,释放该子进程占用的资源
进程等待的方法
wait使用方法
-
-
返回值: 成功返回被等待进程pid,失败返回-1。
-
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
-
例子:
可以观察到一开始有两个进程同时运行,之后有一个进程变成了僵尸进程,又过了一段时间这个僵尸进程也结束了,这个就可以说明首先wait是可以回收僵尸进程的,同时wait的返回值如果正常返回就是等待那个进程的pid,如果异常返回就是返回-1
waitpid使用方法
-
头文件与wait相同:
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非零,提取子进程退出码。(查看进程的退出码)
-
例子:(一定要让父进程通过status得到进程执行的结果)
这里的status就不是简单的一个整型了,我们知道它有32个比特位,但是判断代码运行结果:只使用低16个比特位!高16比特位暂时不考虑(如果没有收到信号代表代码是运行完成的,没有异常终止);次低8位代表的就是进程退出的退出码,前7位就是进程的终止信号,如果程序没有异常终止那么就为0 -
用status来获取进程的退出码或者退出信号
下面这个例子是程序被异常终止,也就是接受到了信号 -
bash是命令行启动的所有进程的父进程,它一定是通过wait的方式得到子进程的退出结果,所以我们能看到echo $?能够查到子进程的退出码
-
操作系统给我们提供了两个宏,可以就是让我们每次查看退出码的时候都进行位操作,一个是WIFEXITED如果没有收到退出信号就为真,就可以用WEXITSTATUS来获取退出码
-
-
options:
- options的值为0:默认行为,阻塞等待;设置为WNOHANG:设置等待方式为非阻塞
- 阻塞的本质:其实就是进程的PCB被放入了等待队列,并将进程的状态改为S状态
- 返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度
- 非阻塞:调度一个接口cpu可以正常返回,cpu不断重复调度父进程就是不断重复waitpid的过程
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的PID。
-
进程程序替换
进程不变,仅仅替换当前进程的代码和数据的技术叫做进程的程序替换
-
为什么要进行程序替换?
想让子进程执行一个全新的程序
-
什么是程序替换?(原理?)
用新进程的代码和数据替换掉原来的代码和数据,其他都不变。
程序替换的本质:就是把程序的进程代码和数据加载到特定的进程上下文中(加载就需要加载器,它的底层原理可以理解成exec*程序替换函数(*可以理解成系列),进程程序替换会更改代码区的代码,这也会发生写时拷贝
-
进程的程序替换使用----怎么办?----阶段一
- 现象:就是替换之后后续的代码不会执行,如果是子进程发生了程序替换,那么只会改变子程序的代码和数据不会影响父进程
- exec* 如果有返回值那么就是调用失败
- 现象:就是替换之后后续的代码不会执行,如果是子进程发生了程序替换,那么只会改变子程序的代码和数据不会影响父进程
-
阶段二
各个程序替换函数的基本使用
替换函数
命名理解
- l(list):表示参数采用列表形式
- v(vector):参数用数组
- p(path):有p自动搜索环境变量PATH
- e(env):表示自己维护环境变量(通俗点就是可以自定义环境变量,具体使用见execve)
execl函数
int execl(count char*path,count char *arg,...);
//path 就是要执行的文件全路径一定要是路径+文件名
// ... 意思是可变参数列表,要执行的目标程序在命令行上怎么执行这里就怎么一个一个传递进去()
//必须以NULL作为传入的参数的结束
execv函数
int execv(const char* path,char *const argv[]);
//第一个参数还是传全路径
//第二个参数传的是一个数组,在命令行上怎么执行就将其存在数组中,然后传进去,要以NULL结尾
与execl除了传参的不同,其他没有区别
execlp函数
int execlp(const char* file,const char* arg,...);
//第一个参数是文件名,需要在环境变量当中才行,会自动去找寻地址
//后面的参数跟execl相同
execvp函数
int execvp(char* file,char *const argv[]);
//不用带路径会自动去环境变量中找,且通过数组传参
execle函数
int execle(const char* path,const char *arg,...,char *const envp[]);
execve函数---->只有这一个是系统调用,其他都是库函数
int execve(const char* path,char *const argv[],char *const envp[]);
用法同execle,唯一的区别就是它传的是数组
execvpe函数
int execvpe(const char *file, char *const argv[],char *const envp[]);
跟上面的没有什么区别就是多了个p,可以参照之前的来看
为什么会有这么多接口?
是为了满足不同的应用场景