【Linux仓库】进程状态【进程·叁】

发布于:2025-06-14 ⋅ 阅读:(19) ⋅ 点赞:(0)

🌟 各位看官好,我是

🌍 Linux == Linux is not Unix !

🚀 今天来学习Linux中理论上进程状态及linux中的进程状态是如何设计的。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦!

理论进程状态

状态决定了进程接下来要做的工作,就像我们人一样吃饱了才有力气干活!!!

那么该怎样表示一个进程的状态呢?

状态在我们看来实际上就是一个数字,一个状态对应一个数字。

#define BUNNING 1
#define BLOCK 2
#define ... 3

struct task struct
{
    int status;
}

理论上进程状态设计: 

但实际上Linux的设计与上图不是一模一样的,因为理论是要用来指导实践的,而实践当中必会碰到一些困难,这些都是要进行修正的。 接下来,我们来看看Linux的进程状态是如何设计的。

双链表链接进程

我们学习数据结构的时候,是通过定义两个前后next和prev指针进行双链表的链接,从而可以指向下一个进程的起始地址,随意访问结构体里面的数据。

struct task_struct
{
    struct list_node *next;
    struct list_node *prev;
    //...
}

而Linux中的源码设计并不是这样子的,那它又是设计的呢?

struct task_struct
{
    struct list_node node;
    //...
}

struct list_node
{
    struct list_node *next;
    struct list_node *prev;
}

linux中将prev和next进行封装,也可以做到链表的前后链接,但是并不是指向下一个进程的起始地址,那又该如何随意访问结构体中的任意元素呢???

既然我想要访问结构体中的任意元素,那么我就要得到该结构体的起始位置,从而做到访问任意元素的目的。

计算偏移量

为什么要这样做呢?意义何在?

  1. 我们实现的双链表,再也与类型无关了!
  2. 可以给每个CPU设置一个调度队列

运行状态

运行状态:从计算机的硬件出发,我们所写的代码在硬盘中,要让程序运行起来就要加载到内存当中, 每一个程序(进程)都会有一个属于自己的PCB,通过PCB来进行排队,等待CPU的调度,为了方便调度管理,操作系统会维护一个运行队列,所有就绪状态的进程的PCB会被加入到这个队列当中, CPU在调度执行时就会通过这个运行队列拿到进程的PCB,进而调度执行该进程,在排队的时候就是运行状态。

我们说过输入、输出属于外设,运算器、控制器属于CPU,而这些都属于资源。而进程的本质是要竞争这些资源,无疑就两类资源:

  1. CPU资源
  2. 外设资源

有些人认为只有将PCB放在调度队列运行起来才能称作运行状态。

这里我们规定:运行状态 -->该进程的PCB必须处在CPU的调度队列runqueue中,只要运行状态在调度队列中,该进程就叫做运行状态 -->随时等待CPU调度执行!

阻塞状态

阻塞状态:在CPU执行一个进程的时候,可能会需要访问系统的某些资源,就比如在C语言中写的scanf(),在使用这个函数的时候,需要调用键盘,等待键盘输入数据,当进程需要键盘资源的时候,会将进程的PCB加入到硬件设备结构描述的等待队列当中,并且把PCB设置为阻塞状态,当PCB在这个等待队列中等待数据资源时,这个状态就叫做阻塞状态。

int main()
{
    int a = 0;
    scanf("%d",&a);
    return 0;
}

操作系统是软硬件资源的管理者,那么它就需要管理硬件。那该如何管理硬件呢?

先描述,再组织!!!

当程序跑到scanf时,此时就会等待键盘的响应,这就是典型的阻塞状态。

那如果键盘迟迟没有就绪呢?

操作系统会直接把当前进程从调度队列runqueue里断链,将PCB移动到键盘的结构体的等待队列里,如上所示。而我们把这种等待设备资源 --> 这种状态称为阻塞状态!

一旦键盘设备就绪时,谁最清楚键盘上有数据呢?操作系统最清楚,因为它是软硬件资源的管理者。此时会再把设备的等待队列放到CPU的调度队列中!!!

本质上:

  • 从阻塞到运行:把设备的等待队列放到CPU的调度队列中
  • 从运行到阻塞:把CPU的调度队列放到设备的等待队列中

挂起状态(阻塞/运行)

内存资源是有限的,而进程加载到内存是要消耗内存的。那么,有没有一种可能内存资源会存在不足呢?这显然有可能。

内存资源不足时,操作系统是如何做的呢?

阻塞挂起状态:

操作系统在内存中发现有一些进程会很晚才会放到cpu中进行调度,此时会将该进程的task_struct放入到设备的等待队列当中(在这又不干啥,占着内存资源的位置,不如先给其他人运行,等你就绪时再重新把你唤回到内存当中)。

运行挂起状态: 

当内存资源严重不足时: 

  • 挂起状态:内存满负荷时,又要增加新的进程显然是不行的。所以操作系统会观察内存中的哪些进程没有被放在任何一个队列里面(在内存里面啥也不干),找到以后就把此进程的代码和数据短期内置换到磁盘上,仅保留此进程的PCB。腾出的这一块空间供新的进程使用。针对于这种情况,操作系统会将阻塞进程的代码和数据置换到外设,此时该进程的状态就被称为挂起状态;
  • 其中阻塞进程的代码和数据一般会存放在磁盘的swap分区,当进程被操作系统调度时,被置换到外设的代码和数据又会重新加载到内存;
  • 一般情况下,swap分区的大小不会太大,大概等于内存的大小,过大的swap分区会导致操作系统过于依赖swap分区,导致效率变低;


