
先讲个我真实遇到过的场景有次接手一台跑着线上业务的服务器load average 飙到 80 多可 CPU 使用率却只有百分之十几。我打开 top 一看进程状态一栏整片都是 D按 9 键发 SIGKILL 过去进程纹丝不动——这玩意儿根本杀不掉。那时候我刚从纯业务开发转过来看系统层被 进程状态 这四个字狠狠上了一课。后来做性能排查、做优先级调优多了才把 Linux 进程状态和进程优先级这套机制彻底捋顺。这篇文章就是那次踩坑之后整理出来的经验。不管你是在虚拟机里装的 Linux还是公司生产环境里的物理机部署只要你会碰 top、ps、查负载、看性能进程状态和进程优先级就是绕不开的两个基础概念。我会把每个状态背后的原理讲透把 D 状态排查和优先级调整的完整操作链路写出来最后再附上几个我实际翻过车的教训供你参考。1. 进程状态全解top 里那串字母到底在说什么1.1 五类核心状态R、S、D、T、ZLinux 下进程状态都写在 STAT 这一列里看起来是一串字母拆开看其实很清楚。我按实际出现频率从高到低讲。SSleeping可中断睡眠进程正在等待某个事件最常见的是等待 IO 完成、等待 socket 数据、等待锁。这时候进程不占 CPU但随时可能被唤醒。你可以理解成一个人在等电梯电梯门开了叫他一声他就走进去。绝大多数常驻进程、网络服务都处于 S 状态。RRunning / Runnable进程正在 CPU 上运行或者已经在运行队列里排队等着拿 CPU。注意一个细节top 里看到的 R 不代表它此刻真的在某个核上执行可能是几十个线程都在可运行队列里轮候。所以你看到一堆 R往往伴随 CPU 占用高但也可能只是排队线程太多。DUninterruptible Sleep不可中断睡眠这是最让人头痛的状态。进程在内核态等待某个事件完成通常是同步等待磁盘 IO、网络 IO 或者其他内核资源。这个状态下进程不响应任何信号包括 SIGKILL。等电梯的人突然把耳朵捂上你怎么喊他都不理——这就是 D 状态。第 2 章我会专门讲它。TStopped / Traced进程被暂停了。通常是收到了 SIGSTOP、SIGTSTP或者被调试器如 gdbattach 住。暂停状态下进程不运行、不消耗 CPU但你随时可以用 SIGCONT 让它继续跑。这个状态对排查问题很有用后面会提到。ZZombie僵尸进程进程已经退出但它的退出状态还没被父进程读取wait 系列调用所以内核保留了它的进程描述符。这时候进程既不吃 CPU 也不吃内存但会占一个进程表项。它已经死透了只是尸体还没被收走。除了这五个还有一个内核线程常出现的 I 状态Idle比如 kworker 这类内核工作线程在没有任何任务时就是 I。I 状态和 D 状态在 ps 里会用 Id 表示但它和 D 的本质区别是它是主动空闲不是被动卡住。方便起见我把最常用的状态整理成一张表状态含义是否响应信号常见场景能否 killR运行中或可运行正常响应CPU 密集任务、编译、脚本能S可中断睡眠等事件正常响应网络服务、等待锁和 IO能D不可中断睡眠等内核事件不响应磁盘/NFS 同步 IO、内核驱动等待基本不能T暂停 / 被追踪响应 SIGCONT/SIGKILL调试、job control能Z僵尸等待父进程回收不响应父进程没 wait不能直接杀I内核线程空闲不响应外部信号kworker 空闲不需要杀1.2 STAT 字段的后缀与附加标志STAT 这一列往往不只一个字母后面还会带一些小写字母这些后缀是用来标记进程附加属性的。我整理几个出现频率最高的进程属于前台进程组。你在终端里直接跑的命令是但通过放后台的就不是。这跟 CtrlC 能发给谁直接相关——只有前台进程组会收到终端的中断信号。s会话领导者session leader通常是你在终端里启动的会话第一个进程可能是 shell 本身。l进程是多线程的内部创建了多个线程。进程具有高优先级。N进程具有低优先级。L进程有页面被锁定在内存中比如用了 mlock。举个例子你看到SNl这个组合就说明这个进程是低优先级、多线程状态低优先级和负 nice 值相关第 3 章讲。看到T说明它是个被暂停的前台进程通常是你按了 CtrlZ。用 ps 命令可以看得很清楚我常用的命令是这样ps -eo pid,ppid,stat,ni,pri,pcpu,comm --sort-pcpu | head -201.3 进程状态是怎么流转的状态不是静态的进程的一生在不同状态间来回切换。理解这个流转规则排查问题时思路会清晰得多。进程创建后进入就绪态R 的排队状态被 CPU 调度器选中后真正运行仍然是 R运行中如果需要读磁盘、读网络数据会通过系统调用进入内核等待此时变成 S如果这个等待在内核里被标记为不可中断就变成 D被外部信号暂停则变成 T运行结束、父进程还没来得及收尸时变成 Z父进程调用 wait 之后它才彻底消失。我通常用一个生活化的类比来理解这套状态机进程像一个在食堂打饭的人R 状态是他在窗口前排队或者正在打饭S 状态是他端着餐盘等人让位D 状态是他已经把手伸进窗口窗口阿姨锁上门去后厨拿菜了他只能等着喊他也没用T 状态是被保安喝住不许动Z 状态就是他吃完饭走了但餐盘还在桌上没人收。判断进程卡住的性质核心就是看它停在哪个状态停在 S 可能是正常等待停在 D 那是内核级别的 exception停在 T 是被谁挂起了。下一章我展开讲最坑的 D 状态。2. 最坑的 D 状态系统假死时的完整排查链路2.1 D 状态的本质内核替进程屏蔽了所有信号D 状态的全称是 TASK_UNINTERRUPTIBLE翻译过来就是不可中断的任务等待。为什么会有这种状态因为进程在内核态执行某些关键操作时无法安全地中途响应信号。最常见的触发场景是同步磁盘 IO进程调用 read() 或 write() 后数据要落盘内核把请求提交给块设备驱动然后进程就在内核态等待 IO 完成。这个等待过程如果被打断磁盘上的数据状态可能无法保持一致所以内核直接屏蔽了信号。你在这个状态下发 SIGKILL信号不会生效进程不会退出。这也是很多人第一次遇到 D 状态时的困惑怎么 kill -9 都杀不掉其实不是杀不掉而是信号压根送不进它内核态的执行流程。哪些操作最容易产生 D 状态按我的经验排序普通磁盘 IO 遇到底层硬件卡死或控制器异常比如机械盘坏道、SSD 固件 bug、RAID 卡重置。NFS、CIFS 这类网络文件系统挂载的服务端无响应。客户端同步等待网络 IO这是 D 状态的重灾区。FUSE 用户态文件系统比如 s3fs、glusterfs 的 FUSE 客户端用户态守护进程卡了会导致内核态同步等待。某些设备驱动在自己的等待逻辑里用了不可中断的 wait_event。2.2 完整排查链路从 top 到 /proc 再到 dmesg当你发现系统 load average 高得离谱、但 CPU 和用户态进程都很安静时按下面这套链路走基本能把 D 状态定位到根因。第一步确认 D 状态进程是谁。ps -eo pid,ppid,stat,wchan:32,cmd | awk $3 ~ /D/这里我故意把wchan字段打出来它记录的是进程在内核里阻塞的内核函数名是定位 D 状态的第一个线索。第二步看wchan和内核栈。如果第一步里 wchan 显示为空或者只有-用 root 去 /proc 下直接挖cat /proc/PID/wchan # 阻塞的内核符号 cat /proc/PID/stack # 内核调用栈需要 root假设 stack 里出现wait_on_page_bit_killable、xfs_wait_buftarg、nfs4_wait_clnt_recover这类函数你就知道它到底卡在哪一层了。第三步看系统级证据。D 状态往往不是单个进程的孤立问题而是某个底层资源出了问题。我习惯把这些命令一次性跑出来iostat -x 1 # 看 %util、await、svctm判断磁盘是否异常 dmesg -T | tail -30 # 看有没有 IO error、task hung、blocked 超过 120 秒的警告 nfsstat -m # 如果有 NFS 挂载看挂载参数和服务端状态 mount -l | grep -E nfs|cifs # 确认哪些目录走的是网络文件系统第四步结合内核日志判断是否触发了 hung_task 机制。Linux 内核默认有一个看门狗如果任务在 D 状态超过 120 秒内核参数 kernel.hung_task_timeout_secs内核会打印一条 blocked for more than 120 seconds 的告警并且把相关进程的栈信息打出来。看到这条日志基本就实锤了。我遇到的一个典型场景是这样的线上服务器挂载了一个 NFS 目录做日志存储某天存储服务端出了故障客户端所有写日志的进程全部进入 D 状态Load 瞬间打满ssh 登录都卡到半天才出提示符。用上面这套链路查下来dmesg 里刷了一整屏的 nfs 超时告警wchan 清一色指向nfs4_wait_clnt_recover。根因就是 NFS 服务端不可达。2.3 找到根因后的处理思路D 状态的处理原则是先救资源再谈杀进程。因为进程在内核态等待你杀不掉它真正能解除 D 状态的只有让等待的事件完成或超时。按情况区分如果是磁盘 IO 卡死先看 RAID 卡状态、磁盘健康状态。能恢复硬件就恢复恢复不了只能等 IO 超时返回错误进程才有可能从 D 状态退出并把错误带回用户态。如果是 NFS/CIFS 挂载超时这就有操作空间了。NFS 客户端有soft和hard两种挂载方式hard 模式会无限期重试表现就是 D 状态拖到天荒地老soft 模式在超时后会向用户进程返回错误进程就不会无限期卡住。我自己的生产环境对非关键目录一律用soft,timeo50,retrans2这种保守参数宁可丢一次 IO 请求也别把整台机器拖死。如果是 FUSE 挂载卡死重启对应的用户态守护进程比如挂 s3fs 的那个进程内核态的 FUSE 等待通常会随之解除。如果确认是内核 bug 或驱动 bug建议先通过重启相关服务绕过然后排查内核版本和驱动版本该升级升级该给内核打 patch 打 patch。这里有一条从我实际经历里总结出来的铁律永远不要对 D 状态进程执行 kill -9 之后就不管了。信号送不进去你杀掉它也不会消失。正确做法是顺着内核栈找等待的资源把资源侧的问题解决进程自己就会活过来。2.4 顺带解决 Z 状态僵尸进程排查状态时经常会连带看到 Z 状态进程。Z 状态出现的原因很单纯子进程退出后父进程没有调用 wait() 读取它的退出码。Linux 内核要求父进程必须回收子进程的退出信息否则就挂着这个尸体。处理僵尸进程有个三步走的套路找出僵尸进程的父进程ps -o pid,ppid,stat,cmd -p PID。如果父进程还健在检查它是不是有 bug——正常程序应该在 SIGCHLD 信号里 wait 子进程。无解时就只能重启父进程。父进程退出后僵尸进程会被 initPID 1收养由 init 完成回收。如果父进程就是 init且僵尸还挂着那通常是容器场景或者某个 init 实现的问题考虑重启容器或检查是否有 PID namespace 的回收逻辑问题。还有个小知识僵尸进程不消耗 CPU 和内存但如果积累了几百上千个会占满进程表导致新进程 fork 失败。所以监控脚本里最好加上僵尸进程数量检查别等到 fork 失败才想起来。3. 进程优先级内核分 CPU 时间片的规则3.1 nice、PRI、实时优先级三个容易混的概念说完状态来拆优先级。Linux 里谈到进程优先级有三个词很容易混nice 值、priorityPRI、实时优先级。我直接给结论nice 值是用户能直接调整的优先级参数范围 -20 到 19默认 0。数值越小优先级越高也就是越不 nice。priorityPR是内核调度器内部使用的实际优先级数值范围 100 到 139普通进程数值越小优先级越高。它和 nice 值有对应关系PR 120 nice不同内核版本略有差异有些地方显示为 20 nice。ps 和 top 里显示的 PRI 或 PR 列都是把内核 PR 做了映射后的结果。实时优先级是单独一类范围 1 到 99数值越大优先级越高。所有实时优先级进程都排在普通进程前面普通进程只能等实时进程让出 CPU。这三个概念的区别用一句话总结nice 是给普通分时进程用的温柔调节旋钮实时优先级是给需要硬性延迟保证的进程用的硬插队权限。用ps -l可以直接看 niceNI 列和 PRI 列ps -l输出里每个进程都会带 UID、PID、PRI、NI、STAT、CMD其中 PRI 和 NI 是两列很多人误以为它俩是一回事其实 PRI 是内核实际优先级NI 是用户层设置参数。top 里默认也只显示 PR 列按 f 可以把 NI 列调出来。3.2 CFS 调度器里 nice 值如何影响 CPU 时间理解了数值还得理解机制。现在主流的 Linux 发行版用的都是 CFS完全公平调度器的改进版本。CFS 内核里维护一个按虚拟运行时间vruntime排序的红黑树每次调度都选择 vruntime 最小的进程运行。每个进程的 vruntime 增长速度和它的权重weight成反比而权重正是由 nice 值决定的。具体量化是nice 值每差 1CPU 时间分配比例大约相差 10%-12%。更精确地说nice 0 的权重是 1024nice 1 的权重约为 820nice -1 的权重约为 1277。这意味着两个进程都满负荷跑时nice 0 与 nice 1 进程的 CPU 时间比大约为 1024:820约为 1.25:1。所以这个机制的真正作用不是把一个进程限制到最多用 20% CPU而是在多个 CPU 密集进程争抢 CPU 时通过权重差异来调整它们各自分到的 CPU 时间比例。让一个后台编译任务 nice 10它只会在有 CPU 竞争时让出更多时间给在线业务如果系统 CPU 本身很空闲它照样能跑满 CPU。3.3 实时调度策略与优先级上限实时优先级对应两类调度策略SCHED_FIFO先入先出和 SCHED_RR时间片轮转。优先级范围是 1 到 9999 最高。SCHED_FIFO一旦开始运行只有它主动让出 CPU、等待 IO、或有一个更高优先级的实时进程出现它才会被切换走。适合对延迟要求极高的任务比如工业控制、音频处理相关的实时线程。SCHED_RR同样是实时优先级但多个同优先级进程之间按时间片轮流跑防止一个实时进程独占 CPU。这里有个非常关键的内核机制要记住只要有实时进程处于可运行状态普通进程包括你 ssh 进去用的 shell就无法获得 CPU 时间片。所以实时优先级用起来必须格外慎重很多生产事故就是有人把普通进程的实时优先级拉满导致整个系统直接锁死。用 chrt 命令可以查看和设置实时策略后面第 4 章实战部分我会给具体命令。4. 调整优先级的实操命令与经典翻车事故4.1 nice、renice、chrt 的完整用法启动一个新程序并指定 nice 值用 nice# 以 nice 10 运行 make编译任务谦让一点 nice -n 10 make -j8 # 以 nice -5 运行某个需要更多 CPU 的进程注意负值需要 root 权限 sudo nice -n -5 /opt/myapp/bin/server调整一个已经运行的进程的 nice 值用 renice# 把 PID 1234 的 nice 值改成 5 renice -n 5 -p 1234 # 同时调整多个进程比如把一组 Python 后台任务的 nice 都提高 renice -n 10 -p 1001 1002 1003 # 按用户调用户 abc 的所有进程统一调低优先级 renice -n 5 -u abc查看某个进程的 nice 值方法很多我用得最多的是ps -eo pid,ni,pri,comm | sort -k3 -n | head调整实时优先级用 chrt# 查看进程 1234 的调度策略和优先级 chrt -p 1234 # 以 SCHED_FIFO、优先级 50 启动某个程序 sudo chrt -f 50 /opt/realtime/bin/collector # 把已有的 PID 改成 SCHED_RR、优先级 40 sudo chrt -r -p 40 1234注意把普通进程改成实时调度需要 root 权限而且改之前一定想清楚后果。我个人的准则是除非你非常清楚这个进程的所有运行路径否则别在生产环境对任何已有进程执行chrt -f类的修改。4.2 什么场景值得调优先级什么场景别乱调我在实际调优中经过多次验证觉得真正有价值的使用场景有这么几类第一类后台编译/打包任务降低优先级。开发机上跑大规模编译比如make -j64、npm run build、容器镜像构建时加一层 nice 10 甚至 nice 15。这样你在同一台机器上继续做交互开发时窗口不会明显卡顿。这个操作收益最高、风险最低我几乎每天都在用。第二类在线交互服务适当提升优先级。比如数据库实例、网关进程可以从默认 nice 0 调到 nice -5。注意不能调更多——负得越多对其他进程的影响越大。我一直建议用 -5 作为生产环境调整的上限除非有非常充分的 profiling 数据支持。第三类关键实时任务用 chrt 指定 FIFO/RR。典型例子是处理时间敏感业务的线程比如工业采集程序、金融行情处理。这类场景从架构设计阶段就应该规划好调度策略而不是事后靠 chrt 硬调。不建议乱调优先级的情况也很明确不要试图用 nice 限制某个进程的 CPU 使用率上限。它做不到。想限制 CPU 上限应该用 cgroup 的cpu.cfs_quota_us或cpulimit这类工具那是另一套机制。不要给简单的重要进程随手设置负 nice。优先级的作用是竞争 CPU 时间如果机器上 CPU 资源充足设置负 nice 毫无意义只会无端增加交互进程的调度延迟。不要在容器环境下孤立调整某个容器内进程的 nice 值。容器内进程的调度还要叠加 host 的 cgroup 权重体系你改的是容器内部视角宿主机的调度器可能根本不买账。4.3 踩坑案例把内核线程拉成实时优先级后系统卡死讲一个我自己经历过的翻车事故希望你别重蹈覆辙。有一回我做性能压测想验证某个采集程序在绝对优先的情况下延迟表现如何于是用了下面的命令把它的优先级拉到 SCHED_FIFO 99sudo chrt -f 99 -p 采集程序PID命令执行的那一瞬间服务器几乎没有延迟地进入了假死状态top 不再刷新ssh 命令行敲什么都无响应连按 CtrlC 都没反应。原因是这个采集程序处理逻辑里有循环等待它一旦拿到 SCHED_FIFO 99就永远占据 CPU内核里其他线程包括 sshd、bash、看门狗全都排不上队。最后只能通过带外管理接口强制重启。更隐蔽的坑是有人会尝试chrt -f 99内核线程比如 kworker、migration 这种一旦成功系统基本只有重启一条路。内核线程本身运行在更高优先级的内核上下文你再去改它的用户态调度属性极易造成不可预测的锁死。这件事之后我总结了几条铁律改实时优先级前先写好回滚方案。至少保留一个能连通终端的方式比如通过串口控制台或者带了实时优先级的应急脚本。先在小范围、低优先级数值比如 FIFO 10 以内上做验证再逐步提高。永远不要给内核线程改优先级。内核线程的调度是内核自己管理的普通用户视角的 nice 和实时优先级对它来说属于越权操作。生产环境如果要调整优先级建议通过 systemd 配置而不是裸命令# /etc/systemd/system/myapp.service.d/priority.conf [Service] Nice5 CPUSchedulingPolicyfifo CPUSchedulingPriority30这样重启后配置依然生效而且有 systemd 的统一管理出问题也方便审计。用 top 快速检查优先级排布时我习惯按下P键按 CPU 排序再按下f把 NI 列显示出来一眼就能看出哪个进程的 nice 值被改动过。排查某个进程怎么突然跑不满 CPU之类的问题时先看一眼它的 NI 和 PR 有没有被别人改过往往能省很多时间。最后再分享一个我的使用习惯监控脚本里除了看 CPU、内存、负载伺候状态的检查也要带上——统计一下 R、D、Z 三种状态的进程数。R 数据骤增说明系统要过载D 数据骤增说明底层存储或网络有问题Z 数据长期居高不下说明有程序不回收子进程。这三项指标比单纯看 load average 更能反映系统的真实健康状况。进程状态和优先级这套知识看起来都是最基础的内容但真正用好的时候都是在别人已经被线上故障折磨得焦头烂额的时候。