深入解析 Linux 进程状态:从 task_struct 双链表到 R/S/D/Z 状态的内核奥秘

1. 进程状态

在进程的PCB(Linux下叫做task_struct)中的进程状态,它的本质上就是一个数字,数字是几就表明了进程是什么状态,改变状态就是修改PCB内部对应的值来达到目的。 进程状态,决定了进程接下来要做的工作。

下图为:一个操作系统实现进程状态变化时要符合的理论,也可以理解为指导思想。 进程状态图(图片来源于网络)

2. task_struct是如何管理的

task_struct通过双链表链接起来,但并不是直接在结构中存在两个类似*next *prev指针来完成,而是在结构中嵌入了一个双链表:

所以task_struct是这样来连接的:

通过源码可以知道,task_struct结构中内嵌了一个list_head结构体变量tasks,那么该如何通过这个双链表来拿到整个task_struct结构中的其他成员?

假设在地址0处存在一个task_struct,那么这个task_struct中的tasks地址不就是其在整个结构体中的偏移量吗,所以可以使用类似这种方法来拿到结构体的起始地址,从而拿到其他的成员。

C 复制代码
// 当前结构体起始地址 = 当前结构体中tasks地址 - 0地址中的tasks地址
// 这种方法在源码中被定义为一个宏
struct task_struct* start = &tasks - &((struct task_struct*)0->tasks)

这样做的好处在于,实现的双链表再也与类型无关。即使是其他结构体类型,也可以通过内置list_head连接起来。

更重要的在于:如果在task_struct中存在更多的list_head类型的成员,那么就可以使这个task_struct既属于链表,也可以同时属于其他的数据结构。并且在源码中也是这么做的。

所以当CPU中的运行队列也存在一个list_head类型成员,就可以做到即使不断链,也可以保证当前的PCB在全局链表管理的同时,将当前PCB链接到运行队列中。

3. 运行状态和新建状态

通过上面了解到了PCB是如何管理的后,就可以得到结论:当一个PCB也就是task_struct被创建出来,但没有被调度时,这种就叫做就绪状态或者也可以叫做新建状态 。而当一个PCB已经链入到运行队列中并且被调度时,这种就叫做运行状态。在Linux中可以弱化这个概念,因为在Linux中只有运行状态,没有就绪状态。

4. 阻塞状态

下面是一段最简单的代码,我们都知道当运行到scanf函数时,如果我们不做输入,系统就无法从键盘上读取数据,程序便不会继续运行,而是卡在这等待输入,这就是一种阻塞状态,那么该如何理解呢?

C 复制代码
int main()
{
    int a = 0;
    scanf("%d", &a);
    printf("%d\n", a);
    return 0;
}

已知操作系统的任务是管理软硬件资源,既然管理软件需要先描述再组织 ,那么管理硬件也是一样需要先描述再组织。所以在操作系统中,也有着一个管理硬件资源的结构。

当上面的程序运行到scanf函数时需要输入,但是键盘没有输入就绪,所以当前进程的PCB就需要从CPU调度队列断链,然后链接到外设的等待队列中。当键盘上输入后,再重新链接到CPU的调度队列。

进程竞争资源本质也就两类:CPU资源、外设资源 。在CPU调度队列当中就是竞争CPU资源,而在外设的等待队列中就是竞争外设资源。网卡、显示屏这些也是一样,当前进程需要哪个资源,就去哪个外设的等待队列中进行等待,当外设没有就绪时,就是阻塞状态

所以阻塞和运行的本质就是让进程的task_struct:

  1. 更改task_struct状态属性。
  2. 链入到不同的队列中。

5. 挂起状态

当一个进程的PCB变为阻塞状态之后,并且此时内存资源严重不足,就会发生以下的对话:

  • 内存:"PCB啊,我看你跑键盘等待队列里去了,键盘准备好没啊?"
  • PCB:"还没有呢,键盘还没理我呢!"
  • 内存:"那我这地方不够了,你看你这代码和数据还在我这呢,我得给后面的同志们腾出地方来啊!这么的,我给你代码和数据先放到磁盘里,等你回来了我再把你的代码和数据拿回来,虽说速度可能慢一点,总比大家都崩了好吧!"

所以当内存资源严重不足,操作系统换出进程的代码和数据,此时内存里只剩PCB,这种状态就叫做挂起状态 。而在磁盘中专门和内存用来交换的这块区域,叫做swap分区

但是内存资源不足也是分等级的,当进程因为等待外设资源在阻塞状态中时,此时的内存因为资源严重不足,从而换出代码和数据这叫做阻塞挂起 ;但当这么做之后内存资源还是严重不足,此时CPU的调度队列中,还没有被调度的进程的代码和数据也会被换出,从而达到释放内存,这就叫做就绪挂起

6. Linux中的进程状态

Linux中的进程状态,要和文章上面所讲述的操作系统状态,有一定程度的对应关系。下面来看Linux源代码怎么说:

C 复制代码
/*
 * 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 *task_state_array[] = {
	"R (running)",		/*  0 */
	"S (sleeping)",		/*  1 */
	"D (disk sleep)",	/*  2 */
	"T (stopped)",		/*  4 */
	"t (tracing stop)",	/*  8 */
	"Z (zombie)",		/* 16 */
	"X (dead)"		/* 32 */
};

6.1 R(running)和 S(sleeping)状态

使用下面代码测试一下:

