Linux hung_task故障分析:khungtaskd检测源码
第2章:hung_task Detector源码——khungtaskd是怎么发现阻塞任务的
0. 源码基线与本章边界
本文以 Linux v6.1 为源码基线,核心文件是 kernel/hung_task.c。对应上游标签提交为 830b3c68c1fb1e9176028d02ef86f3cf76aa2476。
本章只回答一个问题:
Linux怎样发现一个不可中断等待任务已经长时间没有调度进展?
主线固定为:
1 | |
这里先不追查锁的持有者、I/O设备或驱动中断,那些属于发现异常后的根因分析。
2.1 为什么需要hung_task Detector
D状态本身是正常的:任务等待块设备I/O、Completion或内核锁时,都可能短暂进入不可中断睡眠。真正可疑的是:
1 | |
这种故障不会必然让整台机器立即崩溃。其他CPU和任务可能仍在运行,因此需要一个独立观察者周期扫描系统,主动留下任务信息和调用栈。hung_task Detector就是这个观察者。
2.2 hung_task Detector运行在哪里
检测器运行在名为 khungtaskd 的内核线程中。它不是硬中断、软中断或定时器回调,而是一个可以睡眠、醒来并遍历任务的普通内核线程。
1 | |
因此检测存在时间粒度,不会在任务刚进入D状态的瞬间报警。
2.3 hung_task_init()做什么
v6.1的初始化代码可抽出为:
1 | |
它完成三件主要事情:注册panic通知、注册电源管理通知、创建khungtaskd并注册相关sysctl。subsys_initcall()把它接入内核启动初始化序列。
2.4 khungtaskd是什么
kthread_run()创建并唤醒一个内核线程:
1 | |
不要把“线程名”和“C函数名”误认为两个执行阶段。khungtaskd是用户在进程列表中看到的名字,watchdog()才是它实际执行的函数。
2.5 watchdog()主循环
核心代码可以缩减为:
1 | |
逻辑就是:计算距离下一次检查还剩多久;到期后执行扫描,否则让当前内核线程以可中断方式睡眠。
2.6 khungtaskd多久检查一次
实际检查间隔不是简单等于某一个参数,而是:
1 | |
所以:
check_interval=0时,用timeout作为检查间隔;check_interval>timeout时,也会被限制为timeout;check_interval<timeout时,可以更频繁地扫描,但仍按timeout判断任务是否超时。
2.7 hung_task_timeout_secs
sysctl_hung_task_timeout_secs表示“多久没有观察到任务调度进展才算可疑”的阈值,默认值来自CONFIG_DEFAULT_HUNG_TASK_TIMEOUT。
用户空间接口通常是:
1 | |
它是策略阈值,不是任务等待对象自身的超时参数,也不会替任务终止等待。
2.8 hung_task_check_interval_secs
该参数控制扫描频率:
1 | |
例如timeout=120、interval=10,表示检测线程大约每10秒扫描一次,但任务需要满足至少120秒无进展才会被报告。
1 | |
2.9 为什么timeout=0表示关闭
hung_timeout_jiffies()明确处理了0:
1 | |
0被解释为无限等待,khungtaskd不会到达正常扫描时刻,因此关闭检测。修改sysctl时处理函数会调用wake_up_process(watchdog_task),让线程立即重新读取配置,而不是继续按旧值睡眠。
2.10 check_hung_uninterruptible_tasks()做什么
该函数承担一次完整扫描:
1 | |
它负责“找候选者”;真正判断某一个任务是否超时的是check_hung_task()。
2.11 如何遍历系统Task
v6.1使用:
1 | |
g代表线程组首领,t代表其中的具体线程。扫描受sysctl_hung_task_check_count限制,避免一次检查无限扩张。
遍历过程中还会周期调用rcu_lock_break():暂时退出RCU读侧临界区,执行cond_resched(),再重新进入。这避免扫描大量任务时无限延长RCU宽限期,也避免khungtaskd长时间霸占CPU。
2.12 为什么重点检查D状态
状态筛选的真实代码是:
1 | |
它不是简单判断state == TASK_UNINTERRUPTIBLE,而是按状态位筛选:必须包含TASK_UNINTERRUPTIBLE,同时排除可被致命信号唤醒的TASK_WAKEKILL和真正空闲的TASK_NOLOAD。因此TASK_KILLABLE及idle类任务不会按普通D状态处理。
2.13 check_hung_task()做什么
这个函数的职责可以写成一张What Card:
1 | |
其核心分支是“当前切换计数是否与上次记录相同”以及“距上次观察到进展是否已经超过阈值”。
2.14 Linux怎么判断Task有没有进展
检测器无法理解每个文件系统、驱动或网络协议的业务进度。它选择通用而廉价的代理指标:任务的上下文切换计数。
1 | |
只要任务发生过新的上下文切换,这个总数就会改变,说明它在两次观察之间至少获得过某种调度活动。
2.15 nvcsw是什么
nvcsw是voluntary context switches计数,即任务主动放弃CPU的次数。典型原因是等待条件、等待锁、等待I/O或主动调用会调度出去的接口。
用户空间可以在/proc/<pid>/status看到对应的:
1 | |
2.16 nivcsw是什么
nivcsw是involuntary context switches计数,即任务不是主动睡眠,而是被调度器换下CPU的次数,例如被抢占。
用户空间对应:
1 | |
2.17 voluntary与involuntary context switch区别
1 | |
hung_task Detector不需要区分是哪一种进展,只关心二者总和有没有增加。
2.18 为什么context switch计数可以判断Task是否有进展
这个指标具备三项优势:所有任务都有、读取便宜、与具体子系统无关。只要任务重新参与调度,计数通常就会变化。
但它只是近似指标:计数变化证明发生过调度活动,不证明业务操作已经取得有效进展;计数不变也只说明长期没有被再次调度,不能直接证明死锁。
2.19 switch_count为什么需要缓存
task_struct保存两项检测基线:
1 | |
代码为:
1 | |
没有上一轮基线,就无法区分“刚刚睡下”与“已经很久没有动”。
2.20 第一次扫描为什么不会马上报警
第一次遇到一个已有切换记录的候选任务时,当前switch_count通常不同于缓存值,于是检测器更新基线后返回。下一轮扫描如果计数仍未变化,才继续检查last_switch_time + timeout * HZ是否已经到期。
还有一个保护:switch_count == 0的全新任务直接跳过,避免把从未正常切出过的新任务误判为hung。
2.21 blocked for more than xxx seconds怎么来的
超时条件是:
1 | |
一旦到期,日志中的秒数来自:
1 | |
所以它表示“距检测器上次观察到该任务切换计数变化已经过去多久”,不是对等待函数入口进行纳秒级记录。实际报告还会受扫描间隔和调度延迟影响。
2.22 hung_task_warnings
sysctl_hung_task_warnings是日志预算,v6.1默认初值为10。值大于0时,每打印一条hung task警告就递减;变成0后停止继续打印,避免永久故障造成日志风暴。
该sysctl允许最小值-1。由于代码只在大于0时递减,-1保持非零,可用于不限制警告次数。它限制的是警告输出,不等于关闭扫描。
2.23 hung_task_panic
启用hung_task_panic后,发现hung任务会先提高console详细度,并记录需要显示锁和触发panic。扫描结束后统一执行:
1 | |
测试系统可借此立即冻结现场并配合kdump获取转储;生产环境要谨慎,因为它会把局部阻塞升级为整机panic。
2.24 hung_task_all_cpu_backtrace
SMP系统启用该选项后,只要确实输出了hung task警告,就设置hung_task_show_all_bt。扫描完成后调用:
1 | |
它能补充其他CPU正在执行什么,有机会看到锁持有者或事件生产者。但这仍不是自动建立依赖链,只是增加现场证据。
2.25 打印Task信息和Call Trace
报告路径的关键动作是:
1 | |
sched_show_task()打印任务状态和栈等调度信息。Tracepoint还允许ftrace/perf等跟踪工具订阅hung事件。
2.26 为什么Call Trace只是现场
假设调用栈显示:
1 | |
这只能证明任务正在等待Completion。真正根因可能是IRQ没有到达、错误路径遗漏complete()、硬件无响应或负责完成事件的工作线程被另一把锁阻塞。
1 | |
2.27 hung_task Detector能检测什么
它擅长发现:
- 长时间处于不可中断等待的任务;
- I/O、锁、Completion或驱动路径上的长期无调度进展;
- 系统尚能运行但局部任务链已经卡住的故障。
它提供的是“异常候选任务和现场入口”。
2.28 hung_task Detector不能检测什么
它不能直接回答:
- 哪个mutex owner没有释放锁;
- 哪个中断丢失;
- 哪个设备寄存器或固件状态异常;
- 哪个远端服务器没有响应;
- 任务是否形成了严格意义上的环形死锁。
一直处于R状态的busy loop也不属于这条D状态检测主线;soft lockup、hard lockup、RCU stall分别有自己的检测机制。
2.29 False Positive与Long Latency问题
hung_task警告是启发式告警,不是deadlock proof。极慢存储、长时间NFS响应、设备调试暂停等场景,即使最终能恢复,也可能超过阈值。
阈值应该结合系统业务时延、最坏I/O延迟和故障恢复策略设置。阈值太短会制造噪声,太长又会降低故障发现速度。
2.30 本章源码阅读主线
建议不要从文件第一行逐行阅读,而是按因果顺序追踪:
1 | |
这一主线覆盖了初始化、周期调度、候选筛选、单任务判定和报告五个层次。
2.31 本章实验
先确认运行内核启用了hung task检测,再做只读观察:
1 | |
观察某个普通进程的切换计数:
1 | |
预测:睡眠频繁的任务通常增加主动切换计数;持续计算并被抢占的任务更容易增加非主动切换计数。
仅在可恢复的开发板或虚拟机中缩短阈值:
1 | |
不要在生产机上为了实验随意制造D状态、启用panic或无限打印警告。实验后恢复原配置,并确认运行内核版本和本文v6.1源码是否一致。
2.32 工程与性能意义
周期扫描相比给每个D状态任务单独创建定时器更集中、简单,开销也更容易控制。与此同时,全系统任务遍历、打印大量栈和所有CPU回溯都不是免费的,因此存在check_count、检查间隔和warning budget等控制手段。
工程上应把hung事件当作抓取更多证据的触发点,例如冻结ftrace、保存块层和中断状态、采集锁依赖信息,而不是看到第一条栈就修改等待者代码。
2.33 常见误区
误区1:khungtaskd是定时器或中断
它是kthread_run()创建的内核线程,入口为watchdog()。
误区2:所有D状态都会立即报警
检测器会周期扫描,并要求切换计数长期不变且超过timeout。
误区3:timeout就是扫描间隔
timeout是无进展阈值,check interval是扫描频率,两者在watchdog()中共同决定行为。
误区4:check_interval=0关闭扫描
在v6.1中它表示使用timeout作为检查间隔;真正关闭检测的是timeout=0。
误区5:扫描所有含TASK_UNINTERRUPTIBLE的任务
源码还排除了TASK_WAKEKILL和TASK_NOLOAD,并在单任务检查中跳过冻结任务和零切换计数的新任务。
误区6:switch count变化代表业务完成
它只代表发生过调度活动,是“进展”的通用近似值。
误区7:日志秒数就是精确D状态持续时间
它基于last_switch_time和jiffies计算,还受扫描周期影响。
误区8:hung_task直接定位根因
它负责检测并打印现场,根因仍要沿Wait Object、Owner和Producer继续追踪。
2.34 博客总结
Linux v6.1的hung_task Detector由hung_task_init()创建khungtaskd内核线程,线程入口watchdog()按照timeout和check interval周期醒来。check_hung_uninterruptible_tasks()在RCU保护下遍历系统进程与线程,筛选包含TASK_UNINTERRUPTIBLE且不属于killable、idle类的候选任务;check_hung_task()再把nvcsw + nivcsw与任务保存的last_switch_count比较。如果计数发生变化,就更新last_switch_time,视为最近仍有调度进展;如果计数长期不变并超过timeout,内核便输出“blocked for more than … seconds”、任务信息和调用栈,并根据配置追加锁信息、所有CPU回溯或触发panic。
整个机制的本质是:
1 | |
它能告诉我们“谁长时间没有动”,但不能自动回答“谁本来应该唤醒它,以及为什么没有唤醒”。
2.35 学完检查题
khungtaskd是线程名还是函数名?它的入口函数是什么?hung_task_init()通过什么接口创建检测线程?watchdog()为什么可以调用schedule_timeout_interruptible()?- timeout与check interval分别控制什么?
check_interval=0和timeout=0分别表示什么?- 为什么实际interval不能大于timeout?
check_hung_uninterruptible_tasks()与check_hung_task()如何分工?- 为什么遍历任务时需要RCU?为什么还要周期性
rcu_lock_break()? - v6.1的状态筛选为什么排除
TASK_WAKEKILL和TASK_NOLOAD? nvcsw与nivcsw分别表示什么?- 为什么二者之和只能近似代表进展?
last_switch_count与last_switch_time各自保存什么?- 第一次扫描到一个刚进入D状态的任务为什么通常不会报警?
- 为什么
switch_count == 0的任务会被跳过? - 日志中的阻塞秒数使用什么公式计算?
hung_task_warnings=0与timeout=0的效果是否相同?hung_task_panic适合什么场景,又有什么风险?- all CPU backtrace为什么可能帮助寻找Owner?
- 为什么Call Trace只是根因分析入口?
- 请不看本文画出从
hung_task_init()到sched_show_task()的完整主链。
下一章将从“检测器如何发现异常”转向“任务究竟通过什么机制睡下去”,统一分析wait queue、Completion、mutex、rwsem与I/O等待,并建立Waiter到Owner/Producer的依赖链。