.进程控制)
一.生——进程创建之fork函数1.fork()由于之前在讲进程概念的时候已经提到过fork函数所以这里着重强调在调用fork时内核在干什么。进程调⽤ fork 当控制转移到内核中的 fork 代码后内核做• 分配新的内存块和内核数据结构给⼦进程• 将⽗进程部分数据结构内容拷⻉⾄⼦进程• 添加⼦进程到系统进程列表当中• fork 返回开始调度器调度此外再补充一下进程概念这篇文章里fork()函数创建进程里没说清楚的地方就是创建子进程以后fork()以后的代码父进程和子进程各自执行各自的究竟谁先执行由调度器决定。示意图2.写时拷贝的实现原理①.原理在上一篇文章中我们暂且了解了写时拷贝是为了将父子进程在内存上的实际储存空间岔开防止一个进程修改数据时由于实际地址相同导致另一个进程受到干扰。但我们并不知道写时拷贝是如何实现的这里就来说明一下。内容修改之前父子进程的数据和代码是共享的(物理地址相同)。此时父子进程页表上的所有项的权限是只读权限。当某个进程修改内容(写入)时由于只有读权限会出现“错误”系统基于这个“错误”就会作出反应做出类似缺页中断的操作然后磁盘重新加载数据代码进内存形成新的物理地址和空间同时对应项为了能够被修改在页表上的权限也会发生改变不再只有读权限。修改就是改子进程新空间上的内容。②.为什么要写时拷贝1.因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离完成了进程独⽴性的技术保证!2.在创建子进程的新空间时要修改的代码和数据才加载进内存其余与父进程重复的内容不再加载进来这样就能节省内存空间以及创建空间的时间。那其他相同的数据和代码在哪呢答案是接着和父进程共用毕竟这些相同的东西不发生改变就不会触发写时拷贝去开新空间。③.OS如何区分数据段和代码段上篇文章说过mm_struct里有虚拟地址空间各个大区域的起始地址(虚拟地址)。OS一查数据位于哪些区域的起始地址之间就能判别它是隶属数据段还是代码段。3.fork()的常规用法①.执行父进程代码的一部分即⼀个⽗进程希望复制⾃⼰使⽗⼦进程同时执⾏不同的代码段。②.执行一个全新的进程。4.fork调⽤失败的原因• 系统中有太多的进程• 实际⽤⼾的进程数超过了限制归根结底还是内存不足。二.亡——进程终止1.详谈进程终止的返回值(退出码)在进程状态这篇文章里曾经提到一个进程终止后不会马上清除掉进程的所有东西而是会保留PCB(内部含退出信息)和pid处于Z(僵尸)状态等待父进程获取pid和退出信息进而知晓子进程的执行情况。子进程做完事需要父进程来查看执行情况c/cpp的main函数的返回值代表程序的执行情况。执行情况分为三种:• 代码运⾏完毕结果正确-返回0。• 代码运⾏完毕结果不正确-返回非0数(1,2,3...)。• 代码异常终⽌退出码无意义0代表成功即成功就是成功无人去问你如何成功。非0代表失败有多个非0数代表多种失败原因即失败时会去想是由于什么原因出的错。这些不同的返回值叫做退出码。退出码退出状态可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后我们可以知道命令 是成功完成的还是以错误结束的。其基本思想是程序返回退出代码 0 时表⽰执⾏成功没有问题。 代码 1 或 0 以外的任何代码都被视为不成功。这是退出码的对照表子进程执行完毕后退出码写给PCB然后僵尸状态等待父进程(bash)就会从僵尸状态子进程的PCB里读取到退出码进而知晓执行情况。这里有个指令echo $?可以查询到最近一个进程的退出码至于父进程如何读取子进程退出码这是下部分进程等待的事这里暂且不提。2.exit函数#include unistd.h void exit(int status);注这里的status指的是退出码。任何地方调用了该函数就代表一个进程的结束,并将子进程的退出码返回给父进程bash。main函数结束就代表一个进程的结束那么main函数末尾的return n就相当于执⾏exit(n)因为调⽤main的运⾏时函数会 将main的返回值当做exit的参数。3._exit VS exit①._exit这玩意跟上面那个一样的用法但它俩到底有啥区别呢②.对比Ⅰ.调用关系exit的是个库函数_exit是个系统调用函数(system call)库函数在执行的时候会去调用系统调用函数库函数与系统调用函数属于一个上下层的关系(系统调用的知识点在笔者进程概念这篇文章)也就是说exit在执行时会去调用_exit。exit最后也会调⽤_exit,但在调⽤_exit之前还做了其他⼯作1. 执⾏⽤⼾通过atexit或on_exit定义的清理函数。2. 关闭所有打开的流所有的缓存数据均被写⼊。3. 调⽤_exit。Ⅱ.关于缓冲区缓冲区是c语言提供的库缓冲区这里只能轻轻点一下缓冲区的细节要交给后面的文章。三.进程等待1.为何要等待2.什么是进程等待进程等待是一种通过接口wait/waitpid(系统调用)的方式主动阻塞自己(即父进程阻塞在wait()处)等待子进程执行完毕回收子进程的资源获取子进程退出信息(状态)避免子进程出现僵尸状态导致内存泄漏。3.进程等待的方式①.wait()②.waitpid()(更推荐)想要更多的状态控制可以选择waitpid()它拥有更多的形参存在形参pid这就是为什么fork()成功以后会给父进程返回子进程的pid父进程可以通过形参pid来选择等待指定的进程。形参pid值几种情况(只考虑 -1 和 大于0的情况)③.父进程如何获得子进程退出信息Ⅰ.起底status答案是父进程通过wait/waitpid里的返回型参数status。这个返回型参数解引用以后的整数并不是退出码要把这个整数看成一张32位的二进制位图其中我们研究的是低16比特位高16位不考虑其中第八位到第十五位组成的数才是退出码那么父进程要是只想获得退出码这该怎么做呢将退出码的那8位向右移动8位然后再与0xFF进行按位与操作提取出这八位二进制退出码注意示例的status非此status真正的status是地址即status。Ⅱ.那么waitpid是如何拿到子进程的退出码(退出信息)并存进status进而使父进程拿到退出码的呢补充一点系统是如何把退出信息存入status里的呢?答案是系统将错误码和信号按照位图规则打包然后写入status的内存空间中。4.阻塞调用与非阻塞调用默认情况下options的值为0表示父进程在waitpid()处阻塞等待。这种特性叫做阻塞调用。如果把options的值设置为WNOHANG时父进程调用waitpid就变成了非阻塞调用也就是如果子进程不退出父进程不会阻塞住自己在waitpid()处等待而是直接返回。当然返回了以后还是会去反复调用waitpid()函数不停检查子进程有没有退出直至子进程退出。这个反复调用函数的过程叫做非阻塞轮询。非阻塞调用时waitpid()的返回值也有讲究大于0等待结束即子进程退出就不用再非阻塞轮询了。等于0waitpid()一次调用结束即return了此时子进程没有结束父进程需要轮询。小于0等待失败。非阻塞调用效率更高因为可以让等待方做一做自己的事而不让其完全停下来等待。但常用的还是阻塞调用