Linux:进程控制和进程替换 一、进程控制进程控制我们要以三个方面讲:进程的创建、进程的终止、进程的等待进程的创建在前面的文章中我已经讲到了进程如何创建,在这里做个总结1.先调用fork()函数• 分配新的内存块和内核数据结构给⼦进程• 将⽗进程部分数据结构内容拷⻉⾄⼦进程• 添加⼦进程到系统进程列表当中•fork 返回开始调度器调度返回值类型为pid_t(int类型的ypedef),返回值为0,是创建的子进程,返回值为0的数,是父进程,返回值0创建失败2.当共享的数据发生改变我们根据学到的基本虚拟地址空间的知识,一旦发生改变,就会发生写时拷贝,将共享的数据复制重新复制一份到物理内存中还有就是为什么操作系统能知道代码发生了变化因为本来虚拟地址的数据区域映射到页表的标志位的权限默认是可读可写(“rw”)权限,一旦创建了子进程的话就会先把父进程页表中的数据段权限改为只读(“r”),也会拷贝到的子进程,现在父子进程的代码区域权限都是只读,一旦父子进程任意一个尝试修改共享的数据的话,就会被操作系统检查到没有写入的权限,就会判断是要把共享的数据写时拷贝一份在物理内存中,并且恢复父子进程默认的代码权限,以后在不同分区内改代码只会影响自己,这样就能保证进程的独立性调用fork()的原因⼀个⽗进程希望复制⾃⼰使⽗⼦进程同时执⾏不同的代码段。例如⽗进程等待客⼾端请求⽣成⼦进程来处理请求。调用fork()失败原因• 系统中有太多的进程• 实际⽤⼾的进程数超过了限制进程终止进程终⽌的本质是释放系统资源就是释放进程申请的相关内核数据结构和对应的数据和代码。所以我们执行一个进程的退出的条件是什么? 我们最接近的就是我们常写的int main()函数里的return 0退出码它叫做退出码,是我们进程终止的情况之一退出码退出状态可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后我们可以知道命令是成功完成的还是以错误结束的。其基本思想是程序返回退出代码0时表⽰执⾏成功没有问题。代码1或0以外的任何代码都被视为不成功。退出码的含义可以通过strerror传整数然后返回默认的返回数字之下的错误信息常见的退出方法从main返回 (return 返回)调⽤exit_exit这两个退出区别不大exit退出会刷新缓冲区, _exit不会,但是exit底层是调用_exit,所以建议用exit退出信号它是进程终止的情况之一,这里的退出信号异常也会导致进程终止,比如ctrlc结束进程,一般都是空指针的访问等情况就会导致进程直接被终止,我们其实也经历过,我们运行代码弹出来的白色的噔噔响的窗口就是信号异常退出的结果,这里我们都是粗略讲等学到这个具体知识再详谈, 可以通过 kill -l查看进程异常的信号的选项与退出码的区别:退出信号异常代码不会全部运行完,提前退出,而退出码异常是正常写的逻辑跑完后退出额外说明:我们task_struct中有 int exit_signal(退出信号),int exit_code(退出码),将来进程退出后的退出信号和退出码传给task_struct的这两个变量进程退出场景1.代码正确,结果正确也就是退出码为0退出信号为0这里额外说明:为什么我们好像平常写代码运行感受不到,是因为我们是靠printf或cout等输入只能看到程序主动打印出来的内容但进程真实的退出码、终止信号并不会自动打印到屏幕上。这部分信息保存在内核 PCB 中必须由父进程调用wait()/waitpid()系统调用读取 status 参数才能拿到。我们直接在终端运行程序看不到内核保存的这一组退出状态。这是下面进程等待的知识,这里引出来2.代码正确,结果不正确退出码不为0退出信号为0代码没跑完,程序异常了退出码无意义,退出信号为非0进程等待我们之前讲过,当一个子进程结束时,它的状态会先变成僵尸状态,它不会立马被操作系统回收task_struct结构,这时则需要父进程获取子进程的退出状态后操作系统才会回收掉task_struct结构为什么要有进程等待呢?如上面所说,第一能避免内存泄漏,因为将来会回收掉内核数据结构.第二,你创建的子进程的目的不是为了完成父进程的任务吗(本身的进程也是bash的子进程),那完成的结果肯定要让父进程知道完成的怎么样,所以将来的退出码和退出信号组成的退出状态会给父进程进程等待的方法调用的函数使用方法pid_t wait(int * status)子进程退出后,父进程会阻塞式的等待处于僵尸状态的子进程,然后接收退出状态pid_t waitpid(pid_t id,int* status,int options)1.id是等待特定一个处于僵尸状态的子进程,如果传入-1就是等待任意一个僵尸状态的子进程2.status是子进程中退出码和退出信号一起组成的一个整数,3.option是选择阻塞状态(传参 - 传0)还是非阻塞状态(传参-传 WNOHANG这个宏)pid的返回值和参数:返回值成功返回被等待进程pid失败返回-1参数。输出型参数获取⼦进程退出状态,不关⼼则可以设置成为NULLwaitpid的返回值:返回值:1.当正常返回的时候waitpid返回收集到的⼦进程的进程iD2.如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回03.如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在子进程的status• wait和waitpid都有⼀个status参数该参数是⼀个输出型参数由操作系统填充。• 如果传递NULL表⽰不关⼼⼦进程的退出状态信息。• 否则操作系统会根据该参数将⼦进程的退出信息反馈给⽗进程。• status不能简单的当作整形来看待可以当作位图来看待具体细节如下图只研究status低16⽐特位将来给父进程完成任务的结果就是查看status可以通过宏操作看到退出码和退出信号, WIFEXITED(status)查看退出信号,WEXITSTATUS(status)查看退出码阻塞状态和非阻塞状态阻塞状态:就是父进程阻塞在waitpid这里一直等待进程退出的子进程,自己下面的工作没等到子进程是不会执行的非阻塞状态:父进程每过一段时间去等待子进程退出,没等到就继续执行自己的工作区别就是:非阻塞状态会轮询等待,它的等待时间可以做其他的事情,而阻塞状态只能等到子进程才能做其他事情下面是一段基本感受进程等待的代码三、进程替换我们知道fork()之后是父子进程是执行同一个程序中的代码,即使你用pid做分流让子进程做与父进程不同的任务,其本质还是在同一个数据段和代码段执行的,我子进程能不能执行一个全新的程序呢? 可以!进程的程序替换可以来完成这一工作用一个简单例子见一下效果可以看到子进程做了ls这个系统命令的工作,父进程照常等待子进程并实行它的逻辑替换原理我们fork()后在子进程里通过exec系列的函数能让我们当前的进程去一个全新的程序里执行它的逻辑代码这是我们所看到的现象,原理:当进程调用exec系列函数后,我们会把exec函数中要替换的程序的代码区和数据区拷贝到当前这个进程的物理内存的相应位置进行覆盖可以发现该进程的id并没有发生改变只是虚拟空间地址某些区域的大小、页表、物理内存可能会有所改变不仅是可以让进程执行系统命令,也可以执行C/C/java/python等语言写的程序,因为将来它们编译都是转化为二进制程序,本质上没太大区别,系统命令也是用C语言编译好的二进制程序一些替换函数• l(list) :表⽰参数采⽤列表• v(vector) 参数⽤数组• p(path) 有p⾃动搜索环境变量• e(env) :表⽰⾃⼰维护环境变量PATH列表的意思是:你的选项是分开写的 比如调系统程序”ls“ “-a” ”-l“ 这样分开写的数组就是把这些选项写到一个指针数组里面,只要穿指针数组的地址就行是否带路径就是如果是带了p的exec函数,如果你替换的程序是环境变量PATH能找到的就可以直接写程序名就能版本你找到,不需要写绝对路径和相对路径execvpe,你替换的程序的路径是用当前没有改变的环境变量找,所以是你虽然改变了环境变量,要替换后才会覆盖掉旧的环境变量