Linux进程状态

为了弄明⽩正在运⾏的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有⼏个状
态(在Linux内核⾥,进程有时候也叫做任务)。
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状态

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

S状态

S睡眠状态 (sleeping): 意味着进程在等待事件完成(有时候也叫做可中断睡眠(interruptible sleep))(简称 浅度休眠,它能够响应外部的事件 )。
int main()
{
    while(1)
    {
        printf("hello status\n");
        sleep(1);
    }
}

 在上面这段程序中,按照我们的理解应该是R状态才对,但我们看看下图结果。

不对啊???不应该是R状态吗?这是出错的吗?实际并不是。

大部分时间都是显示器不就绪状态,计算机一秒可能上亿次,大量时间都在运行队列和显示器这个外设的这个等待队列里,来回放(外设太慢了,CPU很快)。大量打印在显示器本质在访问外设。 


D状态

D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个
状态的进程通常会等待IO的结束。(深度睡眠,不能响应外部事件)
在这里分享一段职场的小故事:
进程要将用户的1GB数据(含有1亿)写入到磁盘中,此时把自己设为s状态,等待磁盘的反馈(成功/失败)。此时的情况是内存资源严重不足了!!!操作系统本来因为内存资源严重不足而在烦恼,看到该进程正翘着二郎腿、磕着瓜子,占着内存资源的位置。本来就烦恼的操作系统看到该进程的行为更加来气,说到:我内存资源都快不足了,你还有空再这吹着凉风,为了保全自己,二话不说直接把该进程杀掉了。此时,磁盘正在写入用户的1GB数据,但是以失败告终,那么它就要向该进程反馈写入失败,但是找不到该进程了啊,该进程被操作系统杀掉了啊!!!那么磁盘该不该丢掉这1GB数据呢?该磁盘还要写入其他用户的数据,不得不丢掉该用户得的1GB数据。
这段小故事中,是谁犯错了呢?
操作系统说:没看到内存资源严重不足了吗?如果我不杀掉该进程,那么我们可能损失损失的价钱就不只1亿了。要问就去问进程和磁盘。
内存说:我在将用户的1GB数据交给磁盘进行写入啊,此时我再等待磁盘给我反馈结果,你操作系统二话不说就把我杀掉了,好意思说?
磁盘说:我只是执行进程吩咐给我的命令罢了,此时我写入失败了向进程反馈结果,但是找不到了啊。而我还有其他用户的数据需要写入啊,我也不得不丢掉该数据,这能赖上我?
那么,我今后赋予你这个进程一种能力,叫做 D状态。你可以不被OS杀掉,除非你自己醒来!!!


T状态

T停⽌状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的
进程可以通过发送 SIGCONT 信号让进程继续运⾏。
该指令可以向进程发出信号:

停止状态: 


t状态

进程被追踪,因为断点而停下来!
可以看到实际上gdb是创建了个子进程进行调试

X状态

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


Z状态

僵尸进程

为什么要创建进程?进程被创建出来,就是为了完成任务的。

既然有创建,那么就有销毁。因此,进程结束是进程创建的反过程
进程结束的时候,我们需要知道任务完成的怎么样?因此不能立即释放进程的所有资源

进程结束时,需要现处于一种“僵尸状态”,目的就是为了获取信息。代码数据会被释放掉,但是会把task _struct保留。

为什么要保留task_struct呢?

该进程记录着进程退出的退出信息,方便父进程读取出退出码。

  • 僵死状态(Zombies)是⼀个⽐较特殊的状态。当进程退出并且⽗进程(使⽤wait()系统调⽤,后面讲)没有读取到⼦进程退出的返回代码时就会产⽣僵死进程。
  • 僵死进程会以终⽌状态保持在进程表中,并且会⼀直在等待⽗进程读取退出状态代码。模拟僵尸状态:只要⼦进程退出,而⽗进程还在运⾏,但⽗进程没有读取⼦进程状态,⼦进程就会进⼊Z状态。 

孤儿进程

前面说的是子进程先退出了,但父进程一直在运行没有读取子进程状态。

那如果父进程先退出了呢,子进程又该怎么办呢?

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0)
    {
        //child
        printf("I am child, pid : %d\n", getpid());
        sleep(10);
    }else
    {
        //parent
        printf("I am parent, pid: %d\n", getpid());
        sleep(3);
        exit(0);
    }
    return 0;
}

僵尸进程危害

  • 进程的退出状态必须被维持下去,因为他要告诉关⼼它的进程(⽗进程),你交给我的任务,我办的怎么样了。可⽗进程如果⼀直不读取,那⼦进程就⼀直处于Z状态?是的!
  • 维护退出状态本⾝就是要⽤数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态⼀直不退出,PCB⼀直都要维护?是的!
  • 那⼀个⽗进程创建了很多⼦进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本⾝就要占⽤内存,想想C中定义⼀个结构体变量(对象),是要在内存的某个位置进⾏开辟空间!


网站公告

今日签到

点亮在社区的每一天
去签到