【Linux详解】历程的状态 | 运行 阻塞 挂起 | 僵尸和孤儿状态

[复制链接]
发表于 2026-10-3 15:52:10 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

写在前面:这篇讲什么

本文梳理 Linux 进程的几种状态:从操作系统课本里的三态模型,讲到实际 Linux 内核里的 R/S/D/T/Z 这些状态字母,顺便把僵尸进程和孤儿进程的成因、危害、处理办法说清楚,中间配可运行的代码示例。

操作系统里的三态模型

经典教材把进程粗分成三种状态:

  • 就绪(Ready):条件都齐了,只差 CPU。这类进程排在就绪队列里,等调度器发牌。
  • 运行(Running):正在被 CPU 执行。
  • 阻塞(Blocked / 等待):卡在某个条件上,得等事件完成或资源可用才能继续。比如等 I/O 完成、等锁、等网络响应。这类进程会被移进对应的等待队列。

运行、阻塞与挂起

先纠正一个常见误解:进程申请资源不只是申请 CPU。它还会用磁盘、网卡、显卡、显示器等等。申请 CPU 时排队好理解,申请这些慢设备时一样要排队——排队的东西就是进程的 task_struct(内核里的进程描述符 PCB)。

所以当进程去访问磁盘、网卡这类资源,而资源还没准备好、或者正忙着服务别人时:

  1. 当前进程从 runqueue(运行队列)中摘出去;
  2. 挂进对应设备描述结构里的 waitqueue(等待队列)。

一句话总结规律:看 PCB 现在待在哪个队列里,就决定进程是什么状态。

另一种情况是内存吃紧:系统会把某些进程的代码和数据换出到磁盘(swap),腾出物理内存,这个过程叫进程挂起。被挂起的进程在外存上,短期内也跑不起来。

状态转换看一个例子

#include <stdio.h>
#include <unistd.h>

int main()
{
    while (1) {
        printf("进程[%d]正在运行...\n", getpid());
        sleep(1);  // 模拟阻塞
    }
    return 0;
}

跑起来后用 ps aux 观察,会看到这个进程大多数时候标着 S:它在 sleep,也就是挂在了等待队列上。原因是"显示器比 CPU 慢太多了"——对 CPU 而言,等 printf 刷屏的那点时间跟睡觉没区别。

Linux 里的实际状态

Linux 用整数表示进程状态,存在 task_struct 里。日常用 ps aux 或 ps axj 看到的那一列 STAT 字母,就是它的可视化形式。

字母状态名含义
RRunning(运行)正在跑,或者虽然在跑队列里排队等着被调度也算
SSleeping(可中断睡眠)在等某个事件完成,可以被信号唤醒
DDisk Sleep(不可中断睡眠)等 I/O 完成,睡死了连信号都叫不醒
TStopped(停止)被暂停,收到信号可以恢复
XDead(死亡)已终止,从进程表里移除
ZZombie(僵尸)已退出,但父进程还没来读它的退出信息

S 与 D 的关键差别

S 是可中断睡眠,D 是不可中断睡眠,差别在"能不能被信号叫醒":

  • S:等的是普通事件(比如等数据从网络来),来个信号就能打断它,比如 kill 往往能把它干掉。
  • D:正在等内核态的 I/O 完成,处于这种状态下进程既不响应信号也不能被调度,kill -9 也杀不掉。文章里不实操模拟 D 状态,就是因为要造假 I/O 卡死,一不小心能把磁盘拖到满负荷。

生产上 D 状态进程是运维噩梦:它们既占着资源又不肯走,往往得跟着排查对应的存储或 NFS 挂载是否异常。

T 停止状态

下载、看视频、听歌时的"暂停",就能对应到 T 状态:点击暂停,对应的进程代码停止推进。调试场景下更典型——gdb 打断点后程序在断点处停住,本质上就是把进程置成了 T。

手工制造也简单:

kill -19 4026      # 把某个进程暂停(SIGSTOP)
kill -18 4026      # 恢复(SIGCONT)

或者 gdb 里:

$ gdb ./process
(gdb) l        # 看代码
(gdb) b 9      # 在第九行打断点
(gdb) r        # 跑起来,命中断点后进程被停住

T 也是"挂起"的一种形态,只是它停在内存里,没被换出去。

僵尸进程(Z)

为什么会变僵尸

一个进程退出时,Linux 不会直接丢进 X(死亡、资源立即可回收)状态,而是先变成 Z(僵尸)。原因很简单:进程运行期间留下的退出信息得有人来收,父进程或内核要能读到"它是正常退出还是被信号杀了、退出码是多少"。这份信息就写在这个进程的 task_struct 里。

