进程控制的学习
目录
1.进程创建
1.1 fork函数
1.2 fork函数返回值
1.3 写时拷贝
1.4 fork 常规用法
1.5 fork 调用失败的原因
2. 进程终止
2.1 进程退出场景
2.2 进程常见退出方法
2.2.1 从main 返回
2.2.2 echo $? 查看进程退出码
2.2.2.1 我们如何得到退出码代表的含义
2.2.2.1 可以按照我们的要求去定制我们的退出结果
2.2.2.2 一旦进程出现异常,进程的退出码就没有意义了
2.2.3 exit 函数
2.2.4 _exit 函数
exit(status) VS _exit(status)
2.2.5 return退出
3. 进程等待
3.1 进程等待的必要性(为什么?)
3.2 进程等待是什么?
3.3 进程等待的代码
3.3.1 wait
3.3.2 waitpid
3.4 拿到对应的退出信息 status
正确的写法
进程退出码,即main返回值
无异常
异常
退出信息应该存在哪里?
3.5 阻塞等待与非阻塞等待
4. 进程程序替换
4.1 快速看程序替换的效果
4.2 替换原理 -- fork
4.3 程序替换相关接口
4.3.1 execl
4.3.1.1
4.3.1.2
4.3.1.3
4.3.2 execlp
4.3.3 execv
4.3.4 execvp
4.3.5 execvpe
新增式导入
1.直接putenv
2. exec*p,put();environ;
1.进程创建
1.1 fork函数
fork 从已经存在的进程中创建一个新进程,新进程为子进程原进程为父进程
返回值:子进程中返回 0 ,父进程返回子进程 id ,出错返回 -1
进程调用fork,当控制转移到内核中的fork代码后,内核会:
1)分配新的内存块和内核数据结构给子进程
2)将父进程部分数据内容拷贝至子进程
3)添加子进程到系统进程列表当中
4)fork返回,开始调度器调度
fork之前父进程独立执行,fork之后父子两个执行流分别执行
fork之后谁先调度完由调度器决定
1.2 fork函数返回值
子进程返回0
父进程返回的是子进程的pid
1.3 写时拷贝
父子进程代码共享,父子在不写入时,数据也是共享的
当一方试图写入时,就用写时拷贝的方式各自一份副本
因为有写时拷贝,所以父子进程得以彻底分离,完成了进程独立性
当我们创建子进程时,OS除了给我们创建子进程之外,基本上会把我们页表的项目都改成只读的。拿到虚拟地址,OS可以知道我们是代码段还是数据段。我们修改写入时,发现是数据段是只读的,OS就会出错,让os知道我们在访问只读的数据区,就会触发写实拷贝
写实拷贝是最精细的内存控制
减少创建时间
减少内存浪费
1.4 fork 常规用法
1)一个父进程希望复制自己使父子进程执行不同的代码段
eg:父进程等待客户端请求,生成子进程来处理请求
2)一个进程要执行一个不同的程序
eg:子进程从fork返回后,调用exec函数
1.5 fork 调用失败的原因
1)系统中有太多的进程
2)实际客户的进程数超过了限制
2. 进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码
2.1 进程退出场景
1)代码运行完毕,结果正确
2)代码运行完毕,结果不正确
3)代码异常终止
2.2 进程常见退出方法
2.2.1 从main 返回
int main();
main函数的返回值通常表明我们程序的执行情况
而这里的情况有两种
1)代码运行完毕,结果正确 return 0
2)代码运行完毕,结果不正确 return !0 (1,2,3...)--> 在计算机上用不同的值表明不同的 出错原因
返回值是通过寄存器完成的
2.2.2 echo $? 查看进程退出码
./code 是一个进程 父进程为bash
父进程bash要获得子进程退出的退出码
echo $? 打印最近一个程序(进程)退出时的退出码
main函数的返回值我们称之为进程的退出码
进程的退出码是要写到进程的task_struct内部的,task_struct里面保存的是进程的所有属性信息
2.2.2.1 我们如何得到退出码代表的含义
我们这里要实现错误码就是对应的错误描述
因为我们没有这个文件/目录,所有打开失败,就是2
2)
根据退出码来判断当前程序的执行结果
运行结果是否正确由我们进程的退出码决定,0代表正确,非0有不同的退出错误原因
2.2.2.1 可以按照我们的要求去定制我们的退出结果
2.2.2.2 一旦进程出现异常,进程的退出码就没有意义了
进程的退出码不是 66
一旦进程出现异常,进程的退出码就没有意义了
进程一旦出现异常,一般是进程收到了信号
main函数结束,表示进程结束
2.2.3 exit 函数
1)
(忘记写头文件了)
2)
在任何地方调用exit,表示进程结束,并且返回子进程的退出码给父进程bash
一旦调用exit,程序立马结束,函数后续代码不执行,进程直接退出
2.2.4 _exit 函数
谁调用我们就退出谁
exit(status) VS _exit(status)
是我们C语言的 是系统的
如果进程退出的时候, exit() ,进程退出的时候,会进行缓冲区的刷新
如果进程退出的时候,_exit() ,进程退出的时候,不会进行缓冲区的刷新
库函数和系统调用是上下层关系
exit底层封装了_exit
本来就是系统调用
底层调用了_exit
所以我们之前谈论的缓冲区一定不是操作系统内部的缓冲区,它应该是在库缓冲区(C语言·提供的缓冲区)
2.2.5 return退出
return退出是一种更常见的退出进程的方法。
执行 return n 等同于执行 exit(n),因为调用 main 的运行时函数会将 main 的返回值当作 exit 的参数
进程退出的具体做法
1)main 函数里 return
2)exit(status)
控制进程的退出码
3. 进程等待
3.1 进程等待的必要性(为什么?)
1)子进程退出,父进程不管,就可能会造成僵尸进程的问题,进而造成内存泄漏
2)进程一旦变成僵尸状态,kill-9 也没有办法,谁也没有办法杀死一个已经死去的进程
3)我们需要知道父进程派给子进程的任务完成得如何
子进程运行完成,结果正确还是不正确,是否正常退出
4)父进程通过进程等待得方式,回收子进程资源,获取子进程退出信息
(通过等待来解决僵尸内存泄漏问题)
3.2 进程等待是什么?
父进程创建子进程,必须通过相关的接口来等待子进程
这种用wait或waitpid来等待子进程的方式就叫作进程等待
3.3 进程等待的代码
3.3.1 wait
wait 这个接口是等待任意一个退出的进程
等待成功返回的是目标进程的pid
等待失败返回-1
3.3.2 waitpid
等待成功返回的是目标进程的pid
等待失败返回-1
3.4 拿到对应的退出信息 status
wait和waitpid都有一个status参数,这个参数是一个输出型参数,由操作系统填充
如果传递NULL,表示不关心进程的退出状态信息
否则,OS会根据该参数,将子进程的退出信息反馈给父进程
status不能简单地当作整型来看待,当作位图来看待
status 并不是直接一个整数,是被划分成若干区域的
int 整型变量32个bit位,高16位我们不考虑,低16位高8位是退出状态
我们刚刚写的1就是00000001
但是后面还有八位,就是0000000100000000,就是2^8,256
我们要拿到正确的就要右移8位,踢掉低8位
正确的写法
父进程是无法拿到子进程的退出信息的,子进程退出信息是会修改的,定义在全局不能够实现,拿到子进程的退出信息必须通过系统调用
进程退出码,即main返回值
1)运行完毕,结果正确
2)运行完毕,结果不正确
3)代码异常终止
status
代码异常终止,保存异常时对应的信号编号
没有异常?
1.低7个bit位,0
2.一旦低7个bit位 !0,异常退出,退出码无意义
无异常
异常
1)
2)
怎么做到的?
退出信息应该存在哪里?
3.5 阻塞等待与非阻塞等待
options默认为0,表示阻塞等待
非阻塞轮询
非阻塞的效率往往要高一点
阻塞等待
定义:当一个操作需要等待某个条件完成才能继续执行时,如果当前线程被挂起,无法执行其他 任务,这种状态就称为阻塞
特点:在阻塞状态下,程序会一直等待,直到等待的条件满足,操作完成并返回结果后,才能继 续执行后续任务
非阻塞等待
定义:非阻塞是指即使某个操作尚未完成,当前线程也不会被挂起,可以继续执行其他任务
特点:在非阻塞模式下,程序不会等待某个操作的完成,而是会立即返回一个结果或者状态,表
明该操作正在进行中或者已经失败
适用于需要同时处理多个任务或提高资源利用率的场景
可以提高整体执行效率
对比
阻塞等待时,线程被挂起;非阻塞等待时,线程继续执行其他任务
阻塞等待可能导致资源闲置;非阻塞等待可以提高资源利用率
4. 进程程序替换
4.1 快速看程序替换的效果
在程序替换的过程中并没有创建新的进程,只是把当前进程的代码和数据用新的程序的代码和数据进行替换
1)一旦替换成功,就去执行新代码了,原始代码的后半部分已经不存在了,是不会去执行的
前半部分是我们的printf,后半部分就是ls的结果
这个指令可以跑系统的命令
整个替换过程只替换当前的代码和数据
(默认是覆盖掉的)
把我们要替换的对应的指令所对应的代码和数据覆盖式地替换到我们当前的代码和数据
2)exec系列的函数,只有失败返回值没有成功返回值
exec系列的函数不用做返回值判断,只要返回就是失败
替换失败我们设置为1
4.2 替换原理 -- fork
用fork创建子进程后执行的是和父进程相同的程序,子进程往往要调用一种exec函数去执行另一个程序,当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行
调用exec并不创建新进程,所以调用exec前后该进程的id不会改变
4.3 程序替换相关接口
4.3.1 execl
4.3.1.1
我们可以创建一个子进程,让子进程执行程序的替换不影响父进程的执行
进程具有独立性
数据代码发生写时拷贝
4.3.1.2
现在我们来实现替换我们自己写的程序
我们今天就想要用我们的C程序来把other程序调起来
4.3.1.3
验证程序替换不会创建新的进程
4.3.2 execlp
4.3.3 execv
4.3.4 execvp
4.3.5 execvpe
要求被替换的子进程使用全新的env表
而我们如果要新增子进程?方法(把环境变量传给子进程;新增式导入)
新增式导入
1.直接putenv
谁调用它就在谁那里创建环境变量