【Linux取经路】探索进程状态之僵尸进程 | 孤儿进程

在这里插入图片描述

一、进程状态概述

进程状态是指在操作系统中,一个进程所处的不同运行状态,进程状态就决定了该进程接下来要执行什么任务。常见的进程状态有以下几种:

  • 新建状态:进程被创建但还没有被操作系统接受和分配资源。

  • 就绪状态:进程已经获得了所需的资源,并等待被调度执行。

  • 运行状态:进程正在执行指令,占用CPU资源。

  • 阻塞状态:进程因等待某个事件(如IO操作)而暂时停止执行,并释放CPU等资源。

  • 终止状态:进程执行完成或被终止,释放所有资源。

1.1 运行状态详解

在上一篇文章【Linux取经路】揭秘进程的父与子提到过,一般的计算机中只有一个 CPU,而进程却可能有很多个,这就注定了 CPU 是一个少量的资源,对所有的进程来说,运行的本质,就是把它放到 CPU 上,所以每个 CPU 都会维护一个运行队列,CPU 以队列的形式对进程做调度。所有的进程要运行都要在运行队列中排队,参与排队的是每个进程的 PCB 对象。所有在运行队列中的进程,它们所处的状态就叫做运行态(R状态)。

在这里插入图片描述
一个进程只要把自己放到 CPU 上开始运行,并不是一直要执行完毕,才把自己放下来。如果一个进程被放到 CPU 上直到执行完毕才把自己放下来继续去执行其他进程,那当我们的程序中写了一个 while 死循环出来,在运行该程序的时候,其他的应用就会卡住。但现实并不是这样,我们写了一个 while 死循环,其他程序照样可以正常运行。为了避免这种一个进程长时间占用 CPU 资源的情况出现,提出了时间片的概念。

扫描二维码关注公众号,回复: 16239460 查看本文章

时间片是操作系统中任务调度算法的一种思想,即将 CPU 的执行时间划分成固定长度的时间段,每个时间段称为一个时间片。在每个时间片内,操作系统将 CPU 分配给一个任务进行执行,当时间片耗尽时,操作系统会中断当前任务,并将 CPU 分配给下一个任务。时间片一般是10毫秒左右,所以在一个时间段内所有的进程代码都会被执行,我们将这种情况叫做并发执行。这种情况下会有大量的把进程放上 CPU 和从 CPU 拿下来的动作,这就叫做进程切换。

1.2 阻塞状态详解

最常见的阻塞状态就是一个进程需要通过键盘读取数据。当一个进程等待从键盘输入的过程,此时该进程就处在阻塞状态。键盘是一种硬件,在冯诺依曼结构体系中属于输入设备(外设),操作系统对硬件资源的管理是先描述再组织,因此每一个硬件都会对应一个结构体对象,该结构体对象中一定会维护一个等待队列,当一个进程需要利用该硬件资源时,进程的 PCB 对象就会被链入该等待队列,此时进程就处于阻塞状态。

在这里插入图片描述

小Tips:操作系统中的等待队列可能有成百上千个,不仅每一种硬件有等待队列,进程中也有等待队列,可能会出现一个进程等待另一个进程结束后才能继续运行。不同的操作系统,调度算法也会不同。

1.3 挂起状态详解

在一些操作系统的教材上还会出现挂起状态。无论是运行状态还是阻塞状态,一个进程在没有被 CPU 调度的情况下,它的代码和数据是处于空闲的,即没有被使用。之前说过一个进程在内存中有它自己的代码和数据,还有自己的 PCB 对象,当内存空间告急时,操作系统就会把这些没有被 CPU 调度的进程的代码和数据先放到磁盘中存储,只留进程的 PCB 对象在队列中排队,这种进程就处于挂起状态。

在这里插入图片描述
上面介绍的这些属于操作系统学科的理论知识,不同的操作系统可能会有不同的实现方案,下面我们来深入看看具体的 Linux 操作系统中有哪些进程状态。

小Tips:挂起状态对用户是不可见的,这是操作系统的一种行为。就像我们把钱存银行里,我们并不知道银行把我们的钱拿去干嘛了,银行可能把我们的钱借出去了或者给员工发工资了等等,我们作为客户不得而知,我们只知道如果存的是活期,可以随时到银行把钱取出来,如果存的是死期只有到期了才能取出来。

二、具体的Linux操作系统中的进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux 内核里,进程有时候也被叫做任务)。

