【Linux】12:进程状态

摘要:本文系统性地阐述了进程状态的概念及其在操作系统中的核心地位。首先,明确了进程状态本质上是进程控制块(PCB)中的一个状态变量,是操作系统调度和管理进程的关键依据。接着,深入剖析了操作系统理论中的三种基本进程状态:运行状态(进程在CPU运行队列中)、阻塞状态(进程等待资源就绪)和挂起状态(进程代码和数据被换出到磁盘)。然后,重点讲解了Linux系统下的七种具体进程状态:运行(R)、可中断睡眠(S)、不可中断睡眠(D)、停止(T)、跟踪停止(t)、死亡(X)和僵尸(Z),并通过代码示例和命令行操作演示了各状态的触发条件和观察方法。文章最后总结了进程状态转换的本质是PCB在不同队列(运行队列、等待队列)间的移动,并强调了僵尸进程和孤儿进程对系统资源的影响。

目录

一、什么是进程状态

二、操作系统的进程状态

2.1 运行状态

2.2 阻塞状态

​编辑

2.3 挂起状态

2.4 总结

2.5 理解内核链表的话题

三、linux下的7种进程状态

3.1 运行状态R

3.2 浅度睡眠状态S

3.3 深度睡眠状态D

3.4 停止状态T

3.5 进程追踪状态t

3.6 死亡状态X

3.7 僵尸状态Z

3.7.1 僵尸进程

3.7.2 孤儿进程

四、总结


一、什么是进程状态

在谈 进程状态 之前,我们需要了解 什么是进程、怎么去描述并组织进程、如何创建一个进程。

  • 操作系统(OS)的本质是:先描述,在组织
  • 操作系统并非直接管理 -- 进程,而是管理 进程 的 PCB(task_struct)
  • PCB 中有着进程的各种信息,包括:PID、PPID 、进程状态 等
  • 我们可以通过函数 getpid() 获取当前进程的 PID
  • 进程 间存在父子关系,可以通过 fork() 主动创建 子进程
  • 父子进程 相互独立,共享一份代码时,具有 写时拷贝 机制

所谓的 进程状态 其实就是 PCB(task_struct)内部的一个整形变量 ----- int status

操作系统(OS)要更改一个进程状态,只要更改进程的 PCB(task_struct)中的状态属性,它只是一个标志位用来表明进程的状态,仅此而已。

那么 状态 决定了什么呢?

进程后续的动作! ---- 而 操作系统(OS)中可能会存在多个进程都要根据它的状态执行后续动作,所以说明 --状态-- 对于 操作系统后续如何管理 进程十分重要!!

二、操作系统的进程状态

对于进程而言呢,它是操作系统中的概念,如果有学习过《操作系统》这门学科的话应该可以很清楚对于进程而言的话是存在着许许多多的状态,如果一开始刚刚接触的小伙伴一定会感觉这么多状态要怎么区分呀

其实那么多的状态,真正主要的也就那么几个,所以接下去我会中重点讲解以下几种进程的状态

2.1 运行状态

首先我们要谈到的是【运行状态】,这个状态是最普遍的: 进程 PCB 被调度到 CPU 运行队列中且已被分配 CPU 资源,就叫做 ------ 运行状态

一个CPU,一个运行队列,也就是说这些进程都需要在CPU这里进行排队,逐个执行。

而排队的是进程么?

其实不是,排队的是进程的PCB(面试时不是你在排队,而是你的简历信息在排队)。

首先对于一个进程而言,我们知道它是由 内核数据结构 + 所对应的代码和数据 所组成的,所以当系统中存在多个进程的时候就势必会存在多个结构体;当然我们需要将这些进程给链接组织起来(双向链表链接)

那么这些 进程 就相当于是在处在一个运行队列中,我们如果要找到这个队列中的某个进程的话,只需要找到这个进程的头部即可,那我们就可以对应地去调度某个进程,把这个进程所对应的代码和数据放到CPU上去执行