如果父进程始终不来收,Z 状态就会一直挂着。

看一个僵尸示例

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t id = fork();
    if (id < 0) {
        perror("fork");
        return 1;
    } else if (id == 0) {   // 子进程
        printf("子进程[%d]开始运行...\n", getpid());
        sleep(5);
        printf("子进程[%d]退出...\n", getpid());
        exit(0);
    } else {                // 父进程
        printf("父进程[%d]正在睡眠...\n", getpid());
        sleep(30);          // 故意延迟回收
    }
    return 0;
}

另开一个终端盯一下:

while :; do ps axj | head -1 && ps axj | grep mytest | grep -v grep; sleep 1; echo "######"; done

5 秒后子进程退出,但父进程还在睡,这时 ps 里就能看到它标着 Z。

僵尸的危害

僵尸进程已经不再运行,但它的 task_struct 还占着内核资源,对应的 PID 也不会释放。父进程迟迟不回收,进程数就会一直涨;量堆上去后,fork 新进程会失败,严重时表现为系统变慢、服务起不来。

怎么避免

  • 父进程及时 wait() 或 waitpid() 回收:这是最根本的解法。
  • 注册 SIGCHLD 处理函数:子进程退出时内核会给父进程发 SIGCHLD,在回调里做 waitpid(-1, NULL, WNOHANG) 循环回收,比"等主流程走到 wait 才收"更及时。
  • 长生命周期进程慎用 SIG_IGN 忽略 SIGCHLD:某些平台下忽略 SIGCHLD 会让内核自动回收,可读性好;但要清楚这会影响 wait() 的可用性。

孤儿进程

父进程先退出,子进程的"父"就换成了 1 号进程(init 或 systemd),相当于被系统收养,这叫孤儿进程。

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    pid_t id = fork();
    if (id == 0) {                    // 子进程
        int cnt = 5;
        while (1) {                   // 孩子不退出
            printf("我是子进程,我还剩下 %ds\n", cnt--);
            sleep(1);
        }
    } else {                          // 父进程
        int cnt = 3;
        while (cnt) {
            printf("我是父进程,我:%d\n", cnt--);
            sleep(1);
        }
        exit(0);                      // 父进程先走,子进程变孤儿
    }
    return 0;
}

盯它的脚本和上面一样。运行几秒后父进程退出,子进程仍然活着,但它的父已经变成 1 号进程了——用 ps -o ppid= 或直接 top 看 1 号进程就明白了。

两个常见的疑问

疑问一:上面例子里父进程退出了,为什么没看到它变成僵尸?
因为父进程自己的父进程是 bash。你在终端里跑程序,bash 会盯着并返回退出状态,父进程一退它就被回收了。Z → X 这个过程非常快,肉眼根本抓不到。

而自己写的代码里子进程变僵尸,是因为代码压根没写回收逻辑,父进程还活着且不去 wait,僵尸就一直挂着。

疑问二:孤儿进程怎么处理?
被 1 号进程收养后不会变僵尸,因为 init 会持续 wait 掉退出的子进程。所以它本身无害;真正需要操心的是孤儿进程里那批"死了却没人收"的孙进程——父死了、子还活着、孙跟着子,链路上每一环都要有人收。这种情况用 kill -9 直接清理就行(Ctrl+C 只能干掉前台进程,对后台孤儿无效)。

小结

进程状态这一块,记住两条线就够了:

  • 理论线:就绪 → 运行 → 阻塞三态,PCB 在哪个队列就属于哪个状态;内存不足时会被换出到外存,形成挂起。
  • 实战线:Linux 里用 R/S/D/T/X/Z 六个字母表示,S 能被信号唤醒,D 不能(杀不掉,得查存储);T 是暂停(gdb 断点、kill -19);Z 是僵尸——父进程没 wait,要修就补 waitpid 或 SIGCHLD 回收;X 才是真正的死。

运维排查里的经验:看到大量 Z 先找它的父进程 PID,往往父进程就是那个忘了回收的长时间运行服务;看到大量 D 则优先查磁盘、NFS、网络存储,而不是盯着进程本身使劲 kill。

(本文由 qidao123.com企服评测-应用市场 编辑提示整理:示例基于 Linux 标准进程模型,编译可用 gcc main.c -o main,验证命令 ps aux / ps axj 已在 Ubuntu / CentOS 环境实测。)

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表