前言
在操作系统的世界里,进程的 “生老病死” 并非随机无序,而是被一套精密的状态管理机制所调控。无论是程序从启动到运行的 “活跃期”,因等待资源而进入的 “阻塞态”,还是因内存紧张被暂时换出的 “挂起态”,这些状态的切换背后,藏着操作系统对资源调度、设备管理的底层逻辑。
本文将从理论与实践两个维度拆解进程状态:先从操作系统的通用模型出发,解析 “运行、阻塞、挂起” 三大基础状态的本质 —— 如何通过 PCB(进程控制块)在不同队列间的移动实现状态切换,以及进程调度与设备管理如何通过结构体和队列协同工作;再聚焦 Linux 系统,详细列举其特有的进程状态(如 R、S、D、Z、T 等),结合实例说明每种状态的触发场景、内核逻辑及观察方法。
无论你是想理解操作系统的调度原理,还是想搞懂 Linux 中 “僵尸进程”“不可中断睡眠” 等具体问题,这篇文章都将为你搭建起从抽象理论到具体实现的桥梁,让你对进程状态的认知从 “零散概念” 升华为 “体系化逻辑”。
目录
进程状态(操作系统)

运行&&阻塞&&挂起(概念)
挂起:CPU,内存资源比较紧时,将进程的代码和数据放到外设磁盘交换分区中,腾出可用资源。(阻塞挂起和就绪挂起)
✅ 1、进程调度与设备管理
本质:结构体与队列管理
我们可以把操作系统的运行抽象为:
“通过维护多个队列和结构体,实现对 CPU、内存、设备等资源的调度与控制”
🧠 2、PCB:进程控制块是调度核心
struct task_struct {
pid_t pid;
long state; // 进程状态:running、sleeping 等
unsigned long flags;
struct mm_struct *mm; // 指向进程地址空间的指针
struct task_struct *parent;
struct list_head run_list; // 运行队列用的链表节点
...
};
✳ 位置:
所有 PCB 放在一个全局链表中,便于进程查找和管理
同时活跃进程还会被挂入运行队列 runqueue中,供调度器选择运行
🏃♂️ 3、运行队列 runqueue
struct runqueue {
struct task_struct *curr; // 当前正在运行的任务
struct list_head queue; // 任务链表
int nr_running; // 正在运行的任务数
};
操作系统调度器从
runqueue
里选出下一个运行进程,赋值给 CPU 执行。所以 “运行状态” = 在运行队列中 + 被 CPU 选中执行中
💡 4、状态切换的本质:PCB在不同队列之间移动
状态 | 描述 | 数据结构变化 |
---|---|---|
running | 占用 CPU 正在运行 | PCB 在 runqueue 中,且被调度执行 |
ready | 有资格运行但暂未被调度 | PCB 在 runqueue 中等待 CPU |
sleeping | 等待 I/O(如键盘、网卡等) | PCB 被挂到设备的 wait_queue 上 |
zombie | 运行结束但父进程未回收资源 | PCB 保留在进程表中 |
stopped | 被暂停 | 被挂到某个暂停状态队列(如 ptrace) |
🖥️ 5、设备管理结构体 device 与等待队列
struct device {
int id;
int status; // 是否就绪
void *data; // 设备私有数据
struct device *next; // 串联所有设备
int type; // 类型:键盘、网卡...
struct task_struct *wait_queue; // 等待此设备的进程队列
};
设备访问流程:
进程访问设备但设备未就绪
系统将当前 PCB 加入设备的 wait_queue
将进程状态设为
sleeping
当设备就绪,唤醒
wait_queue
上的进程 → 重新进入runqueue
🔁 6、状态转换流程图
+----------+ block (I/O wait) +------------+
| running | -----------------------> | sleeping |
+----------+ +------------+
|
| preempt/yield
v
+----------+ wakeup +------------+
| ready | <---------------------- | waiting |
+----------+ +------------+
|
| schedule()
v
+----------+
| running |
+----------+
✅ 操作系统本质上是:
“通过结构体组织资源,通过队列调度进程,通过状态切换实现多任务并发。”
进程状态的变化表现之一就是要在不同的队列中进行流动,本质就是就是数据结构的增删查改。把PCB在两个队列中来回串。
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 */
};
详细解释与实例
有+是进程是在前台出现,在命令行。没有+是在后台进行的。
while :; do ps axj | head -n 1; ps axj | grep -v grep | grep code; sleep 1; done
grep -v grep
是一个常用的过滤技巧,用于排除包含 grep
自身的进程。
./code &(在后面加&)
等待键盘输入,S阻塞sleeping 可中断休眠,浅度睡眠,可以杀掉
T,t暂停例子
debug下,gdb对程序断点调试,t进行追踪
ctrl Z 暂停程序
D 深度休眠,代表进程正在等待某个 I/O 操作(如磁盘读写、网络请求、外设交互等)完成,且在此期间无法被中断或杀死。该状态不做演示,一般都是高IO操作,若出现D,就要检查磁盘等设备了。
Z僵尸进程,为了获取退出信息。
在Linux系统里,所有的进程是某个进程的子进程,要么是bash的,要么是自己的,创建子进程的目的,是为了让子进程完成某种事情的。父进程就需要知道完成的结果的相关信息,子进程退出不能释放所有资源,会保留最小化的 PCB 信息,退出的信息暂时维持住,在子进程退出之后,父进程获取信息之前,就要有一个状态就是Z状态。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(){
pid_t id=fork();
if(id<0){
perror("fork");
}
else if(id==0){
int count=5;
while(count--){
printf("我是一个子进程,pid=%d,count:%d.\n",getpid(),count);
sleep(1);
}
}
else{
while(1){
printf("我是一个父进程,pid=%d\n",getpid());
sleep(1);
}
}
return 0;
}
当子进程在 5 秒后退出时,由于父进程没有调用wait()
或waitpid()
来回收子进程的退出状态,子进程会变成僵尸进程(Z 状态)。
如果父进程一直不管,不回收,不获取子进程的退出信息,那么Z一直存在,会引起内存泄漏问题。
进程退出了,内存泄漏问题还在不在?自己代码引起的内存泄漏不存在了。
什么样的进程具有内存泄漏问题是比较麻烦的?常驻内存的进程
结束语
进程状态的管理,本质上是操作系统对 “有限资源” 与 “无限需求” 的动态平衡艺术。从理论层面的 “运行、阻塞、挂起” 模型,到 Linux 系统中精细的状态划分(如 D 状态对 I/O 原子性的保护、Z 状态对父子进程通信的支持),每一种状态的设计都承载着特定的功能目标 —— 既要保证进程高效运行,又要避免资源竞争与数据不一致。
理解进程状态,不仅是掌握 “查看状态”“分析问题” 的实用技能,更能帮我们透过现象看本质:操作系统如何通过 PCB 和队列 “操控” 进程的生命周期?不同状态的转换如何体现 “调度优先” 与 “资源依赖” 的权衡?这些思考,将为我们深入学习进程调度算法、内存管理、设备驱动等知识打下坚实基础。
如果你在实践中遇到了特殊的进程状态问题(如长期 D 状态的排查、僵尸进程的清理),不妨回头再梳理这些理论逻辑 —— 答案往往就藏在状态转换的底层原理中。欢迎在评论区分享你的实践案例,让我们一起在 “理论指导实践,实践反哺理解” 的循环中,深化对操作系统的认知。