因为每个进程是需要去竞争CPU资源的,但是呢CPU不可能同时给这么多进程分配资源

所以每一个CPU都会去维护一个运行队列,里面的队 头指针 head 所指向就是第一个进程所对应的【PCB】,队尾指针tail 所指向就是最后一个所对应的【PCB】。所以我们要运行某一个进程只需要将 head 所指向的那个进程放到 CPU上去运行即可

提问:一个 进程 只要把自己放到 CPU 上开始运行了,是不是一直要到执行完毕,才把自己放下来?

不是,每一个进程都有一个叫做:时间片的概念! 其时间大概是在10 ms左右。所以并不是一个进程一直在执行,而是这多个进程在一个时间段内所有代码都会被执行 —— 这就叫做【并发执行】
所以呢这就一定会存在大量的进程 被CPU放上去、拿下来的动作 —— 这就叫做【进程切换】

所以呢我们不要拿自己的时间感受去衡量CPU,其运行一遍速度是非常快的,你根本感受不到这种进程切换的效果

2.2 阻塞状态

  • 阻塞 就是 进程 因等待某种条件就绪,而导致的一种不推进状态(比如等待 键盘输入)。
  • 通俗的来说 阻塞 就是 进程卡住了,原因就是缺少资源

比如在我们日常生活中,常常发生堵车,原因就是道路资源不够用了,车辆这个进程就需要原地等待

那么进程需要什么资源呢?

  • 比如磁盘网卡显卡等各种外设
  • 假设你现在想在steam上下载游戏,当你点击下载按钮后提示磁盘空间不足,此时是无法运行steam下载这个进程的,因为此进程需要等待足够大的磁盘资源
  • 此时我们就称此进程阻塞状态

总结: 进程 阻塞 就是不被调度

  • 此时 PCB(task_struct) 就会被设置为 阻塞状态,并链入等待的资源提供的等待队列。
  • 没错,这里的等待队列 类似于 CPU 运行队列

2.3 挂起状态

挂起(阻塞挂起):

  • 当 内存资源紧张时,将进程的数据和代码交换至磁盘中挂起,此时内存中只有PCB
  • 挂起 可以看作一种特殊的 -- 阻塞状态

详解:

  • 当计算机资源比较吃紧时,操作系统一定要确保自身不会因为资源的紧张而崩溃,所以就会将一些等待资源(阻塞)的进程的代码和数据交换到磁盘的 swap分区 中,这个过程称为唤出。
  • 当需要调度此进程时,就会将磁盘的 swap分区 中保存的内容换回到内存中,这个过程称为唤入。

注意:交换的是进程的代码和数据,不是PCB!!如果PCB被交换出内存了,那操作系统如何管理呢?

所以当某个进程的代码和数据不在内存中,而被换出到磁盘上时,该进程就为挂起状态。

思考:swap分区是越大越好么?

磁盘本质是输入输出设备,每次唤入唤出其实都是非常低效的操作,如果swap分区设置的过大,那么操作系统就会十分依赖它,导致出现更多低效IO,这本身就是一种牺牲效率来确保操作系统能够正常运行的行为。

所以swap分区不宜设置的过大,一般为内存大小或内存大小的一半,具体要看不同的操作系统。

2.4 总结

看了上面的三种基本的进程状态后我们可以来总结一下,如果要看进程是什么状态的话一般看这个进程的PCB在哪里排队

  • 位于【运行队列】的话它就是处于运行状态
  • 位于【阻塞队列】的话它就是处于阻塞状态
  • 一个进程从一个状态切换到另一个状态,本质上就是进程的PCB从一个队列换到另外一个队列的过程,

2.5 理解内核链表的话题

之前我们说明进程之间的PCB是使用双向链表链接起来的,但是我们上面又说运行队列和阻塞队列里面存放的是进程的PCB,一个进程的PCB能属于多种数据结构嘛?一个进程有多个PCB嘛?

