
1. 一个凌晨的告警D状态任务为什么让运维睡不着先讲个我自己的经历。某次凌晨两点监控突然弹了一串告警大量业务节点出现超时登上去一看dmesg里刷满了这样的内容INFO: task kworker/u4:1:36708 blocked for more than 120 seconds. Not tainted 5.10.0-60.el7.x86_64 #1 echo 0 /proc/sys/kernel/hung_task_timeout_secs disables this message.这条报错来自 Linux 内核的hung_task机制也就是标题里说的“内核自检”的一部分。它意味着内核检测到某个任务已经连续在不可中断睡眠状态也就是常说的 D 状态下停留了超过 120 秒而且没有任何被调度回 CPU 执行的迹象。当时的业务表现是 NFS 客户端挂载目录无响应所有访问这个目录的进程全部卡住直接拖垮了一大片业务。很多同学第一次接触 hung_task 都是从这类线上事故开始的而且大概率是带着恐慌情绪接触的。因为这条报错看起来非常严重但实际上它既不是内核 panic也不一定是死锁它更像是一扇车窗上的水雾——真正的问题在玻璃那边内核通过“起雾”来提醒你赶紧处理。能不能看懂这条信息直接决定了你能不能快速定位业务卡死的原因。这篇文章我想把 hung_task 的完整机制讲透它到底检测什么、怎么检测、参数怎么调、线上遇到后怎么一步步排查。不打算写成教科书式源码逐行解读而是结合事故排查的经验把内核源码和实际运维场景串起来。如果你维护的是 Linux 服务器、Kubernetes 节点或者在做存储、内核、嵌入式方向这篇内容应该能帮你省掉不少撞墙的时间。2. 从调度状态说起为什么“不可中断睡眠”会变成问题要理解 hung_task得先理解任务在什么情况下会进入 D 状态。Linux 下进程的状态大多大家都很熟R 是运行态S 是可中断睡眠Z 是僵尸D 是不可中断睡眠。但对 D 状态的“不可中断”到底意味着什么很多人其实是模糊的。2.1 可中断睡眠和不可中断睡眠的差异普通进程等磁盘 IO、等网络数据时大多会进入 S 状态。这个状态下进程会被挂起等条件满足时被唤醒。它的特点是如果有信号递达内核会把进程唤醒去处理信号kill命令能把它叫醒。但有些场景下内核不希望进程被信号叫醒因为一旦中途被打断数据一致性可能出问题。比如进程正在等待底层存储设备返回某个关键 IO 结果如果这个时候被信号打断代码根本不知道该回到哪个点重试也不知道数据到底写进去没有。于是内核干脆把进程置为 D 状态——也就是TASK_UNINTERRUPTIBLE这个状态下普通的信号都会被忽略进程只能靠它等的那件事真正完成才能被唤醒。用生活的类比来说S 状态是你坐在餐厅里等菜服务员喊“你的菜好了”你能听到中途朋友打电话叫你走你也能接然后决定是走是留。D 状态则是你进了手术室麻醉已经打上了这个时候别说朋友叫你就算天塌下来你也得等手术做完才能动弹。对外界来说你就是“完全失联”的。2.2 D 状态不代表有问题但“一直 D”一定有问题这里必须澄清一个容易误伤的认知进程短暂处于 D 状态是极其正常的谁还没有个读写磁盘的时候。哪怕是小到一次cat /proc/loadavg底层也可能触发短暂的 IO 等待。关键是时间尺度。如果 D 状态持续几十秒甚至上百秒基本可以断定这个任务等的东西迟迟没来。可能的情况有几种一是底层存储没响应比如磁盘故障、SCSI 总线错误、Raid 卡卡死二是远端资源不可达比如 NFS 服务端挂死、网络分区导致 RPC 请求迟迟得不到回应三是内核里某个锁被别的任务持有且长期不释放或者是驱动代码有 bug中断没触发、唤醒丢失。当这个等待时间超过内核设定的阈值也就是默认的 120 秒内核的 hung_task 机制就会介入把这个任务的所有信息、调用栈、内核栈全部输出到日志里提醒系统管理员“这里有一台只进不出的机器”。2.3 为什么内核要专门设计一套检测机制你可能会问进程卡住了用户态自然会感知到超时为什么内核要单独搞一个自检原因很简单用户态感知到的永远只是“这个进程不响应了”但不知道它到底死在哪一环。而且很多卡在 D 状态的任务比如内核线程根本没有用户态对应物你甚至没法用kill -9去处理它。hung_task 的价值不在于它能“修复”问题——它本身不负责恢复也不直接杀掉卡死的任务它解决的是“可观测性”问题。它负责把不可见的内核卡顿情况暴露出来让运维和开发能拿到现场证据。内核在这里扮演的角色就像是一个一直盯着仪表盘的副驾驶发现你盯着前方一动不动超过两分钟就拍你肩膀喊一声“前面有情况”。还有一个容易混淆的概念是 softlockup。softlockup 检测的是 CPU 是否长时间被某个任务霸占也就是某个进程在内核态死循环不释放 CPU而 hung_task 检测的是任务长时间不被调度CPU 是空闲的但任务在等一个永远等不到的事件。两者一个有 CPU 空转、一个没 CPU 可跑检测思路完全不同。很多新手把这两个机制混为一谈排查方向就会跑偏。3. khungtaskd 的检测逻辑从定时唤醒到报警落地的完整路径hung_task 在内核里的具体实现集中在kernel/hung_task.c这一个文件里。代码量不大但涉及的设计思路很值得梳理。下面我用源码路径加逻辑拆解的方式讲讲它到底是怎么运作的。3.1 检测线程是怎么被创建和驱动起来的系统启动时hung_task_init会被调用实际上它是一个late_initcall在这个初始化函数里内核会创建了一个专用的内核线程名字叫khungtaskd这个线程的职责就是反复执行检测逻辑。这个线程没有用普通的定时器而是用一个wait_event_freezable配合超时时间按照一定周期睡眠后再醒来扫描任务列表。这里的周期不是拍脑袋定的而是跟超时时间有联动关系具体规则我放在参数那一节详细说。核心是khungtaskd 每隔一段时间就遍历一次系统中的所有任务把状态异常且超时的找出来。这里有一个值得注意的设计细节khungtaskd 用的是wait_event_freezable也就是说系统休眠时这个线程是可以被冻结的。不然每次电脑合盖、服务器挂起再唤醒这个线程会因为错乱的时间戳而产生大量误报。3.2 内核是怎么判定一个任务“hung”了的每次遍历任务时内核会逐个检查任务的state字段。判定逻辑可以概括成三步第一步看状态位。只有任务处于TASK_UNINTERRUPTIBLED 状态才会被纳入检查范围。如果你看到进程是 S 状态哪怕它一年不醒hung_task 也只会路过并不会管它。毕竟大部分 S 状态是有意为之比如 daemon 在等事件这属于正常休眠。第二步看唤醒标志。内核有一个特殊的状态组合叫TASK_KILLABLE它的意思就是“我可以被信号杀掉但我不会被普通唤醒打断”。这类任务虽然底层的 state bit 也是不可中断睡眠但它允许致命信号递达。hung_task 发现任务带上了TASK_WAKEKILL标志就会跳过因为这类任务即便卡很久系统管理员还能用kill -9救场不算完全的“死局”。第三步看时间戳。内核在任务每次被调度出 CPU 时都记录一个时间点也就是last_switch_time这个值记录的是“这个任务最后一次被切换出去的时间”。如果当前时间 - 最后切换时间 超时阈值并且该任务不满足豁免条件就判定为 hung。这一段对应到源码核心逻辑简洁到了近乎粗暴的程度static void check_hung_task(struct task_struct *t, unsigned long timeout) { unsigned long switch_count t-nvcsw t-nivcsw; if (t-flags PF_FROZEN) return; if (!switch_count || switch_count ! t-last_switch_count) { t-last_switch_count switch_count; return; } if (time_after(jiffies, t-last_switch_time timeout * HZ)) { hung_task_show_callchain(t); ... } }注意这里还藏了一个很巧妙的细节它不只是看“时间是否超了”还看“这个任务有没有发生过调度”。如果一个任务在最近一段时间里虽然也处于 D 状态但中间有短暂的切换记录比如被打断处理信号那就说明它还在活动不算卡死。只有从那一刻起任务的切换次数完全没变、CPU 再也没有碰过它并且时间超过了阈值才会走到报警逻辑。3.3 命中之后内核到底打印了什么信息当一个任务被判定为 hung内核会执行hung_task_show_callchain把以下信息一股脑输出到内核日志任务名和 PID任务状态、父进程 PID内核栈回溯也就是这个任务当时卡在哪个内核函数里每个 CPU 上当前正在执行的任务这个信息在排查时非常有用是否带hung_task_panic标志来决定是否直接触发 panic实际打印出来的内容排布大概是这样的INFO: task dbus-daemon:919 blocked for more than 120 seconds. Not tainted 3.10.0-1160.6.1.el7.x86_64 #1 echo 0 /proc/sys/kernel/hung_task_timeout_secs disables this message. dbus-daemon D 12816 1 919 0x80000000 Call Trace: [ffffffff8b1ab5e9] __schedule0x2a9/0x8d0 [ffffffff8b1abed1] schedule0x31/0x80 [ffffffff8b1af4a6] rwsem_down_read_failed0x156/0x1f0 ...开头那句echo 0 /proc/sys/kernel/hung_task_timeout_secs disables this message.就是提示你如果不想看到这个报错可以怎么暂时关掉。但注意“暂时”两个字关掉检测不等于解决问题这行提示是给紧急情况止血用的正常业务环境不建议长期关闭。4. sysctl 参数不是摆设四个关键旋钮的取舍逻辑hung_task 机制的运行参数都在/proc/sys/kernel/下可以通过 sysctl 动态调整。它们分别控制超时时间、检测频率、告警次数和故障行为。理解这四个参数才能做到“让监控服务人而不是折磨人”。4.1 超时时间和检测周期是联动关系最重要的参数是kernel.hung_task_timeout_secs默认 120。这个值的含义是一个任务连续 D 状态超过多少秒就判定为 hung。它是判定阈值但并不是说每过 120 秒检查一次。实际检测周期由kernel.hung_task_check_interval_secs决定影子默认值是 0此时内核会把检测周期定为超时时间的一半也就是 60 秒检查一次。如果你把hung_task_check_interval_secs设成一个非零值则按这个值作为检查周期。举个例子我把超时时间设为 300检测周期保持默认 0那么实际就是每 150 秒检测一次如果我把检测周期显式设为 60那就每 60 秒看一遍发现某任务连续 300 秒没有切换才报警。前者省 CPU后者更灵敏但检测更频繁。常规服务器负载下遍历任务列表的开销可以忽略不计但如果跑的是几万核的 HPC 集群检测成本就值得琢磨了。4.2 打印次数和 panic 开关的取舍kernel.hung_task_warnings控制内核最多打印多少次这种告警。默认值 0 表示不限制次数也就是说只要一直有任务卡死就会一直打日志。这在一台日志收集系统里很容易把磁盘打爆所以在生产环境建议设一个上限比如 10 次避免告警风暴演变成磁盘故障。kernel.hung_task_panic是最需要谨慎对待的参数。把它设为 1 后一旦检测到 hung 任务就立刻触发内核 panic整机重启。这套机制在部分高可用场景下是合理的比如双机热备里一台机器卡死了不如赶紧重启让流量切走。但在没有外围高可用机制的单机上设为 1 等于自杀式监控业务还没等来救援机器先把自己重启了。我见过不少容器化节点把hung_task_panic设成 1理由是 kubelet 会重新拉起 Pod。看起来没问题但如果卡死的根因是底层存储故障每台节点都 panic、都重启然后全部挂载同一个故障存储再一起 panic形成“重启风暴”整个集群都会被打崩。所以我的建议是panic 开关一定要配合根因隔离能力使用没有隔离手段之前保持默认 0。4.3 参数调整实例从默认到适合业务形态下表是我在不同场景下的推荐配置供参考参数默认值通用业务建议强 HA 集群建议低频 IO 场景建议hung_task_timeout_secs120300~60060~120600~1800hung_task_check_interval_secs00跟随半周期300hung_task_warnings0不限5~105~1010hung_task_panic00可选 10注意这些数值不是拍脑袋拍的。通用业务场景里文件系统、数据库、容器网络都可能因为底层抖动出现几十秒的 D 状态如果把阈值定太死正常抖动就会触发报警反而掩盖了真正的问题。低频 IO 场景里比如冷数据备份集群一次大规模归档任务可能让进程在磁盘等待上停留很长时间这时候阈值就得放大到分钟级别否则整个备份过程会一直响起误报。还有一个关键细节hung_task_timeout_secs设为 0 会直接禁用整个 hung_task 检测机制。有些优化教程会让你这么做来“消除误报”我非常不建议除非你已经确认这个系统上没有长期阻塞的风险——但说实话敢这么拍胸脯的人我还没见过。日志轰炸的正确止血方式是调大超时和限制告警次数而不是彻底关掉这双眼睛。5. 线上排查链路从一条日志到揪出底层根因日志打出来了下一步怎么查我按照自己的排查习惯整理成一套可复用的链路这套链路帮我处理过不少实际事故每次走完基本都能定位到根因所在层次。5.1 第一步从调用栈判断卡在哪一层拿到日志后先看 Call Trace 里的最后一个调用或者说看它卡在哪个内核函数。hung_task 打印的调用栈是任务被调度出 CPU 时的栈回溯虽然不代表此刻它正在执行但能反映它最后一次进入等待时路径。举个例子如果栈底是rwsem_down_read_failed、down_read、mutex_lock这一串说明它是在等一个内核信号量或读写锁。这类阻塞通常意味着另一个任务持有锁不放这时候重点要查的是另一个任务的栈看它是真死锁还是持有锁之后做慢速 IO 去了。如果栈底是wait_on_page_bit、filemap_read这类说明是在等内存页回写或读盘问题大概在文件系统和底层 IO。如果栈底是nfs4_wait_clnt_recover或者rpc_wait_bit_killable说明是在等 NFS 的 RPC 响应那就该把目光移到网络和 NFS 服务端了。5.2 第二步结合/proc/PID/stack和 sysrq 确认阻塞现场/proc/pid/stack是查看内核栈的快捷方法不过普通用户可能没有权限需要 root。更常用的手段是echo w /proc/sysrq-trigger这个指令会让内核把当前所有处于不可中断睡眠的任务的调用栈全部输出到dmesg看整个系统的 D 状态任务分布情况。一次性看到哪些进程卡在哪个函数里比单看一条日志更容易判断是不是“集体等待同一个资源”。配合ps -eo state,pid,comm,wchan:36命令能看到任务在内核里的等待点也就是 wchan。虽然它显示的是符号名不是完整调用栈但已经能帮我们快速筛选出异常进程。5.3 第三步区分三宗最典型的根因结合我经手过的案例线上最常见的 hung_task 根因分三类第一类是存储设备无响应。这种最直观调用栈一般落在 io_schedule、wait_on_page_bit 这一类。查看底层存储确认磁盘是否故障、RAID 卡是否在重建或者用iostat -x 1看%util和avgqu-sz是否异常。真实案例里一块盘扇区重映射导致的 IO 长时间卡死会让所有等待该盘的任务全部 D 住。第二类是网络文件系统远端不可达。NFS 挂载点一旦服务端响应超时内核里的 NFS 客户端任务会长期阻塞。这种情况要检查网络连通性、NFS 服务端状态并且确认挂载参数里是否启用了soft、timeo、retrans这些选项。很多团队图省事直接挂hard模式结果服务端一挂客户端所有进程全卡 D 状态。第三类是内核资源锁死或驱动 bug。这种最难查因为调用栈和实际持有的锁可能不在同一个进程上。我通常的做法是配合hung_task_panic在测试环境触发一次抓完整的 panic 日志再用crash工具分析死锁链。5.4 一个具体的排查案例拆解之前遇到过一个问题数据库节点报 hung_task调用栈指向ext4_filemap_fault和filemap_write_and_wait_range本质上是在等内存页写回。当时第一反应是存储慢但 iostat 看下来存储压力很低。后面把排查重点放到内存回收上才发现是内存碎片化严重写回线程长时间拿不到足够的内存页导致写回任务被卡住进而拖住整个 ext4 的 IO 路径。这个案例挺有代表性它说明调用栈只是入口问题根因可能隔了两三层得沿着 IO 路径一层层剥开。遇到 hung_task 时先别急着改参数按“阻塞点 → 资源状态 → 底层组件”的顺序查大多数情况都能在半小时内定位到一个可行动的方向。6. 架构设计与故障演练把被动告警变成主动防御hung_task 的告警本质上是一种“滞后指标”——它告诉你已经坏了而不是快要坏了。想让系统不被这类问题打得措手不及需要在架构设计和日常演练上多做一步。6.1 识别天然容易踩线的组件有些组件天生就容易成为 hung_task 的常客。最常见的是基于网络的文件系统NFS 是重灾区CIFS/SMB 也会出现类似问题。再一个是本地存储的磁盘直通场景特别是底层没有超时机制的老旧磁盘阵列。容器网络里如果使用了网络存储插件且插件链路较长也可能因为网络抖动把 IO 路径拖垮。针对这些组件我的建议是提前打预防针。比如 NFS 挂载时尽量启用soft模式或者在客户端和服务端之间加 IO 超时检测存储层面配置磁盘的 IO 超时参数服务端和客户端都设置合理的超时上限。做过这些前置处理后即使底层出现抖动也往往是进程得到错误码然后重试而不是无限期卡死。6.2 监控不能只看 dmesg要主动采样dmesg 里的 hung_task 告警是“事后诸葛亮”但如果能提前收集 D 状态任务的数量和等待点变化趋势就能在触发阈值之前发现苗头。我喜欢用一个简单脚本定期采样/proc下的进程状态统计处于 D 状态的任务数和它们的内核栈符号分布跑一遍就能看到哪些组件在某个时段集中进入阻塞。内核里也提供了更底层的观测手段比如 eBPF 可以在调度点挂探针观测任务从运行到睡眠的切换耗时。这类数据对判断“某条路径是不是越来越慢”非常有价值。如果你所在团队已经维护了内核级监控完全可以在 hung_task 触发之前捕捉到 IO 延迟曲线异常抬升的过程。6.3 演练时如何验证系统的兜底能力故障演练这一步往往被忽略但关键时刻它能救命。我建议做两个演练场景。第一个是模拟存储无响应通过 fault injection 或者直接给虚拟机做磁盘快照冻结观察系统在底层 IO 长时间不返回时应用层是否依然能保持核心功能可用hung_task 告警是否按预期出现告警信息里是否包含了足够多的定位线索。第二个是模拟 NFS 服务端故障关掉 NFS 服务端或切断网络观察客户端的表现是立即进入重试还是所有进程都卡进 D 状态以及监控告警能否在业务影响扩散前触发。把这两轮演练做完你会对自己系统的“卡死耐受度”有非常具体的感知。哪些组件的韧性足够哪些组件一旦底层故障就会全线崩溃心里就有数了。6.4 最有效的策略是不让等待发生说到底hung_task 检测到的并不是问题的根源根源是“系统里发生了无界等待”。我个人的经验是无论调多少参数、写多少巡检脚本都不如把每一个可能产生无界等待的路径都显式加上超时和重试逻辑。从底层设备驱动到文件系统再到网络协议栈每一层都设置合理的超时上限并把它做成可配置项D 状态的持续时间就会被层层截断不会被放任到 120 秒以上。对普通业务开发者来说最简单的落地方式就是不要依赖无限阻塞的系统调用。自己去调研一下自己的存储挂载参数、数据库连接超时、网络 RPC 超时设置很多地方只要显式地配置了超时时间hung_task 的触发概率就会直线下降。内核的这套自检机制是一道最后防线但它不该是第一道更不该是唯一一道防线。