linux-进程--02
文章目录
- linux-进程--02
- 查看进程
- 创建子进程--fork初识
- fork特性
- 进程状态
- 运行状态,阻塞状态和挂起状态
- 进程状态具体表示
很高兴和大家见面,给生活加点impetus!!开启今天的编程之路
作者:٩( ‘ω’ )و260
我的专栏:Linux,C++进阶,C++初阶,数据结构初阶,题海探骊,c语言
欢迎点赞,关注!!
linux-进程–02
查看进程
指令:ps axj | head -1 && ps axj | grep xxx
ps是一个指令,作用是查看进程,后面的axj是选项,顺序可以不做区分,随后通过管道将显示的进程的第一行显示出来,此时显示的是表头,同时再对显示的进程进行过滤操作,找到我们想要的进程信息。
有关ps的选项我们后面再来说。
既然ps是查看指令,那么ps是在哪里查看的呢?
我们知道linux下一切皆文件,所以进程肯定会使用一个文件来存储,该文件是根目录下的proc文件。而且该目录中存储正在运行的进程。
proc目录中的进程文件是使用进程的pid进行命名的。
在proc目录下的进程文件中,我们需要知道cwd属性的作用。
情景:当我们fopen打开文件时,如果说改文件不存在,就会在当前路径下创建该文件,什么是当前文件?
一开始没有file.txt文件,这里直接说结论:进程中的cwd属性表示当前路径,由PCB存储,决定cwd的路径是进程启动时的的路径在哪里。我们来验证一下,使用chdir接口来对该cwd属性进行修改,再来看file.txt文件的位置。
这样我们就完成了对cwd属性的修改
创建子进程–fork初识
有子进程就有父进程,那么我们如何创建一个子进程呢–fork函数
前面我们已经学习了如何查看子进程的pid,查看父进程的pid的接口为getppid()。
为什么这里代码执行是这样的我们放到fork的特性中来讲解,这里只需要知道fork能够用来创建子进程即可。
那么父进程的父进程是什么呢?
通过前面我们已知父进程的pid为11338,我们通过id找到进程名称是什么?
指令:ps -p pid -o comm-
结论:在xshell中自己编写的程序和内置的指令程序父进程都是命令行解释器(即bash),bash底层创建子进程的方式其实就是fork函数。而且,同一个父进程可以创建多个子进程,父进程:子进程 = 1 : n。即linux的进程结构也是多叉树结构。
那么我们思考一下,父进程再向上追溯,计算机的第一个进程是什么?操作系统。当我们计算机开机的时候,其实就是在加载操作系统。
fork特性
fork函数其实是有返回值的
如果子进程创建成功,会给子进程返回0,会给父进程返回子进程pid,如果子进程创建失败,返回-1。
既然fork函数给父进程和子进程的返回值不相同,我们就可以利用返回值不同的特点来对父子进程进行分流。
为什么要对父子进程分流,创建子进程的目的是让子进程完成和父进程不同的任务,如果任务相同,那为什么还创建子进程呢?直接交给父进程完成就好。
我们先来以一个例子来讲解:
问题1:为什么给父进程返回的是子进程的pid,为什么给子进程返回的是0?
问题2:为什么return能够返回两次,即返回两个值?
问题3:为什么id一个变量能够同时接收两个值,即又 >0 ,又 =0?
先来解决问题1,给父进程返回子进程的pid的目的是父进程方便对子进程进行管理,而给子进程返回0是表示子进程创建成功了。
问题2:为什么能够返回两次,在之前我们学习算法的过程中,当我们return时,表示该函数的核心逻辑代码已经执行完毕,当fork函数返回时,是不是代表此时子进程已经创建了?是,当只有父进程时,运行父进程中的代码是从哪里来的?磁盘文件,当我们fork子进程,子进程的代码从哪里来?没有地方来,所以父进程的PCB和代码数据就会用来初始化子进程(除了表示进程的属性,如pid)。即代码共享。父子进程会执行同一份代码。所以会返回两次。
问题3:为什么一个id变量能够保存两个值?此处涉及程序空间地址,这里只能浅浅的讲一下原因。
当一个程序编程二进制文件时,程序中的变量会被二进制文件记录下来吗?不会,二进制文件此时存储的是变量的地址。
当一个进程创建时,也会创建一个虚拟地址空间,变量的地址被存储在其中,当创建子进程时,由于父子进程数据共享,使用父进程的PCB和代码初始化子进程,同时也会使得父进程的虚拟地址空间初始化子进程的虚拟地址空间,此时就有两个相同的地址对应到物理内存上。如果对该位置进行写入的话,就会发生写时拷贝,会重新开辟一个新的空间来存储新的值。
理解方式:可以理解为有两个相同的引用变量引用了相同的物理内存空间,该引用变量叫id,当发生写入时,理解为深拷贝创建新的空间。
为什么会写时拷贝需要创建新的空间?为了独立父子进程,因为我对父进程或者子进程进行写入时,不希望影响到其他进程。即增强了进程间的独立性
什么时候发生写入操作?return时,return就是将寄存器中的临时数据move(写入),此时发生写时拷贝。
进程状态
运行状态,阻塞状态和挂起状态
进程状态的大致流程。
首先,在cpu中,有一个操作系统调度队列(runqueue),其中存储的类型是
struct task_struct head*,特性是先进先出,可以类比投递工作,hr管理的是简历,而不是人。在调度队列中的进程,此时就处于运行状态。
阻塞状态我们从两个角度来理解,第一个角度是现象,其次是OS角度理解阻塞。
现象:当我们程序中有scanf的代码时,如果我们一直不输入数据,进程就会阻塞。
从操作系统角度解释。
首先,操作系统的作用是管理软硬件资源,通过先描述,再组织的方式。所以产生struct device的类来描述硬件资源。在该类中含有struct task_struct* waitqueue。当进程处于运行状态的时候,执行scanf,OS发现管理键盘的类中没有就绪(没有数据输入),该进程会从runqueue脱落,连接到管理键盘类的waitqueue类中,处于阻塞状态当用户从键盘上输入数据时,数据就绪,该进程就会被再次链入到runqueue中,此时又处于运行状态了。
结论:阻塞运行的本质,PCB在谁提供的队列中。即一个进程需要什么资源,PCB就会链入到对应硬件的描述结构体所对应的队列中,操作系统等待资源就绪时,进程重新链入运行状态
当cpu正在执行进程中的代码和数据时,突然发现内存空间不够了(如正在下载东西,或者打开一个耗费内存的杀毒软件),如果cpu执行的进程中的代码需要malloc内存资源,虽然内存不够,但是OS还是会允许开辟空间,为了腾出多于的内存,会让某些进程的代码数据拷贝到磁盘上的swap分区,内存中的代码数据就会删除。该过程进程处于挂起状态
特点:PCB还在内存中,代码和数据在磁盘中。
问题1:swap分区是什么?
问题2:如果把进程放在swap分区中,内存还是不够怎么办?
问题1:磁盘中的每一个盘都有一个swap分区,当内存空间不足时,用于与内存中的代码交互,代码数据从内存swap out到swap分区,反之swap in到内存,可以理解为动态扩展内存,一般swap分区时内存大小的1~2倍,swap分区不推荐太大,防止过度swap,因为swap会进行一次IO操作,过度swap导致速度下降。
问题2:比如阻塞状态的代码,此时进程在等待键盘事件,即进程此时什么事都没干,就是在等,所以可以直接将阻塞状态的代码和数据swap out(换出)到swap分区,即阻塞挂起状态
如果内存还不够,OS就会将运行状态的进程也会swap out到swap分区,即运行挂起状态,如果还不够,OS就会杀掉进程,比如:程序突然闪退就是在杀掉进程。
进程状态具体表示
通过上面的学习,我们了解到了进程状态,那么,进程状态的具体表现是什么状态呢?
分为7种状态
R(runing):包含就绪状态 + 运行状态。通过代码观察现象
问题:问什么这里有无printf导致进程状态不同?为什么我在运行这个内存但是却不是R状态而是S状态(睡眠状态)?
printf需要和显示器硬件交互,内存和显示器硬件的交互速度远远小于cpu与内存的交互速度,printf底层遇到\n会调用系统级函数write往显示器上写内容,即刷新缓冲区,printf的内容需要先到达缓冲区,刷新缓冲区内容才能够显示到显示屏上,由于cpu和内存交互速度很快,缓冲区很快就能够填满,所以,进程大量的时间都是在等待显示器刷新缓冲区等操作的。所以是S状态。同理,sleep函数也会和硬件计时器交互,同样也是S状态
S(sleep):可被中断休眠(浅度睡眠),包含阻塞状态
阻塞状态前面我们已经说了,在这里补充前后台进程。请问上面图片,我们在查看进程状态时,状态后面的+号是什么意思?
前台进程是状态后面带+号,反之是后台进程。
两者区别:退出状态的差别。
前面我们强制退出程序都是使用ctrl + c,前台进程可以使用ctrl + c结束程序还可以使用信号的形式。
指令:kill -9 进程pid,-9其实就是一种信号,信号的种类后面会慢慢讲到的。
来看演示:
后台进程只能够使用信号方式终止,使用ctrl + c无法终止,从现象来看,望文生义,在手机中不用的程序我们放在后台,linux中也是如此,使用ctrl + c的话OS不知道需要终止哪个程序。从底层来看,ctrl + c触发一个系统事件信号(中断信号),这种终端触发的信号都会发送给占据前台的进程。
来演示之前,前台进程转后台进程的指令:
./进程名 &
能够发现按下ctrl没有任何反应。只能使用信号中断
D(disk sleep):不可被中断程序,即深度睡眠,包含阻塞状态
情景:当我们进程要求磁盘对数据进行写的操作时,倘若需要写入到磁盘中100M,该进程需要接收磁盘读写的最终结果,看是否读取成功,但是磁盘在写的过程中,,内存突然不够了且到了需要杀进程的地步,OS发现了这个进程一直在等待,OS将该系统杀了。如果磁盘数据读取失败,磁盘会将已经读取的无效数据删除,这样就造成了资源损失。
为了解决该情况,linux中添加了D状态,表示即使内存空间很少很少了,该进程也不能杀。
运用场景:数据拷贝,数据迁移,大量IO。
创建D状态的进程指令:
dd if=/dev/sdx_nonexistent of=/dev/null bs=1M,类似数据拷贝,只不多数据在文件中,if(input file)和of(output file)。这里知道就行了
如果大量进程出现D状态,说明操作系统快废了,因为D状态的进程信号都杀不掉(前面已经说了,防止数据丢失),只能等IO结束。如果说磁盘有问题,一直给不出写的结果,但是该进程就会一直进行,最终资源耗尽,内存泄漏。
T(stoped):暂停状态
暂停状态是错误操作时,进程会直接暂停。
暂停进程指令:kill -19 进程pid
继续进程指令:kill -18 进程pid
这里不过多讲解
t(trace stop):追踪暂停
该状态主要使用在gdb中,gdb中遇到断点就会暂停,可以理解为在断点行发送了kill -19信号,点击n(next,逐语句)等价于发送了kill -18信号。逐过程也如此
在第7行打断点,并且r运行到断点处。
并且此时状态修改
z(zombie):僵尸进程
x(dead):死亡进程
设置进程的目的是完成某个任务,当进程将要结束时,需要检查进程任务是否完成等等退出信息。当僵尸进程被父进程通过OS回收时,进程状态修改为x状态。
所以僵尸进程需要维护自己进程的PCB信息。
来看看僵尸进程,关键是不要被父进程回收:
此时我的父进程一直在循环,没有对子进程进行任何回收操作。如果回收了,z状态就会变成x状态,x状态是一瞬间的,基本上看不到
为什么有僵尸进程?目的是OS检测进程退出信息。
退出信息中包含什么呢?
数字 + 退出信号,如在int main函数为什么最后要返回0,即根据返回值查看代码是否正常进行完毕。退出信号检测由于什么信号退出,收到的信号值是什么
退出信息保存在哪里?进程自己的task_struct中。回收的本质获取进程退出时,该进程中的退出信息并将进程状态设置为x
如何回收?
利用函数,需要子进程pid,所以fork时需要给父进程传递子进程id。
注意:僵尸进程必须回收,因为僵尸进程一直会维护自己的PCB,不回收,造成内存泄漏