Linux系统编程:(十一)进程状态&&Linux中的僵尸状态

1. 进程状态的概念

进程状态就是task_struct内的一个整数。

1.1 运行状态

运行状态并不能简单理解为进程正在被CPU执行。CPU会按照先来先服务(FIFO)调度算法,从调度队列中依次取出进程的PCB并执行。因此运行态(running) 的完整定义为:只要进程位于调度队列中,就属于运行状态。换言之,处于该状态的进程,要么正被CPU执行,要么在调度队列中排队,随时等待CPU调度运行。

由此也能看出,我们常说的广义运行状态,实际包含了狭义运行态与就绪态两部分。

1.2 阻塞状态

在C/C++开发中, scanf 与 cin 等待外部输入的过程,是理解进程阻塞的经典案例。进程进入阻塞状态,核心原因是主动等待硬件设备、外部资源完成响应,键盘输入场景下,便是等待键盘设备产生输入数据。

操作系统对软硬件资源的管理遵循"先描述,再组织"的设计思想:系统通过 task_struct 结构体抽象、管理所有进程;各类硬件设备则由 struct device 结构体进行统一封装,该结构体除保存硬件固有属性外,还维护着专属的等待队列。

当进程执行阻塞式IO操作(如读取键盘输入)时,操作系统会将该进程从运行队列剥离,并挂载到对应硬件(键盘)的等待队列中。由于进程不再参与系统调度,暂时停止运行,此时进程正式处于阻塞状态。

所以从运行状态变为阻塞状态的本质是:把PCB列入到不同的队列结构当中。

当用户按下键盘完成输入后,键盘设备触发中断,系统会遍历自身的等待队列。此时处于键盘等待队列中的目标进程,等待的资源已经就绪,操作系统便将该进程从设备等待队列中移出,重新放回系统运行队列。重回调度队列后,该进程具备了被CPU调度执行的资格,一旦获得时间片,就会从阻塞状态切换回运行状态,继续执行后续代码。

同理,阻塞态转为运行态的本质,依旧是调整PCB所属的队列结构,使其重新参与系统调度。

自此,可以获得一个结论:

进程状态的变化,表现之一就是要在不同的队列中进行流动,本质都是数据结构的增删查改!

1.3 挂起状态

当系统内存资源不足时,操作系统会借助内存页面置换机制,将暂时不会被调度执行的进程及对应的内存数据,转移到磁盘的swap交换分区。这类仅保留PCB、代码和数据都被移出内存的进程,就被称作阻塞挂起进程

当系统内存资源恢复充足后,操作系统会执行换入操作,将存放在磁盘swap交换分区里的进程代码与数据重新加载回物理内存。原本仅保留PCB的阻塞挂起进程,数据和程序完整回归内存,便解除挂起状态,恢复为普通的阻塞进程。

倘若物理内存资源持续不足,仅对阻塞进程执行换出操作仍不能满足需求,操作系统会将运行队列中靠后的进程置换到磁盘交换分区。待该进程获得调度机会时,再从swap分区重新调入内存并加入运行队列,此类进程定义为运行挂起进程。

综上,无论何种挂起状态,其本质都是在内存与磁盘swap交换分区之间,对进程执行换入、换出操作。

2. 理解内核链表

普通双向链表的节点融合了数据成员与前后指针,指针直接寻址相邻数据节点。Linux 内核专属的双向链表基于 struct list_head 构建,该结构体仅有 next 、 prev 两个指针成员,遵循链表与数据分离的思想,被嵌入各类功能结构体中。链表指针仅指向其他 list_head 实例,而非数据主体,凭借这一特性,内核链表拥有出色的通用性,可应用于不同类型的内核对象。

Linux 内核链表的 struct list_head 内嵌在业务结构体中,仅存有前后指针,无法直接访问外层结构体的成员变量,这里需要借助 container_of 宏完成地址转换。

计算成员偏移量时,可假设整个业务结构体的起始地址为 0,此时结构体内部各成员的地址数值,就等于该成员相对于结构体首地址的偏移量。 container_of 宏正是利用这一原理:先通过 list_head 指针减去它自身的偏移量,反向推导出外层业务结构体的首地址,拿到完整结构体地址后,就能正常访问结构体中的所有成员变量。在遍历链表时,先依靠 next 、 prev 指针跳转各个 list_head 节点,再通过该宏还原出对应的业务结构体,进而读写内部数据。

&((struct task_struct*)0)->links

