Linux 第十三讲 --- 进程篇(三)进程状态

前言:

在《Linux 第十二讲 — 进程篇(二)初识进程》中,我们学习了进程的基本概念,包括进程的创建(fork)、终止(exit)以及父子进程的关系。我们通过代码实践,理解了进程是程序运行的实例,是操作系统资源分配和调度的基本单位。
本讲(进程篇三)将深入探讨进程的核心特性之一:进程状态。进程并非一直处于运行中,其生命周期会经历多种状态(如运行、就绪、阻塞等),状态的切换由操作系统调度器控制。理解这些状态及其转换逻辑,是掌握进程管理和系统调度的关键基础。


目录

前言:

一、进程状态

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

2.1 补充知识:运行队列

2.2 运行状态-R

2.3 睡眠状态-S

2.4 磁盘休眠状态-D

2.5 暂停状态-T

2.6 僵尸状态-Z

2.7 死亡状态-X

三、两个特殊的进程状态

3.1 僵尸进程

3.12 僵尸进程的危害

3.2 孤儿进程

四、总结


一、进程状态

一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器,有时虽有空闲处理器但因等待某个时间的发生而无法执行,这一切都说明进程和程序不相同,进程是活动的且有状态变化的,于是就有了进程状态这一概念。

其实在计算机操作系统相关学科对于进程状态的定义是非常多的。如下图所示。

但是实际在Linux操作系统当中我们没有必要考虑这么多状态。

这里我们具体谈一下Linux操作系统中的进程状态,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 *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*/
};

我们实际上需要考虑的就是上面的七种状态不过它们之间也是有内在联系的,我们下面慢慢讲解。 

小贴士: 进程的当前状态是保存到自己的进程控制块(PCB)当中的,在Linux操作系统当中也就是保存在task_struct当中的。

在Linux操作系统当中我们可以通过 ps aux 或 ps axj 命令查看进程的状态。如下:

  1. R运行状态(running) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  2. S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep)。
  3. D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  4. T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  5. X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
  6. Z僵尸状态(zombie):这个状态是一个已经运行完的子进程,等待父进程回收他的返回值。


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

2.1 补充知识:运行队列

运行队列:

进程是如何在CPU上运行的:CPU在内核上维护了一个运行队列,进行进程的管理。让进程进入队列,本质就是将该进程的task_struct 结构体对象放入运行队列之中。这个队列在内存中,由操作系统自己维护。

需要进程执行运算,就把对于进程的PCB放入队列当中,准备执行。

其实真实在内核的结构肯定是不会怎么简单的,这部分还在后面的进程优先级当中还有涉猎。

因此,所谓的进程不同的状态,本质是进程在对应的队列之中,等待某种资源。

2.2 运行状态-R

一个进程处于运行状态(running),并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个R状态的进程。

不过由于CPU的调度太快了,一般很难直接通过ps指令查看真在运行的进程。

小贴士: 所有处于运行状态,即可被调度的进程,都被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。

2.3 睡眠状态-S

一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。

例如执行以下代码:

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

int main()
{

    printf("我是一个小小的进程正在休眠\n");
    sleep(100);
    return 0;
}

 当我们执行可执行程序后,在使用ps命令查看进程状态时,发现他是S状态。

而处于浅度睡眠状态的进程是可以被杀掉的,我们可以使用kill命令将该进程杀掉。 同样也可以使用外面上一讲所讲的crtl+c将他干掉。

2.4 磁盘休眠状态-D

一个进程处于“D”状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。

例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)

当然这种连操作系统都无法杀死的进程在系统当中是较少数的,之所以要设置这个状态,主要是防止系统压力太大不得不将向磁盘写入的进程杀掉,并且向磁盘的写入失败了,那么写失败的数据是要还是不要呢?

操作系统不想被这个问题卡住,所以将选择权交给了用户自己决定。

2.5 暂停状态-T

在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行。其实可以认为暂停状态也是阻塞状态的一种,下面我们来看一下如何让一个运行状态下的进程暂停:

我们可以使用kill的19号指令将一个进程从运行状态变为暂停状态。

不过这种状态只是操作系统将进程停止了,后面是可以恢复的。

如果想要使得休眠中的进程重新恢复运行状态只需要执行kill的18号命令即可:

这里我们可以看到进程又重S状态,但是为什么进程重新进入S状态后后面的 +号 消失了呢?

其实,进程状态后面的 +号 表示的是这个进程是一个前台进程,如果没有 +号就表示这个进程是一个后台进程。对于前台进程我们可以使用Ctrl + c将其杀死,但是对于后台进程,我们只能使用kill命令杀死他。

小贴士: 使用kill命令可以列出当前系统所支持的信号集。

不过关于信号部分,外面后面也会有专题进行讲解的。

2.6 僵尸状态-Z

当一个进程将要退出的时候,在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态(zombie)。

首先,僵尸状态的存在是必要的,因为进程被创建的目的就是完成某项任务,那么当任务完成的时候,调用方是应该知道任务的完成情况的,所以必须存在僵尸状态,使得调用方得知任务的完成情况,以便进行相应的后续操作。
例如,我们写代码时都在主函数最后返回0。

实际上这个0就是返回给操作系统的,告诉操作系统代码顺利执行结束。在Linux操作系统当中,我们可以通过使用echo $?命令获取最近一次进程退出时的退出码。

小贴士: 进程退出的信息(例如退出码),是暂时被保存在其进程控制块当中的,在Linux操作系统中也就是保存在该进程的task_struct当中。

2.7 死亡状态-X

死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)。


三、两个特殊的进程状态

3.1 僵尸进程

当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸)程。僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

外面下面来看一段代码:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>



int main()
{
    pid_t id=fork();
    while(1)
    if(id == 0)
    {

        printf("这是子进程,pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
    }
    else if( id >0)
    {
        printf("这是父进程,pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
    }


    return 0;
}

当我们杀掉子进程后,由于父进程并没有读取子进程的退出状态码,所以子进程进入了Z(僵尸状态),如果父进程一直不读取子进程的退出状态码,那么子进程就会变成僵尸进程

我们可以借助下面的循环指令去打印一下信息,观察现象。

while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo"######################";sleep 1;done

3.12 僵尸进程的危害

僵尸进程的退出状态必须一直维持下去,因为它要告诉其父进程相应的退出信息。可是父进程一直不读取,那么子进程也就一直处于僵尸状态。
僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护。
若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏。

3.2 孤儿进程

在Linux当中的进程关系大多数是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程。但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程。
若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。

例如,我们复用上面的代码,fork函数创建的子进程会一直打印信息,而父进程在打印几次信息后我们人为的把他杀死,此时该子进程就变成了孤儿进程。

 这里我们可以看到:如果将父进程杀死后,父进程并不会进入僵尸状态,这是因为父进程在退出后会被父进程的父进程——bash所读取他的退出状态码。还有子进程被1号进程领养后会由前台进程变成后台进程。


四、总结

通过本讲的学习,我们深入剖析了进程的七个状态,结合实例分析了两个特殊进程,并借助 ps 命令观察了进程的实际状态(如 R(运行)、S(可中断睡眠)、D(不可中断睡眠))。
核心结论

  1. 进程状态是操作系统资源调度策略的直接体现,决定了多任务并发的效率。

  2. 状态转换逻辑直接影响系统的响应能力和资源利用率,是理解进程管理的核心视角。

在《Linux 第十四讲 — 进程篇(四)环境变量与命令行参数》中,我们将探索进程运行时的动态上下文环境:我们下一讲见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值