ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

彻底搞懂Linux hung task:原理、检测与实战排查

彻底搞懂Linux hung task:原理、检测与实战排查 搞Linux内核和系统运维这些年要说哪种故障最让人头疼hung task绝对排得上号。机器看着没死ssh还能通但某个进程就是卡死在D状态kill -9也杀不掉业务直接hang住最后只能重启。这种“活死人”状态背后就是Linux内核的hung task检测机制在发挥作用。这篇文章我会从内核原理讲到实战排查结合我处理过的真实故障案例把这个机制彻底讲透。无论你是做内核开发、嵌入式系统还是搞SRE运维都应该能从中获得可直接落地的经验。1. 先搞懂hung task到底是什么1.1 从一次“杀不死的进程”说起先还原一个我早年踩过的典型场景。某天早上业务方反馈数据库连接全部超时登录服务器一看load average飙到80多但CPU使用率却不怎么高。用ps aux查看发现大量mysqld进程处于D状态后面跟着一堆类似D 0 12345的输出。我下意识执行kill -9 12345结果进程纹丝不动连内核线程都没反应。这里的关键就是D状态即TASK_UNINTERRUPTIBLE不可中断睡眠。这种状态下进程既不会被调度器调度也不会响应任何信号包括SIGKILL。它只能等内核代码路径自己醒来。如果内核代码因为等待IO、等待锁、等待某个硬件事件而长时间无法返回进程就会一直卡在D状态时间一长就成了内核要检测的hung task。很多刚接触Linux的人会有个误区以为D状态就是普通的“卡住”重启一下服务就好。实际上D状态进程卡在内核态服务管理工具systemd、supervisor等根本拿它没办法kill不掉stop也停不了唯一相对干净的办法是重启整机。这也是hung task问题最让人崩溃的地方——它不像用户态程序那样可以强制终止。1.2 D状态和hung task的区别严格来说D状态是一个瞬时状态进程在内核里等待IO完成、等待锁释放时都会进入D状态这是正常现象。比如你在终端里运行cat读取一个慢速设备cat进程大概率会短暂进入D状态但只要IO完成它立刻恢复运行。hung task则是“D状态持续过久”的结果。内核维护了一个阈值默认120秒如果某个进程或内核线程持续处于不可中断睡眠超过这个时间内核就判定它为hung task并触发告警。需要特别强调的是“持续”这两个字很重要。我见过不少人在排查时看到ps输出里有个D状态进程就紧张其实只要它能在几十毫秒内恢复完全不是问题。真正的hung task特征是进程状态长期不变内核栈固定在某处比如阻塞在wait_on_page_bit、mutex_lock、io_schedule等函数上并且持续超过hung task阈值。1.3 内核为什么要专门做这个检测内核引入hung task检测机制目的很简单尽早发现“假死”并留下线索。想象一个场景某个内核线程因为驱动bug死锁了整个子系统可能都跟着瘫痪但系统负载很低、CPU空闲监控根本不会报警。如果没有hung task检测机制你可能要等到业务超时、用户投诉才发现问题而且重启后线索全丢。现在的实现会在检测到hung task时打印完整的调用栈、进程信息、内核版本这些信息对于定位根因至关重要。所以我的态度是不要把这个机制当成“多此一举”的告警它其实是内核在帮你做“尸检”把故障现场记录下来。这个机制也特别适合三类人学习一是写内核模块/驱动的开发者最容易trigger这种问题二是嵌入式Linux工程师因为嵌入式环境IO稳定性差hung task发生率更高三是负责生产环境的运维或SRE需要快速读懂告警、定位问题而不是只会重启。2. hung task检测机制的底层原理拆解2.1 关键配置项和内核线程hung task检测在Linux内核里由kernel/hung_task.c实现编译开关是CONFIG_DETECT_HUNG_TASK。绝大多数发行版内核都默认开启但你可以在内核配置里确认一下grep DETECT_HUNG_TASK /boot/config-$(uname -r)如果输出是CONFIG_DETECT_HUNG_TASKy说明启用。该机制运行时依赖一个名为khungtaskd的内核线程它由hung_task_init在系统初始化时创建。这个线程优先级不低周期性地被唤醒扫描全系统的任务状态。相关的运行参数都挂在/proc/sys/kernel/下一共有5个参数名默认值作用hung_task_timeout_secs120D状态超过该秒数判定为hung task设为0则完全禁用检测hung_task_check_interval_secs0检测线程的唤醒间隔0表示自动取timeout的一半hung_task_check_count32768每次扫描最多检查的任务数防止遍历耗时过长hung_task_warnings10打印告警的次数上限避免刷爆内核日志设为-1则无限告警hung_task_panic0检测到hung task后是否直接panic生产环境需要慎重配置这些参数都可以用sysctl -w在线修改也可以写进/etc/sysctl.conf固化。有一点要注意hung_task_timeout_secs的单位是秒最小粒度是秒级内核内部会换算成jiffies时钟节拍来比较。2.2 检测流程从唤醒到判定了解参数后我直接给你梳理检测流程这段逻辑对排查有指导意义。khungtaskd线程的入口是hung_taskd函数内部是一个无限循环根据hung_task_check_interval_secs计算间隔时间。如果该值为0则取hung_task_timeout_secs / 2保证最迟在超时阈值的一半时间内发现异常。schedule_timeout_interruptible(interval)让线程睡眠等待。唤醒后调用check_hung_uninterruptible_tasks遍历全系统任务。遍历时先看任务状态是否为TASK_UNINTERRUPTIBLE然后比较任务的last_switch_timestamp实际是task_struct中的last_switch_count和sched_last_update。如果jiffies - t-sched_last_update timeout_jiffies说明任务超过阈值没有进行过调度切换触发check_hung_task。check_hung_task里会打印任务名、PID、调用栈统计warning次数最后决定要不要调用panic。这里有个关键细节需要注意内核不是直接记录“进程进入D状态的时间”而是用sched_last_update这个字段判断。该字段在进程被调度器切出、唤醒时都会更新反映的是进程最近一次“可运行”的时间点。如果一个进程进入D状态后再也没被调度过这个时间戳就会停留在进入D状态前的那次更新。为什么这么设计因为如果一个进程频繁在D和R状态之间切换例如IO间歇性恢复它就不算是hung task。只有那种“完全卡死、一次都没机会运行”的进程才值得告警。这种设计避免了大量误报。另外一个容易忽略的细节是cgroup冻结任务。如果任务被cgroup freeze它也会长时间停留在不可中断状态但这种情况不应该报hung task。内核在检测时会调用is_global_init和cgroup_task_frozen等判断来过滤这类任务。我实际排查时遇到过容器平台批量冻结cgroup导致误报的案例当时就是忽略了这层逻辑。2.3 一条hung task告警日志怎么读来看一条真实的内核告警日志INFO: task kworker/u4:2:120 blocked for more than 120 seconds. Not tainted 5.10.0-60.18.0.50.hl.example.x86_64 #1 echo 0 /proc/sys/kernel/hung_task_timeout_secs disables this message. task:kworker/u4:2 state:D stack: 0 pid: 120 ppid: 2 flags:0x00000000 Call Trace: __schedule0x2d1/0x890 schedule0x35/0xa0 io_schedule0x12/0x40 wait_on_page_bit0x101/0x120 __filemap_fdatawait_range0xba/0xf0 filemap_fdatawait0x1c/0x20 __sync_blockdev0x22/0x40 sync_blockdev0xa/0x10 ext4_sync_fs0x33/0x50 __sync_filesystem0x4d/0x70 sync_filesystem0x1a/0x30 do_emergency_remount0x35/0x90 do_emergency_remount_callback0x16/0x20 iterate_supers0x8d/0xc0 emergency_remount0x17/0x20 do_syscall_640x2d/0x40 entry_SYSCALL_64_after_hwframe0x44/0xa9逐行拆解INFO: task kworker/u4:2:120 blocked for more than 120 seconds.被检测对象是kworker/u4:2内核线程PID 120已阻塞超过120秒。Not tainted内核没有被加载未知模块污染基本排除第三方内核模块的直接嫌疑但间接驱动问题仍可能。echo 0 /proc/sys/kernel/hung_task_timeout_secs提示如何临时关闭该告警这是内核给排查者留的后门。state:D确认进程处于D状态。Call Trace从下往上读是函数调用的反向路径。最上面的wait_on_page_bit就是它卡住的地方表示在等一个page内存页的IO完成。读调用栈有一条核心经验盯着最顶层的非调度函数看。__schedule、schedule、io_schedule这些是调度器通用路径所有hung task都会经过它们没有区分度。真正的线索是__filemap_fdatawait_range、ext4_sync_fs这些具体业务函数它们告诉你进程到底在等什么资源。2.4 和soft lockup、hard lockup的区别排查hung task的过程中你经常会同时遇到另外两个相近概念soft lockup和hard lockup。我把它们放在一起对比方便你建立完整的知识框架机制检测对象触发条件检测线程/手段典型后果hung task进程/内核线程D状态超过hung_task_timeout_secskhungtaskd周期扫描打印调用栈可配置panicsoft lockup单个CPU该CPU超过20秒未发生调度切换watchdog高优先级线程检查打印CPU栈可配置panichard lockup单个CPU该CPU中断被长期屏蔽NMI看门狗nmi_watchdog直接panic默认情况下这三个机制有一个共同点都是用来检测“系统还在跑但局部已经死了”的状态。区别在于hung task关注的是进程soft/hard lockup关注的是CPU。实际故障中它们往往伴随出现——某个内核线程在自旋锁上死等既会让它自己变成hung task也会让所在CPU出现soft lockup。我在处理一次存储驱动故障时dmesg里先是出现了soft lockup告警过了几十秒又出现hung task告警。很多人看到两个告警就懵了其实它们指向的是同一个根因驱动在中断上下文里死循环CPU被占死其他进程得不到调度于是hung task检测也相继触发。理解这一点你就能在多个告警里找到真正的源头而不是被表象干扰。3. 实战从告警到根因的处理流程3.1 第一步确认影响面和故障时间线处理hung task问题我的第一个建议是先别急着改参数或者重启先花两分钟确认影响面。具体做三件事第一查看告警出现的时间点。用dmesg -T可以看到带人类可读时间戳的内核日志配合业务监控判断从用户报障到hung task告警之间有没有时间差。如果告警时间远早于业务感知时间说明问题已经持续很久需要更严肃对待。第二统计D状态进程数量。执行ps -eo state,pid,comm | awk $1D {count} END {print count}如果只有一两个进程处于D状态可能是局部问题如果有几十上百个基本可以判断是底层资源存储、网络、内存出了问题。第三快速查看系统全局状态cat /proc/meminfo | grep -E MemFree|Buffers|Cached cat /proc/loadavg iostat -x 1 3有些hung task其实是内存回收死锁的“果”而不是“因”。系统内存严重不足时内核回收线程可能长时间阻塞导致大量进程在page fault路径上进入D状态。如果free -g显示内存耗尽或者iostat显示磁盘util 100%那处理思路就完全不同。3.2 第二步顺着调用栈定位阻塞点hung task告警里最有价值的就是调用栈。我总结了一套快速定位的方法先截取“顶级函数”再结合内核源码搜索最后聚焦到具体子系统。常见的顶级函数和对应线索如下表调用栈顶级函数阻塞资源可能原因方向wait_on_page_bit/filemap_fdatawaitpage IO存储层读写卡住、磁盘坏道、文件系统卡死mutex_lock/down_write内核锁驱动持锁不释放、文件系统死锁rpc_wait_bit_killableRPC请求NFS服务端无响应、网络分区ext4_writepages/journal_start文件系统日志ext4日志卡住、磁盘IO异常blk_status_to_errno/submit_bio_waitblock层IO块设备驱动bug、硬件故障sock_alloc_send_pskb/sk_stream_wait_memorysocket内存TCP发送缓冲区受限、网络栈问题拿到顶级函数后用grep -r 函数名 /usr/src/linux-source-*/或者在内核源码网站上搜索找到对应函数所在文件。我一般会关注这么几个信息函数前面有没有加锁操作锁的持有者是谁函数等待的是IO请求完成还是等待某个条件变量该函数所在路径是否跟某个驱动模块强相关举个例子如果调用栈是mutex_lock加nfs_rmdir那你几乎可以断定是NFS环境下的目录删除操作争抢inode锁失败。如果调用栈是wait_on_page_bit加ext4_writepages那大概率是ext4写回页缓存时底层块设备IO没有返回。3.3 第三步针对典型场景的处置手段定位到根因方向后处置手段就清晰了。我按高发场景分类说明场景一NFS/网络文件系统卡住这是我最常见到的hung task来源。NFS客户端发起RPC请求后如果服务端长期无响应网络抖动、服务端重启、NFS服务hang住客户端进程就会阻塞在RPC等待路径上。此时处理方式先确认服务端状态和网络连通性ping服务端IP、showmount -e 服务端IP。如果网络恢复大多数情况下NFS客户端会自动重试并恢复D状态进程会逐渐退出。如果长期无法恢复几乎只能重启客户端或者等nfs的timeo参数超时默认比较久。建议所有生产环境的NFS挂载都设置hard,timeo30,retrans2既能保证数据一致性又不至于让进程无限期阻塞。场景二磁盘坏道/存储控制器故障wait_on_page_bit加存储IO相关调用栈同时dmesg里有I/O error、blk_update_request之类的日志基本可以判断是底层磁盘问题。这种场景下先用smartctl -a /dev/sdX检查磁盘SMART信息重点关注Reallocated_Sector_Ct、Current_Pending_Sector。确认是否有RAID卡或存储阵列的告警。如果是硬件故障业务方确认后走更换流程如果磁盘暂时还能用可以尝试blockdev --setro将磁盘设为只读避免进一步写入。对于D状态进程只要硬件恢复它们大多能自动退出。不要盲目重启因为重启后文件系统可能因为未完成写入而需要长时间fsck。场景三驱动bug导致的内核锁死锁这种最麻烦。调用栈里出现驱动特有函数比如网卡驱动的e1000_xmit_frame、GPU驱动的某个提交命令函数然后卡在mutex_lock或spin_lock上。处理建议先查驱动版本对比厂商是否有已知bug修复。查看内核日志中是否有BUG:、WARNING:、Oops等其他线索。尝试卸载并重新加载驱动模块rmmod加modprobe但注意如果模块正被使用rmmod会失败。如果驱动无法恢复考虑升级驱动或内核版本。这类问题通常需要保留完整现场建议配合kdump抓取crash dump交给驱动或内核开发者分析。场景四内存压力导致的回收死锁当/proc/meminfo中MemFree极低、kswapd进程持续占用CPU、大量D状态进程阻塞在内存分配路径时要警惕回收死锁。这种情况在内存较小的嵌入式设备上尤其常见。可以临时尝试调低vm.dirty_ratio和vm.dirty_background_ratio减少写回压力。调高vm.min_free_kbytes为内存回收保留紧急水位。关掉vm.swappiness设为0可能反而加剧回收需要慎重。最终方案通常是加内存或者显著降低系统内存占用。3.4 使用sysrq手动获取现场如果hung task告警已经打印但信息不够或者你想在重启前获取更全面的现场sysrq是最后的手段。两个最常用的按键echo t /proc/sysrq-trigger把系统上所有任务的状态和调用栈打印到内核日志。echo w /proc/sysrq-trigger只打印处于D状态的任务比t更聚焦日志量也小很多。我一般先按w看有哪些D状态任务跟hung task告警对照确认影响范围。如果怀疑是CPU问题按l打印所有CPU的backtrace。特别提醒这两个操作有一定开销特别是在进程数很多的机器上打印全部栈可能导致系统短暂停顿但不会致命。真正危险的是echo c /proc/sysrq-trigger它会强制触发panic仅用于抓取crash dump的场景千万别在生产环境随便敲。当时我处理数据库hung task故障时先在确认业务方已经切换流量后再按w抓现场拿到了完整的进程栈列表最后定位到是存储多路径软件multipath的一条路径失效后没有及时切换导致大量IO卡死。如果没有这些现场信息重启后问题很难复现。4. 参数调优实践把hung task变成系统兜底机制4.1 五个核心参数的调整场景hung task检测机制不仅仅是“告警器”配置得当的话它还能变成系统的高可用兜底手段。但它的参数调优非常依赖场景我结合实践给你一份可直接参考的方案。hung_task_timeout_secs默认120秒。对于大多数业务系统120秒是个合理值。如果业务对IO超时很敏感比如数据库日志写入要求毫秒级确认可以把阈值调小到30秒甚至10秒尽快发现IO异常。反之如果业务本身就有长时间IO等待比如批量处理大量文件的脚本可以调到300秒以上减少误报。我通常建议生产环境保持120秒或90秒太小容易误报太大则失去“早发现”的意义。hung_task_warnings默认10次。这个参数的意思是对于同一个任务hung超过阈值的情况最多打印10次告警之后不再打印。调成-1表示无限次告警。我在实际运维中强烈建议设为-1或00也是无限告警需要确认实际内核默认10次设为-1表示无限告警因为告警刷屏虽然烦人但丢失告警线索更可怕。当然如果你的/var/log/messages容量有限可以保持默认。hung_task_panic这是最需要谨慎的参数。设为1后一旦检测到hung task内核直接panic系统会自动重启如果配置了watchdog或者重启策略。好处是快速failover到备用节点坏处是可能把“还能抢救一下”的系统直接杀掉。我的建议是单机无HA场景保持0别开panic。数据库主从、k8s节点等有外部HA机制的场景可以开1但一定要确认crashkernel、kdump已配置否则panic后没有任何crash dump整个事件变成“无头悬案”。嵌入式设备建议开1因为嵌入式设备往往无人值守hung task后系统已经不可用自动重启反而是最优解。hung_task_check_interval_secs默认0表示自动取timeout的一半。我一般不动它最多显式设为timeout/2的值让行为更可预测。hung_task_check_count默认32768覆盖绝大多数系统。如果机器上线程数非常多比如某些高并发应用有数万个线程需要检查这个值是否够用。设置太小会导致扫描不到所有任务漏检。4.2 生产环境推荐配置分享一套我在生产环境验证过的配置cat /etc/sysctl.d/99-hung-task.conf EOF # 检测阈值90秒 kernel.hung_task_timeout_secs 90 # 扫描间隔自动取45秒 kernel.hung_task_check_interval_secs 45 kernel.hung_task_check_count 65536 # 不限制告警次数 kernel.hung_task_warnings -1 # 启用panic配合外部HA做快速failover kernel.hung_task_panic 1 kernel.panic 30 EOF sysctl --system这里有两个细节说明。第一我设置了kernel.panic 30意思是panic后30秒自动重启。配合hung_task_panic 1系统的行为变成检测到hung task - 内核panic - 30秒后重启 - 业务由外部HA切换到备用节点。这套策略在数据库一主两从的架构里实测效果不错故障恢复时间从原来的“人工发现重启”的十几分钟缩短到1分钟内。第二hung_task_check_count 65536是针对高线程数应用特别调整的普通机器用默认值即可。4.3 我踩过的坑希望你避开这部分是我最想分享的实战经验每个坑都付过学费。坑一为了“美化监控”调大超时时间。曾经有团队因为hung task告警太频繁直接把hung_task_timeout_secs调成600秒。结果问题依旧只是告警变成了10分钟一条监控界面好看了但业务早就超时了。这个思路完全错误——hung task告警是系统在喊救命正确的做法是治病而不是把生命体征监测仪关掉。坑二以为kill -9能杀掉D状态进程反复重试。我见过有同事对一个D状态进程连续执行kill -9几十次每次都说“再试一次”。D状态下进程在内核态信号处理要等它回到用户态才会执行而它根本回不去。反复kill除了让你自己更焦虑没有任何作用。坑三只盯hung task告警忽略之前出现的soft lockup。我处理过一个存储驱动的故障dmesg先后出现了soft lockup、hung task、RCU stall三波告警。很多人直接盯着最后的hung task分析但真正的根因在最早的soft lockup——驱动的某个临界区没有正确释放锁。排查时一定要把时间线拉全从第一条异常日志开始看而不是只看最后一条。坑四开启hung_task_panic却没配kdump。这是最致命的坑。打开panic开关后系统确实会在hung task时重启但由于没有配置crashkernel和kdump重启后你手上只有一串内核日志里的hung task告警没有完整的内存转储无法分析到底是谁持有了锁不放。结果是系统确实快速恢复了但根因永远查不出来下次还会再犯。开关配合crashkernel512M参数和kdump服务使用才算完整方案。5. 排查工具箱命令速查与常见问题速查5.1 最简单的排查命令集整个过程熟练之后我通常用下面这套命令组合拳目的命令查看所有D状态进程ps -eo state,tid,pid,comm,wchan:30查看指定进程内核栈cat /proc/pid/stack查看进程阻塞在内核哪个函数cat /proc/pid/wchan查看带时间戳的内核日志dmesg -T查看最近内核告警journalctl -k -bsystemd系统sysrq打印所有D状态任务栈echo w /proc/sysrq-trigger查看磁盘IO状态iostat -x 1查看挂载的NFS状态mount其中/proc/pid/stack需要root权限而且内核需要开启CONFIG_STACKTRACE。有些发行版默认关闭执行时会报cat: /proc/.../stack: Input/output error此时可以用echo t /proc/sysrq-trigger替代。5.2 常见问题速查表现象大概率原因建议操作大量D状态进程iostat显示磁盘100%磁盘IO饱和或坏道检查iostat、smartctl评估是否需要扩容或换盘D状态进程阻塞在NFS相关函数NFS服务端异常或网络问题先查网络和服务端必要时重启客户端只有个别驱动相关线程卡住驱动bug或硬件故障保留现场尝试rmmod/modprobe考虑升级驱动hung task告警前有soft lockupCPU被某个临界区占死抓取soft lockup调用栈分析持锁者开启hung_task_panic后系统频繁重启阈值过小或误报调大阈值或先关闭panic观察一段时间echo 0 hung_task_timeout_secs后仍然告警检测线程未及时读取参数重启或确认参数写入生效5.3 和业务方协作的沟通技巧处理hung task故障时跟业务方沟通也很重要。很多运维同事只会在群里丢一句“系统hung task了”业务方一脸懵。我给一个更实用的模板完整记录故障时间、涉及业务、D状态进程数量、根因判断、处置动作、预计恢复时间这6个要素然后在恢复后24小时内输出一份简要的复盘文档把调用栈截图和参数调整记录附上。这样业务方会觉得你专业下次有类似问题也更容易配合你做变更窗口。我在实际工作中光是靠一个结构清晰的故障汇报模板就减少了很多不必要的“升级投诉”。技术重要把技术翻译成人话同样重要。最后分享一点个人体会处理hung task问题这些年我最大的体会是它本质上是一种“系统僵死”信号而不是“系统崩溃”信号。崩溃是瞬间的、明确的而僵死是渐进的、模糊的。正因为它模糊所以需要一套完整的检测和定位体系来支撑。内核给了我们khungtaskd这个“哨兵”但哨兵只能报警真正解决问题还得靠你顺着调用栈一步步往下挖。另外如果你在嵌入式环境工作我建议你在开发阶段就开启hung task检测并把hung_task_panic设为1。嵌入式设备一旦跑起来就是无人值守与其让设备“半死不活”地挂在现场不如让它快速重启恢复。但记得配好串口日志输出否则重启后你什么线索都留不下。这个取舍每个团队都要提前想清楚别等到故障发生时才临场做决定。
返回列表