2.1 Linux内核源代码

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
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 */
};
  • R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么是在运行队列里。

  • S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep)),它对应操作系统理论中的阻塞状态。

  • D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待 IO 的结束。

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

  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

2.2 查看进程状态

先来看看下面这段代码执行起来后的进程状态。

int main()                                                    
{
    
                                                                 
    while(1)                                                  
    {
    
        
         printf("Hello Linux\n!");                                                           
    }    
    return 0;    
}  

在这里插入图片描述
可以看出这段代码执行起来后的进程状态是 S睡眠状态,将 while 循环中的打印去掉再去执行代码看看进程状态。

int main()                                                    
{
    
                                                                 
    while(1);                                                  
    return 0;    
}  

在这里插入图片描述
此时代码中只剩一个 while 死循环,去执行这段代码,进程状态变成了 R运行状态。为什么会出现在这种情况呢?原因是 CPU 的执行速度是非常快的,第一段代码中的 printf 是要去频繁的访问显示器设备,而我们的显示器可能并不能被该进程直接去写入,所以该进程大部时间都在显示器的等待队列里等待显示设备就绪,因此最终查出来的进程状态是 S睡眠状态。当我们去掉 printf 之后,该进程就不会去访问显示器设备,始终都在运行队列里,所以最终查出来的进程状态是 R运行状态。

小Tips:查询结果中显示的+表示该进程在前台运行,这意味我们此时在 bash 命令行输指令是不会有任何反应的,可以在输入指令的后面加上&,此时表示让该进程在后台运行,要终止掉该进程只能通过指令kill -9 进程PID

2.3 D磁盘休眠状态(Disk sleep)

D状态也是一种阻塞状态,在 Linux 系统层面我们称作深度睡眠,S状态称作浅度睡眠。浅度睡眠是可以被唤醒的,即可以响应外部的变化,我们可以通过 kill 指令(其他进程)将浅度睡眠的进程终止掉。下面通过一个情景剧来给大家介绍为什么要有 D 状态,以及 D 状态的作用。

有这样一个场景,一个进程需要向磁盘中写入大量数据。在正常情况下往磁盘中写入数据,进程是需要等待的,等磁盘写完后给进程一个信号,然后进程才能继续去运行。有一天进程A就在向磁盘中写入大量数据,磁盘在写入的过程中,进程A就在内存中翘着二郎腿,嗑着瓜子在等待磁盘写完了给它发信息,此时路过的操作系统发现了进程A,它对进程A说:“我这内存压力都大的不行了,你小子倒好,占着内存不干正事,还在这嗑瓜子!”。于是乎操作系统就将进程A kill 掉了。此时磁盘傻眼了,数据写到一半进程没了,因为进程没了,所以磁盘就把写入的数据删除了,最终结果就是数据没有被写入磁盘。究竟是谁导致了这场悲剧的发生呢?于是乎法官就出来,它先审问操作系统,进程是你 kill 掉的,你怎么解释?操作系统说,我命苦呀,我只是完成了我的本职工作呀,为了给用户提供流畅的运行环境,将一些进程 kill 掉是我的职责呀,这不是我的问题呀。接着法官又来问磁盘,数据是你丢失的,你该如何解释?磁盘说,我祖祖辈辈都是这样工作的呀,进程它让我写入数据,结果自己不见了,其它磁盘遇到这种情况也是将数据丢弃掉呀,你如果判我有罪,那岂不是我的父亲、母亲都有罪呀。最后法官来问进程A,进程还没等法官开口就扑通跪下说,法官大人您明察秋毫呀,我才是被 kill 掉的那个,我属于被害人呀,我怎么会有罪呢。法官听了一圈,感觉大家都没罪,最终法官宣判了,你们三个都没罪,是制度问题,回去我改改操作系统,当进程在向磁盘中写入数据的时候任何人都不能将该进程 kill 掉。于是 D 状态就诞生了。当一个进程处于 D 状态的时候,它不会响应任何请求,任何人和操作系统都不能将该进程 kill 掉。

小Tips:结束掉 D 状态的方法有两种,一是等待某个条件满足,如等待数据写完,二是直接断电。如果被用户查到 D 状态的进程,那就预示着这个操作系统离崩溃不远啦。所以 D 状态会有,但是一般出现的时间都非常短。

2.4 T停止状态(stopped)

在 Linux 内核源代码中我们可以看到连个 T 状态,一个是 T ,一个是 t,我们可以认为这两个 T 状态是一样的,对于一个进程,我们可以通过下面这条指令将它设置成停止状态。