我们之前定义结点的方式如下:

这是我们之前学习到的双链表,但是linux内核里面的双链表并没有这样设计。

linux设计如下:

首先,linux内核里面设计了list_head类型的结点,如下所示:

struct list_head

{

struct list_head* next,*prev;

}

list_head类型的结点里面包含了两个指针,next执行了下一个list_head类型的数据,而prev指向了前一个list_head类的数据。

我们在定义PCB的时候,可以在PCBl里面存放一个list_head类型的对象,让这个对象指向下一个和前一个PCB里面的list_head类型的对象,如下所示:

这样定义之后,问题就来了,现在我们只有指向list_head对象的地址,list_head对象是属于task_struct内部的,那我们如何找到对应的task_struct地址呢?我们有了task_struct类型的地址才能访问PCB,才能访问PCB里面的其他属性啊。

首先需要有一个c语言的基础知识,一个结构体对象的地址在数字上==结构体对象中第一个属性的地址的。有了这个基础之后,我们认为在0号地址存放了一个PCB类型的变量,现在我们要访问0号地址对应的PCB对象中的links结构体对象:&((struct task_struct*)0->links)。这就是相当于links对象距离task_struct对象的偏移量了,有了这个偏移量之后,现在我知道了下一个PCB的links地址,我们通过下一个下一个PCB的links地址减去这个偏移量就可以知道下一个PCB的地址了。

(struct task_struct*)(next/list - &((struct task_struct*)0->links)),我们得到PCB的地址之后,就可以访问PCB对象中的所有属性了。

现在我们就回答了上面的问题:这样定义之后,问题就来了,现在我们只有指向list_head对象的地址,list_head对象是属于task_struct内部的,那我们如何找到对应的task_struct地址呢?我们有了task_struct类型的地址才能访问PCB,才能访问PCB里面的其他属性啊。

在一个PCB类型里面定义了一个list_head类型的对象之后,我们当然就可以将所有的PCB通过双链表的方式链接起来。那么我们可以在PCB里面定义一个list_head了,是不是可以多定义几个list_head类型的对象呢?第一个list_head表示将所有的双向链表链接起来,第二个list_head表示运行队列,第三个list_head表示阻塞队列。。。。

这样一个PCB是不是即属于链表又属于队列了呢。让PCB即属于全局链表,又属于运行队列。

如果我们想要将一个运行进程切换到阻塞队列,就可以将PCB中的list_head断开,给他链接到阻塞队列的list_head中。

如下所示:

所以进程的PCB只有一份,并且属于多种数据结构!!

三、linux下的7种进程状态

在介绍完操作系统学科下的三种最主要进程状态后,我们对进程的状态有了基本的概念,接下去就让我们正式地来学习一下Linux 系统7种进程状态

先来小结并回顾一下上面所学:

  1. 如果当前是【运行状态】,那么接下来就需要被调度运行
  2. 如果当前是【阻塞状态】,那就等条件就绪,等设备准备好就把当前进程投递到运行队列里,然后再被CPU调度运行
  3. 如果当前是【挂起状态】,要做的就是把当时换出的代码和数据重新换入,然后再把所对应的进程列入到运行队列中

以下就是关于进程的所有状态

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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

3.1 运行状态R

测试代码如下:

1 #include <stdio.h> 2 #include <unistd.h> 3 int main() 4 { 5 while(1) 6 { 7 printf("hello world\n"); 8 sleep(1); 9 } 10 return 0; 11 }

编译运行如下:

我们再开一台xshell来显示进程信息,并且每隔一秒显示进程信息。

命令如下:

while :; do ps axj | head -1 ;ps axj |grep mycode;sleep 1 ;done

如下所示:

我们为什么查看mycode进程的状态是S呢?我们的mycode进程正在运行,这个进程的状态应该是R嘛?这是为什么呢?

