• 【Linux】进程状态|僵尸进程 |孤儿进程



    在之前我们听过很多很多进程的状态,像是运行、新建、就绪、挂起、阻塞、等待、停止、挂机、死亡等等。

    推荐阅读:通俗易懂帮你理清操作系统
    接着上回说到了操作系统的先描述再组织,在操作系统内部用一个个的结构体来专门管理这些硬件,以及我们写好一个程序它要从磁盘加载到内存也是一个一个的PCB结构体来管理和维护的,
    在这里插入图片描述
    所谓进程状态就是进程内部的属性,这些属性都在task_struct内通过一些int类型的整数来表示例如:1代表运行状态、2代表停止状态、3代表挂起状态、4表示死亡状态等等。就绪和新建比较简单说的是这个进程PCB刚刚被创建好。

    运行状态:

    那么有这么多需要管理的硬件和进程,一般我们只有一个CPU怎么办呢?
    其实CPU维护了一个进程队列,让这个进程在CPU上运行本质上是将该进程的task_struct结构体对象放入运行队列中!进程PCB在这个运行队列runqueue里,就是运行状态,不是这个进程正在CPU上运行,才是运行状态。

    阻塞状态

    我们知道CPU很快但是硬件很慢,而很多的进程又或多或少的要访问硬件(printf访问显示器、文件操作要访问磁盘等等),不能认为进程只会等待(占用)CPU资源,进程也可能随时随地要外设资源。但硬件也少,很多进程都要访问同一个硬件怎么办呢?其实每一个硬件对应的结构体里也有等待队列,当CPU的一个进程假如说(fwrite)需要访问磁盘的时候,而恰好这个时候别的进程也在访问,磁盘还没有被准备好去让fwrite去访问,那么这个fwrite对应的PCB结构体对象就只能从CPU的运行队列里剥离出来,去磁盘的等待队列里这个PCB结构体对象此时的状态,所以这种进程PCB结构体对象等待外设的状态就是所谓的阻塞状态。
    在这里插入图片描述
    等磁盘准备好了之后,操作系统需要把fwrite对应的PCB结构体对象的状态改为运行状态,然后再把它放到CPU的运行队列里,此时操作系统就不需要管了,接下来的工作由CPU去做了。

    挂起状态

    假如说由于下面三个进程因为外设没有准备好而处于阻塞状态,短期之内这些代码和数据就不会被执行,也就是不会被CPU调度,需要等待外设资源就绪才可以,那么加载进来的代码和数据依旧要占用内存,那么万一内存空间不够了怎么办?把代码和数据暂时保存回磁盘上,但他的内核数据结构PCB不大,还在内存中。此时就节省出来一部分空间再去运行别的程序了,所以一个进程暂时把它的代码和数据换出到磁盘的这种进程我们叫做该进程被挂起了,等什么时候这个进程可以被执行了,那么就可以再把这部分磁盘中的代码和数据再换回到内存中,把PCB再改为运行状态,进入CPU的运行队列里,再被CPU运行。把这种将进程的相关数据,加载或者保存到磁盘的行为叫做内存数据的换入换出。
    在这里插入图片描述

    所以所谓的进程不同的状态,本质是进程在不同的队列中,等待某种资源

    看看Linux是怎么做的

    其实Linux进程PCB是一个比较大的结构体,这里就把它内部的表示状态的内容节选下来。

    static const char * const task_state_array[] = {
    "R (running)", /* 0 */
    "S (sleeping)", /* 1 */
    "D (disk sleep)", /* 2 */
    "T (stopped)", /* 4 */
    "t (tracing stop)", /* 8 */
    "X (dead)", /* 16 */
    "Z (zombie)", /* 32 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行状态R

    • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

    首先我们写一个这样的程序,查看现象:
    在这里插入图片描述
    上面这个死循环没有访问外设,运行起来查看他进程的状是R状态(运行状态)。
    在这里插入图片描述


    睡眠状态S

    • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
      (interruptible sleep))。其实也就是Linux下的阻塞状态。

    还是上面的那个程序我们把表示注释的//去掉,让它持续不断地打印1,就会发现这时候进程的状态就变成了S状态,因为printf本身要访问显示器是外设,是外设就比较慢,本身打印的话很快就打印了,但是等显示器就绪要花比较长的时间,这样的进程有99%的时间都是在等IO就绪,1%在执行打印代码,所以其实大部分访问外设的进程的状态基本都是S状态
    在这里插入图片描述


    停止状态T

    • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

    在命令行输入kill -l,就可以看到kill下其实是有很多选项的
    在这里插入图片描述
    其中第19个选项,找到对应的pid号输入kill -19 pid号或者ctrl+z都是使当前进程停下来的方法。
    在这里插入图片描述

    查看进程的状态的时候就发现它是T状态。
    然后再使用kill -18 pid号可以再让这个进程运行起来,
    在这里插入图片描述
    但是此时无法再被暂停的,在命令行输入命令也无济于事,细心的小伙伴可以发现虽然暂停前和暂停后都是S状态,但之前的S后面跟另一个‘+’,后面那个却没有’+',那么当前没有‘+’的状态就是后台进程,有‘+’的就是前台进程。这样的后台进程只能用kill -9 pid号来杀掉当前进程。
    在这里插入图片描述
    至于上面说到的挂起状态,可以看到似乎并没有一种叫做挂起的状态,那是因为操作系统认为挂起状态不需要暴露给用户,用户只需要知道进程在不在运行、是不是休眠、是不是暂停、是不是退出了也就可以了,就算让用户知道也没有任何意义和作用,所以在这么多状态里就没有一种叫做挂起的状态。

    其他状态:


    • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。D状态也叫深度睡眠状态,相比于上面的S状态,D状态是无法被kill -19 pid号或者ctrl+z来暂停的,在该状态的进程无法被操作系统杀掉,只能通过断电或者进程自己醒来来解决。
    • t (tracing stop)状态举一个例子来说就是当我们用gdb调试起来的时候,此时他的状态就是t状态。

    从上面你可以看出来那么R就是运行状态、S是一种浅度睡眠其实它就是一种阻塞状态、D也是一种阻塞状态,t、T它都是阻塞状态只不过这些阻塞状态是为了满足于不同的应用场景。

    • 僵尸状态Z:进程退出的时候,不能立即释放该进程对应的资源!需要保存一段时间,让父进程或者0S来进行读取!这一段时间内这个进程的状态就叫僵尸状态。

    • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态,是把进程PCB回收之后的状态。也就是说一个进程正常退出的时候是先是Z状态,然后是X状态


    下面这幅图来描述这几个状态之间的关系。
    在这里插入图片描述

    两个特殊的进程:

    僵尸进程

    当一个进程运行完毕时,它所占用的内存资源和执行的退出状态需要被它的父进程回收。如果父进程没有回收这些东西,这个进程就得是一个僵尸状态。如果父进程长时间不回收进程数据,那么这个进程将长期处于僵尸状态,其PCB和代码依旧存储在内存中,占用空间但是不会被执行。

    我们写一个代码:
    在这里插入图片描述

    父子进程同时运行,此时杀掉子进程,子进程就停止运行了,但此时父进程还在忙着执行自己的代码,也就回收不了子进程的空间与返回状态,所以此时子进程就处于僵尸状态了,注意下面大写Z。
    在这里插入图片描述

    对于僵尸进程的理解:

    • 进程的内存占用与退出结果对操作系统而言十分重要,不管成功还是失败,子进程都要告诉它的父进程自己的任务完成得怎么样了。但如果父进程一直不读取这些数据,那子进程既不执行又占用空间,会一直处于Z状态。
    • 只要进程没有死亡进程基本信息就不会被清除,保存数据的PCB就会继续被操作系统维护。所以从上面可以看到即使把子进程杀掉,他的进程信息依然存在,那么就可以得出结论:僵尸进程不会自动退出释放所有资源,也不会被kill命令杀死
    • 如果一个父进程创建了很多子进程,子进程先于父进程之前执行完毕那么父进程是不会回收子进程的,进程的PCB和执行完毕或者因错误终止的代码就会一直保留在内存中,就会造成内存资源的浪费。这很类似于我们C/C++中的内存泄漏问题

    孤儿进程

    孤儿进程正好反过来了是指父进程先于子进程退出,父进程死亡,子进程就没有了父进程,这时的子进程就会变成孤儿进程。但是父进程的死亡不代表孤儿进程就没有了父进程,每一个孤儿进程都会被一号进程也就是操作系统领养。这种被领养的进程就叫孤儿进程。

    用上面相同的代码,这次我们干掉父进程,同时查看进程状态。 我们可以发现子进程的父进程pid变为了1,也就是说子进程会被1号进程收养,并在子进程执行完毕后回收所对应的资源。
    在这里插入图片描述
    同时我们也可以看到,子进程状态从之前的S+变为S,也就是说从这个被收养的子进程变为了一个后台进程了,也就是不能用ctrl+z暂停,只能用kill -9来杀掉这个进程。

  • 相关阅读:
    nginx实现vue和后端的双机负载
    荐书丨《哥德尔、艾舍尔、巴赫书:集异璧之大成》:机器人与音乐的次元壁破了
    go的堆内存结构分析
    LVS负载均衡-TUN模式(模拟不同网段)
    全世界 LoRA 训练脚本,联合起来!
    从2022投影行业最新报告,读懂2022年家用智能投影仪该怎么选!
    给程序员准备的“蜜糍”--SOD框架简介
    Python 全栈系列200 Redis Agent
    docker常用命令总结
    LeetCode 59. 螺旋矩阵 II
  • 原文地址:https://blog.csdn.net/qq_55712347/article/details/128156164