分享知乎上看到的一句话,共勉: 学习周期分为学习,思考,实践,校正四个阶段,周期越短,学习效率越高。
前面讲的都是操作系统如何管理进程,接下来,看看用户如何进行进程控制。
1.进程创建
先介绍一下函数: pid_t fork(void); 返回值: 子进程返回0 父进程返回子进程id,创建子进程失败则返回-1
当一个进程调用fork后,就会产生两个二进制代码相同的代码。并且它们都运行到相同的地方。现在,给大家演示一下:
由此,我们得到验证: fork之前父进程独立运行,fork之后,父子两个执行流分别执行。
fork原理:
进程调用fork后,当控制权转移到内核的时候,内核会做四件事:
1.分配新的内存块和数据结构给子进程
2.将父进程部分数据结构拷贝给子进程
3.添加子进程到系统进程(PCB)列表中
4.fork返回,开始调度
写时拷贝技术:
实际上fork就是给子进程创建pcb,将父进程pcb数据拷贝过来,这时候父子进程因为虚拟地址空间页表完全一样,因此他们的代码和数据看起来都是一样的。
但是,进程应该具有独立性,修改任意进程数据不应该影响其他进程,所以子进程也应该开辟自己的物理内存来存储数据。
但是,如果子进程不修改数据,再开辟空间,更新页表,就浪费了资源,所以,操作系统一开始并不开辟内存拷贝数据,而是等一个进程修改数据后再给子进程开辟新的物理内存,拷贝数据---写时拷贝技术
再简单介绍一下vfork --- 父子进程共用一块虚拟地址空间 , 因为vfork在很多系统上有问题, 所以不建议使用。
vfork子进程运行,父进程阻塞,直到子进程结束。
2.进程终止
进程退出的时候,会保存退出原因,从而知道进程任务是否正确完成。
进程退出场景:
1.正常退出,结果正确;
2.正常退出,结果错误;
3.异常退出,结果不能作为判断标准
进程的退出方式:
1.main中return;
2.调用exit函数; --- 1,2都是先刷新缓冲区,做了其他释放工作才推出
3.调用_exit函数; --- 粗暴退出,直接释放所有资源
3.进程等待
为什么要进程等待呢? 因为要避免僵尸进程。
因为父进程不知道子进程什么时候退出,所以创建子进程后要一直等待子进程退出。
函数介绍:
阻塞: 为了完成操作发起调用,如果当前不具备完成条件,则一直等待,直到完成操作。
阻塞和非阻塞的区别: 发起调用后是否立即返回。
获取子进程status:
wait和waitpid都有一个输出型参数status,由操作系统填充。
status不能简单当做整型来看,而是一个位图,具体细节如下:
测试验证代码:
coredump标志 : 核心转储标志
在程序异常退出时,保存程序的堆栈信息,方便事后调试 (核心转储通常默认是关闭的: 安全隐患 和 空间占用)
4.程序替换
前面我们知道,一个进程运行什么代码,取决于虚拟地址空间中的代码段映射物理地址中的那个真实代码区域。
意味着如果将虚拟地址代码段映射到物理内存的代码位置替换成另一个程序的位置,那么进程将运行另一个程序。
是什么 : 替换代码映射的代码位置成为另一个内存区域代码的位置,并且重新初始化数据段
为什么 : 大多数情况下,我们创建一个子进程都是为了让子进程运行另一个程序,做其他的任务
实现 : 操作系统提供了一套接口都能实现程序替换,统称为exec函数族
#include<unistd.h> int execl(const char* path,const char* arg,...); //后面以NULL结束 int execlp(const char* file,const char* arg,...); int execle(const char* path,const char* grg,...,char* const envp[]); int execv(const char* path,const char* argv); //argv数组也要以NULL结束 int execvp(const char* file,const char* argv); int execve(const char* path,const char* argv,char* const envp[]); //envp数组也是NULL结束 //事实上,只有execve是系统调用,其它函数都调用了execve
2.函数参数
path:表示对应的文件在哪个目录下(也表示我要执行的程序是哪一个);
file:只用填文件名,系统会在PATH下找;
envp[]:表示自己设置的,替换后的程序的环境变量
3.命令理解
l(list):表示参数采用列表形式
v(vector):参数采用数组形式
p(path):有p自动搜索环境变量PATH
e(envp):表示自己设置(维护)环境变量(自己设置的环境变量是什么,则最新程序的环境变量就是什么)
理论讲了这么多,不如写个测试代码,看看怎么用吧:
#include <unistd.h> int main() { char *const argv[] = {"ps", "-ef", NULL}; char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径 execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量 execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径 execvp("ps", argv);
// 带e的,需要自己组装环境变量 execve("/bin/ps", argv, envp); exit(0); }