进程状态
github地址
0. 前言
进程状态是操作系统课程的核心概念之一,也是理解进程调度的关键基础。本文将带您深入探讨操作系统中进程状态的底层机制,特别是Linux
系统中的具体实现。我们将从经典的操作系统理论出发,逐步过渡到Linux
内核中的实际状态管理,揭示R
(运行)、S
(睡眠)、D
(磁盘休眠)、T
(停止)等状态的本质区别。通过代码演示和命令操作,您将直观看到:
- 进程如何在运行队列和等待队列中迁移
- 僵尸进程(Z状态)的产生原理和危害
- 孤儿进程如何被操作系统领养
- 挂起状态的底层内存交换机制
无论您是操作系统初学者,还是希望深入理解Linux进程管理的开发者,本文都将为您揭示进程状态背后的技术真相。让我们开始这段探索之旅!
1. 一般操作系统学科的进程状态及相关概念
- 进程的不同状态,就是把进程的PCB放在不同的队列中
- 放入运行队列,为运行态
- 放入等待队列,为阻塞态
1.1 运行状态
1. 运行队列、运行态
一个CPU
会维护一个运行队列,需要被运行的进程的task_struct
会被链入到运行队列中。
多个CPU
会维护多个运行队列
运行队列
- 每个
CPU
都有一个进程的队列,存放每个进程的PCB
。进程的PCB
在该队列中排队,这个队列就叫运行队列
运行状态(R状态)
- 处于运行队列中(代表进程已经准备好了,随时可以被调度)的进程或正在被CPU运行的进程,所处的状态就叫运行态
2. 时间片
思考?
- 一个进程只要把自已放到CPU上开始运行了,是不是一直要执行完毕,才把自己放下来?
不是!!! 当我们的程序内部写了
while(1);
时,系统内的其他进程依然可以正常运行!如果一直要执行完毕才把自己放下来,那么就意味着其他的进程再也不会被调度了
为了防止进程上去就不下来的情况,每个进程都有一个叫做 时间片 的概念
时间片就是一个int
类型的变量,用来记录每个进程在CPU
上运行的时间
//时间片
int time_space; // 时间片为一个固定的时长
- 每个进程运行固定的时间片后,会被放置到运行队列的尾部,重新排队等待运行
3. 并发执行
正因为有了时间片的概念,在一个足够长的时间段内,所有的进程才都会被执行!
- 多个进程,在一个足够长时间段内都会被执行,称为并发执行。每个进程交替运行一个时间片后重新在运行队列中排队
4. 进程切换
- 多个进程,在一个时间段内,都会被执行,称为并发执行,每个进程交替运行一个时间片
- 多个进程要交替运行,因此一定会存在,大量的把进程从
CPU
上放上去,拿下来的动作。这个动作叫做进程切换
1.2 阻塞状态
- 在等待特定设备(硬件)就绪的进程,该进程所属的状态,叫做阻塞状态
- 如果一个进程要用
scanf
读取键盘的输入,但键盘内一直没有输入时,进程就只能在该键盘设备的等待队列中进行等待。在键盘的等待队列中,等待键盘的输入。 - 内存中有成百上千个等待队列,每个设备都有自己的等待队列
- 进程需要等待设备时,操作系统就把该进程的
PCB
链接到设备的等待队列 - 进程需要等待进程时,操作系统就把该进程的
PCB
链接到特定进程的等待队列
- 进程需要等待设备时,操作系统就把该进程的
- 如果一个进程要用
PCB
处于等待队列中的进程,所处的状态,就叫做阻塞状态
等待队列的本质
- 等待队列是操作系统内核为每个资源(如外设)维护的数据结构,用于管理因等待该资源而阻塞的进程。例如:
- 当一个进程需要读取键盘输入但键盘数据尚未就绪时,它会被加入键盘设备的等待队列。
- 当设备数据就绪(如键盘缓冲区有输入),内核会唤醒该队列中的所有或部分进程。
- 等待队列的实现通常基于双循环链表,通过等待队列头(
wait_queue_head_t
)管理所有等待项(wait_queue_entry_t
),每个项关联一个休眠的进程。
1.3 挂起状态
挂起状态:一个进程,只有
PCB
在内存中,代码和数据不在内存中,被操作系统换出了,此时该进程称为挂起状态例如,一个进程在长时间的等待键盘输入,很长时间键盘都没有输入,一直在等待,该进程处于阻塞状态
该进程在等待的过程中,操作系统突然内存资源严重不足了,操作系统为了保证运作正常,需要想办法省出来内存资源
一个进程,无论属于运行态还是阻塞态,只要在没有被CPU调度运行的那一刻,当前进程的代码和数据,在内存中是处于空闲状态的(没有被CPU运行)。此时操作系统想的办法是,把当前进程的
PCB
保留下来,再把当前进程处于内存中的代码和数据,交换到外设中(例如交换到磁盘中)。相当于一个进程,只有PCB
在排队,对应的代码和数据存放在外设中。这样就为其他进程省出来了内存空间
内存数据的换出与换入
- 换出:把当前进程的
PCB
保留下来,把当前进程处于内存中的代码和数据,交换到外设中(例如交换到磁盘中),该过程称为换出 - 换入:换出后的某个时刻,进程在等待的设备就绪了,把这个进程放入运行队列,再把处于外设(比如磁盘)中的代码和数据加载到内存中,该过程称为换入
- 换出:把当前进程的
以上所说的的挂起,严格来说,是阻塞挂起状态。挂起就是挂起到外设中了
还有运行挂起状态,就绪挂起状态,但很少遇见
- 对于挂起状态的进程,用户通常是无法感知到的
操作系统的理论,放在任何一个特定的操作系统上,都是成立的,但是不同的操作系统的实现不一样。下面我们来看
Linux
系统对操作系统理论的具体实现。
2. Linux的进程状态是如何维护的
- 进程的不同状态,就是把进程的PCB放在了不同的队列中,就有了不同的状态
- 不同的状态,决定了该进程当下要做什么事
- r状态:接下来要被调度运行了
- **阻塞状态:**等条件就绪。等设备准备好,把状态改成运行状态,将PCB链入到CPU的运行队列中,等待CPU再调度运行
- **挂起状态:**运行之前,把曾经换出的代码和数据换入。再把进程状态改成运行状态,将PCB链入到CPU的运行队列中,等待CPU再调度运行
我们来看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* 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 */
};
- Linux系统中进程状态分为七种:
R
运行状态(running)
:并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。S
睡眠状态(sleeping)
:意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
D
磁盘休眠状态(Disk sleep)
有时候也叫不可中断睡眠状态(uninterruptible sleep)
,在这个状态的
进程通常会等待IO的结束。T
停止状态(stopped)
: 可以通过发送SIGSTOP
信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。t
状态:暂不区分T
和t
状态X
死亡状态(dead)
:这个状态只是一个返回状态,你不会在任务列表里看到这个状态
不要用我们人的感受去衡量CPU的速度
1. 运行状态(R)
1. 辨析R与S
一个程序正在被CPU运行或在运行队列中排队,所处的状态就是运行状态R
。来看如下程序:
#include <stdio.h>
int main() {
while1(1) {
printf("hello Linux World\n");
}
return 0;
}
为什么该进程在运行,状态却是S状态;
- 再看如下代码:
#include <stdio.h>
int main() {
while1(1);
//{
// printf("hello Linux World\n");
//}
return 0;
}
将
while
循环中的printf
函数去掉后,进程的状态就变成了R
,这是为什么呢?
while
循环中有printf
时,进程为S
状态,去掉后变为R
状态。下面我们来分析原因:- 不要用我们人的速度去衡量CPU的速度
原因是,由于CPU
的速度极快,printf
向显示器中打印时,显示器并不是一直处于可以写入的状态,该进程需要等待显示器设备就绪,需要花费大量的时间等待显示器设备就绪,因此进程长时间处于S
状态,S
状态对应于操作系统理论中的阻塞状态
去掉printf
后,没有访问任何外设,无需等待IO设备就绪,因此处于R
状态。也就是运行状态
2. top命令的R与S状态
- 再看我们常用的
top
命令
top
命令也是检测进程的信息将其打印在显示器上,需要等待显示器设备就绪,因此top
进程会时隐时现
top
运行是R
状态,等待显示器就绪时是S
状态。
3. 神秘的+号和&号
- 细心的宝子,就会发现,我们在查看进程状态时,标识进程状态的字母右上角都有一个
+
号,这是什么含义?
这里的R+标识是进程在前台运行。
- 前台运行:进程运行后,
bash
命令行无法再输入命令。这就属于前台运行的现象。
如果我们想让进程在后台运行,可以进行如下操作:
- 执行时加上
&
可以让进程在后台运行。
加上&
让myproc
在后台运行。再次查看时,进程的状态就变成了R
,bash
中也就可以输入命令了。
另外需要注意的是:
如果一个进程处于
R
状态(非R+
)状态,那么该后台运行的进程无法通过ctrl+c
终止。原因是bash命令行终止,我们的命令行失效了,无法读取输入,ctrl+c
就也失效了。需要通过
kill
命令杀掉
2. 阻塞状态(S)
S
睡眠状态(sleeping)
: 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
阻塞状态(S),又被称为浅度睡眠状态,可以随时被唤起
系统中的大部分进程都处于S
状态。因为当系统中没有任务时,大部分的进程处于等待资源的状态,因此处于阻塞状态
bash
进程大部分时候也处于阻塞状态,等待用户输入命令
- 我们来看以下处于阻塞状态的进程:
进程等待键盘的输入时,该进程也就处于了阻塞状态。
int main() { //while(1){ // printf("hello world\n"); // sleep(1); //} int a = 0; printf("Please Enter #:"); scanf("%d", &a); printf("%d\n", a); return 0; }
3. 深度睡眠状态(D)
disk sleep
操作系统在内存资源严重不足时,是会杀掉进程的
disk sleep D
状态的一个例子就是:进程在等待磁盘写入完毕期间,该进程不能被任何人kill
掉,包括操作系统
状态定义
- 进程因等待I/O操作(如磁盘读写、网络传输或外设访问)而进入阻塞,且无法被任何信号中断,包括
SIGKILL
(kill -9
)。
其他特征
- 数据完整性保障:操作系统设计此状态是为了确保关键I/O操作(如写磁盘)不被意外中断,防止数据损坏或丢失。
- 资源占用:进程的
task_struct
(PCB)仍驻留内存,但不会参与CPU
调度。
深度睡眠状态(D)为什么不会被杀死,因为D状态的进程不响应操作系统的任何请求
4. 暂停状态T(stopped)
1. 定义与触发机制
- 本质:进程被信号(如
SIGSTOP
)强制暂停,进入不可自主恢复的停滞状态。 - 触发方式:
- 用户级停止:终端按
Ctrl+Z
发送SIGSTOP
信号。 - 系统级停止:管理员通过
kill -SIGSTOP <PID>
手动暂停进程。 - 调试器控制:被调试器(如GDB)跟踪时,在断点处暂停(此时状态为
t
)。
- 用户级停止:终端按
- GDB调试为进程暂停状态T(stopped)被触发的主要场景。
2. 核心特性
- 暂停执行:进程代码停止运行,不占用
CPU
资源,但保留内存等上下文。 - 资源保留:进程的
task_struct
(PCB)和内存数据不被释放,可随时恢复。 - 信号响应:
- 不响应普通信号:如
SIGTERM
(终止信号)无法终止T状态进程。 - 仅响应特定信号:必须通过
SIGCONT
(继续信号)恢复运行。
- 不响应普通信号:如
可以通过向进程发送特定信号的方式,使进程暂定和恢复
- 当前进程每隔一秒输出一个
hello world
,处于睡眠状态(阻塞状态) - 可以发送18和19号信号使其恢复和暂停
- 发送19号信号后,进程暂停,终端出现相应提示
- 对暂停的进程发送18号信号后,进程继续运行
3. S状态和T状态的区别
1. 定义与触发机制
- T 状态(Stopped)
- 定义:进程被强制暂停执行,通常由调试信号或管理操作触发。
- 触发方式:
- 用户或管理员发送
SIGSTOP
信号(如终端按Ctrl+Z
或执行kill -19 <PID>
)。 - 调试器(如 GDB)在断点处暂停进程(此时状态为
t
,即跟踪停止)。
- 用户或管理员发送
- Sleep 状态(S 状态)
- 定义:进程因等待资源就绪(如 I/O 操作、数据输入)而主动进入休眠。
- 触发方式:
- 调用阻塞式系统调用(如
scanf()
等待键盘输入、read()
等待磁盘 I/O)。 - 资源未就绪时由内核自动挂起(如网络请求未响应)。
- 调用阻塞式系统调用(如
2. 核心特性对比
特性 | T 状态(Stopped) | Sleep 状态(S 状态) |
---|---|---|
可中断性 | ❌ 不响应普通信号(如 SIGTERM ) |
✅ 可被信号唤醒(如 SIGINT 、SIGKILL ) |
恢复条件 | 必须接收 SIGCONT 信号(kill -18 ) |
资源就绪时自动恢复,或由信号强制唤醒 |
资源占用 | 保留内存和文件描述符,不消耗 CPU | 保留内存和资源,不消耗 CPU |
典型场景 | 调试断点、系统维护、批处理任务暂停 | 等待 I/O(磁盘、网络)、用户输入 |
3. 行为差异详解
- 响应信号的能力
- T 状态:仅响应
SIGCONT
(恢复)和SIGKILL
(强制终止),不响应SIGTERM
等普通终止信号。 - S 状态:可被任意信号中断(如
kill -9
能立即终止进程)。
- T 状态:仅响应
- 恢复流程
- T 状态:必须通过
kill -18 <PID>
显式恢复,或由调试器控制(如 GDB 的continue
命令)。 - S 状态:当等待的资源就绪(如磁盘 I/O 完成、键盘输入到达),内核自动将其重新加入运行队列。
- T 状态:必须通过
- 对系统的影响
- T 状态:常用于主动控制进程生命周期(如调试时检查变量),暂停后进程状态稳定,无数据损坏风险。
- S 状态:若因 I/O 故障长时间阻塞,可能拖慢系统响应,但可通过优化 I/O 或硬件解决。
4. 常见误区与注意事项
- 混淆点:
T
和t
的区别:t
是调试专用的暂停(如 GDB 断点),比T
多一层保护(不响应SIGCONT
)。- Sleep 的子状态:
S
是可中断睡眠,D
(Disk Sleep)是不可中断睡眠(如写磁盘关键数据),后者无法被任何信号终止。
- 风险提示:
- T 状态进程长期暂停:可能导致依赖进程超时(如网络服务),需及时恢复。
- S 状态滥用:频繁 I/O 阻塞可能引发系统负载升高,需优化代码逻辑或使用异步 I/O。
💡 一句话区别:
T 状态是“被人为按了暂停键”,S 状态是“自己躺下等快递” ——前者需外部唤醒,后者到货自动起床。
5. 死亡进程(X)与僵尸进程(Z)
1. 死亡状态(X状态)
- 定义与核心特性
- 瞬时性:X状态是进程完全终止后的最终状态,表示进程所有资源(包括内存、文件描述符、内核数据结构)已被操作系统彻底回收。
- 不可见性:该状态仅存在于进程资源释放的瞬间(通常持续微秒级),无法通过
ps
或top
等工具观察到。 - 触发条件:当进程通过
exit()
系统调用正常结束,且父进程已通过wait()
/waitpid()
读取其退出状态后,进程立即进入X状态并消失。
- 设计意义
- 资源清理:X状态是内核回收资源的最后一步,确保无内存泄漏或PID浪费。
- 状态闭环:标志进程生命周期正式结束,是状态机中的终点(如状态转换图:
Z → X
)。
2. 僵尸状态(Z状态)
定义与核心特性
- 残留性:子进程已终止(代码停止执行),但父进程未调用
wait()
回收其退出状态,导致进程的task_struct
(PCB)仍驻留内核进程表。 - 资源占用:
- 释放所有内存和CPU资源,仅保留PCB记录退出码(约1.7KB内存)。
- 持续占用PID号,可能导致系统PID耗尽(上限由
/proc/sys/kernel/pid_max
设定)。
- 不可杀性:无法通过
kill -9
终止,因进程实际已死亡,仅留“尸体”。
- 残留性:子进程已终止(代码停止执行),但父进程未调用
触发场景与危害
- 典型场景:父进程未正确处理
SIGCHLD
信号,或存在逻辑缺陷(如未调用wait()
)。 - 系统风险:
- 内存泄漏:大量僵尸进程积累消耗内核内存,极端情况导致无法创建新进程(PID耗尽)。
- 性能下降:进程表满负荷时,系统调度效率降低。
- 典型场景:父进程未正确处理
检测与清除
检测命令:
ps aux | grep ' Z ' # 筛选Z状态进程 ps -eo stat,pid,cmd | grep '^Z' # 显示僵尸进程详情
清除方法:
- 终止父进程:父进程退出后,僵尸进程被
init
(PID 1)接管并自动清理。 - 修复父进程:添加
SIGCHLD
信号处理函数,调用waitpid()
回收子进程。 - 系统重启:极端情况下强制重启释放所有残留资源。
- 终止父进程:父进程退出后,僵尸进程被
3. 操作演示及其注意事项
以下代码:
- 子进程循环5次后结束,父进程为死循环
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// child
int cnt = 5;
while (cnt) {
printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
} else {
// father
while (1) {
printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
// 父进程没有针对子进程干任何事情
}
return 0;
}
5秒过后,子进程退出,父进程仍在运行。父进程的代码中没有对子进程的资源进行回收,因此变成僵尸进程
- 子进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态。进程的相关资源尤其是
task_struct
结构体不能被释放。<defunct>
的意思即为死的,意为僵尸进程- 未来父进程将子进程回收后,操作系统才能将子进程的资源进行释放。
- 如果一个进程一直处于僵尸进程,自身的内存资源会被一直占用,从而引发内存泄露。之后可以在父进程中调用
waitpid();
函数解决该问题
4. 僵尸进程的危害
**僵尸进程(Zombie Process)**在Linux系统中是已终止但未被父进程完全回收资源的子进程。尽管其代码执行已结束,仍会占用系统资源并引发连锁问题。主要危害如下:
1. 资源占用与进程表耗尽
- 进程表项占用:每个僵尸进程占据内核进程表中的一个条目,导致系统可用进程槽位减少。进程表大小有限(通常由内核参数
pid_max
定义),大量僵尸进程可能耗尽所有槽位。 - 新进程创建失败:当进程表满时,系统无法创建新进程(如
fork()
系统调用返回失败),导致服务崩溃或任务调度中断。
2. 系统性能下降
- 管理开销增加:操作系统需持续维护僵尸进程的元数据(如PID、退出状态),占用CPU时间和内核资源,尤其在频繁创建进程的场景下(如Web服务器),显著拖慢响应速度。
- 内存与文件描述符泄漏:若僵尸进程未释放打开的文件或网络端口,可能间接导致文件描述符耗尽或端口占用,影响其他进程的正常操作。
3. 孤儿进程
现象
- 这次我们使用以下代码测试,让父进程先退出,观察结果。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// child
int cnt = 500;
while (cnt) {
printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
} else {
// father
int cnt = 5;
while (cnt--) {
printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
// 父进程没有针对子进程干任何事情
}
return 0;
}
- 以上代码父进程五秒后先退出,子进程500秒后再退出,现象如下
==那么PID为1的进程是什么进程呢?==我们使用top
命令查看
孤儿进程概念
- 孤儿进程(Orphan Process)指父进程已终止或退出,但子进程仍在运行的进程。此时子进程失去父进程的管理,成为“孤儿”。操作系统会将其交由
init
进程(PID=1)接管,确保资源正常回收。
父子进程中,如果父进程先退出,子进程的父进程会被改成1号进程,也就是操作系统进程。此后子进程会运行在后台
定义:父进程是1号进程的进程,被称为孤儿进程,标识该进程被系统领养!
产生原因
孤儿进程的形成通常源于以下场景:
- 父进程意外终止:如程序崩溃、系统错误或强制杀死父进程(
kill -9
)。 - 父进程设计缺陷:父进程未正确处理子进程退出逻辑(如未调用
wait()
回收资源)。 - 异步进程创建:父进程启动子进程后立即退出,未等待子进程结束。
- 长时间运行的子进程:父进程先于子进程结束(常见于后台任务或守护进程)。
- 孤儿进程因父进程退出而被init进程收养,自动转为后台运行,且不受原终端影响
- 在
ubuntu22.04
系统中,1号进程为init
进程,和CentOS
系统中的systemd
进程一样,都表示系统进程
为什么要被系统领养?
因为孤儿进程的相关资源也需要被释放,否则会引起内存泄漏
此处,父进程终止后,原来的子进程直接被操作系统领养,变成孤儿进程。操作系统直接将该进程领养并回收了。
父进程终止后,僵尸子进程会被 init
(或 systemd
)接管,并由其回收资源,不再是僵尸进程
- 因此一个进程创建子进程后,父进程的代码中要有对子进程的回收操作。
4. 结语
通过本文的系统性解析,我们深入理解了操作系统进程状态的核心机制及其在Linux
中的具体实现。关键要点总结如下:
- 状态本质:进程状态实质是PCB在不同队列中的位置映射
- 状态转换:
R
状态在运行队列等待CPU
调度S/D
状态在设备等待队列阻塞Z
状态因父进程未回收资源而滞留
- 特殊进程:
- 僵尸进程:必须通过父进程
wait()
回收资源 - 孤儿进程:由**init进程(PID=1)**自动接管回收
- 若父进程因未调用
wait()
导致子进程僵尸化,终止父进程是有效的清理手段:原僵尸子进程会被init
进程回收,彻底释放资源。此机制是Linux系统的重要设计,确保即使父进程异常退出,僵尸进程也不会永久残留。 - 实际建议:在编写多进程程序时,应通过
wait()
或SIGCHLD
信号处理函数主动回收子进程,避免依赖父进程终止作为常规清理方式
- 僵尸进程:必须通过父进程
- 实践启示:
- 避免僵尸进程积累(防止PID耗尽)
- 长时间阻塞操作使用
D
状态(保障数据完整性) - 后台进程使用
&
符号管理(R
状态无+
)
通过
while(1)
配合printf
观察S/R
状态转换,或使用fork()
创建僵尸进程的实验,能极大加深对进程状态的理解。建议读者实际操作文中的代码示例,观察不同状态下的进程行为。
以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步
分享到此结束啦
一键三连,好运连连!
你的每一次互动,都是对作者最大的鼓励!
征程尚未结束,让我们在广阔的世界里继续前行!
🚀