我们将代码修改一下,让这个代码不要想屏幕打印,如下所示:

1 #include <stdio.h> 2 #include <unistd.h> 3 int main() 4 { 5 while(1) 6 { 7 //printf("hello world\n"); 8 //sleep(1); 9 } 10 return 0; 11 } ~

我们让他什么都不用做,只循环,编译运行如下所示:

此时进程的状态就变成了R状态了。

为什么我们将printf去掉之后,再次编译运行就从S状态变成R状态了呢?

原因就在于printf打印语句它是属于IO流的一种,我们往显示器打印字符,它一直在等设备就绪,所以其进程状态就为S+,对应的即是在操作系统中的【阻塞状态】,这个进程就一直在运行状态和阻塞状态来回切换,那为什么我们看不到R呢?这是因为我们每隔1s查看一次,进程代码运行的是非常快的,大部分时间都在等待IO,所以监测的状态为S状态!!

但是当我们去掉printf这种lO流之后呢,它就是在纯纯运行,没有lO,那也就变成了R状态

这里再补充说明一下这个SR后面的+

这里的R+代表的就是这个进程是在前台运行的,所以我们在输入任何指令后不会对其造成 任何的影响。如下所示:

我们可以在执行程序的时候,将代码放到后台去运行,如下所示:

放到后台运行之后,此时我们就可以执行命令了,然后再次查看进程状态,如下所示:

此时就从R+变成R了。

总结:
"+"代表是前台运行,无"+"代表后台运行,后台运行时可在命令行继续输入指令并执行,但无法用ctrl+c结束,需要用kill -9 pid杀死。想要后台运行某个程序就在后面加"&",如:./test &

3.2 浅度睡眠状态S

测试代码如下:

1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 5 int main() 6 { 7 printf("我是一个进程,我的pid:%d\n",getpid()); 8 int x; 9 scanf("%d",&x); 10 printf("x=%d\n",x); 11 return 0; 12 }

运行如下所示:

我们在另外一个xshell查看状态,如下所示:

将该进程运行起来我们可以看到其是出于S+的状态,因为【shell】此时正在等待用户的输入,这个就对应到了我们上面所讲到的阻塞状态。

这个进程在等待键盘设备的输入,这个进程的PCB就会链入键盘的阻塞队列中,当我们从键盘中输入了一个整数,就会把这个进程给唤起,放入运行队列继续运行。

3.3 深度睡眠状态D

除了【浅度睡眠】之外呢,还有一种叫做【深度睡眠】,它们俩呢,都是阻塞状态

对于浅度睡眠来说,之所以称为“浅度”,是有原因的:也就是处于这种状态的进程容易被唤醒。例如说我们在上面所讲到的这个处于阻塞状态的进程,我们使用kill -9 PID向这个进程发送【9号信号】,那么这个进程就被杀死了,虽然你在浅度睡眠状态,但是是可以被杀死的

  • 深度睡眠状态/不可中断睡眠状态/磁盘休眠状态,顾名思义,在这个状态的进程不会被杀掉,哪怕是操作系统也不行,通常会等待IO的结束。
  • 例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。
  • 如果在这个过程中,操作系统能够杀死该进程,那么就有可能丢失数据。

总结一句就是:

浅度睡眠状态S可以被杀死,深度睡眠状态D不可以被杀死

3.4 停止状态T

T和下面的t都属于暂停状态。

测试代码如下:

1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 5 int main() 6 { 7 while(1) 8 { 9 printf("hello world\n"); 10 } 11 return 0; 12 }

我们编译运行代码,然后按下ctrl+z暂停进程,此时就会从S状态换成T状态了,如下所示:

我们按下ctrl+z之后,进程就从S状态换成T状态了

暂停状态又是什么呢?暂停状态和S状态不一样。暂停状态时linux系统的特有的一种状态,暂停是操作系统怀疑这个进程有问题,给这个进程暂停,然后告知用户,让用户来决定这个进程是否要继续。

