友谊路街道网站建设,网站流量利用,室内设计心得体会500字,百姓网网站建设进程控制上篇文章介绍了进程的相关概念#xff0c;形如进程的内核数据结构task_struct 、进程是如何被操作系统管理的、进程的查看、进程标识符、进程状态、进程优先级、已经环境变量和进程地址空间等知识点#xff1b; 本篇文章接着上篇文章继续对进程的控制进行展开#x…进程控制上篇文章介绍了进程的相关概念形如进程的内核数据结构task_struct 、进程是如何被操作系统管理的、进程的查看、进程标识符、进程状态、进程优先级、已经环境变量和进程地址空间等知识点 本篇文章接着上篇文章继续对进程的控制进行展开主要包括进程的创建fork进程的退出和终止、写时拷贝、进程等待防止僵尸进程的产生使得内存泄漏进程替换的相关知识 文章目录进程创建fork()的使用场景fork()调用失败的原因子进程的数据和代码默认是与父进程共享的创建子进程的操作系统需要干什么父子进程是具有独立性的父子进程既然共享代码和数据如何实现独立性写时拷贝详解进程终止进程退出的场景有哪几种进程退出的方式exit()和_exit()的区别如何获取一个进程的退出码或者退出信号方法一通过位运算来获取方法二通过宏来获取进程等待进程等待的用处是什么进程等待的方式wait()函数waitpid()函数阻塞等待非阻塞等待进程替换为什么要有程序替换呢原理是什么六种程序替换函数execl()execlpexecleexecv()execvpexecvpeexecveexec系列函数用法总结进程创建
进程的创建在上篇文章中也有介绍过进程创建的方式有两种一种是我们将一个程序跑起来它就会变成一个进程还有一种就是通过fork()函数创建子进程,主要的创建方式就是通过fork函数所以我们再来回顾一下fork()函数吧.
fork()函数是一个系统调用接口
//fork()
pid_t fork(void);
返回值: pid_t 类型实际上是无符号整数 如果创建子进程成功返回新创建的子进程的pid大于0的给它的父进程,返回0给它自己,如果创建失败就返回-1注意fork()成功创建了子进程后就会有父子两个进程那么也就是说会有两个执行流fork()会返回两次分别对父进程和创建出来的子进程进行返回给父进程返回子进程的pid,因为父进程和子进程的关系是一对多的所以父进程需要去唯一标识子进程而进程的pid是天然的标识一个进程的标志所以fork()返回给父进程的是新创建出来的子进程的pid而子进程他自己是被创建的时候就知道了自己的pid和其父进程的pid的,所以它不需要fork()函数对他返回任何值所以fork()就默认给它返回0意思意思一下。上面是创建成功的情况如果创建失败就会返回-1给当前进程 fork()的使用场景
一个进程希望将自己分身可以一个人做多份工作那么一个进程就可以通过创建子进程的方式来实现这个目的可以将自己要干的事分担给子进程让子进程去帮自己完成。一个进程需要执行别的可执行程序就可以通过创建子进程的方式让子进程通过程序替换去帮自己完成程序的执行我们使用的shell就是这样的我们执行命令命令也是可执行程序的时候bash就会创建子进程然后通过程序替换去执行可执行程序。
fork()调用失败的原因
原因非常简单类比你的手机下来太多东西容量不够了就不能在下载东西了
所以fork()创建子进程失败的原因无非就是操作系统中存在太多的进程内存不够了。
子进程的数据和代码默认是与父进程共享的
我们学习语言的时候了解过继承子类会继承父类的成员变量这个理念在进程中同样被使用
一个进程如果通过fork()函数创建子进程成功那么它的子进程就会以它的父进程为模板拷贝它的代码和数据。
创建子进程的操作系统需要干什么
fork()是系统调用那么就必须要由操作系统来完成子进程的创建工作那么os会做什么事情呢 根据上篇文章的知识进程是被操作系统管理起来的一个进程就是一个task_struct结构体该结构体包括所有的进程的属性比如进程的pid、进程的代码对应的地址数据对应的地址进程的退出状态退出信号进程地址空间等等 操作系统用task_struct 结构体 将进程描述好再对这些结构体管理实现对进程的管理工作 知道了上面这些那么我们就很容易猜到 创建一个进程操作系统就肯定会先创建一个新的task_struct 结构体该结构体就是那个新新创建的子进程创建好结构体还没完还需要对其进行初始化根据继承的理念子进程会默认继承父进程的代码和数据那么操作系统就会把父进程的task_struct中的代码和数据的地址拷贝给子进程
父子进程是具有独立性的
进程之间是具有独立性的即使是父子进程也是如此各个进程之间都是独立运行的
父子进程既然共享代码和数据如何实现独立性
既然子进程创建出来后是和父进程共享的代码和数据那么子进程对继承自父进程的数据或代码进行修改岂不是会对父进程造成影响吗那怎么还说进程是具有独立性的呢
子进程是和父进程共享的代码和数据没错进程间具有独立也没有错错的是子进程修改和父进程共享的数据和代码是不可能会影响到父进程的因为操作系统考虑到了这个问题并且让父子进程之间具有写时拷贝的机制使得在父子一方对共享的资源进行修改是就会将修改的资源分离使得父子各有一份从而实现进程的独立性
下面具体介绍写时拷贝~
写时拷贝详解
未发生写时拷贝 未发生写时拷贝的时候父子是共享这数据和代码的。 它们通过各自的页表将相同的虚拟地址映射到相同的物理地址。 发生写时拷贝后 当子进程对父子共享的数据或者代码进行修改时因为进程之间要有独立性操作不可能直接让子进程将共享的数据给改掉那样会直接影响到父进程 所以操作系统会在子进程对父子共享的代码或者数据进行修改的时候会开辟出一块新的物理空间将要被修改的代码或者数据拷贝一份之后让父子中先对共享资源修改的一方去修改新拷贝出来的数据再让先修改共享资源的一方的页表去重新映射新开辟出来的物理空间实现父子进程的资源分离互不影响这就是写时拷贝的基本原理~ 当有一方要对共享资源修改时为其开要修改的资源开辟新空间再对该空间的值进行修改使得二者都有对应的资源但是被修改的资源不是同一块物理空间了 注写时拷贝的相关操作是由操作系统的内存管理模块完成的
为什么要有写时拷贝直接在创建子进程的时候就为其数据和代码开辟新的空间将其和父进程的数据和代码分离开不好么
1.父子进程的数据子进程不一定全用即使使用也不一定全都会写入修改----------存在空间浪费2.最理想的情况是只有会被父子修改的数据才会进行分离拷贝不会修改修改的共享即可 ------理论可以技术角度上无法实现父子对数据的修改是不可提前预测的3.如果fork的时候就无脑的将父进程的数据拷贝给子进程就会增加fork的成本内存和时间层面上
所以既然写时拷贝存在就有它存在的原因存在即合理
写时拷贝是解决上述问题的较为合理的方法所以才会被采用。写时拷贝是一种演示拷贝的策略只有当你会对数据进行修改的时候才会给你开辟新的空间将数据分离当你不修改的时候就不会给你新开辟空间这样省下来的空间就可以被其他进程使用了体现了os良好的内存管理方案
进程终止
进程退出的场景有哪几种
第一种代码运行完毕结果正确第二种代码运行完毕结果不正确第三种代码都没执行完发生了异常操作系统直接终止进程
进程退出的方式
第一种通过main函数返回第二种通过调用exit()函数或系统调用_exit()退出进程第三种给进程发信号将进程终止(kill)
上面提到了exit()和_exit()它们的功能都是让进程退出两者之间有什么区别呢
exit()和_exit()的区别
exit()是封装_exit()的函数, _exit()是系统调用。exit() 最终也还是会去调用 _exit() , exit()在 _exit()的基础上增加了新的功能就是:
执行用户通过atexit或on_exit定义的清理函数关闭所有打开的流所有的缓存数据都会被写入(刷新)
如何获取一个进程的退出码或者退出信号 exit(int status)和 _exit(int status)中的参数status就是进程的退出状态码 status只有低16位才有价值它的0-7位是存储着进程的退出信号8-15位是存的进程的退出码 进程正常终止时的退出信号是为0的退出码为进程设置的退出码 进程被信号所终止时退出码为0退出信号为进程所收到的信号 方法一通过位运算来获取
//获取退出码
status80xFF
//获取退出信号
status0x7F#includestdio.h
#includestdlib.h
#includesys/types.h
#includesys/wait.h
#includeunistd.h
int main()
{pid_t idfork();if(id0){//childint n10;while(n--){printf(i am child! i am running pid:%d ppid:%d\n,getpid(),getppid());sleep(1);}exit(110);}else if(id0){sleep(10);int status0;waitpid(id,status,0);if(id0){sleep(3);printf(等待子进程成功\n);printf(子进程退出码%d 退出信号:%d 子进程pid:%d \n,status80xFF,status0x7f,id);//位运算获取退出码和退出信号的方式}printf(父进程退出\n);}else {printf(fork error!\n);}return 0;
}运行结果 方法二通过宏来获取
操作系统提供对应的宏来协助我们获取对应的退出信息
宏说明WIFEXITED(int status)子进程正常终止则返回真可以通过WEXITSTATUS(int status)获取子进程退出码WIFSIGNALED(int status)子进程异常终止返回真若为真可通过WTERMSIG(int status)获取子进程的终止信号WIFSTOPPED(int status)子进程若为暂停状态返回真WIFCONTINUED(int status)子进程被暂停后将其继续的状态返回真WEXITSTATUS(int status)获取子进程退出码 status的次低8位WTERMSIG(int status)获取子进程终止信号 stauts的低7位
#includestdio.h
#includestdlib.h
#includesys/types.h
#includesys/wait.h
#includefcntl.h
#includeunistd.h
int main()
{pid_t idfork();if(id0){while(1){printf(i am child pid:%d \n,getpid());sleep(1);//exit(110);}}else {int status0;int retwaitpid(id,status,0);//阻塞式等待子进程if(ret0){// if(WIFEXITED(status)||WIFSIGNALED(status))if(WIFEXITED(status)){printf(子进程退出 退出码:%d\n,WEXITSTATUS(status));}if(WIFSIGNALED(status)){printf(子进程退出退出信号:%d\n,WTERMSIG(status));}}}return 0;
}进程等待
进程等待的用处是什么
我们知道子进程退出时如果父进程未读取其相关的退出信息那么该子进程就会变成僵尸进程僵尸进程会有内存泄漏浪费系统资源且操作系统无法回收就算是kill 也不能将它怎么样因为它已经僵尸了kill可以杀死在运行的进程但是它做不到杀死一个已经死去的进程
所以进程等待就是防止产生僵尸进程的处理方式。通过让父进程等待子进程退出然后再读取它的退出信息那么子进程就不再僵尸会变成终止状态随时等待被系统回收资源创建子进程一般都是让子进程去完成父进程给它分配的任务那么父进程就需要知道子进程最终完成的如何所以父进程有必要等待子进程退出读取它的退出状态
进程等待的方式
wait()函数 pid_t wait(int *stauts)wait会等待任意一个子进程 参数 status是输出型参数可以通过status获取退出子进程的退出状态退出码或终止信号 返回值 成功返回对应子进程pid,失败返回-1 waitpid()函数 pid_t waitpid(pid_t pid,int* status,int options)waitpid相对wait的可选性更多 参数 pid 为-1时代表着等待任意一个子进程大于0时代表等待指定pid的子进程 status输出型参数可以通过它获取子进程的退出状态退出码或终止信号 options等待方式当options为0 时代表父进程阻塞式的等待子进程退出当options为WNHANG时代表着非阻塞等待当waitpid返回值为0时说明子进程还未退出父进程不会一直在那等待而是会去干别的事父进程会以轮询的方式来获取子进程是否退出。 返回值等待成功返回等待的子进程的pid 失败返回-1 阻塞等待
笼统的理解
所谓的阻塞式等待就是将waitpid中的参数options设置为0那么父进程就会一直停留在waitpid()这条语句这里什么也不干就是干等着子进程退出之后再往下执行后续代码。
系统的理解
父进程阻塞式等待子进程就是当子进程未退出时操作系统会将父进程的PCBtask_sttuct)放到子进程的等待队列当中根据前面的进程状态知识可以知道一个进程等待着某种资源就绪的状态叫做阻塞状态子进程在等待着父进程退出就是父进程等待着资源就绪只要子进程不退出那么父进程就会一直再其等待队列中直到子进程退出操作系统才会将父进程继续放回运行队列往下运行后续代码
非阻塞等待
设置非阻塞等待的方式就是将waitpid()中的options设置为WNOHANG即可那么父进程就会去询问子进程是否退出如果退出了就返回子进程的pid未退出返回0出错返回-1
通过这种返回就可以让父进程去以轮询的方式去询问子进程是否退出如果退出就将其回收否则父进程就可以去干其他事情而不是一直在原地阻塞着硬等着子进程退出非阻塞等待的方式可以解放父进程的时间
进程替换
为什么要有程序替换呢
子进程被创建出来是共享着父进程的代码的并且会从fork()之后的代码处开始往后执行那么执行的是和父进程一样的代码这是没有什么意义的 我们通常是创建子进程去让子进程去完成其他的工作比如让子进程去执行其他的可执行程序那么这里就需要用到进程的替换。
原理是什么
进程替换的原理就是当进程调用exec系列的进程替换函数后当前进程的用户空间的代码和数据就会全部被新的程序所替换接下来就会执行的是新的程序的代码
注意进程替换只是将一个进程的用户空间代码和数据用新的程序的代码及数据来替换整个过程中是没有创建新的进程的原进程的pid是不变的变的只有代码和数据 六种程序替换函数 execl() int execl(const char *path, const char *arg, ...);函数名中的l代表list,指代传执行程序的方式是按列表的方式传参的 参数列表 path代表着要执行的程序的绝对路径 arg:执行该程序的方式这里的arg是一个可变参数列表。执行指令方式的多个字符串都可以被arg接收但是传给arg的最后一个字符串必须是NULL 例如执行ls 命令时 我们敲的是 ls -a -l 那么让ls去替换子进程时首先path 就是传的ls的绝对路径/usr/bin/ls)剩下的就是我们的执行方式 ls -a -l 那么arg就得是“ls “-a” “-l”,当然最后得串一个空代表执行命令结束了所以传给arg的是ls,“-a”,“-l”,NULL 执行结果子进程执行了lsls是一个命令 也是一个程序!) execlp int execlp(const char *file, const char *arg, ...);解释 函数名中的p代表着PATH的意思带p的替换函数就会自动去搜索环境变量PATH 所以在带p的替换函数中执行某个程序的时候就可以不用传某个程序的绝对路径只需传其程序名即可程序替换函数会自己去环境变量中去找这个程序。 参数列表 第一个就是替换的程序路径不用写全第二个就是可变参数列表接收的是执行的方式同execl. execle int execle(const char *path, const char *arg,..., char * const envp[]);解释 函数名带l说明传参方式是列表 函数名中的e代表着environment的意思也就是环境变量了。execle函数可以自己组装环境变量。 execv() int execv(const char *path, char *const argv[]);解释 path代表着执行的程序的绝对路径 argv代表着执行替换程序的方式用数组的方式传参。 execvp 解释 execvp和execlp只相差了一个字母一个是l,一个是vl代表list(列表v代表vecor数组 也就是带l的执行程序的方式是以列表的方式传递的带v的则以数组的方式传递 execvpe 解释 类比execle和execlp带v可知execvp的程序的执行方式以数组形式传递带p可知其会自己搜索环境变量PATH,带e可知该函数可以自己组装环境变量。 execve 上面的六个函数都是语言封装的execve函数 execve是系统调用 注意
exec系列的函数的参数args是接收的程序执行的方式其必须以NULL结尾
exec系列函数用法总结
函数名args(执行方式)的传参形式是否带路径PATH)是否使用当前环境变量execl(list)列表否是execlp列表是是execle列表否自己组装环境变量execv(vector)数组否是execvp数组是是execvpe数组是自己组装环境变量execve数组否自己组装环境变量