这个表达式就是通过把结构体首地址当作 0,计算出 links 成员在 struct task_struct 中的偏移量。

有了偏移量,用next指针所在地址减去偏移量便是结构体的首地址:

(struct task_struct*) ((struct list_head*)next - &((struct task_struct*)0)->links)


有了上面这些基础,我们就能够理解为什么进程即存在于全局队列中,又存在于运行队列中了,即一套PCB隶属于不同的数据结构中:

图中每个task_struct 里有好几组 next/prev ,就代表这个进程挂在了不同的链表上:

  • 比如最上面的一组 next/prev 属于全局进程链表,所有进程的这组节点互相连成一条链;

  • 中间的某一组 next/prev 属于运行队列链表,可运行的进程用这组节点连成一条链。

当进程正在运行时,它的两组节点分别连在两条链上,所以它同时存在于"全局进程链表"和"运行队列"里。调度器只看运行队列,而内核管理所有进程时会遍历全局链表,两者互不冲突。

而当进程进入阻塞状态时,会把运行队列里的 list_head 摘下来,但全局链表的节点还在,所以此时它只在全局链表,不在运行队列里;只有当它恢复为可运行状态,才会重新把运行队列的节点挂回去,再次"同时存在于两个队列"。

3. Linux系统的进程状态

3.1 Linux源码

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

下面的状态在kernel源代码中定义:

