ARTICLE DETAIL

资讯详情

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

Linux内核Hung Task机制探秘:从原理到排查实战

Linux内核Hung Task机制探秘:从原理到排查实战 1. Hung Task到底是什么为什么搞崩你的服务器做嵌入式Linux和服务器运维的人大概率都见过这么一段内核日志INFO: task kworker/0:1:44 blocked for more than 120 seconds. echo 0 /proc/sys/kernel/hung_task_timeout_secs disables this message.这就是内核hung task检测机制触发后的典型输出。很多人第一次看到时一脸懵以为是进程崩溃或者OOM其实不是。这是内核在告诉你某个任务task在内核态长时间得不到调度已经卡死超过阈值系统文件系统、IO栈或者某个锁可能已经陷入僵局业务进程再等下去也没意义了。Hung task机制本质上是一套内核自带的“看门狗”专门盯住那些在内核态长时间睡眠、始终无法被唤醒的任务。它解决的问题是当系统出现死锁、IO无限阻塞、驱动逻辑写死等严重故障时系统不能毫无反应地干等必须要有能力发现“这个任务已经死透了”然后把现场信息打印出来方便开发者定位和恢复。这个机制适合三类人重点研究一是搞嵌入式Linux底层驱动和BSP的工程师因为这类问题在嵌入式环境里极其常见二是做服务器内核调优和稳定性保障的运维hung task往往意味着硬件故障或者存储栈异常三是正在阅读内核源码、准备面试内核岗位的朋友这个知识点是面试官特别爱问的“内核如何保活”类问题。我最早遇到hung task是在一块ARM板上跑业务文件系统频繁读写隔三差五系统就卡死串口还能敲命令但业务进程全部D状态。那会儿不懂机制只知道重启。后来静下心追踪才发现是SD卡驱动在极端情况下的超时处理有缺陷触发了内核的hung task。从那之后我开始完整地梳理这套机制的原理和排障方法今天就把干货一次讲透。2. Hung Task检测机制原理拆解2.1 内核是 “怎么发现” 任务卡死的要理解hung task先明白一个前提内核态的调度和用户态不一样用户态进程卡住通常还能被kill掉而一旦任务在内核态进入D状态不可中断睡眠它既不听信号也不响应调度器的抢占只有在它等待的内核条件满足后才会被唤醒。Hung task检测机制的核心思想是定期扫描系统中的所有任务检查它们的调度时间戳。如果一个任务在TASK_UNINTERRUPTIBLE状态下持续的调度时间超过了阈值就判定为hung task。内核通过一个专用内核线程khungtaskd来执行这个扫描。它并不是每时每刻都在扫描而是按照hung_task_timeout_secs参数设定的时间间隔去唤醒一次。默认情况下这个值是120秒也就是说内核会给卡死的任务一个“宽限期”超过120秒再报告。为什么会有宽限期因为有些时候任务只是短暂地阻塞在IO上比如磁盘响应慢了如果稍微慢一点就上报会让系统频繁产生误报所以内核把阈值设得比较大。扫描的具体流程可以这样理解khungtaskd会遍历全局的任务链表对每个任务获取它的调度信息重点看两个字段任务当前的状态和任务的last_switch_time最后一次切换时间。如果任务状态是TASK_UNINTERRUPTIBLE且现在的时间和last_switch_time之差超过阈值内核就会触发hung task报告。这里有一个很重要的细节内核并不是直接把这个任务杀掉而是打印一段信息然后根据配置决定后续动作。默认配置下内核打印完调用栈后只是做个标记任务继续保持原状态系统继续运行。但如果开启了hung_task_panic参数内核会直接触发panic让系统重启这种方式适合生产环境中希望快速恢复的场景。2.2 关键代码路径与核心参数解读源码层面的实现主要集中在kernel/hung_task.c这个文件里。虽然不同版本的内核实现细节上略有差异但整体逻辑是一致的。核心函数是check_hung_task和check_hung_uninterruptible_tasks前者检查单个任务后者遍历全局任务。看源码的时候有几个关键点值得注意第一check_hung_task中判断“任务是否真的hung住”的条件值得细看。它比较的是task-last_switch_time与当前值now的差值但这里有个trick如果系统里恰好有多个CPU在同时运行高优先级任务导致低优先级任务长期得不到调度这种情况下last_switch_time同样会很旧内核也可能会误判为hung task。所以在高负载的多核机器上hung task有时候是因为调度延迟而不是真正的死锁。第二hung_task_timeout_secs是控制检测频率和触发阈值的关键参数。它必须大于等于0如果是0则完全禁用hung task检测。修改方式有两种运行时修改echo 60 /proc/sys/kernel/hung_task_timeout_secs永久生效写入/etc/sysctl.conf执行sysctl -p。第三hung_task_panic参数决定了检测到hung task后是否立即触发panic。生产环境如果开启这个参数建议同时配合panic_timeout设置自动重启否则系统只是panic在那里照样不可用。另外内核还有一个细节hung task机制只会检测TASK_UNINTERRUPTIBLE状态的任务不会检测TASK_KILLABLE这种状态其实可以响应致命信号和TASK_INTERRUPTIBLE。这也是为什么很多优化后的驱动会把睡眠状态从TASK_UNINTERRUPTIBLE改成TASK_KILLABLE目的就是避免这类“不可恢复的卡死”。2.3 与内核watchdog机制的区别很多人会把hung task和内核的watchdog混为一谈其实它们是两套不同的机制解决的问题也不同。Hung task针对单个任务长期不可调度通常是锁死、IO死循环、驱动睡眠逻辑错误导致的。Kernel watchdog软锁/硬锁检测针对整个CPU核长时间不调度软锁或者中断被长期屏蔽硬锁通常由中断处理程序死循环、关抢占后死循环等导致。看门狗机制基于NMI非屏蔽中断或周期性的时钟中断如果在一个CPU上连续几个时钟周期都没有发生调度或中断处理就认为该CPU卡死。而hung task是软件线程扫描不依赖时钟中断单纯看任务的睡眠时间。实际排查问题时这两套机制会一起看。比如一个驱动里自旋锁使用不当既可能导致CPU软锁因为CPU在自旋等待也可能导致其他任务hung住因为持有锁的任务始终不释放。日志里通常能同时看到rcu_sched detected stalls on CPUs/tasks以及task blocked for more than 120 seconds这时候要抓住最底层的原因。3. 触发Hung Task的典型场景与真实案例分析3.1 存储IO卡死导致的文件系统阻塞这是我在实际中遇到的最常见的一种hung task触发场景。某个进程比如jbd2ext4日志线程在写日志时块设备层下发了一个IO请求结果底层驱动或者硬件异常导致这个请求永远不会有完成回调那么jbd2就会一直阻塞在等待IO完成的状态。这时候其他想要访问文件系统的进程全部依赖jbd2的推进于是连锁反应大量业务进程进入D状态。这种场景在SD卡、eMMC、USB存储这类嵌入式设备上特别容易复现。因为这类存储介质经常出现热插拔、电压不稳、质量参差不齐驱动没有做好超时重试机制的话IO请求就可能卡死。在服务器上则可能是磁盘损坏、RAID卡故障、网络存储掉线本质逻辑是一样的底层IO不还债上层任务就永远等待。排查时的关键动作是看dmesg里hung task报告的同时是否伴有块设备层的信息比如[ 1524.878239] INFO: task jbd2/mmcblk0p2-8:247 blocked for more than 120 seconds. [ 1524.878245] echo 0 /proc/sys/kernel/hung_task_timeout_secs disables this message. [ 1524.878249] jbd2/mmcblk0p2-8 D 0 1167 2 0x00000000 [ 1524.878255] Call trace: [ 1524.878263] [ffffff80080874a0] __switch_to0x70/0x8c [ 1524.878269] [ffffff80082b2c28] wait_transaction_locked0x30/0x80这段trace里wait_transaction_locked表示进程在等待jbd2事务提交而jbd2线程自身又卡在IO上。这时候基本可以确定文件系统层和块设备层之间存在IO漏斗。3.2 自旋锁、互斥锁使用不当造成的内核卡死内核锁使用不当是让开发者最头痛的hung task来源。比如一个驱动在中断上下文里尝试获取一个被其他上下文持有的互斥锁mutex这本身就不合法因为mutex可能导致睡眠而中断上下文不能睡眠。在这种错误逻辑下持有锁的任务可能在等待锁释放而等待锁的任务又在中断里不退出两个互相等待形成死锁。内核调度器会发现双方都在TASK_UNINTERRUPTIBLE状态同时超过阈值输出两条hung task报告这时很容易看出来是两个任务互相卡住。还有一种更隐蔽的spin_lock占据时间过长。虽然自旋锁的特点是忙等待但如果在持锁期间发生了长延时操作比如读寄存器时带了个大延时循环其他核上的任务会不断自旋等待但因为没有睡眠所以hung task可能不会触发反而会触发CPU stall。这种问题在SMP多核环境下容易把系统搞瘫日志里能看到多个CPU都卡在同一个回溯栈上。处理锁类问题时优先用CONFIG_DEBUG_ATOMIC_SLEEP、CONFIG_PROVE_LOCKING这类内核调试选项。开启后能直接在死锁发生前就输出锁依赖关系节省大量排查时间。3.3 驱动代码里的睡眠逻辑缺陷驱动开发中最容易踩的坑就是在wait_event、wait_for_completion这类等待机制里没有设置超时而是无限等待某个硬件中断。一旦硬件在特定条件下没有产生中断比如gpio电平检测失败、DMA传输中途被异常终止但没上报错误中断驱动就永远等待进程挂死。还有一类是延迟工作schedule_delayed_work里重复调度自身但工作函数前半段在等待某个标志位而这个标志位只有同一个工作函数后半段才能设置逻辑上就出现了自我锁死。这类问题很难一眼看出只能靠堆栈回溯。我自己就犯过这类错误。当时写一个触摸屏驱动在wait_event_interruptible时少写了一个唤醒条件导致触摸屏上报线程一旦在高负载下漏掉一次中断唤醒就永远睡下去。应用层打开/dev/input/eventX后一直读不到事件dmesg里出现hung task报错指向的正是我的驱动线程。后来加了超时唤醒和错误计数问题彻底解决。4. 完善的生产环境排查与问题处理方案4.1 利用SysRq紧急获取内核现场当系统已经出现hung task但还没完全卡死串口或者SSH还能响应时第一时间使用SysRq组合键来手动触发内核转储信息。这个动作非常关键因为它能帮你在日志刷掉之前把调用栈抓下来。SysRq相关操作如下echo 10 /proc/sys/kernel/sysrq开启SysRq功能10表示开启常用功能echo t /proc/sysrq-trigger把当前所有任务的状态和调用栈打印到内核日志echo w /proc/sysrq-trigger只打印处于D状态的任务信息信息更精简在串口控制台或带外管理卡上执行echo t /proc/sysrq-trigger后内核日志里会输出大量进程调用栈。这些栈信息是分析卡死原因的第一手资料。生产环境建议提前在sysctl.conf里把kernel.sysrq设成1或10否则紧急时候你发现不能触发会非常被动。拿到调用栈后重点看卡死任务的栈底函数调用链的终点看它最后在执行什么操作。如果多个任务的栈都指向同一个驱动函数或者同一个锁凶手基本就能锁定。4.2 使用ftrace、perf等工具进行动态跟踪SysRq拿到的是静态快照适合先看现象。如果想进一步定位复现ftrace和perf是两款更强大的工具。ftrace通过/sys/kernel/debug/tracing目录操作可以跟踪内核函数调用。比如想跟踪某个进程在哪个函数上睡眠可以先把当前进程的PID过滤出来echo 12345 /sys/kernel/debug/tracing/set_ftrace_pid echo function /sys/kernel/debug/tracing/current_tracer echo schedule /sys/kernel/debug/tracing/set_ftrace_filter echo 1 /sys/kernel/debug/tracing/tracing_on这样打印出的流程可以看到该进程最后一次调度和被唤醒的路径。这类信息对驱动睡眠逻辑的排查特别有帮助。perf工具则适合做锁竞争分析perf lock子命令可以统计锁竞争情况快速找出哪个锁被长时间持有。虽然perf lock在内核版本较老的系统上表现不佳但新内核上效果很好值得尝试。4.3 内核参数调优与服务恢复策略如果hung task只是偶发且业务可以接受一定的重启时间可以考虑设置自动恢复策略避免人工介入周期过长。生产环境推荐这类组合参数# 当检测到hung task时直接panic重启系统 echo 1 /proc/sys/kernel/hung_task_panic # panic后10秒自动重启 echo 10 /proc/sys/kernel/panic # 调整检测阈值为60秒缩短发现时间 echo 60 /proc/sys/kernel/hung_task_timeout_secs注意hung_task_panic设为1之后系统一旦出现hung task就会立即panic这会产生两个问题一是如果任务只是因为高负载调度延迟导致的“伪hung”那系统会被白白重启业务中断二是panic时的RAM内存转储crash dump如果没有配置好故障现场可能丢失。所以是否开启panic策略需要权衡业务可用性和问题可定位性。个人建议研发测试环境一定开启hung_task_panic并配合kdump配置好crash dump这能帮你快速发现驱动问题。生产环境则可以视业务SLA决定如果业务进程全部D状态已经不可服务panic重启反而能更快恢复前提是确保文件系统在重启后能够正确恢复。4.4 从代码层面规避Hung Task的几种设计模式真正负责的工程师不能只靠检测机制兜底更要从设计上让任务不容易hung。一是睡眠必须带超时。尤其是驱动中等待硬件状态尽量用wait_for_completion_timeout替代wait_for_completion用wait_event_timeout替代wait_event。返回超时后要做错误处理不要直接假设事件一定发生。二是区分可中断和不可中断状态。能用TASK_KILLABLE的地方就不要用TASK_UNINTERRUPTIBLE。mutex_lock_killable、wait_event_killable这类接口意味着进程可以被致命信号唤醒虽然本质上还是等待但至少给了系统一个“人为干预”的入口。在文件系统、块设备这类只需一致性的场景里killable比uninterruptible更实用。三是IO请求必须有超时和错误上报。块设备驱动和网络驱动里的每个请求都要考虑硬件无响应的情况。通过blk_mq层的timeout机制或者驱动的自定义超时定时器确保IO请求不会变成僵尸请求。四是锁的嵌套层次要简单清晰。嵌套过深锁顺序不一致容易导致死锁。通过checkpatch的LOCKDEP检测把死锁风险消灭在测试阶段。5. 常见问题速查表与避坑经验下面是我这些年排查hung task问题时积累的一张速查表遇到相似情况可以直接对照参考。现象可能原因优先排查动作所有任务D状态dmesg只有hung task报错底层IO卡死块设备驱动bug查看卡在IO的进程栈检查设备驱动超时机制两个任务互相等待trace里看到mutex内核死锁开启LOCKDEP重新编译内核复现中断上下文出现hung task中断处理中错误睡眠代码review中断上下文严禁睡眠类调用高负载下偶发hung task调度延迟大非真实死锁放宽阈值观察趋势同时看CPU是否soft lockup只有特定进程卡死其他正常该进程触发驱动/文件系统缺陷用ftrace跟踪该进程的睡眠路径重启后故障立即消失硬件状态异常或软错误检查硬件日志升级固件驱动补充一个很多人容易忽略的避坑细节当你通过/proc/sys/kernel/hung_task_timeout_secs调大阈值或者清零后hung task日志确实不再打印了但这只是掩盖了问题不是解决了问题。卡死的任务依然在消耗内核资源D状态任务会一直占着一个进程描述符长期累积会导致系统无法创建新进程。所以在生产环境上宁可让系统panic重启也不要长时间保持一个满身“D”状态僵尸任务的系统。另一个经验是在排查hung task时先确认时间源是否准确。如果系统使用了NTP频繁校时或者虚拟机的时钟有大幅跳变last_switch_time这类时间戳字段会受到干扰有可能导致误报或者漏报。在虚拟化环境里看到疑似误报的hung task时先检查kvm-clock和tsc相关内核参数这是很多人忽略掉的一个点。6. 关于内核动态加载file_operations拦截机制的延伸联想内核态研究过程中有不少人会尝试通过动态替换file_operations来实现对文件读写的拦截常见于透明加密、安全审计等功能模块。这项技术和hung task检测机制看似不相关但在实际问题处理时往往会纠结在一起。我前面提到过替换file_operations时如果没有处理好并发读写和锁的关系很容易把读写的文件系统操作卡死进而触发hung task。比如在read回调里等待加密/解密模块的任务完成而那个任务又在等待当前文件锁形成交叉等待。对于这种需要动态注入内核函数的场景务必要遵守几条原则替换file_operations前先备份原函数指针保证出错时可以还原。在回调函数内尽量避免长时间持有锁尤其不能持锁等待IO。使用rcu机制保护全局操作表避免多核并发时出现野指针。做完整的并发压力测试覆盖多线程、多进程同时读写同文件的情况。其实不管是hung task检测还是动态拦截底层的核心逻辑相通内核并发环境下任何等待都必须有边界任何锁都必须有顺序任何回调都必须考虑异常退出路径。理解了这些再复杂的题也难不倒你。个人经验更为直接的总结是排查hung task技术和工具只是辅助真正重要的是对操作系统“等待模型”的理解。你在驱动、文件系统或内核模块中写的每一行“等待”代码都要问自己三个问题等什么等到之后如何醒等不到会怎样这三个问题回答清楚了90%的hung task问题在设计阶段就已经被消灭。
返回列表