kill -19 进程PID

在这里插入图片描述
可以通过下面这条指令来结束停止状态。

kill -18 进程PID//

在这里插入图片描述
小Tips:结束停止状态的进程会到后台运行,要终止掉这个进程只能通过 kill -9指令。T状态和S状态很像,其中S状态的进程一定是在等待某种资源,而T状态的进程可能是在等待某种资源,也可能是在被其他进程控制。我们在打断点调试一段代码的时候,该进程就会处于T状态。

在这里插入图片描述

三、僵尸进程

一个进程在退出时并不是立即将自己所有资源全部释放,当一个进程退出时,操作系统会把当前进程的各种信息维持一段时间,这个状态就叫做 Z 僵尸状态。维持信息是给关心它的“人”,也就是父进程来查看的。如果父进程一直没有来关心退出的子进程,那么这个子进程将长时间处于 Z 状态。

int main()    
{
    
        
    pid_t id = fork();    
    if(id == 0)    
    {
    
        
        int cnt = 5;    
        while(cnt)    
        {
    
        
            printf("我是子进程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);                    
            sleep(1);    
            cnt--;    
        }    
       _exit(0);    
    }    
    else 
    {
    
    
        while(1)
        {
    
    
            printf("我是父进程,PID是:%d,PPID:%d\n",getpid(),getppid());
            sleep(1);
        }
    }
    return 0;
}

上面这段代码在 process 进程中通过调用 fork 接口创建了一个子进程,子进程在执行完五次打印后就会被终止掉,其中的 exit 函数就是用来终止一个进程,父进程将一直运行。

在这里插入图片描述
子进程执行完5次打印后就处于 Z 状态并且后面跟了一个单词 defunct,该单词有死了的,不存在的意思,只不过它还再等父进程来回收它的资源。处于 Z 状态的进程的相关资源尤其是 task_struct 结构体不能被释放。只有当父进程把子进程的相关资源回收后,子进程才能变成 X死亡状态。我们将这种处于 Z 状态的进程就叫做僵尸进程,如果父进程一直不来回收,那这种进程会长时间占用内存资源,造成内存泄漏。

3.1 僵尸进程危害总结

  1. 进程的退出状态必须被维持下去,因为它要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就将一直处于 Z 状态。

  2. 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在 PCB 对象中,换句话说,Z状态一直不退出,PCB一直都要维护。

  3. 一个父进程如果创建了很多的子进程,就是不回收,会造成内存资源的浪费,因为 PCB 对象本身就要占用内存。

  4. 造成内存泄漏。

四、孤儿进程

上面我们是让子进程先退出,父进程一直运行,接下来我们让父进程先退出,子进程一直运行,看看会有什么结果。

int main()    
{
    
        
    pid_t id = fork();    
    if(id == 0)    
    {
    
    
    	//子进程    
        int cnt = 500;    
        while(cnt)    
        {
    
        
            printf("我是子进程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);    
            sleep(1);    
            cnt--;    
        }    
       _exit(0);    
    }    
    else    
    {
    
    
    	//父进程    
        int cnt = 5;
        //这里的cnt是5,意味着父进程会先执行结束    
        while(cnt--)    
        {
    
        
            printf("我是父进程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);
            sleep(1);                                                                        
        }    
    }    
    return 0;    
}

在这里插入图片描述
可以看到父进程在执行结束后就只剩下子进程,为什么父进程不会处在 Z僵尸状态呢?答案是父进程也是 bash 的子进程,父进程在执行结束后,它的父进程 bash 会将其回收掉,并且过程非常快,所以我们我们没有看到父进程处在 Z僵尸状态。其次我们发现,当父进程结束后,它的子进程的父进程会变成1号进程,即操作系统。我们将父进程是1号进程的进程叫做孤儿进程,该进程被系统领养。因为孤儿进程未来也会退出,也要被释放,所以它需要被领养。

小Tips:所有的进程只对它的“儿子”,即子进程负责,不会对它的孙子进程负责,因为代码中只有创建子进程的逻辑,并没有创建孙子进程的逻辑,所以并不是不想让爷爷进程来回收孙子进程的资源,是因为爷爷进程没有这个本事,而操作系统会直接从内核层面进行回收,所以当一个进程的父进程结束后,会把该进程交给操作系统,让操作系统来充当它的父进程。

五、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_63115236/article/details/132274130