Linux CPU性能优化:D状态和Z状态排查与处理

文章目录

  • 一、Linux进程五大基本状态
    • [1. 运行状态(R,Running / Runnable)](#1. 运行状态(R,Running / Runnable))
    • [2. 可中断睡眠状态(S,Interruptible Sleep)](#2. 可中断睡眠状态(S,Interruptible Sleep))
    • [3. 不可中断睡眠状态(D,Uninterruptible Sleep)](#3. 不可中断睡眠状态(D,Uninterruptible Sleep))
    • [4. 停止状态(T,Stopped / Traced)](#4. 停止状态(T,Stopped / Traced))
    • [5. 僵死状态(Z,Zombie / Defunct)](#5. 僵死状态(Z,Zombie / Defunct))
  • [二、D 状态和Z状态排查](#二、D 状态和Z状态排查)
    • [1.D 状态排查方法](#1.D 状态排查方法)
      • [1. wchan & stack](#1. wchan & stack)
      • [2. hung_task](#2. hung_task)
    • [2. Z 状态排查方法](#2. Z 状态排查方法)
    • 3.防止Z状态产生

一、Linux进程五大基本状态

在 Linux 系统中,常用的五大基本状态(通过 ps、top 等命令可查看)分别是:

1. 运行状态(R,Running / Runnable)

含义

进程当前正在运行(占用 CPU)或者处于可运行队列中、只要获得 CPU 时间片就能立刻执行。
特点:

包括正在 CPU 上执行的进程,以及就绪等待调度的进程。

这是进程争取 CPU 时的活跃状态。
示例:

一个不停计算的 while(1) 程序通常处于 R 状态;在多核系统上,多个 R 状态进程可能同时运行。

2. 可中断睡眠状态(S,Interruptible Sleep)

含义:

进程正在等待某个事件完成(如等待 I/O 输入、等待锁、等待信号等),并且该睡眠可以被信号唤醒。
特点:

最常见的睡眠状态(大部分时间系统进程都在此状态)。

当等待的资源可用或收到信号时,进程会回到 R 状态。

可以被 kill 命令或其它信号打断。
示例:

Shell 等待用户输入、网络服务等待客户端连接、进程调用 sleep() 或 nanosleep()。

3. 不可中断睡眠状态(D,Uninterruptible Sleep)

含义:

进程正在等待某些不可被中断的 I/O 操作(如直接读写磁盘、等待硬件响应),期间无法响应任何信号。
特点:

通常用于磁盘 I/O、某些设备驱动中的关键操作。

即使发送 SIGKILL 也无法立即终止该进程(必须等待 I/O 完成或系统重启)。

这种设计是为了防止在关键数据写回磁盘时被中断导致数据不一致。
示例:

sync 命令刷新缓冲区、dd 直接写入块设备、NFS 因网络故障僵住时的等待操作。
注意:短时间的 D 状态是正常的,若长时间存在且数量增多,往往提示 I/O 瓶颈或存储设备问题。

4. 停止状态(T,Stopped / Traced)

含义:

进程的执行被暂停,通常是由于收到了 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 等信号,或者被调试器(如 gdb)暂时接管。
特点:

进程不会获得 CPU 时间,也不响应普通信号(除 SIGKILL、SIGCONT 外)。

可以通过发送 SIGCONT 信号让进程恢复到 R 状态继续执行。
示例:

在终端按下 Ctrl+Z 将前台进程挂起 或 gdb 中设置断点后命中暂停 或 使用 kill -SIGSTOP 显式暂停。

5. 僵死状态(Z,Zombie / Defunct)

含义:

进程已经结束运行(调用了 exit 或收到终止信号),但其进程描述符(task_struct)仍然保留,等待父进程调用 wait() 或 waitpid() 来读取其退出状态。
特点:

该进程不再占用任何内存或 CPU 资源(只保留内核中的一个极小结构体)。

无法被杀死(因为它已经"死"了),只能通过让父进程回收它来消除。

若父进程不回收且不退出,僵尸进程会一直存在;若父进程先退出,僵尸进程会被 init(PID=1)进程收养并自动回收。
示例:

父进程编写不当(未调用 wait),导致子进程结束后一直处于 Z 状态。大量僵尸进程可能耗尽进程号上限,影响系统运行。

本文将具体介绍D状态和Z状态的排查与处理。

二、D 状态和Z状态排查

1.D 状态排查方法

1. wchan & stack

wchan(wait channel)能看到 D 状态进程"卡"在内核的哪个具体函数

例如:

bash 复制代码
ps -eo pid,state,wchan,comm | awk '$2=="D"'

输出

bash 复制代码
 PID S WCHAN                COMMAND
 123 D io_schedule          dd
 456 D wait_on_page_bit     mysqld

这个例子中进程dd(PID 123) 卡在了 io_schedule, 即磁盘读写,可能的原因是磁盘慢、坏道、NFS 挂了。

而进程mysqld(PID 456)卡在了wait_on_page_bit,即内存页回写,可能的原因是内存压力大。

也可以直接读取某进程的wchan,例如:

bash 复制代码
cat /proc/798/wchan

可能输出为:

bash 复制代码
rpc_wait_bit_killable

即卡在RPC 层。

然后,可以通过stack中内容进一步排查具体卡在了哪个函数,例如在刚才的例子中继续检查stack:

bash 复制代码
cat /proc/789/stack

可能输出为:

bash 复制代码
[<0>] rpc_wait_bit_killable+0x??/0x?? [sunrpc]
[<0>] __rpc_execute+0x??/0x?? [sunrpc]
[<0>] rpc_run_task+0x??/0x?? [sunrpc]
[<0>] nfs_write_rpc+0x??/0x?? [nfs]
[<0>] nfs_file_write+0x??/0x?? [nfs]
[<0>] vfs_write+0x??/0x??
[<0>] sys_write+0x??/0x??

由此能看出是 NFS 写操作卡在 RPC 层。

2. hung_task

当 D 状态进程超过 120 秒(默认),内核会主动打印带有hung task 关键字的log,因此使用dmesg 指令可以可以获取

bash 复制代码
dmesg -T | grep "hung_task"

可能输出为:

bash 复制代码
[Thu May 25 10:15:32 2026] INFO: task dd:123 blocked for more than 120 seconds.
[Thu May 25 10:15:32 2026]      Tainted: G        W
[Thu May 25 10:15:32 2026] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
[Thu May 25 10:15:32 2026] dd            D    123     1 0x00000000
[Thu May 25 10:15:32 2026] Call Trace:
[Thu May 25 10:15:32 2026]  [<ffffffffa0000000>] io_schedule+0x??/0x??

可以看出 dd(PID 123)卡在 io_schedule 超过 120 秒。

2. Z 状态排查方法

wchan、hung_task 等指令对Z状态无用,我们需要一直找到进程的父进程进行处理。

例如我们已经找到了一个Z状态进程:

bash 复制代码
ps aux | awk '$8=="Z"'

输出:

bash 复制代码
USER   PID  STAT  COMMAND
root   123 Z     [sh] <defunct>

我们需要找到它的父进程:

bash 复制代码
ps -o ppid= -p 123

例如输出456,我们通知父进程回收:

bash 复制代码
kill -CHLD 456  

以此来回收Z状态子进程123.

如果不行,可以选择直接杀死父进程:

bash 复制代码
kill -9 456

3.防止Z状态产生

最好防止Z状态产生的方式实在代码中确保父进程回收子进程的退出状态。

例如一个非阻塞式回收的代码示例如下:

cpp 复制代码
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

void sigchld_handler(int signo) {
    int saved_errno = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0);  // 回收所有已结束的子进程
    errno = saved_errno;
}

int main() {
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sa, NULL);

    pid_t pid = fork();
    if (pid == 0) {
        printf("Child exiting\n");
        return 0;
    } else if (pid > 0) {
        printf("Parent doing other work...\n");
        sleep(2);   // 模拟父进程的工作
    }
    return 0;
}

或者使用通过第二次 fork,使实际工作的子进程成为孤儿,被 init(PID=1)接管。init 会自动回收子进程,从而避免僵尸。

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

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 第一次子进程
        pid_t pid2 = fork();
        if (pid2 == 0) {
            // 第二次子进程:实际工作的进程
            printf("Working child (will be adopted by init)\n");
            sleep(2);
            return 0;
        } else if (pid2 > 0) {
            // 第一次子进程直接退出,使第二次子进程成为孤儿
            return 0;
        }
    } else if (pid > 0) {
        // 父进程回收第一次子进程(很快完成)
        wait(NULL);
        printf("Parent: first child reaped, grandchild adopted by init\n");
        sleep(3);
    }
    return 0;
}
相关推荐
Flash.kkl5 小时前
网络层协议IP、数据链路层、NAT详解
服务器·网络·网络协议·tcp/ip
张小姐的猫7 小时前
【Linux】多线程 —— 线程同步 | 生产者消费者模型 | POSIX 信号量
linux·运维·服务器
tedcloud1237 小时前
academic-research-skills部署教程:构建AI辅助科研环境
服务器·人工智能·word·excel·dreamweaver
Ether IC Verifier8 小时前
TCP 重传机制详解
服务器·网络·网络协议·tcp/ip·php
随便做点啥8 小时前
Intel Arc B60 Qwen3-Omni-30B-A3B 压测报告
服务器·经验分享
2401_873479408 小时前
主流IP离线库(IP数据云、纯真、IPIP.NET)怎么选?全面对比分析
服务器·网络·数据库
huangdong_9 小时前
图片下载工具性能优化:并发控制与内存管理
性能优化
Harm灬小海9 小时前
【云计算学习之路】学习Centos7系统-Linux下用户及组管理
linux·运维·服务器·学习·云计算
扛枪的书生9 小时前
HAProxy 学习总结
linux