Linux相关概念和重要知识点(9)(父进程、子进程、进程状态)

1.父进程、子进程

(1)父进程

CLI本质上是一款命令行界面的软件,是用户调用接口层面的程序(上层,可以和系统调用接口做沟通),CLI和GUI是同级别的。用户的操作都是建立在CLI和GUI之上的。

但是CLI是一种界面类型的称呼,它实现的具体软件统称叫Shell(命令行解释器),所有的Shell都是命令行交互。为满足不同需求,Shell还有很多子类,他们有着不同的特点、指令、优势以满足不同用户的需求。其中bash就是Linux的Shell(命令行解释器),我们也可以说bash就是Linux的CLI。

当我们在Linux里登录之后,bash进程会最先加载(要满足用户的命令需求),因此它是我们用户登陆后最先出现的进程(它不是系统最父的进程,它本质也是systemd或init的子进程)进程的创建遵循树的结构,也就是bash是用户所有运行进程的父进程,每个子进程也可以创建自己的子进程。

当查看进程状态时,我们往往关注当前进程、它的子进程以及它的父进程,pid就是标志当前进程的标志,ppid就是标志其上一层的父进程的pid,我们也可以使用getppid()获取它,并查看它是谁。

运行起来是

我们可以看到,当我们用户直接启动程序时,是bash父进程分配一个子进程来帮助我们处理任务。也能看到bash进程也不过是一个当前用户的父进程而已,其pid也没什么特别的。事实上,每当我们打开一个用户,systemd或init都会创建一个新的bash为当前用户处理命令,同时成为用户下所有进程的父进程。

我们可以看到,我们打开了三个用户界面,自然需要三个bash进程来帮助我们处理命令行(时刻注意bash的本质是命令行解释器的进程,并且是因为它最早出现,所有进程都是在它的界面下启动的,因此才成为用户的父进程)

我们会注意到当我们启动./proc时,进程属性COMMAND那里显示./proc,COMMAND的意思是显示当前正在运行的进程对应的可执行程序的名称。因此我们grep bash查找到的就是正在运行bash程序的进程,也就是每个用户的最父进程。同时,因为这三个用户都是登录Shell创建的,所以前面有个-,显示为-bash。

总结:每当用户登录Shell时,Linux的systemd或init会自动创建一个专属的bash来提供命令行交互服务,同时因为其出现最早、用户的子进程都是在bash基础上创建的,所以每个bash充当对应用户的所有进程的父进程。进程的创建遵循树状结构,创建子进程是一种手段,其目的是为了保障父进程的安全(如果子进程出现问题,父进程不会直接受到影响,体现进程独立性),同时也分摊了任务,保证高效率完成

(2)创建子进程

按树状结构来讲,每个子进程都能创建自己的子进程。我们可以使用fork()来实现。fork()函数体内会自动帮我们创建一个子进程(代码数据 + PCB),之后运行起来。

fork()创建子进程并不会从磁盘中再拷贝一份代码和数据,而是从父进程那里拷贝。代码和父进程共享;数据单独保存一份,以实现进程与进程之间互不干扰,即独立性;PCB在拷贝的基础上稍加修改(如pid,ppid等属性)得到

结果是

我们发现,fork()函数体内部创建子进程之后,两个进程同时运行,当执行到return语句时已经是两个进程了,所以fork()能对应两个进程返回两个值,并且fork()根据自己的判断给子进程返回了0,给父进程返回了子进程的pid。这样父进程能够管理子进程,而子进程不需要关注太多,所以得到了0

我们还能验证父进程在fork()内创建子进程时,共享代码,独立数据,因此父进程的count一直没变,而子进程的一直在变。我们要深刻体会多进程的特点,就是各走各的(独立性),根据自己进程的数据来进行代码逻辑判断,互不干扰,所以你会发现两个while都能被执行,这在单进程里是不可能的。在多进程中,对于父进程,它在第一个while判断不成立,第二个成立,进入第二个while;对于子进程,第一个while就成立,所以执行。通过它们打印的信息,我们也能看出子进程的父进程就是创建它的那个进程,即子进程可以创建子进程,自己变成父进程。

我们进一步还能发现子进程和父进程的执行顺序其实不完全规律,有的时候一个进程连续执行多次。这是因为子进程PCB链入执行队列struct runqueue之后,调度器会通过管理runqueue来管理PCB。CPU调度器引入时间片保证尽量公平处理进程(OS自主决定),但不会绝对公平。

2.进程的状态

(1)操作系统进程的状态

在所有操作系统中,进程的状态主要分为如下几种,在不同操作系统中有些许不同。

