Linux内核hung task机制深度解析:D状态进程卡死排查实战
有一次我处理线上故障服务器 load average 飙到 200 多可 CPU 使用率却不到 10%连 SSH 敲个命令都像得了拖延症。翻 dmesg 才发现Linux 内核早就刷了一屏 hung task 告警一堆进程卡在不可中断睡眠状态。那次之后我认真把 Linux 内核的 hung task 检测机制完整啃了一遍也踩过几次误报和误杀的坑。这篇文章就把原理、源码链路、误报场景和排查经验一次讲透适合遇到 D 状态进程卡死、内核假死类问题的运维、SRE 和内核初学者参考。1. 一次线上“假死”与hung task报警内核在看什么1.1 故障现象与真凶很多人在生产环境第一次见到 hung task都是从一个诡异的现象开始的机器没断电CPU 占用也不高但业务进程就像冻住了一样请求超时、命令卡顿load average 却非常高。如果你用top或ps看一眼会发现一堆进程的状态是大写的 D怎么 kill 都杀不掉。我当时的第一步不是重启而是先看内核日志dmesg -T | grep -i hung task很快看到类似这样的输出[Mon Jun 5 14:22:10 2023] INFO: task mysql: 23456 blocked for more than 120 seconds. [Mon Jun 5 14:22:10 2023] task:mysql state:D stack: 0 pid: 23456 ppid: 1 flags:0x80000002 [Mon Jun 5 14:22:10 2023] Call Trace: [Mon Jun 5 14:22:10 2023] __schedule0xaaa/0xbc0 [Mon Jun 5 14:22:10 2023] schedule0x5a/0xc0 [Mon Jun 5 14:22:10 2023] io_schedule0x12/0x40 [Mon Jun 5 14:22:10 2023] wait_on_page_bit0x1c0/0x2b0 [Mon Jun 5 14:22:10 2023] filemap_fdatawait_range0x9b/0x100 [Mon Jun 5 14:22:10 2023] blkdev_issue_flush0x8d/0x100那一刻才算真正明白这个机制不是万能的“卡死检测器”它专门盯一类状态——不可中断睡眠。1.2 D状态是什么为什么喊不醒Linux 进程的 D 状态对应TASK_UNINTERRUPTIBLE意思是进程在内核态等待某个条件比如等待磁盘 IO 完成、等待信号量、等待硬件中断回复。这个状态不会因为收到普通信号就醒过来只有等待的事件发生才会被唤醒。打个比方一个人进了一间没有窗户的房间门被系统锁死了外面怎么喊发信号都没用必须有人从里面拿钥匙把门打开。内核里的“钥匙”通常是中断、IO 完成回调或者其他内核路径释放锁。hung task 检测机制就是要盯住这些“进房间太久没出来”的任务。当某个进程在 D 状态停留超过预设阈值通常是 120 秒内核就会判定它为“hung task”然后打印调用栈和进程信息来提示你介入。注意它不会主动杀进程这是很多人容易误解的地方。1.3 别混淆hung task/soft lockup/hard lockup/RCU stall初次接触的人经常把几种看门狗机制搞混。我用一个表把它们分开机制检测对象典型触发条件常见后果hung task任务是否长时间处于 D 状态磁盘 IO 卡住、驱动等待硬件、远端存储失联进程卡死load 偏高可以 kill 或等恢复soft lockup单个 CPU 是否长时间未发生调度内核死循环、关中断时间过长CPU 核“假死”watchdog 线程无法运行hard lockup中断是否长时间被禁用驱动 bug、硬件异常整个 CPU 无法响应中断RCU stallRCU grace period 是否延迟CPU 被长临界区占住、中断风暴系统不稳定可能触发 panic排查时必须先分清是哪一种。如果dmesg里是INFO: task xxx blocked for more than 120 seconds那就是 hung task如果是watchdog: BUG: soft lockup那是另外一套逻辑别拿着 D 状态排查思路硬套。2. khungtaskd扫描链路核心原理与关键参数2.1 内核线程与四个关键阈值hung task 检测机制在内核中对应kernel/hung_task.c核心是一个名为khungtaskd的内核线程。它周期性地遍历系统中的任务找出长时间处于不可中断睡眠状态的任务并打印告警信息。khungtaskd线程在hung_task_init()中创建随后进入循环执行check_hung_uninterruptible_tasks()。这个线程本身优先级不高但它依赖时钟调度来保证周期性运行。你可以通过一批 sysctl 参数控制它的行为常见参数如下参数默认值作用kernel.hung_task_timeout_secs120任务连续 D 状态超过该秒数就告警设为 0 可关闭检测kernel.hung_task_check_count32768每次扫描最多检查多少个任务kernel.hung_task_warnings10最多打印多少次告警设为 -1 表示不限制kernel.hung_task_panic0检测到 hung task 后是否触发内核 panickernel.hung_task_check_interval_secs0扫描间隔为 0 时使用超时时间的一半作为间隔生产环境里我一般会先把这几个值打出来看看sysctl kernel.hung_task_timeout_secs kernel.hung_task_check_count kernel.hung_task_warnings kernel.hung_task_panic如果某台机器经常因为真实硬件故障触发 hung task又希望快速让故障节点下线可以考虑kernel.hung_task_panic1。但要注意这只适合有高可用冗余、能接受节点重启的场景。下面展开讲判断逻辑。2.2 时间戳与判断条件扫描函数会逐个遍历任务核心判断简化后大致是if (!(t-state TASK_UNINTERRUPTIBLE)) continue; if (time_after(jiffies, t-last_switch_timestamp timeout)) goto hung;它借助调度器维护的last_switch_timestamp时间戳判断“任务最后切换出去之后距今是否已经超过 timeout”。换句话说它不是看进程从创建到现在跑了多久而是看进程是否一直处于不可中断的等待状态。一旦判定为 hung task内核会调用hung_task_show_trace()打印这个进程的内核栈并记录t-hung_task_since。如果告警次数还没达到hung_task_warnings上限它会继续跟踪这个任务如果设置了hung_task_panic1则直接触发 panic配合 kdump 可以留下完整内存转储。这里有个经常被忽略的细节内核冻结场景下的任务会被跳过。系统在休眠或 cgroup freezer 时很多任务会主动进入不可中断等待这是正常行为所以代码里会检查PF_FROZEN标志不会误报。2.3 两个容易误解的细节第一hung task 检测的不是“进程是否长时间没有运行”而是“进程是否长时间处于不可中断睡眠”。一个 CPU 密集型任务跑满 24 小时只要它是 R 状态hung task 就不管它一个进程如果频繁被调度但每次都在等待也不会触发。第二hung_task_timeout_secs并不是一个高精度的实时计时器。它是通过周期扫描来发现超时的实际告警时间可能比阈值略晚这在排查时不用抠那一两秒。如果业务要求更敏感的检测可以调低该值但要注意误报概率也会上升尤其是网络文件系统和慢存储场景。3. 这些场景最容易误报和漏报先学会分辨3.1 NFS/网络存储远端hang导致本地D状态最典型的误报来源是网络文件系统。NFS 的硬挂载方式下客户端进程发起读写请求后会进入不可中断的等待如果 NFS 服务端不可达、网络分区或者服务端 hang 住客户端进程就会一直卡在 D 状态。这类故障的调用栈通常能看到nfs_page_waitrpc_wait_bit_killablecall_rwsem_wait如果是soft挂载进程通常还能超时返回如果是hard挂载就很容易触发 hung task。排查时不要急着重启机器先用mount | grep nfs看看挂载选项再确认网络和服务端状态。很多情况下网络恢复后D 状态进程会自动退出业务也就回来了。我遇到过一次 iSCSI 存储链路抖动多路径一条链路失效后所有 IO 都堆在另一条路径上。那时 dmesg 里的 hung task 调用栈全在blk_mq等待上直接看存储侧日志才发现远端没有响应。恢复路径后阻塞进程自动恢复整个数据库没有重启。3.2 慢设备与驱动缺陷打印调用栈里的设备名物理设备的慢响应也会触发 hung task。硬盘坏道、SSD 固件 bug、NVMe 命令超时、USB 存储设备被拔出等都可能导致驱动在内核路径里等待。这类故障的调用栈经常看到wait_for_completionmutex_lockbio_endio相关的等待排查时可以结合dmesg里的 IO error、设备名称再用iostat -x 1看设备的%util和await。如果某一台盘的await动辄几百毫秒甚至几十秒那就不是内核问题而是设备或驱动问题。还有一次是网卡固件异常卡在内核rtnl_lock上把整个网络配置路径锁住很多网络相关进程全部 D 状态升级固件后才解决。3.3 内存回收与锁等待看不见的内核路径内存压力大但外部看着还“不太满”的时候容易忽略这种场景。当系统内存不足内核线程kswapd会持续做内存回收回收过程中可能等待 IO 写回脏页进而卡在 IO 路径上。如果同时还有大量进程在申请内存就可能看到很多人堆在内存锁上。典型栈包括do_try_to_free_pagesshrink_page_listwait_on_page_bitzone-lock相关等待判断方法很简单看free -g和vmstat 1如果si/so一直不为零或者内存大部分被 page cache 和不可回收的 kernel memory 占用就要考虑是不是内存回收引起的连锁反应。3.4 CGroup/虚拟化导致的伪hung容器环境里CGroup 的 IO 限制或 freezer 操作也会让进程长时间等待。blkio.throttle配置得太紧一个高 IO 任务硬生生被限速到接近零如果hung_task_timeout_secs设置得又很短就会误报。虚拟机场景也一样宿主机存储慢虚拟机的 virtio-blk 队列就无法及时返回客户机操作系统就会认为磁盘卡死进而打印 hung task。这种问题要从宿主机和存储层找根因光在虚拟机里杀进程没有任何意义。4. 实战案例三类典型hung task问题排查全记录4.1 案例一存储链路抖动multipath路径失效现象一台数据库服务器多个连接卡住业务侧超时load average 持续走高但CPU使用率很低。排查步骤dmesg -T | grep -i hung task看到多个进程 blockedcat /proc/pid/stack确认调用栈集中在await_proc_lock和blk_mq_make_request附近iostat -x 1看到某个 sdb 设备的await达到几十秒multipath -ll发现 dm-0 的一条路径状态为failed另一条路径状态是active。按理说 multipath 有冗余路径不应该完全卡住。但该环境配置的是queue_if_no_path当所有路径都不可用的时候IO 会在队列里无限等待。继续查发现存储侧两个控制器都触发了故障切换导致路径反复横跳。处理方式在存储侧恢复控制器状态后再次multipath -ll确认两条路径都恢复正常D 状态进程陆续退出数据库连接也恢复了。整个过程没有重启任何一台机器。这个案例的教训是遇到 D 状态进程先确认是否有外部依赖在等待而不是第一时间重启或 kill。4.2 案例二驱动卡死以太网卡固件缺陷现象某台物理机网络服务间歇性中断内核日志同时出现 hung task 和大量的网卡掉线记录。排查步骤dmesg 中看到e1000e驱动报错同时有INFO: task kworker/u:xxx blocked for more than 120 seconds用cat /proc/pid/stack看到 kworker 阻塞在rtnl_lock上而持有锁的是某个网卡初始化流程查看网卡ethtool -i eth0发现固件版本很旧搜索驱动提交记录确认这个固件版本在特定流量下有概率造成内部队列死锁。处理方式在维护窗口升级网卡固件和驱动重新加载后故障消失。这个案例给了一个非常实用的判断原则当 hung task 的调用栈指向某个锁而该锁又被一个长时间不退出的内核线程持有时重点要查持锁者的调用栈而不是只看被阻塞的任务。可以配合echo t /proc/sysrq-trigger把全部任务状态和栈输出到内核日志比自己一个个cat高效得多。4.3 案例三内存压力引发的连锁hung现象应用服务器内存配置较小某次流量突增后出现大量 D 状态进程连 SSH 都变得不稳定。排查步骤free -g发现几乎无可用内存buffer/cache 也很低vmstat 1看到si和so数值很高交换分区在疯狂读写dmesg 里 hung task 调用栈大量指向shrink_page_list、try_to_free_pagestop -o %MEM定位到一个内存占用非常大的 Java 进程。处理方式先临时增加 swap 空间还是老实用途实际上 swap 只能缓解真正的处理是重启内存占用最高的非核心业务进程释放内存后再扩容配置。内存释放后被阻塞的 IO 路径迅速恢复D 状态进程全部退出。这个案例说明hung task 并不总是“某个进程卡死”它有时只是内核资源紧张的一个下游信号。如果一上来就杀 D 状态进程很可能杀不掉也解决不了根因。正确的思路是先看内存、IO、锁这些通用资源再回到具体进程的调用栈。5. 快速响应与根治从临时恢复到长期防护5.1 排障六步checklist我个人在实际处理 hung task 时基本按下面六步走先确认现象top、ps -eo state,pid,cmd | awk $1D看有多少 D 状态进程收集日志dmesg -T | grep -i hung task顺便看有没有 IO error、dump 信息抓栈cat /proc/pid/stack、cat /proc/pid/wchan必要时用echo t /proc/sysrq-trigger输出全部任务栈看资源iostat -x 1、free -g、vmstat 1、multipath -ll区分存储、内存、网络问题判断临时动作如果是远端存储网络抖动优先恢复通道如果是内存压力释放内存如果是驱动 bug升级或卸载驱动最后才考虑重启型方案通过 sysctl 开启或关闭 hung_task_panic配合 kdump 留证然后重启故障节点。这套流程里前四步通常能在几分钟内定位大概方向。不要一上来就重启因为丢失现场后根因就难查了。5.2 hung_task_panic、kdump与调参策略kernel.hung_task_panic默认是 0也就是只告警不处理。很多团队不敢开怕正常业务波动触发 panic 导致整个节点宕机。但在设计良好的高可用集群里与其让一个 hung 住的节点拖垮整个业务不如让它快速退出让流量切到其他节点。开启方式sysctl -w kernel.hung_task_panic1 echo kernel.hung_task_panic 1 /etc/sysctl.conf但强烈建议同时开启 kdump确保 panic 后能留下 vmcoresystemctl enable kdump kdumpctl restart这样即使触发 panic也能通过 crash 工具分析内存转储找到持锁者或等待的 IO 资源。把hung_task_timeout_secs调大并不能解决根因只会让问题出现得更晚反而可能掩盖真正的故障。真正合理的调参是结合业务容忍度比如数据库主从切换能在 60 秒内完成就把超时调到 60让失败节点快速出局。5.3 根治思路驱动、超时与资源配额长期根治要注意几个层面。首先是设备层。磁盘、网卡、HBA 卡、存储多路径工具的固件和驱动要和内核版本匹配尤其是新内核搭配老驱动或老内核搭配新硬件很容易出现等待超时的 bug。保持稳定基线别盲目追新。其次是文件系统层。NFS 挂载尽量明确timeo、retrans、hard/soft策略ext4/xfs的日志模式、挂载参数也要根据业务写模型调整。对于数据库这类对 IO 延迟敏感的负载避免使用质量差的网络存储。最后是资源配额层。CGroup 的 IO 限制要结合真实业务流量设定容器内存要留出足够余量避免内存回收风暴把 IO 路径拖垮。多个租户共享底层存储时还要考虑存储 QoS 优先级否则一个掉速的磁盘就能拖垮整台宿主机的 IO 调度。6. 最后聊聊排障心得说句实在话hung task 检测机制本身并不复杂它就是在内核里放了一个“盯梢”的线程看谁在不可中断睡眠里待太久。但实际处理问题时难点从来不是看懂源码而是区分“谁是真凶谁只是受害者”。内存压力可以让几十个进程全部卡在 IO 等待上真正的故障是内存不足网络存储抖动可以让大量业务进程卡在 NFS 回调上真正的故障是远端。D 状态进程只是报警器不是病灶。我踩过最大的坑是看到 D 状态进程就想用kill -9解决。但不可中断睡眠的进程根本响应普通信号杀不掉不说还可能在内核路径上留下未释放的锁导致连锁问题。正确做法是先定位等待的资源恢复资源或修复依赖进程往往会自己醒来。最后分享一个容易被忽略的小技巧线上排查时最好提前把 sysrq 打开也就是sysctl kernel.sysrq1这样需要抓全量任务栈时可以直接echo t /proc/sysrq-trigger不必等 hung task 的下一次告警。配合kdump和稳定的hung_task_timeout_secs这套机制才能真正变成你手里一张可靠的故障应对牌。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →