
往期《Linux系统编程》回顾:/------------ 入门基础 ------------/ 【Linux的前世今生】 【Linux的环境搭建】 【Linux基础 理论+命令】(上) 【Linux基础 理论+命令】(下) 【权限管理】 /------------ 开发工具 ------------/ 【软件包管理器 + 代码编辑器】 【编译器 + 自动化构建器】 【版本控制器 + 调试器】 【实战:倒计时 + 进度条】 /------------ 系统导论 ------------/ 【冯诺依曼体系结构 + 操作系统基本概述】 /------------ 进程基础 ------------/ 【进程入门】
hi~,小伙伴们大家好啊, (´∀`)♡ 今天是2025年11月17日,星期一,时间过得可真快,一转眼鼠鼠已经走到了大三上半学期的第十二周了。再过5、6周,这半学期就要画上句号了⏳⊙﹏⊙∥ 手机上也接连收到了寒潮和大风蓝色预警❄️,天气是越来越冷了,时间也是越来越紧了,不知道大家那边都怎么样了呢?(◕ᴗ◕)
---------- 2025 年 11 月17日(九月二十八)周一 |
|---|
大家注意保暖的同时也让我们继续踏上Linux系统的学习之旅吧! 今天我们来学的是 【进程状态】:🎉٩(◕‿◕)۶🎉
进程状态:将会脱离教材中对进程状态的抽象解释,通俗生动的介绍Linux中具体的进程状态 ₍₍ ◝(・ω・)◟ ⁾⁾
进程状态:是操作系统内核对进程当前活动情况的描述。

从上面的图示中我们可以看到,进程的状态种类较多,而且这些状态之间是可以相互切换的。 在众多进程状态中,我们主要学习
运行、阻塞和挂起这三种状态,因为这三种状态是 Linux 系统中最主要的进程状态。
进程的运行状态:它反映了进程在 CPU 调度层面的活跃程度。 进程处于运行状态时,有两种可能的情况:
简单来说:运行状态是进程 “有机会、有能力” 使用 CPU 执行任务的状态,是进程活跃性的核心体现之一。
进程的阻塞状态:它反映了进程因等待某个特定事件完成,暂时失去了 “使用 CPU 执行任务的能力”,即使操作系统的调度器分配 CPU 时间片,该进程也无法立即执行。
“等待事件、无法就绪”
我们可以用 C 语言中
scanf的场景来直观理解进程的阻塞状态:当程序运行到scanf语句时,会暂停执行并等待用户的键盘输入,这时程序对应的进程就处于阻塞状态。
这一过程的底层逻辑是这样的:每个硬件设备(比如:键盘、鼠标、磁盘等)在操作系统中都对应一个 “等待队列”
当 CPU 执行到scanf语句时,操作系统会检测键盘的状态 —— 如果此时没有任何按键被按下,意味着进程需要的资源(输入数据)尚未就绪,继续占用 CPU 只会浪费资源。
这时,操作系统会做出两个关键操作:
此后,这个进程就进入了阻塞状态,不再参与 CPU 调度,程序也就表现为 “卡住” 的状态 —— 这就是我们直观感受到的 “等待输入时程序没反应”。
那么请问,当用户按下键盘时,这个阻塞中的进程会主动 “知道” 吗? 其实不会。即便它正在等待输入,进程本身也无法感知硬件状态的变化。真正的流程是:
这样,当 CPU 下次调度时,这个进程就有机会重新获得 CPU 时间片,继续执行scanf后续的逻辑(读取输入数据并处理)
进程的挂起状态:它表示进程被临时从正常的执行流程中暂停,并且通常会被转移到外存(如:硬盘)中,以释放内存资源,便于操作系统调度其他更需要资源的进程。
产生原因:
在操作系统中,磁盘上有一个专门的分区叫做 swap 分区(交换分区),它本质上是一块被划出来用作 “临时内存” 的磁盘空间。 swap 分区的大小通常建议设置为物理内存的 1.5 倍到 2 倍(具体可根据系统需求调整),其核心作用是:在物理内存资源紧张时,临时存放从内存中 “挪出” 的进程数据。
当系统物理内存严重不足时,操作系统会主动排查当前进程的状态,优先盯上那些处于阻塞状态的进程 —— 因为这些进程本就因等待外部事件(如:键盘输入、I/O 完成)而无法执行,暂时用不到 CPU 和内存资源。 操作系统会对这些阻塞进程执行 “换出” 操作:
此时,这些被 “换出” 到磁盘的阻塞进程就进入了 阻塞挂起状态 它们既不占用物理内存,也不会参与 CPU 调度,相当于 “暂时休眠” 在 swap 分区中,为其他急需内存的活跃进程腾出了空间。
当阻塞进程等待的事件终于发生(比如:用户按下了键盘),操作系统会立即执行 “换入” 操作:
这里需要明确一个关键概念:“挂起” 的核心是 “位置转移”—— 即把进程的核心数据从内存转移到磁盘(swap 分区),而非简单的状态暂停

如果系统内存紧张到极致,即便将所有阻塞进程都 “换出” 到 swap 分区后,内存空间仍不足以支撑活跃进程的运行,操作系统就会采取更激进的策略:盯上 CPU就绪队列中的进程。
这些被临时 “挪出” 内存的就绪进程,就处于 就绪挂起状态
它们虽然仍有执行资格,但因核心数据在磁盘中,无法直接参与 CPU 调度。
简单来说:swap 分区是内存的 “备胎”,而 “阻塞挂起” 和 “就绪挂起” 都是操作系统在内存不足时的 “应急策略”—— 通过将暂时不用或优先级低的进程 “存” 到磁盘,换取系统的稳定运行,待资源充足后再恢复这些进程的执行。
核心是理解 “侵入式链表” 的思想 —— 把链表节点嵌入到数据结构体中,而非让数据结构体依附于链表
一、普通链表 vs 内核链表(侵入式链表)
先看普通链表的典型结构(类似图中上方的 struct Node):
struct Node
{
int data; // 数据域:存储节点自身的数据
struct Node *prev; // 指针域:指向前一个节点
struct Node *next; // 指针域:指向下一个节点
};普通链表的特点是 “数据 + 链表指针” 紧耦合 —— 每个节点既存数据,又存链表的前后指针。
这种设计的问题是:如果有多种不同的结构体(如:struct task_struct 表示进程、struct file 表示文件)都要组织成链表,每种都得单独实现一套链表逻辑,代码冗余且不通用。

二、内核链表的核心:struct list_head
Linux 内核为了解决这个问题,设计了侵入式链表,核心是 struct list_head 结构体:
// 内核链表的“节点”结构:仅包含前后指针,不包含数据
struct list_head
{
struct list_head *prev;
struct list_head *next;
};然后,把 struct list_head 嵌入到任意需要链表组织的结构体中(比如:表示进程的 struct task_struct):
struct task_struct
{
// 进程的核心属性(简化示意)
pid_t pid;
int priority;
// ... 其他属性 ...
// 嵌入链表节点:用于接入“就绪进程链表”
struct list_head run_list;
// 嵌入链表节点:用于接入“父子进程链表”
struct list_head child_list;
// 嵌入链表节点:用于接入“等待I/O的进程链表”
struct list_head io_wait_list;
};这种设计的核心是 “链表逻辑与数据逻辑解耦”
list_head 负责链表的 “连接”task_struct 负责存储进程的业务数据且一个 task_struct 能同时通过不同的 list_head 接入多个链表。

三、偏移量:从 list_head 反向找到 task_struct
由于 list_head 是嵌入在 task_struct 内部的,内核需要一种方法:通过 list_head 的指针,反向计算出它所属的 task_struct 的起始地址。
这依赖于 “编译期确定的偏移量”:
编译时,编译器会计算出 list_head 成员在 task_struct 中的 “偏移量”(即该成员相对于 task_struct 起始地址的字节差)
运行时,只要拿到 list_head 的指针,就能通过指针运算得到整个 task_struct 的地址
// 伪代码:通过 list_head 指针找到所属的 task_struct
struct task_struct *task = (struct task_struct *)
((char *)list_head_ptr - offsetof(struct task_struct, list_head_member));四、多链表共存:一个进程,多链管理
图中多个 struct task_struct 实例,每个内部都嵌入了多个 list_head,这意味着:
list_head 都独立维护自己的双向链表(next/prev 指针只连接同用途的 list_head)
Linux 内核通过 “侵入式链表” 设计,实现了 “一套链表逻辑,管理所有结构体”:
这种设计极大提升了内核代码的复用性与灵活性,是内核数据结构设计的经典范例。
下面这些状态是在 Linux 内核源代码中定义的:
/*
* 任务状态数组是一种特殊的“位图”,用于表示进程睡眠的原因
* 因此,“运行中”的状态值为 0,而其他状态可以通过简单的位测试来组合判断
*/
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:僵尸状态 */
};各状态的具体含义如下:
等待 I/O 操作完成、等待信号量等资源,在等待期间会暂时让出 CPU,当等待的事件发生时,进程会被唤醒并进入运行状态SIGSTOP 信号来让进程进入停止状态。 SIGCONT 信号来让进程继续运行Ctrl + Z 可以将前台进程暂停,使其进入停止状态,之后若想恢复进程运行,可使用相关命令发送 SIGCONT 信号gdb)跟踪时,可能会进入这种状态,以便调试器对进程进行单步调试等操作ps 等命令很难观察到处于该状态的进程。代码执行完毕或者被信号杀死),但它的进程控制块(PCB)还没有被父进程通过 wait() 系列函数回收。 
在 Linux 系统中,使用
ps ajx命令查看进程状态时,输出结果中的STAT列代表进程的状态:
S:表示进程处于可中断睡眠状态。在这个状态下,进程正在等待某些事件的发生。 +:表示该进程是前台进程组的一部分。 Ctrl+C 发送中断信号来终止进程) 综上所述:进程状态为 S+ 则反映了该进程处于可中断睡眠状态且属于前台进程组的特性。

疑问:为什么注释掉printf语句后,运行该程序时,其对应的进程的状态就变成了R+?
一直在 CPU 上运行或者在就绪队列中等待调度(因为 CPU 可能同时被其他进程占用), 所以进程处于运行状态,显示为R
进程的阻塞状态:在 Linux 系统中对应可中断睡眠状态(S,Interruptible Sleep)和不可中断睡眠状态(D,Uninterruptible Sleep),是进程在等待特定事件发生时所处的一种状态
可中断睡眠状态(S)
等待的事件发生或者接收到特定的信号(如:SIGKILL、SIGCONT等)时,进程会被唤醒,并从阻塞队列转移到就绪队列,等待被 CPU 调度执行read函数读取磁盘文件内容,由于磁盘 I/O 操作相对较慢,在数据读取完成之前,进程会进入可中断睡眠状态,此时如果收到SIGCONT信号,进程可能会被唤醒 ,但如果是读取数据的事件完成了,也会唤醒进程
不可中断睡眠状态(D)
不会响应信号,只有当它等待的事件发生后才会被唤醒阻塞状态是操作系统实现并发处理的重要机制,它使得进程在等待资源或事件时不会占用 CPU 资源,从而提高了系统整体的资源利用率和运行效率。 同时,可中断和不可中断睡眠状态的区分,也保障了系统在不同场景下的稳定性和数据安全性。
进程朝着磁盘喊话:“这里有 100M 的数据,麻烦你帮我存起来。” 磁盘探出 “脑袋”,像是刚被唤醒,揉了揉 “眼睛” 回应道:“行是行,但你得等我一下,别着急走。毕竟写入过程中可能会失败,比如:磁盘空间满了之类的情况,我也没法提前预料。不过不管成功还是失败,我都会告诉你结果,再由你去告知用户操作是成功还是失败。” 进程听后,觉得在此期间确实没什么别的事可做,只能干等,于是便进入了睡眠状态。 没过多久,操作系统从进程身边路过,问道:“进程,你在这儿干什么呢?” 进程回答:“我正等磁盘把数据写完,好把结果告诉用户。” 操作系统面露难色:“你没看到我忙得团团转吗?现在内存空间严重不足,能腾的地方我都腾了,可还是不够用。” 说完,操作系统就把这个进程 “杀死” 了。由于进程受操作系统管控,只能 “自杀” 退出。 结果,磁盘在写到 90M 数据时,发现磁盘空间满了,写入失败,赶忙朝着进程大喊:“100M 数据写入失败了,你快告诉用户!进程,你还在吗?” 可进程已经 “死” 了,磁盘拿着这 100M 数据,不知道该如何是好。重写是写不进去了,还有其他进程等着让它写数据,它忙得不可开交,最后只能把这 100M 数据丢弃。 就这样,从系统层面来看,100M 的数据被丢掉了,而且用户还毫不知情。
要是这 100M 数据是银行一天的转账记录,那对银行来说,损失可就大了。 银行行长把进程、磁盘和操作系统都叫到了办公室。行长开口问道:“这次事故,你们三个谁来承担责任?” 行长先看向磁盘,磁盘赶忙说:“行长,您别看着我呀,您又不是不知道,我就是个‘打杂’的,人家让我干啥我就干啥。我都跟进程说了,让他一定要等我,可最后我写入失败时他不在了,我有什么办法呢。” 行长接着看向进程,进程理直气壮地说:“您看我干什么?我才是受害者呢。我在等待队列里好好等着,突然来了个‘不长眼’的把我杀了,我跟它理论不过,也打不过它,只能乖乖退出了。” 行长最后看向操作系统,操作系统解释道:“行长,您知道我对您是最忠心的。您当初赋予了我权力,当内存空间严重不足时,我有权‘杀’进程。而且如果今天我不‘杀’这个进程,要是因为资源不够导致我操作系统崩溃,那会有几百个进程结束,丢失将近一个 G 的数据呢。”
行长听完他们三个的话,发现每个人说的都有道理,难道错的是自己吗? 最后,银行行长决定:“数据丢了就丢了吧。从今天开始,进程你新增一个状态,叫‘不可中断睡眠’。处于这个状态时,你有权对任何想要‘杀’你的操作不做任何响应。要是进程处于这个状态,操作系统,你就没权‘杀’它了。” 操作系统答应道:“好的。” 进程也说道:“这还差不多。”
所以:从此之后,在对像磁盘这类关键数据存储设备进行高 IO 操作时,进程的状态不再设为S(可中断睡眠),而是设为D(不可中断睡眠)
当进程处于不可中断睡眠状态时,你只能等待该进程自己醒来,或者对操作系统进行重启操作。但有时候,即便重启系统也无法 “杀掉” 处于这种状态的进程,只有通过断电的方式才能将其终止。
进入停止状态的原因
SIGSTOP 信号(编号为 19)和 SIGTSTP 信号(编号为 20) SIGSTOP 是无条件停止进程,且该信号不能被捕获、阻塞或忽略SIGTSTP 通常由终端产生,比如用户在终端中按下 Ctrl+Z 组合键 ,就会向当前前台进程组中的所有进程发送 SIGTSTP 信号,使它们进入停止状态gdb)对进程进行调试时,调试器可以向进程发送控制信号,使进程在特定的断点处或满足特定条件时进入停止状态,方便调试人员检查进程的内存状态、变量值、调用栈等信息 gdb 中设置断点后,当程序执行到断点处,进程就会停止停止状态进程的特点
SIGCONT 信号(编号为 18),可以将其唤醒,使其重新进入就绪队列,等待 CPU 调度执行停止状态

假设在终端中运行一个长时间执行的程序
code.exe:
Ctrl+Z 组合键时,myprogram 对应的进程就会收到 SIGTSTP 信号,进入停止状态T如果想要恢复该进程的执行。可以使用:
fg 命令(将停止的前台进程恢复到前台运行)bg 命令(将停止的前台进程恢复到后台运行) 它们本质上是向进程发送了 SIGCONT 信号。

跟踪停止状态

今天早上你正在户外跑步,途中从你身旁忽然飞快地跑过一个人。就在你继续往前跑时,突然看到前方约 50 米处,那个人 “扑通” 一声直直倒在了地上。你心里一紧,立刻加快脚步跑过去查看,发现他已经没有了呼吸。 你来不及多想,马上掏出手机拨打了 110 报警。没过多久,警察就赶到了现场。他们会不会一来就说 “赶紧把人抬走”?当然不会。警察做的第一件事,是迅速拉起警戒线封锁现场,防止无关人员破坏可能存在的证据;紧接着联系他的家属告知情况,同时通知法医到场 —— 因为必须先明确他的死因:是自杀、他杀,还是突发疾病导致的自然死亡?这些关键信息都需要通过现场勘查和法医鉴定来确认。 在这个人倒下死亡后,到法医完成现场采样、家属赶来将遗体接走之前,他一直躺在原地等待 “处理” 的这段时间,就可以类比为进程的 “僵尸状态”—— 虽然 “生命活动” 已经停止,但还需要等待 “负责方”(警察、法医、家属)完成必要的信息确认和后续处理,不能直接 “清理”。 而当法医采集完有效证据、家属将遗体正式接走后,意味着所有 “后续流程” 已完成,这个状态就相当于进程进入了 “死亡状态”,彻底从现场(系统)中消失。
很多人会有疑问:直接用 “死亡状态” 不就够了吗?Linux 系统为什么还要单独设计一个 “僵尸状态”? 答案的核心,其实藏在父子进程的协作逻辑里
这就决定了子进程 “退出” 不能是 “一键清除”,而需要一个过渡状态来留存关键信息 —— 这就是僵尸状态存在的意义。
具体来说,当子进程执行完任务并退出时,Linux 系统会做两件关键操作:
退出状态码(比如:“0” 表示成功,非零表示失败原因)退出信号(比如:是否被SIGKILL强制终止)CPU 使用时间等核心信息 —— 这些正是父进程需要的 “任务完成报告” 而从子进程退出、系统释放其 代码/数据,到父进程调用wait()/waitpid()函数从 PCB 中读取退出信息的这段时间里,子进程就处于 “僵尸状态”
它的本质是:进程实体已消亡,但 “任务结果凭证”(PCB)仍在,等待父进程确认接收
如果没有僵尸状态,直接让子进程退出后进入 “死亡状态”(即:立即释放包括 PCB 在内的所有资源),会出现什么问题? 父进程将彻底无法获取子进程的执行结果
简单说:僵尸状态就是 Linux 为父子进程设计的 “结果交接缓冲区”:
最终实现了 “资源高效回收” 与 “进程间信息同步” 的平衡 —— 这正是它无法被 “死亡状态” 替代的核心原因。

僵尸进程:处于僵尸状态的子进程是僵尸进程。
这里有个知识点需要思考:进程都已经退出了,内存泄漏问题还存在吗?
僵尸进程本身不会像常规程序中由于动态内存分配未释放等原因导致典型的 “内存泄露”(持续占用堆内存等用户可操作的内存空间且无法回收)
但从系统资源管理的角度,它会造成内存相关的不良影响(进程控制块(PCB)占用)
那操作系统为什么不主动回收僵尸进程的 task_struct(进程控制块,PCB 的核心载体)呢?
task_struct 里保存着子进程的退出状态(比如:是正常结束,还是因信号终止)等关键信息,操作系统需要把这些信息完整地交给父进程task_struct 不释放,直到父进程主动来获取。也正因为如此,“回收僵尸进程、避免内存泄漏” 的责任,就落到了开发者身上
孤儿进程:当一个子进程还在运行时,它的父进程却提前终止了,此时这个子进程就会成为孤儿进程。
产生原因:
程序崩溃、收到致命信号(如:SIGKILL)等原因突然结束运行,而此时它创建的子进程可能还在执行任务,没有来得及完成,这种情况下子进程就会变成孤儿进程
我们之前常说:“只要登录 Linux 系统,就会有一个 bash 进程为我们创建”,那到底是谁创建了这个 bash 呢? 答案是 “系统”,而这里的 “系统”,本质上就是 Linux 中的1 号进程(在传统系统中是 init 进程,现代系统中多为 systemd 进程)—— 它是系统启动后创建的第一个用户态进程,负责初始化系统服务、管理后续进程,堪称系统进程的 “总管家”。
有人可能会问:“既然有 1 号进程,那有没有 0 号进程呢?” 其实是没有持续存在的 0 号进程的
疑问:1 号进程为什么要 “领养” 孤儿进程?如果不领养会怎样? 所谓 “领养”,就是当一个子进程的父进程意外终止时,1 号进程会主动接管这个失去父进程的子进程,成为它的新父进程。 这么做的核心目的,就是避免子进程退出后变成僵尸进程
wait()系列函数回收它的退出状态简单说:在 Linux 系统中,能 “管” 子进程退出回收的只有两方:
这就像现实世界里,孩子的成长首先由家人负责;如果家人无法照料,政府就会出面接管,确保孩子得到妥善安置 ——1 号进程的 “领养”,就是系统层面的 “兜底保障”。
1 号进程领养孤儿进程后,这个孤儿进程通常会转变为后台进程,不再与原终端进行交互绑定。 “后台运行进程” 的概念:平时我们在终端中用
./自己的命令 &这样的格式启动程序时,就是主动将进程放到后台运行。 这种主动启动的后台进程,与孤儿进程转变而来的后台进程,在运行特性上是一致的:
Ctrl+C 发送的中断信号(SIGINT)只会作用于前台进程组的进程,所以用 Ctrl+C 无法终止后台进程标准输出(如:printf 打印的内容)和标准错误信息输出到当前终端界面,不会因为在后台就停止消息打印ps 或 jobs 命令找到它的进程 ID(PID),再用 kill -9 [进程ID] 发送强制终止信号(SIGKILL),才能将其彻底杀死 —— 这是终止后台进程最常用且有效的方式