我们除了使用ctrl+z来暂停进程,还可以使用信号来暂停和启动进程,如下所示:

我们也可以使用信号来启动进程,如下所示:

总结:

kill -19 pid :暂停进程

kill -18 pid:启动刚刚暂停的进程

kill -9 pid:杀死进程

3.5 进程追踪状态t

我们在编译的时候加入-g选项,然后加入断点信息,然后按r运行,此时进程就停在断点信息那一行了,此时我们查看进程状态就是t状态,如下所示:

状态信息如下所示:

此时在断点这一行,进程被暂停了。

3.6 死亡状态X

对于【死亡状态X】来说呢,这个状态只是一个返回状态,你不会在任务列表里看到这个状态

3.7 僵尸状态Z

接下去我们来介绍一种状态叫做【僵死状态Z】,对于这个状态,我们要涉及到两个特殊的进程叫做僵尸进程孤儿进程

3.7.1 僵尸进程

首先我们要来介绍的是僵尸进程,这里呢通过一个故事来进行引入

  1. 你呢,很喜欢晨跑🏃‍,这一天早晨6点又起来跑步了,当你路过一个公园的时候,遇到了一个晨练的【程序员】,边跑边掉头发 但是呢,跑着跑着,此时突然他就“啪叽”倒了下来。那你此时就非常担惊受怕了,马上拨打了110和120的电话,然后守在他的身边等待救援来到,周边的人看到也都纷纷围了过来,其中不免有一些人会紧急救援。不过等了一会这个人就没了呼吸
  2. 过了十几分钟后,救护车和警车都来了,警察先封锁了,让法医过来检验一下其状况,就说:“这个人已经没救了,赶紧通知家属准备后事吧~。”

好,我们回归正题,来说说这个【僵尸进程】

  • 因为在救护车来之前这个人其实就已经死亡了,但是其状态还没有被检测,当前并不知道它的死因,所以我们操作系统就可能会维护当前的这个状态,这个状态即为Z状态,即[僵尸状态]

那我现在想问了:有一个进程暂时退出了,它要将它的状态暂时维持一段时间,问题是它维持给谁看呢?

答:父进程!当一个进程退出的时候,那最关心它的便是【父进程】。因为这个父进程费了很大的劲才将这个子进程fork出来,此时呢它突然挂掉了,那么此时父进程就必须去关心一下对应的子进程退出时的原因

在子进程结束之后,父进程得到子进程的状态之前,子进程的状态就为Z状态。

测试代码如下:

1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 5 int main() 6 { 7 pid_t id=fork(); 8 if(id==0) 9 { 10 int count=5; 11 while(count) 12 { 13 printf("我是子进程,我正常运行:%d\n",count); 14 sleep(1); 15 count--; 16 } 17 }else 18 { 19 while(1) 20 { 21 printf("我是父进程,我正在运行...\n"); 22 sleep(1); 23 } 24 } 25 return 0; 26 }

子进程会执行5次printf语句,然后退出,父进程会一直运行,运行如下所示:

子进程结束之后,父进程得到子进程结束信息之前,子进程就是Z状态。

一旦父进程得到了子进程的结束信息,处理子进程的退出信息之后,子进程就会变为X状态,就看不到这个子进程的信息了。

如果父进程一直不管子进程的结束信息,不回收,不获取子进程的退出信息,那个子进程就一直是Z状态。子进程的PCB一直存在在内存中,OS需要一直维护PCB,会一直占用内存,导致了内存泄漏。所以僵尸进程就会导致内存泄露。

所以父进程需要处理子进程的退出信息,将子进程的PCB数据释放。

我们总结一下:

💬 进程一般退出的时候,一般其不会立即彻底退出。如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态,这也是为了方便后续父进程读取子进程的相关退出结果。

那对于上面的这种子进程,我们就将其称作为是【僵尸进程】,不过呢这种进程是存在一定危害的!