运行命令打开监控,可以看到当前我的可执行程序名字叫做myfork。而在STAT这一列就是进程状态,R 就代表着当前是运行状态。

再来看这段代码:

运行后可以看到进程变成了S状态 ,称作浅度休眠 ,对应着就是阻塞状态 ,此时能够响应外部事件,比如在Linux中按ctrl+c就可以强制中止。

再来看另一种情况:

当运行后会发现还是S状态 ,这是因为printf函数会频繁调用显示器,而在阻塞状态等待显示器资源的时候是大多数,所以会显示是S状态 ,如果频繁来查看进程状态的话也会有概率看到显示R状态

6.2 D(disk sleep)状态

在Linux中D状态 叫做磁盘休眠,也叫做深度睡眠状态。和S状态都属于阻塞状态。因为配置原因不方便演示,这里引入一组对话来理解:

  • 进程A:"磁盘在吗?我要向你写入1GB数据。"
  • 磁盘:"好的,不过我可能会写入失败,你稍等我一下,成功与否我都会告诉你。"

此时发生内存空间严重不足的情况。

  • 操作系统:"进程A,现在内存严重不足,你不是S状态吗,我把你的代码和数据换出去了,但还是不够,我只能把你Kill掉了。"

这时磁盘写入失败回来找进程A,但没有找到,并且还要执行别的任务,所以只好把这1GB数据丢掉,造成了数据丢失。

  • 管理者:"现在造成了事故,所以我要追究你们三个:操作系统、进程、磁盘,的责任。"
  • 磁盘:"这件事情不能怪我啊,我让进程A等着我,但是我找他的时候他不在,我还需要继续执行任务,只能这样做了。"
  • 操作系统:"也不能怪我啊,我需要管理所有的资源,如果我不Kill掉进程A的话,整个系统都会崩掉,那时的损失就不止1GB了。"
  • 进程A:"更不能怪我了,我正在等着写完数据呢,内存不够就把我Kill掉了,我还冤呢!"

管理者看着它们三个觉得都有道理,于是想到了一个办法。

  • 管理者:"这件事就不追究了,但以后凡是在进行数据量较大或磁盘级I/O操作的进程,不要再使用S(浅度睡眠)状态了,全部表示自己为D(深度睡眠)状态;而操作系统你以后只能Kill掉S状态的进程。

处在D状态下的进程不对外部事件进行任何响应,操作系统也Kill不掉,除非进程自己醒来。如果感兴趣的话可以使用dd 命令模拟进程进入D状态(具体方法可以询问AI)。

6.3 T(stopped)状态

首先还是使用这段代码运行起来:

现在进程处于R状态:

这时可以使用kill命令发19信号,进程就变成了T状态(暂停状态):

如果再次发送18信号,进程就会再运行起来:

但为什么此时的进程状态不是R+而是R了呢,这里先说一个比较简单的理解,后面文章会写到:

kill命令:

6.4 t(tracing stop)状态

当使用gdb 调试时,会发现只有一个gdb 进程,而在gdb 中打断点后输入r后开始调试,gdb 会为我们创建出一个进程,此时创建出来的myfork就是t状态。

所以在gdb 调试的场景中,进程就是t状态,表示进程因为被追踪,进程遇到断点停下来。

6.5 Z(zombie)和 X(dead)状态

在操作系统中进程只有结束一种状态,而在Linux中,进程结束有两种状态:

  • Z(zombie):在进程结束的时候要先处于一种 "僵尸状态" ,将当前进程代码和数据释放掉,但会保留task_struct,记录进程的退出信息,从而方便父进程读取退出码,当父进程读取后,当前进程就会变为X状态 。但如果一直不读取,就会一直停在 "僵尸状态"
  • X(dead):表明进程真正结束, 完成了所有工作。

使用这段代码进行测试,运行时通过ps命令来观察,可以看到当子进程退出的时候父进程没有读取退出信息,所以子进程变为Z状态。

如果将来父进程一直不处理,这个僵尸进程就会一直存在,当僵尸进程大量存在的时候,就会造成内存泄漏。

X状态这里不做演示,因为从Z到X(也就是父进程回收子进程)是一瞬间完成的,所以很难观察到。

相关推荐
李天琦4 小时前
git查看commit属于那个tag
linux·git·云计算
liulilittle4 小时前
关于DDOS
linux·运维·服务器·网络·ddos·通信
LetsonH5 小时前
Ubuntu 22.04 系统下 Docker 安装与配置全指南
linux·ubuntu·docker
pianmian17 小时前
3D Tiles高级样式设置与条件渲染(3)
linux·服务器·前端
清晨朝暮7 小时前
【Linux 学习计划】-- 命令行参数 | 环境变量
linux·运维·学习
聂 可 以7 小时前
Nginx基础篇(Nginx目录结构分析、Nginx的启用方式和停止方式、Nginx配置文件nginx.conf文件的结构、Nginx基础配置实战)
linux·运维·nginx
Joker 0078 小时前
Ubuntu 安装 FSL 及多模态脑MRI的去颅骨处理(含 HD-BET 深度学习方法)
linux·深度学习·ubuntu
代码讲故事9 小时前
解决 xmlsec.InternalError: (-1, ‘lxml & xmlsec libxml2 library version mismatch‘)
linux·python·pip·lxml·xmlsec·libxml2
xiaofann_9 小时前
【数据结构】单链表练习
linux·前端·数据结构
☆凡尘清心☆9 小时前
LNMP环境中php7.2升级到php7.4
linux·nginx·centos·lnmp