创建:PCB + 代码和数据;就绪:创建好的进程链入调度队列(struct runqueue);运行:调度器将PCB交给CPU执行;阻塞:运行时有事件发生(I/O请求,等待竞争资源);终止:进程任务完成

①就绪、运行状态

struct runqueue队列包含int nums、PCB等。当CPU要执行时,调度器直接找struct runqueue,执行一个进程之后再链接到队尾,这是基于队列先进先出(FIFO)的原则以及根据时间片切换进程才实现的。在有的操作系统中,就绪状态和运行状态统称为运行状态,因为只要是在runqueue中,都意味着进程时刻会被调度器调度。

②阻塞状态

操作系统要管理底层的硬件,根据先描述,再组织的原则,内存中会创建struct device描述不同设备以及属性,struct device之间用数据结构联系起来,最后以struct devices*作为管理接口,OS通过对struct devices*的管理实现对硬件的管理。

每个struct device里面有一项成员变量PCB* wait_queue,当CPU处理时获得I/O请求时,调度器就会将整个PCB从struct runqueue中拿出来,通过PCB* wait_queue链入到要进行I/O操作的设备的struct deveice中。这样整个进程都被阻塞了,如果是代码也不会继续执行了,因为CPU需要进程的PCB才能操作,而此时PCB在设备队列里。运行状态和阻塞状态的本质区别就是PCB链入的队列不同。在进程阻塞时,CPU仍可以继续执行其他runqueue里面的进程而不受影响。

当I/O设备相应将数据写入或读取后(要成功读取完或写入完才行,而不只是响应),OS就会通过struct devices*得到信息(操作系统是硬件管理者),再次将PCB移回runqueue,此时进程的状态再次被切换为运行状态。

注意每次进程状态切换时,PCB内部的相应成员变量的值就会变,不同值对应不同进程状态,OS通过这个值能正确判断进程的状态,进而实现不同操作。

所以每次进行scanf时,就能看到进程被卡住了。对于连续几百行printf而言,我们则几乎看不到运行状态,因为CPU处理速度太快了,缓冲区能瞬间写满(内存相对较快),大部分时间PCB都是在阻塞状态等待硬件响应并将缓冲区的数据写入硬件(硬盘相对较慢)。

③阻塞挂起状态

当内存资源严重不足时,由于进程在阻塞期间CPU不调度,这个时候操作系统会把进程的对I/O无影响的代码和数据换出到磁盘里(进程 = PCB + 代码和数据,PCB保持不变);当I/O完成后更改状态,并把磁盘的代码和数据换入到内存里,再将PCB从struct device链入runqueue。硬盘中专门存储换出数据的磁盘分区叫swap分区。这种进程的状态叫阻塞挂起状态,是在阻塞的前提下将代码和数据换出到swap。

运行时挂起风险大,OS一般没开启。挂起状态 + swap分区是用时间换空间,swap分区不大,可以在装系统时自定义。云服务器上一般会禁掉swap分区,大型企业的空间比时间宝贵,因此要么优化软件要么买硬件。

阻塞挂起进程终归是一个紧急处理办法,它只能缓解当前操作系统内存紧缺的问题。在内存紧缺到一定程度时,OS会选择直接杀掉一些进程,OS会优先保证自己安全。

(2)Linux的进程状态

①R(running)运行状态:只要PCB在runqueue里都属于运行状态

在STAT那里,我们可以看到R状态,其中R+的加号表示前台程序当前台程序运行时,命令行解释器无法正常执行其它指令,就像Windows的前台任务那样,一次只能执行一个。

我们可以使用Ctrl + c来杀掉前台进程。除此之外,使用kill我们有更多管理进程的选项,其中kill -9 pid就是强制杀掉进程(包括前台和后台),kill -l可以查看选项

