Linux系统:进程状态与僵尸、孤儿进程
本节重点
- 理解进程状态的概念与分类
- 运行、阻塞和挂起状态
- 理解运行队列与阻塞队列
- 理解僵尸与孤儿进程的形成与危害
一、基本概念
进程状态是操作系统调度和管理进程的重要依据,反映了进程当前是否正在占用 CPU、等待资源、被暂停或已终止等状态。Linux 通过 task_struct
结构体中的状态字段(state
)跟踪进程状态:
/*
* 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 */
};
二、运行、阻塞和挂起
根据冯洛依曼体系,CPU在数据层面只和内存打交道,所以软件和程序运行时必须首先将自己的代码或者数据从磁盘加载到内存之后才能让CPU进一步地访问处理,在之前我们说过操作系统中我们历史上执行的所有指令、工具以及自己的程序运行起来全都是进程。所以本质上就是CPU对各类进程的访问与处理。
2.1 运行状态
2.2.1 运行队列
在引入运行队列这个概念之前,我们需要明白的是CPU中的资源总是有限的,在多任务操作系统中,多个进程或线程需要共享CPU资源。如果不加以合适地组织调度,调度器可能无法有效管理等待执行的进程,导致某些进程长时间得不到执行(即“饥饿”现象)。
如果CPU空闲时没有进程可执行,会导致资源浪费;反之,若多个进程同时竞争CPU,可能导致上下文切换开销过大。
所以为了解决上述问题操作系统引入了运行队列(Run Queue)的概念:
运行队列是调度队列的一种,专门用于管理处于就绪状态的进程或线程。这些进程已准备好执行,但尚未被分配CPU时间。
运行队列存储所有等待CPU调度的进程,确保它们按调度算法(如优先级、时间片轮转)有序执行,除此之外运行队列通过合理调度,减少CPU空闲时间,提高资源利用率。
2.2.2 什么是运行状态
在这里我们称当进程处于运行队列中时进程就处于运行状态,也就是说当进程处于运行队列等待被调度和占用CPU都可以称为运行状态。
即:运行状态=进程占用CPU+进程处于运行队列
2.2 阻塞状态
2.2.1 阻塞队列
我们知道操作系统对进程的管理与维护采用“先描述,再组织”的管理方法,对各种硬件设备的管理也是如此。类似的是,操作系统为了描述和管理硬件设备的各类属性会在内存中创建DCB(设备控制块),其内容涵盖了设备的状态、属性、配置和访问控制等信息。
typedef struct {
char device_id[20]; // 设备标识符
int device_state; // 设备状态
int device_type; // 设备类型
unsigned long device_address; // 设备地址
struct process *wait_queue; // 等待队列指针
void (*driver_entry)(); // 驱动程序入口地址
int irq_number; // 中断号
int dma_channel; // DMA通道号
struct buffer *device_buffer; // 缓冲区信息
} DCB;
// 初始化DCB示例
DCB dcb1 = {
.device_id = "COM1",
.device_state = 0, // 0表示空闲
.device_type = 1, // 1表示字符设备
.device_address = 0x3F8,
.wait_queue = NULL,
.driver_entry = com1_driver,
.irq_number = 4,
.dma_channel = -1, // -1表示未使用DMA
.device_buffer = NULL
};
除此之外,DCB中还包含了一种特殊的数据结构叫做阻塞队列,队列元素就是 task_struct(PCB),当进程访问特定硬件设备的资源但是该资源并未就绪(如申请打印机但设备忙)时操作系统就会将该进程链入该硬件设备DCB中的阻塞队列,从而主动让出CPU。
在操作系统中,阻塞队列是调度器用于管理和存储因等待资源(如I/O、锁、信号等)而进入阻塞状态的进程的一种队列结构。它是实现高效资源管理和进程调度的核心机制之一。
2.2.2 什么是阻塞状态
在操作系统中,阻塞状态是进程生命周期中的一个重要阶段,表示进程因等待某种资源(如I/O操作、锁、信号等)而暂时无法执行,此时即使CPU空闲,进程也无法运行,因为所需资源尚未就绪。操作系统会保存其上下文(如寄存器状态),待事件发生后恢复执行。
此时为了保持CPU的计算效率,防止进程“饥饿”操作系统会在CPU寄存器中保存处于阻塞状态的PCB的上下文并从运行队列中链入该硬件设备DCB的阻塞队列中,当硬件设备资源就绪时,操作系统会重新将该PCB链入运行队列调度执行。
核心特征:进程因缺乏所需资源而暂停执行,主动让出CPU。
2.3 挂起状态
在操作系统中,进程挂起状态是指进程被暂时移出内存,以便为其他进程腾出空间。
当内存中的空间严重不足时,操作系统就会将一部分非关键进程的代码与数据暂时移动到磁盘的Swap分区并释放内存以便为其他进程腾出空间。当内存中有空闲或因为高优先级被唤醒时,操作系统会重新将被挂起进程磁盘中的代码和数据加载到内存。
2.3.1 运行挂起
将运行队列中的PCB所对应的代码和数据唤出到swap分区中
2.3.2 阻塞挂起
将阻塞队列中的PCB所对应的代码和数据唤出到swap分区中
三、Linux进程状态的分类
表中列举出了Linux进程的各种状态以及对应的描述信息,除此之外有以下几个进程状态需要我们特别注意:
3.1 可中断睡眠态(S)
可中断睡眠状态也可以理解为阻塞状态,表示进程正在等待某个事件或者资源就绪(如I/O完成)。
#include<stdio.h>
int main()
{
int a;
scanf("%d",&a);
return 0;
}
此时查询该进程状态信息如下:
此时终止进程有两种方法:一种是使用Ctrl+c终止进程,此外也可以使用指令
kill -9 进程所对应的PID
来杀死进程。
3.2 不可中断睡眠态(D)
在操作系统中,不可中断休眠状态(通常称为深度睡眠状态)是一种特殊的进程状态,此时进程既不响应异步信号,也不被调度器考虑,直到特定的条件被满足才会被唤醒。这种状态通常用于进程必须等待某些关键事件(如磁盘I/O完成)且不能被中断的场景。
也就是说进入D状态后进程在此状态下不响应任何信号(包括强制终止信号SIGKILL
),确保操作(如磁盘I/O)的完整性。
与可中断休眠状态的区别
特性 | 不可中断休眠状态 | 可中断休眠状态 |
---|---|---|
信号响应 | 不响应(包括SIGKILL ) | 可被信号唤醒 |
典型场景 | 磁盘I/O、硬件操作 | 用户输入、网络等待 |
风险 | 长时间阻塞导致系统挂起 | 无(可被信号终止) |
风险:
-
系统挂起:长时间处于
D
状态的进程占用资源,导致系统无响应。 -
调试困难:无法通过
kill -9
终止,需重启系统或修复硬件/驱动。
四、僵尸与孤儿进程
4.1 僵尸进程(Z状态)
在了解僵尸状态之前我们需要明白的是,我们为什么要创建一个子进程呢?我们创建一个子进程的目的是为了让子进程完成某件事情,完成的结果或者子进程返回的信息需要返回给父进程,当子进程结束到父进程获取子进程返回值的这一段时间中,子进程就会处于僵尸状态(Z状态)。
4.1.1 定义
僵尸进程是在Unix/Linux系统中,当子进程已经结束运行,但父进程还没有调用wait()或waitpid()来获取其退出状态,导致子进程的进程表项仍然保留,无法被释放的现象。这时候子进程就变成了僵尸进程。
4.1.2 产生原因
- 子进程终止,父进程未回收:当子进程退出时,若父进程未调用
wait()
或waitpid()
读取或回收子进程的退出信息,子进程会在自己的PCB中保留退出状态(如退出码、资源使用统计)并进入僵尸状态。 - 父进程忽略或延迟处理:父进程可能因逻辑错误、阻塞或崩溃,未能及时回收子进程。
4.1.3 特征与危害
- 特征:在
ps
或top
命令中显示为Z
(如<defunct>
) - 危害:僵尸进程不占用 CPU 和内存,但会占用进程表条目。若大量积累,可能导致系统无法创建新进程(进程表耗尽)。
4.1.4 代码示例
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//child
int cunt=5;
while(cunt)
{
printf("我是一个子进程 pid=%d ppid=%d\n",getpid(),getppid());
sleep(1);
cunt--;
}
}
//father
//父进程啥也不做,不去回收子进程的退出信息
else
{
while(1)
{
printf("我是一个父进程 pid=%d ppid=%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
此时我们反复查询进程状态:
while : ; do ps axj | head -1 ; ps axj | grep code | grep -v grep ; sleep 1 ;done
.c 文件运行结果:
查询结果:
4.2 孤儿进程
4.2.1 定义
当父进程先于子进程终止,子进程会失去父进程,此时该子进程被称为孤儿进程。系统会自动将孤儿进程的父进程设置为 init
进程(PID=1)或现代 Linux 系统中的 systemd
进程,由它们负责接管。
4.2.2 产生的原因
- 父进程崩溃或被强制终止(如
kill -9
),但子进程仍在运行。 - 父进程未正确处理子进程退出(如未调用
wait()
),但父进程自身提前退出。
4.2.3 影响
孤儿进程可能会长期运行,占用系统资源(如果没有正确退出),但是我们也不必太过关心因为只要孤儿进程形成,init / systemed 进程接管。
4.2.4 代码示例
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//child是一个死循环,使其一直存在
while(1)
{
printf("我是一个子进程 pid=%d ppid=%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//father循环5次后退出
int cunt=5;
while(cunt)
{
printf("我是一个父进程 pid=%d ppid=%d\n",getpid(),getppid());
cunt--;
sleep(1);
}
}
return 0;
}
运行结果:
需要注意的是当子进程被系统领养的时候,子进程会自动变为后台进程无法被 ctrl+c 终止。
4.2.5 僵尸与孤儿进程的区别
对比项 | 孤儿进程 | 僵尸进程 |
---|---|---|
进程状态 | 仍在运行 | 已终止,但残留信息未清理 |
产生原因 | 父进程终止,子进程继续运行 | 父进程未回收子进程的退出状态 |
资源影响 | 由init 接管,无资源泄露风险 | 占用进程表条目,可能浪费资源 |
处理方式 | 由init 进程自动接管和清理 | 需父进程调用wait() 或信号处理 |
系统危害 | 通常无害 | 大量积累可能导致进程表耗尽 |