那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存

那么对于上面的这种危害我们就称作为是【内存泄漏】,要如何去进行避免呢,之后的文章会做讲解~

补充一个知识点:

如果进程退出了,内存泄漏的问题就不存在了。

什么样的进程具有内存泄漏问题,是比较麻烦的?

一个进程是永远在运行,并且从来不退出的,我们叫这种进程叫常驻进程,这样的进程最怕内存泄漏!!!

3.7.2 孤儿进程

  • 上面我们讲到,当一个子进程突然退出但是父进程并没有去主动回收的话,那么此时这个子进程就会变成【僵尸进程
  • 父子进程关系中,如果父进程先退出,子进程要被1号进程领养,这个被领养的进程(子进程)就叫做孤儿进程

测试代码如下:

1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 5 int main() 6 { 7 pid_t id=fork(); 8 if(id<0) 9 { 10 perror("fork fail"); 11 return 1; 12 }else if(id==0) 13 { 14 //子进程 15 while(1) 16 { 17 printf("我是一个子进程,pid:%d,ppid:%d\n",getpid(),getppid()); 18 sleep(1); 19 } 20 21 }else 22 { 23 //父进程 24 int cnt=5; 25 while(cnt) 26 { 27 printf("我是一个父进程,pid:%d,ppid:%d\n",getpid(),getppid()); 28 sleep(1); 29 cnt--; 30 } 31 } 32 return 0; 33 }

代码解释:我们在父进程中创建一个子进程,子进程一直循环打印,父进程执行5次打印就退出,父进程退出之后,子进程还在一直执行。

监视进程命令:while :; do ps axj | head -1 ;ps axj |grep mycode;sleep 1 ;done

运行如下所示:

我们使用命令来查看1号进程是什么:ps axj | head -1 && ps axj | grep systemd

如下所示:

我们可以直接理解1号进程就是操作系统。

为什么子进程要被1号进程领养呢?

如果1号进程不领养子进程,子进程结束之后,子进程将退出信息给父进程,然后父进程来处理子进程的结束状态,然后释放子进程的PCB.

但是我的父进程已经退出了啊,已经接受不到子进程结束信息了,那么子进程就会一直处在Z状态,导致内存泄漏。

所以需要被1号进程领养!!

四、总结

在本文中,我们主要讲解了两个大点,一个是在操作系统中所常见的一些进程状态,它们分别为:运行状态、阻塞状态、挂起状态。

  • 所被调度的、处于运行队列里的这些进程所处的状态我们称之为 运行状态
  • 处于等待队列中,同时在等待外设相应的进程所处的状态我们称之为 阻塞状态
  • 当把一个进程的 数据和代码都重新交换到外设当中,进程所处的状态我们称之为 挂起状态
  • 接下去我们又介绍了在Linux系统下的七种进程状态,分别是:运行状态R、浅度睡眠状态S、深度睡眠状态D、停止状态T、进程跟踪状态t、死亡状态X、僵死状态Z
  • 当一个进程正在运行,没有在等待IO流的时候,其就会处于 运行状态R
  • 当一个进程请求服务(IO)时,会使一个进程处于浅度睡眠状态S,可以被杀死
  • 如果不想让一个进程在等待磁盘操作的时候被操作系统杀掉,则可让其处于 深度睡眠状态D
  • 可以向一个进程发送【19】号信号使其暂停,处于停止状态T继续发送【18】号信号的话则可以使其重新启动
  • 在【cgdb】的环境下去运行一个断点的话则可以使其处于进程跟踪状态t
  • 使用kill -9 PID就可以杀掉一个进程,使其处于死亡状态X
  • 如果让一个子进程在父进程不知晓的时候退出,那么其就会处于僵死状态Z,变为【僵尸进程】;若是在父子进程中父进程先于子进程退出的话,那么这个子进程就会变成【孤儿进程】