②S(sleeping)睡眠状态(阻塞状态,可中断睡眠,浅睡眠

当进入有多次连续I/O请求时,休眠状态就很频繁了,因为CPU将信息写入到内存缓冲区很快,但内存缓冲区满了就要刷新到显示器文件里,这个过程就很慢了,相较于内存读写至少慢了一个数量级。因此PCB大部分时间都是在等显示器响应并写入(写完了才会变成R+)在这种频繁打印的情况下就能看到S状态。

就算是S状态,它也依然是一个前台程序,前台程序运行过程中,我们的指令时无法执行的

③D(disk_sleep)磁盘睡眠状态(阻塞状态,不可中断睡眠,深度睡眠)

当有I/O请求时,PCB被列入磁盘等待队列struct device中等待响应和写入(读取),进程该状态为S,这似乎没什么问题。但是当操作系统资源严重不足时,挂起状态无法缓解内存不足时,操作系统为了自身安全有可能直接杀掉这种磁盘级别的I/O进程,这样就会导致数据丢失。

为了防止这种数据丢失,就有D状态,当进行磁盘级别的I/O时,PCB就会把自己的状态设置为D。当内存严重不足时,OS也会根据状态判断忽略状态为D的进程,转而去杀其它进程。这就像一个附身符一样。

只有在安装大型软件(几十或上百G)或者进行大量数据迁移时才会较长时间出现D,在日常使用中D状态几乎是瞬时的,硬盘只是相对内存慢,SSD的读写速度也能达到每秒几个G。

④T(stopped)暂停进程

暂停进程意味进程做了非法但不致命的操作,被OS暂停。我们可以自己人为暂停,本质上也是我们人为发现进程做了非法但不致命的操作才暂停的,如果真的致命了我们肯定也是杀进程。使用kill -19 pid即可让代码暂停执行。

kill -18 pid可以恢复(重启)进程,注意恢复不是从第一行代码开始执行,而是接着停下来的代码继续执行

T状态重启后进程的状态会发生改变,会由前台进程变为后台进程

这里的S没有加号可以印证这一点。这就像我们Windows里面的后台下载任务一样,前台被腾出了位置,我们的指令可以正常执行了。

我们甚至可以让前台任务和后台任务一起执行,也不会冲突

后台任务是不能通过Ctrl + c来杀掉的,它只能杀掉前台进程,我们只能用kill -9来帮我们强制杀进程

⑤t(tracing stop)追踪进程

我们要使用循环指令来监视追踪进程,其中while : ;do (指令) ;done可以实现

调试打断点的时候,进程就被设置为了跟踪进程,人们可以自己掌控代码的执行情况。

⑥Z(zombie)僵尸进程

子进程被父进程创建的原因是基于某种应用,为了完成某项任务。当子进程的任务完成后,它需要向父进程传递信息,告诉父进程子进程的任务完成的怎么样(进程的退出信息),父进程才可以即时调整安排。完成告诉父进程任务完成的怎么样的这个任务是由PCB来完成的。

创建进程时PCB先被创建,代码数据才被导入。在进程结束之际,代码数据先被OS回收,PCB最后被销毁。PCB先创建是为了能在数据代码导入后能第一时间管理。而PCB后回收则是为了保障PCB里存储的关于该任务完成情况的信息被父进程读走,而在PCB等待父进程读取信息之前,代码和数据就已经被释放了,这个阶段就叫僵尸进程。当进程进入Z状态后,严格上来说它已经不再是一个进程了(进程 = PCB + 代码和数据,缺一不可),Z状态的进程只是一个仅有PCB信息的躯壳,我们也无法kill掉一个已经进入Z状态的进程(进程本质上已经死了)。

new、malloc的堆区空间也叫代码和数据,只要进程进入Z状态,它就会直接被系统释放,并不会造成所谓内存泄漏。内存泄漏一般是指那些无法退出、需要长期运行的程序(如杀毒软件,常驻内存进程),对于那些进程而言如果不及时释放数据,确实会一直增加内存占用,但那种一跑起来就退的进程有点内存泄漏其实不影响。我们只关注常驻进程的内存泄漏

但现在的问题是强调僵尸进程的意义何在?

对于由系统父进程创建的子进程而言,Z状态基本上就是一瞬间的事,但对于我们自己创建的进程,情况就有些不一样了。

当我使得父进程陷入死循环,子进程结束时就会向父进程发送信号,而父进程处于大量I/O阻塞中,会错过信号,子进程的PCB就无法被读取,就会陷入Z状态。

这种情况下,子进程的PCB就无法被释放。PCB本身也是一个占用较大的结构体,这样的Z进程多了,也会造成计算机卡顿。<defunct>就标志着僵尸进程或死进程,这种进程无法通过kill删除掉(可以kill让子程序进入僵尸状态,因为它们已经死了。

这个时候只有对父进程进行管理才行

还有一种情况是父进程比子进程先结束

我们可以看到子进程的ppid变成了1,这是系统的systemd进程。当父进程比子进程先结束时,父进程会被直接回收,而子进程就会交给系统接管(systemd领养),这种进程称为孤儿进程,孤儿进程会退到后台运行

注意僵尸进程不会被systemd领养,因为僵尸进程是有父进程的,如果这时父进程结束,两个进程会一起回收,不会出现领养的情况

⑦X(dead)死进程

这个进程状态是一瞬间的,当PCB信息被读取后,就会进入X状态,OS会直接回收PCB。此时就标志一个进程彻底结束了。

echo $?可以获取最近一个进程退出时的信息,一般情况下0表示正常执行,非0表示出错

这个数字其实就是C语言main函数的返回值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值