引言:
北京时间:2023/3/21/7:17,这篇博客本来昨天晚上就能开始写的,但是由于笔试强训的原因,导致时间用在了做题上,通过快2个小时的垂死挣扎,我充分意识到了自己做题能力的缺陷和运用新知识的缺陷,所以我需要把重心给转移一下了,以后做题才是我的头号目标,虽然我在很久以前可能就说过这样的话,但是这次我是认真的,就算我做题不行,但是我看代码还是行的,所以我每天必看一些做题代码,来加深自己对知识的运用,希望不久之后,能有所进步吧!哪怕只是一丝丝!所以今天我们就来学习上篇博客剩下的有关进程控制的内容,和有关进程程序替换的知识吧!
复习进程等待
昨天已经浅浅的了解了什么是进程等待,进程等待的目的就是为了终止僵尸进程,回收僵尸进程所占的资源,并且获得进程的退出码和退出信号,并且我们了解了进行进程等待的两个接口函数wait/waitpid,通过这两个接口,我们就可以去调用操作系统提供给我们的系统调用,这样就可以利用系统调用的形式来完成进程等待了;我们昨天已经知道使用wait接口就是用来等待一个进程的子进程,并且有代码演示,这里不多做理解,昨天我们也了解了waitpid这个接口,发现该接口有三个参数,并且其中第一个参数pid,它的作用和wait的参数差不多,本质就是为了等待一个进程,pid=0等待的是该进程的子进程,pid>0,等待的则是任意一个pid相等的进程,和第二个参数status,我们了解到,该参数是一个位图结构的参数,不单单是整形类型,本质为位图结构就是为了可以同时从一个参数上获取退出码和退出信号两个码值,第三个参数options,就是用于判断子进程是否正常终止,父进程是否继续等待的问题,所以接下来,我们就承接这上篇博客,把剩下的有关位图结构,也就是有关status参数的知识再深入学习一下,如下:
继续谈status
上篇博客,我们了解到status是一个位图结构,可以同时返回退出码和退出信号,并且退出码是由32个比特位中的后16个比特位中的次第8位构成的,退出信号是由最后7个比特位构成的(core down先不谈),所以按照这个逻辑,此时我们想要拿到,status中的退出信号和退出码是有办法的,没错,就是使用我们在C语言中所学的位运算的概念,具体这里不多做讲解(具体就是左移和右移),左移以为表示的就是该值乘2,右移一位表示的就是除2,并且还涉及到了按位或、按位与、按位异或的知识,如下图:
通过上述的 (status >> 8)& 0xFF、status & 0x7F 此时我们就可以获得该进程的退出码和退出信号了,但是要知道,上述获取子进程退出码和退出信号的方式,是我们自己通过位图结构的概念,通过位运算获得的,但是注意,在Linux操作系统内部,它本身是为我们提供了获取子进程退出码和退出信号的方式,如下图:
所以按照我们上述所说,父进程就可以通过使用waitpid接口,来调用系统调用,从而获取子进程的退出码和退出信号,但是具体在调用系统调用接口的时候,本质上这些工作还是由我们的操作系统来完成了,因为只有操作系统才有获取进程退出码和退出信号的资格,所以此时就有一个问题,就是父进程具体是如何通过操作系统来获取子进程的退出码和退出信号呢?
首先明白,无论是父进程还是子进程,它们都是一个独立的进程,所以每个进程都拥有自己的地址空间,进程pcb、页表和物理内存等资源;并且明白,子进程pcb内不仅有从父进程pcb中继承的相关属性,而且还有两个和子进程退出码和退出信号至关重要的属性 (int exit_code ,int exit_signal) ,从该属性的名字就可以看出,这两个子进程pcb属性代表的就是进程退出码和退出信号,表示的仅仅就是两个整数而已;
明白了这些之后,此时就可以回答该问题了,如下:
当子进程执行完毕之时,操作系统会将main函数对应的返回值写到该进程pcb的int exit_code当中,如果该进程异常,则将对应的退出信号写到该进程pcb的int exit_signal当中,并且最后在进程退出之后,操作系统会将该进程的pcb维护起来,所以当该子进程pcb被操作系统维护起来之后,此时父进程就可以通过调用系统接口 wait/waitpid 通过操作系统来去找到该退出进程对应的进程pcb,可以找到子进程pcb的具体原理是因为,父进程在调用waitpid接口的时候,已经把子进程的pid告诉了父进程(也就是传参的时候已经把进程子pid给给了waitpid接口),所以最后通过子进程pid,进而找到该子进程的pcb,进而找到pcb中的该子进程的退出码 int exit_code 和退出信号 int exit_signal,最后再利用waitpid中的第二个参数status(输出型参数)将退出码和退出信号返回给父进程(当然也就是上述的ret_id变量)
总:父进程获取子进程的退出码和退出信号的本质就是通过操作系统读取子进程的内核数据结构(pcb)
并且有的同学非常的好奇,就是有没有一种可能,就是父进程在等待的时候,子进程一直没有退出呢?或者说父进程在等待的时候具体是在干什么呢?
所以此时我们就借着上述问题来谈一谈,什么是阻塞等待和非阻塞轮询等待
首先我们要明白,当子进程没有退出的时候,父进程在干什么呢?
谈到这个,此时又不得不谈谈waitpid的第三个参数,options,上篇博客我们说了,这个参数的作用就是用于判断子进程是否正常终止,父进程是否继续等待问题,并且该参数我们默认给给的是一个叫 WNOHANG 的参数,具体意思:就是用于判断该子进程是否终止,如果该子进程未终止,那么此时waitpid()函数就返回0,如果该子进程终止,则返回该子进程的pid,此时就可以很好的通过这第三个参数来判断一个进程的子进程是否处于终止状态,所以明白了这点,此时就可以明白父进程是可以通过该参数来控制自己的行为,也就是上述所说的阻塞等待和非阻塞轮询等待!
谈谈父进程的阻塞等待
从上述的知识,我们可以知道,如果waitpid()函数返回0,表示的就是该子进程没有结束,子进程没有结束,那么父进程此时可以干什么呢?首先第一种情况,父进程一直在调用waitpid进行等待 ,此时的一直进行等待,本质上表示的就是阻塞等待,也就是表示父进程此时从本来的正常运行状态变成了一个阻塞状态,变成了一个等待子进程的状态;所以我们可以明白,当父进程处于阻塞状态之时,父进程是不在CPU的运行队列之中,而是在某一个阻塞队列之中;并且此时再深入理解,还可以明白,父进程处于阻塞等待之时,子进程处于运行状态,那么等子进程执行完之后呢?父进程如何从阻塞状态重新变成运行状态来为子进程收尸呢? 谈到这个问题,此时就又涉及到了,子进程的进程pcb,在子进程pcb内部,我们可以理解存在一个指向父进程的指针,所以当子进程一但退出(注意此时父进程肯定是不在运行队列,而是在阻塞队列的),此时子进程就可以利用pcb中指向父进程的指针,重新找到父进程(唤醒),此时父进程检测到之后(被唤醒),就又会从阻塞状态变成运行状态,然后被操作系统又重新链接到运行队列之中,最后父进程继续执行 wait/waitpid,最终就将子进程给回收
所以上述就是父进程阻塞等待全过程
谈谈父进程的非阻塞轮询等待
搞懂了上述父进程的阻塞等待,此时就可以明白父进程肯定还有一种非阻塞等待(毕竟我是父进程,你是小子,我才是老子,我凭什么一定要等你呢?)所以,此时还是同理,当我们通过waitpid()函数的返回值,判断出该子进程还没有结束时,我们就可以让父进程不需要一直进行等待(不需要一直处于阻塞状态),具体代码如下:
所以此时通过上述的代码,我们就可以很好的理解什么是父进程的阻塞等待和非阻塞轮询等待了,并且可以将其很好的抽象成一个生活中场景,如下:
场景一:
周末出去玩,李四打电话给张三,问张三去不去,此时张三说去,但是要准备一下(子进程未终止),李四说好,我等你(父进程等待),但是电话别挂(阻塞等待)
场景二:
如法炮制,还是周末出去玩,李四打电话给张三,问张三去不去,张三还是去,并且也还是需要准备一下,此时李四还是说好,但是此时李四却把电话给挂断了,在电话挂断之后,李四想了想也去准备了一下,过了一会之后,李四准备的差不多了,就又打电话给张三,张三却还是没有准备好,此时李四就又把电话挂了,又去准备东西了,同理,李四一直打电话问张三好了没有(非阻塞轮询等待),而张三却一直没好,直到最后好了(子进程终止)
所以上述的两种情况(李四给张三打电话,表示的就是父进程使用waitpid()等待子进程,)第一种情况电话不挂表示的就是父进程处于阻塞队列,这种情况也就是阻塞调用;第二种情况,表示我打完电话,问完情况,我们把电话挂掉,此时表示的就是父进程不处于阻塞等待,也就是表示该调用,此时是属于非阻塞调用,(非阻塞调用的好处,就是父进程可以干自己想干的事情,例:准备自己的东西)
所以上述的两种场景就是父进程阻塞等待和非阻塞等待的两个现实生活抽象场景,可以非常好的凸显出父进程等待的两种情况
总:父进程可以通过参数进行判断,然后来控制自己的行为,也就是上述所说的阻塞等待和非阻塞轮询等待!
总结:进程控制的三个话题:进程创建、进程等待、进程终止此时就算告一段落啦!现在让我们呢一起开始新知识的学习吧!
程序替换的原理
首先明白,创建一个进程,该进程肯定是有对应的pcb、页表、地址空间和物理内存,所以我们就从这些进程的必要条件入手,来看看进程替换到底是什么,如下图:
从图中,我们可以了解到以下知识:
当前进程的代码等数据是需要通过页表映射到物理内存的特定区域,所以当一个进程执行了部分代码后,当其执行到了系统调用接口(execl等……),进程此时就会根据我所传入的程序的路径(Linux操作系统下的bin目录)和我要执行的程序的名称和选项(“ls” “a”),然后把磁盘当中(通过对应的路径找到bin)的其它的程序的代码和数据加载到内存,最后用新程序的代码来替换我们原程序中的代码,用新程序的数据来替换原程序中的数据(这个过程不是在地址空间完成的,是在物理内存中完成的);所以总的来说: 程序在替换时,就相当于原进程的内核数据结构不变,把原进程在物理内存中的代码和数据替换为新进程的代码和数据(从磁盘中加载),此时这个行为的发生就叫做程序替换。
并且要明白,进程在进行程序替换的时候,是没有创建新的进程的,本质还是程序替换的本质。
并且此时明白,可以进行进程程序替换的接口一共有7个,此时我们已学其一,剩下的6个都是同理,只是在使用上不同而已,所以我们今天浅浅的摸了一下程序替换就行了(明天要晨跑),今天博客就这样吧!撤了!
补充小知识点:
fork函数的两个返回值浅显理解:一个是父进程返回子进程的pid,一个是子进程没有子进程所以返回0,感兴趣的同学,可以参看下面这个链接;为什么fork有两个返回值
北京时间:2023/3/21/22:45