【Linux】进程状态全解析:从 R/S/D/T 到僵尸 / 孤儿进程


【Linux】深入理解进程状态:从创建到终止全流程解析

  • 摘要
  • 目录
    • 一、Linux进程状态
      • [1. R(runing)--- 运行状态](#1. R(runing)— 运行状态)
      • [2. S(sleeping)--- 睡眠状态](#2. S(sleeping)— 睡眠状态)
      • [3. D(disk sleep)--- 磁盘休眠状态](#3. D(disk sleep)— 磁盘休眠状态)
      • [4. T(stopped)--- 停止状态](#4. T(stopped)— 停止状态)
      • [6. X(dead)--- 死亡状态](#6. X(dead)— 死亡状态)
    • 二、僵尸状态进程
      • [1. 定义](#1. 定义)
      • [2. 危害](#2. 危害)
    • 三、孤儿进程
  • 总结

摘要

Linux进程状态是进程在其生命周期中所处的不同状况,主要包括运行(R)、睡眠(S)、磁盘休眠(D)、停止(T)、僵尸(Z)和死亡(X)。这些状态反映了进程是正在执行、等待资源还是已经终止。其中,僵尸状态是一个关键概念,指子进程已退出但其退出信息未被父进程回收,导致其进程控制块无法释放,从而占用系统资源。若父进程先退出,子进程则会成为"孤儿进程",并由1号init进程接管,以确保其最终能被正确回收,避免资源泄漏。


目录

一、Linux进程状态

一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽然可以运行但分不到处理器,有时虽然有空闲处理器但因等待某个时间的发生而无法执行,这一切都说明进程和程序不相同,进程是活动的且有状态变化的,于是就有了进程状态这一概念。

cpp 复制代码
//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*/
};

注意:

  1. 进程的当前状态是保存到自己的进程控制块(PCB)当中的,在Linux操作系统当中也就是保存在task_struct当中的。

  2. 在Linux操作系统当中我们可以通过 ps aux 或 ps axj 命令查看进程的状态。

ps ajx

ps aux


1. R(runing)--- 运行状态

运行状态:一个进程处于运行状态,并不说明他一定正在运行中。

它可能正在CPU中运行;也可能在CPU对应的运行队列中准备接受调度。

cpp 复制代码
//通过一个死循环来查看运行状态
  1 #include<stdio.h>
  2 int main()
  3 {
  4     while(1)
  5     {
  6 
  7     }
  8 
  9     return 0;                                                                                                                                
 10 } 

此时我们可以看到STAT(state)状态栏那一栏我们的进程对应的状态为R+,R是运行状态,+代表是前台运行,没有+就是后台运行。

我们的死循环程序会不断的重复进行代码的执行,占用CPU的资源,它也不满足访问外事进入到阻塞状态或者其他状态的情况,所以它是运行状态。


2. S(sleeping)--- 睡眠状态

睡眠状态说明进程在等待某件事情的完成。

处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。

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

int main()
{
    while(1) {
        printf("i am a process\n");
        sleep(1);  // 睡眠1秒
    }
    return 0;
}

我们通过ps ajx | head -1 && ps ajx | grep 文件名来查看它的状态

  • 虽然从代码逻辑上看,程序确实在不断地循环执行,但在操作系统层面的进程调度视角下,情况却有所不同。
  • 当程序执行到 sleep(1) 时,会发生一个关键的状态转变:进程会主动让出 CPU 的使用权,并进入等待队列。此时操作系统内核会将这个进程标记为睡眠状态(S 状态),因为进程明确表示它将在接下来的1秒钟内不需要 CPU 资源。这种睡眠不是被动的等待,而是进程通过系统调用主动发起的请求。
  • 在这个过程中,printf 函数的执行确实需要极短的 CPU 时间,但相对于整整1秒的睡眠期来说,这个执行时间几乎可以忽略不计。当 printf 完成输出后,进程立即进入睡眠状态,在接下来的1秒内都处于这种等待状态。操作系统的进程调度器在扫描进程状态时,几乎总是捕捉到进程在睡眠而不是在运行。
  • 更重要的是,sleep() 函数在底层是通过设置定时器并调用等待函数来实现的。进程会挂起自己,直到定时器超时才会被重新唤醒。
cpp 复制代码
#include <stdio.h>

int main()
{
  int a = 0;
  scanf("%d", &a);

  return 0;
}

我们的程序需要从键盘上读取数据,不断等待外设键盘准备好,如果我们不去敲击键盘输入数据,那么键盘就会一直不准备好,那么此时进程就会在键盘设备的阻塞队列中等待,此时就对应操作系统学科的阻塞状态,所以此时我们的进程处于休眠状态

而处于浅度睡眠状态的进程是可以被杀掉的,我们可以使用kill命令将该进程杀掉。


3. D(disk sleep)--- 磁盘休眠状态

D磁盘休眠状态(disk sleep): 也叫做深度睡眠,处于这个状态的进程通常会等待IO的结束,不响应任何请求,同时其也对应操作系统学科上的阻塞状态的一种特殊情况,处于深度睡眠的进程,不响应操作系统的任何请求,也无法被 kill -9杀死

您描述的场景很好地解释了深度睡眠(Uninterruptible Sleep)的设计原理和必要性。在这个假设的磁盘写入困境中,确实展现了一个典型的数据完整性保护机制。

当进程向磁盘发起写入请求后,便进入等待回应的状态。

  • 此时如果操作系统因内存资源严重不足而决定终止该进程,就会引发数据一致性问题。进程被强制杀死后,其内存中的代码和数据随之释放,而磁盘那边由于空间不足也丢弃了待写入的数据,这就造成了不可挽回的数据丢失。在这种复杂情境下,操作系统为了维护系统稳定性而清理资源,磁盘为了最大化利用有限空间而优先处理可完成的任务,各自的行为从局部视角看都有其合理性,但整体协作却导致了用户数据的损失。

  • 为了解决这种责任边界模糊但后果严重的数据丢失风险,系统引入了深度睡眠状态。处于深度睡眠的进程对操作系统的终止信号不予响应,就像进入了一种受保护的休眠。只有当磁盘完成操作并返回明确结果后,进程才会苏醒。这种机制确保了数据操作的原子性------要么完整写入,要么完全失败,但绝不会停留在悬而未决的中间状态。


4. T(stopped)--- 停止状态

在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行。

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

int main()
{
  while(1)
  {
    printf("i am proc,my PID:%d,my PPID:%d\n",getpid(),getppid());
    sleep(6);
  }

  return 0;
}

暂停: kill -SIGSTOP PID或者kill -19 PID

继续: kill -SIGCONT PID或者kill -18 PID


6. X(dead)--- 死亡状态

注意:这个状态只是一个返回状态,并不会在任务列表中看到这个状态,当Z僵尸状态结束后就会进入Z死亡状态


二、僵尸状态进程

1. 定义

僵尸进程(zombies):子进程退出的时候,如果父进程没有主动读取回收子进程的信息,那么子进程会让自己一直处于Z僵尸状态,即对应子进程相关资源尤其是task_struct结构体不能释放。

  • exit系统调用接口可以终止一个进程,使用exit可以保证我们的子进程或父进程被终止
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(count){
			printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("child quit...\n");
		exit(1);
	}
	else if(id > 0){ //father
		while(1){
			printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
	}
	else{ //fork error
	}
	return 0;
} 

程序开始运行时,初始进程处于睡眠状态,这是因为它等待外设屏幕输出主动让出CPU。当fork系统调用执行后,创建出的子进程与父进程各自进入独立的执行流,两者都因包含sleep调用而周期性处于睡眠状态。

子进程在完成五次输出后通过exit正常退出,此时进程的执行已经结束,但由于父进程正忙于自己的循环输出而没有调用wait系列函数来回收子进程的退出状态,子进程便进入了僵尸状态。僵尸状态的特点是进程的执行已终止,核心资源已被释放,但在进程表中仍保留着退出状态信息等待父进程读取,这时使用ps命令便能观察到标记为Z的子进程。


2. 危害

  • 僵尸进程的长期存在确实会带来显著的系统资源浪费问题。当一个子进程结束后,其退出状态信息必须被保留在进程控制块(PCB)中,这是为了向父进程汇报任务执行的结果。这些状态数据并非凭空存在,它们需要占用实实在在的内存空间来存储。

  • 如果父进程始终不去读取子进程的退出状态,那么子进程就会一直保持僵尸状态。这意味着操作系统必须持续维护这些已经终止进程的PCB结构,而每个PCB都包含着进程的详细信息,如同C语言中每个结构体变量都需要在内存中分配特定空间一样。

    • 设想一个父进程频繁创建大量子进程却从不进行回收的情况,这会导致系统中积累越来越多的僵尸进程。每个僵尸进程的PCB都无法被释放,它们占据的内存空间也就无法被重新利用。这种场景本质上就是一种内存泄漏------系统的宝贵内存资源被这些已经死亡但未被清理的进程残留信息所占据,可用的内存空间逐渐减少,最终可能影响系统的整体性能和稳定性。因此,及时回收子进程不仅是良好的编程习惯,更是保证系统健康运行的重要措施。

三、孤儿进程

在Linux当中的进程关系大多数是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程。

但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程。

若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(1){
			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
			sleep(1);
		}
	}
	else if(id > 0){ //father
		int count = 5;
		while(count){
			printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("father quit...\n");
		exit(0);
	}
	else{ //fork error
	}
	return 0;
} 


  1. 当执行程序后,打印字符串,然后进程调用了fork会创建子进程,当前进程成为父进程,子进程和父进程通过if分流,进入不同的执行流去执行不同的代码块,使用ps查看此时子进程和父进程都是处于S+睡眠状态(前台进程)
  2. 子进程会一直死循环间隔睡眠1s打印进程的PID和PPID
  3. 父进程会打印5次进程的PID和PPID后被exit系统调用终止进程,那么此时子进程的父进程的进程信息被bash回收释放了,所以子进程就没有父进程回收进程信息了。
  4. 操作系统肯定不能让这种事情发生,因为任何一个进程都要退出,也要被释放,那么此时操作系统收养了当前的子进程。
  5. 注意此时的子进程被操作系统收养,在后台运行,并且写的是死循环打印,那么子进程不会主动退出,那么只能使用kill -9 进程标识符,杀死子进程进行退出。

这里的bash进程算的上是当前子进程的爷爷进程,可是任何一个父进程只对子进程负责,即bash使用fork创建出来的当前子进程的父进程, bash只对当前子进程的父进程负责,bash的代码逻辑中没有要为当前子进程负责的逻辑,所以bash不能回收当前的子进程。


总结

总的来说,Linux进程状态机制是操作系统进行资源管理和调度的核心。运行、睡眠、停止等状态确保了CPU等资源的合理分配;而僵尸和孤儿进程则揭示了进程间父子关系与资源回收的重要性。深刻理解这些状态及其转换,对于诊断进程问题、编写健壮的程序(尤其是正确回收子进程资源)以及维护系统稳定性和性能至关重要,是系统编程和运维的基础知识。


✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 !

🚀 个人主页 :不呆头 · CSDN

🌱 代码仓库 :不呆头 · Gitee

📌 专栏系列 :

💬 座右铭 : "不患无位,患所以立。"

相关推荐
IT_陈寒4 分钟前
SpringBoot自动配置差点让我加班到凌晨
前端·人工智能·后端
yumgpkpm17 分钟前
Acceldata ODP(Open Data Platform)3.3.6.4(RHEL9)保姆级完整安装手册
大数据·人工智能·hive·hadoop·kafka·hbase·cloudera
程序员cxuan19 分钟前
腾讯又来一王炸,开源版 WorkBuddy 太夯了!
人工智能·后端·程序员
邓工说电30 分钟前
智慧断路器安全吗?数据加密、离线保护与合规认证全解读
大数据·数据库·人工智能·智能断路器·炜晔科技
阿里云大数据AI技术32 分钟前
Lance 数据检索怎么选,当然阿里云 Milvus 向量湖
人工智能
出海客34 分钟前
跨境电商多语言客服知识库怎么建:资料结构、检索边界与人工升级
大数据·人工智能
xh didida1 小时前
Linux -- 基础IO
linux·服务器·开发语言·c++
xsd202411181 小时前
从自主导航到视觉读表:一台工业巡检机器人的全栈技术链路拆解
人工智能
袁哥大话安全1 小时前
巡隐WEBSHELL扫描软件
人工智能·安全·web
论文复现现场1 小时前
8卡4090能跑70B吗?Llama-2显存预算、QLoRA与通信瓶颈
人工智能·深度学习·分布式训练·llama·显存·qlora·算家云