cpp 复制代码
/*
*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):有时候也叫不可中断睡眠状态(uniterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发生SIGCONT信号让进程继续运行。
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里面看到这个状态。

3.2 实际操作

(1)R---运行状态

首先创建一个.c文件myprocess.c,然后在vim编辑器中随便敲入一个运行程序,例如:

cpp 复制代码
  1 #include<stdio.h>
  2 int main()
  3 {
  4   while(1)
  5   {
  6     printf("hello\n");                                                                                    
  7   }
  8   return 0;
  9 }

然后编译运行这个程序,随后打开一个xshell窗口来监视这个运行的进程,输入以下指令:

bash 复制代码
while :; do ps ajx | head -1; ps ajx | grep myprocess; sleep 1;done

发现输出结果为:

可以观察到运行状态为S,这是因为程序中含有printf函数,需要进行IO操作所以进程状态为S状态,如果想要使其变为R状态,只需要将其改写为不含有printf函数的函数即可,输出结果就会变为:

可以发现上面的输出结果中显示的运行状态后面都有一个+号,这是因为当前进程是在前台(命令行)运行的,此时的命令行无法输入其他指令,当我们输入如下指令来运行可执行程序时:

bash 复制代码
./ myprocess &

程序便会在后台运行,就不会运行此时的命令行输入其他指令了,此时的监视窗口输出结果为:

当我们想要删掉这个进程,可以输入如下指令:

bash 复制代码
kill -9 11130
//11130为进程的pid

此时监视窗口的输出结果变为:


(2)S---阻塞状态

当把myprocess.c文件中的内容改为一个scanf函数的程序时,就会涉及到键盘输入,结合前面章节的知识,运行该进程时遇到了scanf函数就会把该进程从运行队列移入到硬件PCB中的等待队列中去,等到键盘输入内容时,再移回到运行队列中去,该进程状态就为阻塞状态,对应Linux中的S状态。

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

运行该进程:

绿色方框为等待键盘输入内容。

监视窗口对应输出结果:

前面我们提到进程状态在PCB中为一个整数:

只需要指定其中的变量state为0,那么就是运行状态,指定为0就为阻塞状态,其他状态同理。


(3)T---暂停状态&&t---追踪状态

在前面进程状态介绍中,并不包含暂停状态,但是在Linux系统中i,含有暂停状态。

当我们在Linux系统中使用gdb对程序进行debug调试时,如果在程序中打了断点然后运行程序,那么程序运行到断点处就会停下来,那么相应的监视窗口的输出结果就会显示进程处于t状态,也就是追踪状态:

当我们把myproc.c文件中的代码内容改为如下:

然后编译运行该程序:

可以观察到命令行中不断输出打印结果,这个时候用户通过键盘操作按下ctrl+z:

程序就会被暂停,相应的监视窗口输出结果显示为T状态,也就是暂停状态:

Linux 系统中,进程进入暂停状态 (T 状态,表示 Stopped 或 Traced),通常是由于操作系统检测到进程执行了某些需要用户或调试器介入的特定行为 。比如,当进程尝试执行非法的内存操作、访问受限资源,或触发了预设的断点(如通过 ptrace系统调用调试)时,内核会主动将其挂起。这种机制类似于一种"保护性暂停",目的是在进程继续执行可能导致不可控后果(如数据损坏、系统崩溃)之前,将控制权交还给用户或调试器,以便及时诊断错误、修复异常,或观察进程内部状态。因此,暂停状态不仅是异常处理的"安全阀",也是系统调试和程序追踪的核心机制之一。

除了通过键盘操作ctrl+z使进程暂停之外,也可以通过kill指令以命令行的方式实现,运行:

bash 复制代码
kill -l

可以查看kill指令的所有用法:

bash 复制代码
[cyq@VM-0-5-centos home]$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

可以观察到,对应的19号为暂停操作,所以可以通过以下指令来对进程进行暂停:

bash 复制代码
kill -19 [进程的pid]

如果想要暂停的进程再次运行,可以使用对应的第18号指令:

bash 复制代码
kill -18 [进程的pid]

(4)D状态

S状态在Linux系统中被称为可中断休眠或者浅休眠,其实就是阻塞状态。如果一个进程自己处于S状态,这个进程可以被用户kill,随后进程监视窗口会响应kill这个动作给出反应,那么这个进程就是浅休眠或可中断休眠。而D状态对应的就是深度休眠或不可中断休眠

在 Linux 系统中,D 状态 (全称 Uninterruptible Sleep ,不可中断睡眠状态)是一种特殊的进程状态,表示该进程正在等待某个必须完成的内核 I/O 操作,且在该操作完成前,进程不会被信号(如 SIGKILL、SIGTERM)打断。

为什么会有 D 状态?

Linux 将进程置于 D 状态,主要是为了保障系统 I/O 的一致性内核数据结构的完整性。当进程执行涉及关键资源的系统调用(如磁盘读写、某些硬件设备操作等)时,内核会将其标记为 D 状态,防止其在等待 I/O 的过程中被意外终止。

试想一个场景:一个进程正在向硬盘写入关键的系统数据结构(如文件系统元数据),如果此时允许用户通过**kill -9**强行杀死进程,可能会导致磁盘上的数据处于不一致状态,轻则数据损坏,重则文件系统崩溃。为了防范这种风险,Linux 内核将这类进程"保护"起来,使其在等待 I/O 期间不受任何信号干扰,直到内核收到硬件设备返回的 I/O 完成通知后,才会唤醒进程并允许其继续运行(或响应信号)。

常见触发场景:

  1. 读写慢速 I/O 设备(如硬盘故障、网络存储无响应)

  2. 某些内核驱动或模块陷入长时间阻塞

  3. 网络文件系统(NFS)挂载点无响应

所以D状态也属于阻塞状态的一种,即在Linux系统中,阻塞状态包含了S状态和D状态!

(5)Z状态

在 Linux 系统中,Z 状态 (Zombie,僵尸状态)是进程生命周期中一种必要的中间状态,其核心作用是确保父进程能够正确获取子进程的退出信息

当一个子进程运行结束时,其占用的内存、文件描述符等资源会被内核回收,但系统仍需为其保留基本的进程描述信息(如退出码、运行时间等),并将该子进程标记为僵尸状态。此时,这个进程已经"死亡",但其进程控制块(PCB)仍然存在,等待父进程通过 wait()waitpid()系统调用来读取退出状态。只有父进程完成对其退出信息的收集后,内核才会彻底清除该僵尸进程的残留信息。

僵尸进程的存在本质上是一种内核与父进程间的信息同步机制,它保证了进程间"父子关系"的完整性,使得父进程能够准确掌握子进程的终止原因。然而,如果父进程未能及时回收僵尸进程,大量残留的进程控制块将会占用系统资源,这也是编程中需要正确处理子进程退出的重要原因。

简而言之,Z 状态是进程退出过程中的"最后一程",是操作系统为维护进程树管理完整性而设计的必要状态。


为了能够模拟出Z状态,将myprocess.c文件中的内容改写为以下内容:

cpp 复制代码
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int count = 5;
        while(count)
        {
            printf("我是一个子进程,我正在运行:%d\n",count);
            sleep(1);
            count--;
        }
    }
    else
    {
        while(1)
        {
            printf("我是一个父进程,我正在运行.....\n");
            sleep(1);
        }
    }
    return 0;
}

然后对这个程序进行编译运行,运行过程中,使用ctrl+c停止程序的运行,此时可以观察到进程的监视窗口中输出结果如下:

可以观察到,当程序停止运行时,子进程进入了 Z 状态(僵尸状态)。这是因为父进程在子进程退出后没有对其进行回收(即未调用 wait()获取其退出状态)。在这种情况下,子进程的进程控制块(PCB)依然会保留在系统中,一直占用资源。如果这种情况持续存在,就会导致内存泄漏问题。

(6)X状态

在 Linux 系统中,X 状态 (Dead/Exit,死亡状态)并不是一个在常规进程列表(如 pstop输出中)可见的稳定状态,而是进程生命周期结束时的瞬时状态,可视为"进程的彻底终结"。

当一个进程完成执行、释放了所有资源,并且其退出状态已被父进程接收(通过 wait()回收)后,内核会将其标记为 X 状态,并立即销毁其所有剩余的内核数据结构,包括进程控制块(task_struct)。此时,进程从系统中完全消失,不再占用任何资源,其 PID 也可被后续新建的进程复用。

X 状态的存在体现了操作系统资源管理的严谨性 :它确保进程在退出时能够被干净、完整地清理,避免残留信息导致内存泄漏或 PID 耗尽等问题。与僵尸状态(Z 状态)不同,X 状态不会持久存在,它只是内核销毁进程前最后一刻的内部标记,因此用户通常无法在进程列表中观察到这一状态。

可以说,X 状态是进程生命周期的终点,是操作系统回收资源、维护系统稳定运行的最终保障机制。

3.3 补充知识点

1.如果进程退出了,那么内存泄漏问题还在不在?

当进程正常终止时,操作系统会回收其分配的所有用户态内存,包括堆、栈和全局数据区。因此,从这个角度讲,进程"自己造成"的内存泄漏会随进程结束而自然消失,并不会持续占用系统资源。

然而,这并不意味着开发人员可以忽略内存管理。如果进程在退出前未能正确释放某些内核资源 (如共享内存段、信号量、文件描述符等),或者在多进程协作场景中异常退出,导致与其交互的其他进程陷入非预期状态,就可能造成系统级资源残留,进而影响整个系统的稳定性与性能。

**所以对于常驻内存的进程如果具有内存泄漏问题,那么是比较麻烦的!**因此,尽管进程退出能清理大部分内存泄漏,但良好的编程习惯仍强调在进程中主动、优雅地释放所有资源。这不仅是对单进程负责,更是对系统整体健壮性的保障。

2. Linux系统中对于数据结构对象的缓存

在 Linux 内核中,为提升系统性能、降低内存分配与释放的开销,广泛采用了对象缓存(Object Cache)机制 。该机制通过预先创建并维护一批可复用的内核数据结构对象(如进程描述符 task_struct、文件对象 file、内存页描述符 page等),形成对象缓存池。

当一个内核对象不再使用时,并不会立即将其占用的内存交还系统,而是由内核将其标记为"空闲"状态,保留在相应的缓存池中。后续当内核需要分配同类型对象时,便可直接从缓存中获取,从而避免了频繁的内存分配与初始化开销,显著提升内存分配效率。这种"分配-释放-重用"的缓存循环,尤其适用于生命周期短、分配频繁的小型内核对象。

Linux 中经典的实现是 Slab 分配器(及其后续变体 Slub、Slob),它将内存按对象大小与类型进行分组管理,在保证内存局部性的同时,有效减少了外部碎片。对象缓存机制不仅提升了内核内存操作的性能,也成为 Linux 高效处理高并发、动态资源请求的核心基础设施之一。

相关推荐
洵有兮1 小时前
Shell 脚本编程学习总结(基础 + 变量 + 条件 + 流程控制 + 函数数组)
linux·学习
我命由我123451 小时前
SEO 与 GEO 极简理解
java·linux·运维·开发语言·学习·算法·运维开发
我材不敲代码1 小时前
Python基础:注释的写法(单行、多行、文档注释)
服务器·python·microsoft
楼兰公子1 小时前
RK3588 Linux驱动开发大纲
linux·驱动开发
红辣椒...2 小时前
codex+第三方模型
java·服务器·前端
Web极客码2 小时前
AI的下一个风口:智能助力超越ChatGPT
服务器·人工智能·ai编程
!沧海@一粟!2 小时前
Linux高并发内核优化
linux·运维·oracle
perfect123126452 小时前
轻量运维工具fastdp v6版本
linux·运维
linksinke2 小时前
在 CentOS 7.x 外网环境离线构建便携式 Python 3.11.4 的方案参考
linux·python·centos