Linux hung_task故障分析:khungtaskd检测源码

第2章:hung_task Detector源码——khungtaskd是怎么发现阻塞任务的

0. 源码基线与本章边界

本文以 Linux v6.1 为源码基线,核心文件是 kernel/hung_task.c。对应上游标签提交为 830b3c68c1fb1e9176028d02ef86f3cf76aa2476。

本章只回答一个问题:

Linux怎样发现一个不可中断等待任务已经长时间没有调度进展?

主线固定为:

1
2
3
4
5
6
hung_task_init()
-> kthread_run(watchdog, ..., "khungtaskd")
-> watchdog()
-> check_hung_uninterruptible_tasks()
-> check_hung_task()
-> sched_show_task()

这里先不追查锁的持有者、I/O设备或驱动中断,那些属于发现异常后的根因分析。

2.1 为什么需要hung_task Detector

D状态本身是正常的:任务等待块设备I/O、Completion或内核锁时,都可能短暂进入不可中断睡眠。真正可疑的是:

1
TASK_UNINTERRUPTIBLE + 长时间没有调度进展

这种故障不会必然让整台机器立即崩溃。其他CPU和任务可能仍在运行,因此需要一个独立观察者周期扫描系统,主动留下任务信息和调用栈。hung_task Detector就是这个观察者。

2.2 hung_task Detector运行在哪里

检测器运行在名为 khungtaskd 的内核线程中。它不是硬中断、软中断或定时器回调,而是一个可以睡眠、醒来并遍历任务的普通内核线程。

1
2
3
4
5
khungtaskd
-> 睡眠一段时间
-> 醒来扫描任务
-> 输出必要诊断
-> 再次睡眠

因此检测存在时间粒度,不会在任务刚进入D状态的瞬间报警。

2.3 hung_task_init()做什么

v6.1的初始化代码可抽出为:

1
2
3
4
5
6
7
8
9
10
static int __init hung_task_init(void)
{
atomic_notifier_chain_register(&panic_notifier_list, &panic_block);
pm_notifier(hungtask_pm_notify, 0);

watchdog_task = kthread_run(watchdog, NULL, "khungtaskd");
hung_task_sysctl_init();
return 0;
}
subsys_initcall(hung_task_init);

它完成三件主要事情:注册panic通知、注册电源管理通知、创建khungtaskd并注册相关sysctl。subsys_initcall()把它接入内核启动初始化序列。

2.4 khungtaskd是什么

kthread_run()创建并唤醒一个内核线程:

1
2
线程显示名称:khungtaskd
线程入口函数:watchdog()

不要把“线程名”和“C函数名”误认为两个执行阶段。khungtaskd是用户在进程列表中看到的名字,watchdog()才是它实际执行的函数。

2.5 watchdog()主循环

核心代码可以缩减为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int watchdog(void *dummy)
{
unsigned long hung_last_checked = jiffies;

for (;;) {
unsigned long timeout = sysctl_hung_task_timeout_secs;
unsigned long interval = sysctl_hung_task_check_interval_secs;
long t;

if (interval == 0)
interval = timeout;
interval = min_t(unsigned long, interval, timeout);
t = hung_timeout_jiffies(hung_last_checked, interval);

if (t <= 0) {
if (!atomic_xchg(&reset_hung_task, 0) &&
!hung_detector_suspended)
check_hung_uninterruptible_tasks(timeout);
hung_last_checked = jiffies;
continue;
}
schedule_timeout_interruptible(t);
}
}

逻辑就是:计算距离下一次检查还剩多久;到期后执行扫描,否则让当前内核线程以可中断方式睡眠。

2.6 khungtaskd多久检查一次

实际检查间隔不是简单等于某一个参数,而是:

1
interval == 0 ? timeout : min(interval, timeout)

所以:

  • 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
cat /proc/sys/kernel/hung_task_timeout_secs

它是策略阈值,不是任务等待对象自身的超时参数,也不会替任务终止等待。

2.8 hung_task_check_interval_secs

该参数控制扫描频率:

1
cat /proc/sys/kernel/hung_task_check_interval_secs

例如timeout=120、interval=10,表示检测线程大约每10秒扫描一次,但任务需要满足至少120秒无进展才会被报告。

1
检查频率 != hung判定阈值

2.9 为什么timeout=0表示关闭

hung_timeout_jiffies()明确处理了0:

1
2
return timeout ? last_checked - jiffies + timeout * HZ :
MAX_SCHEDULE_TIMEOUT;

0被解释为无限等待,khungtaskd不会到达正常扫描时刻,因此关闭检测。修改sysctl时处理函数会调用wake_up_process(watchdog_task),让线程立即重新读取配置,而不是继续按旧值睡眠。

2.10 check_hung_uninterruptible_tasks()做什么

该函数承担一次完整扫描:

1
2
3
4
5
确认系统未进入panic/die状态
-> RCU保护下遍历进程和线程
-> 筛选目标状态
-> 对候选任务调用check_hung_task()
-> 必要时显示锁、所有CPU栈或panic

它负责“找候选者”;真正判断某一个任务是否超时的是check_hung_task()。

2.11 如何遍历系统Task

v6.1使用:

1
2
3
4
5
rcu_read_lock();
for_each_process_thread(g, t) {
...
}
rcu_read_unlock();

g代表线程组首领,t代表其中的具体线程。扫描受sysctl_hung_task_check_count限制,避免一次检查无限扩张。

遍历过程中还会周期调用rcu_lock_break():暂时退出RCU读侧临界区,执行cond_resched(),再重新进入。这避免扫描大量任务时无限延长RCU宽限期,也避免khungtaskd长时间霸占CPU。

2.12 为什么重点检查D状态

状态筛选的真实代码是:

1
2
3
4
5
state = READ_ONCE(t->__state);
if ((state & TASK_UNINTERRUPTIBLE) &&
!(state & TASK_WAKEKILL) &&
!(state & TASK_NOLOAD))
check_hung_task(t, timeout);

它不是简单判断state == TASK_UNINTERRUPTIBLE,而是按状态位筛选:必须包含TASK_UNINTERRUPTIBLE,同时排除可被致命信号唤醒的TASK_WAKEKILL和真正空闲的TASK_NOLOAD。因此TASK_KILLABLE及idle类任务不会按普通D状态处理。

2.13 check_hung_task()做什么

这个函数的职责可以写成一张What Card:

1
2
3
4
5
6
symbol: check_hung_task
input: 候选task_struct、超时秒数
reads: __state、nvcsw、nivcsw、last_switch_count、last_switch_time
updates: last_switch_count、last_switch_time、诊断控制标志
output: 无返回值;必要时输出事件与日志
not_responsible_for: 分析任务正在等待的真实资源和根因

其核心分支是“当前切换计数是否与上次记录相同”以及“距上次观察到进展是否已经超过阈值”。

2.14 Linux怎么判断Task有没有进展

检测器无法理解每个文件系统、驱动或网络协议的业务进度。它选择通用而廉价的代理指标:任务的上下文切换计数。

1
unsigned long switch_count = t->nvcsw + t->nivcsw;

只要任务发生过新的上下文切换,这个总数就会改变,说明它在两次观察之间至少获得过某种调度活动。

2.15 nvcsw是什么

nvcsw是voluntary context switches计数,即任务主动放弃CPU的次数。典型原因是等待条件、等待锁、等待I/O或主动调用会调度出去的接口。

用户空间可以在/proc/<pid>/status看到对应的:

1
voluntary_ctxt_switches

2.16 nivcsw是什么

nivcsw是involuntary context switches计数,即任务不是主动睡眠,而是被调度器换下CPU的次数,例如被抢占。

用户空间对应:

1
nonvoluntary_ctxt_switches

2.17 voluntary与involuntary context switch区别

1
2
主动切换:任务自己遇到等待点,主动让出CPU
被动切换:任务本来仍可运行,但被调度器抢占或换下

hung_task Detector不需要区分是哪一种进展,只关心二者总和有没有增加。

2.18 为什么context switch计数可以判断Task是否有进展

这个指标具备三项优势:所有任务都有、读取便宜、与具体子系统无关。只要任务重新参与调度,计数通常就会变化。

但它只是近似指标:计数变化证明发生过调度活动,不证明业务操作已经取得有效进展;计数不变也只说明长期没有被再次调度,不能直接证明死锁。

2.19 switch_count为什么需要缓存

task_struct保存两项检测基线:

1
2
last_switch_count:上次观察到的切换总数
last_switch_time :最近一次发现切换计数变化的jiffies

代码为:

1
2
3
4
5
if (switch_count != t->last_switch_count) {
t->last_switch_count = switch_count;
t->last_switch_time = jiffies;
return;
}

没有上一轮基线,就无法区分“刚刚睡下”与“已经很久没有动”。

2.20 第一次扫描为什么不会马上报警

第一次遇到一个已有切换记录的候选任务时,当前switch_count通常不同于缓存值,于是检测器更新基线后返回。下一轮扫描如果计数仍未变化,才继续检查last_switch_time + timeout * HZ是否已经到期。

还有一个保护:switch_count == 0的全新任务直接跳过,避免把从未正常切出过的新任务误判为hung。

2.21 blocked for more than xxx seconds怎么来的

超时条件是:

1
2
if (time_is_after_jiffies(t->last_switch_time + timeout * HZ))
return;

一旦到期,日志中的秒数来自:

1
(jiffies - t->last_switch_time) / HZ

所以它表示“距检测器上次观察到该任务切换计数变化已经过去多久”,不是对等待函数入口进行纳秒级记录。实际报告还会受扫描间隔和调度延迟影响。

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
panic("hung_task: blocked tasks");

测试系统可借此立即冻结现场并配合kdump获取转储;生产环境要谨慎,因为它会把局部阻塞升级为整机panic。

2.24 hung_task_all_cpu_backtrace

SMP系统启用该选项后,只要确实输出了hung task警告,就设置hung_task_show_all_bt。扫描完成后调用:

1
trigger_all_cpu_backtrace();

它能补充其他CPU正在执行什么,有机会看到锁持有者或事件生产者。但这仍不是自动建立依赖链,只是增加现场证据。

2.25 打印Task信息和Call Trace

报告路径的关键动作是:

1
2
3
4
5
trace_sched_process_hang(t)
-> pr_err()打印任务名、PID、阻塞秒数和内核版本
-> sched_show_task(t)
-> 必要时debug_show_all_locks()
-> 必要时所有CPU backtrace

sched_show_task()打印任务状态和栈等调度信息。Tracepoint还允许ftrace/perf等跟踪工具订阅hung事件。

2.26 为什么Call Trace只是现场

假设调用栈显示:

1
2
3
schedule
-> wait_for_completion
-> driver_transfer

这只能证明任务正在等待Completion。真正根因可能是IRQ没有到达、错误路径遗漏complete()、硬件无响应或负责完成事件的工作线程被另一把锁阻塞。

1
2
Call Trace = Waiter当前停在哪里
Root Cause = 谁应该改变等待条件、为什么没有改变

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
3
4
5
6
7
8
9
10
11
12
13
hung_task_init()
└─ kthread_run(watchdog, NULL, "khungtaskd")
└─ watchdog()
├─ hung_timeout_jiffies()
├─ schedule_timeout_interruptible()
└─ check_hung_uninterruptible_tasks()
├─ for_each_process_thread()
├─ 状态位筛选
└─ check_hung_task()
├─ 比较switch_count
├─ 比较last_switch_time
├─ sched_show_task()
└─ 设置锁栈、全CPU栈或panic动作

这一主线覆盖了初始化、周期调度、候选筛选、单任务判定和报告五个层次。

2.31 本章实验

先确认运行内核启用了hung task检测,再做只读观察:

1
2
3
4
5
6
ps -eLo pid,tid,comm,state | grep khungtaskd

cat /proc/sys/kernel/hung_task_timeout_secs
cat /proc/sys/kernel/hung_task_check_interval_secs
cat /proc/sys/kernel/hung_task_warnings
cat /proc/sys/kernel/hung_task_panic

观察某个普通进程的切换计数:

1
grep -E 'voluntary_ctxt|nonvoluntary_ctxt' /proc/<pid>/status

预测:睡眠频繁的任务通常增加主动切换计数;持续计算并被抢占的任务更容易增加非主动切换计数。

仅在可恢复的开发板或虚拟机中缩短阈值:

1
sudo sysctl -w kernel.hung_task_timeout_secs=10

不要在生产机上为了实验随意制造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
3
4
5
周期观察任务状态
+
用上下文切换计数近似衡量调度进展
+
超过策略阈值后保留诊断现场

它能告诉我们“谁长时间没有动”,但不能自动回答“谁本来应该唤醒它,以及为什么没有唤醒”。

2.35 学完检查题

  1. khungtaskd是线程名还是函数名?它的入口函数是什么?
  2. hung_task_init()通过什么接口创建检测线程?
  3. watchdog()为什么可以调用schedule_timeout_interruptible()?
  4. timeout与check interval分别控制什么?
  5. check_interval=0和timeout=0分别表示什么?
  6. 为什么实际interval不能大于timeout?
  7. check_hung_uninterruptible_tasks()与check_hung_task()如何分工?
  8. 为什么遍历任务时需要RCU?为什么还要周期性rcu_lock_break()?
  9. v6.1的状态筛选为什么排除TASK_WAKEKILL和TASK_NOLOAD?
  10. nvcsw与nivcsw分别表示什么?
  11. 为什么二者之和只能近似代表进展?
  12. last_switch_count与last_switch_time各自保存什么?
  13. 第一次扫描到一个刚进入D状态的任务为什么通常不会报警?
  14. 为什么switch_count == 0的任务会被跳过?
  15. 日志中的阻塞秒数使用什么公式计算?
  16. hung_task_warnings=0与timeout=0的效果是否相同?
  17. hung_task_panic适合什么场景,又有什么风险?
  18. all CPU backtrace为什么可能帮助寻找Owner?
  19. 为什么Call Trace只是根因分析入口?
  20. 请不看本文画出从hung_task_init()到sched_show_task()的完整主链。

下一章将从“检测器如何发现异常”转向“任务究竟通过什么机制睡下去”,统一分析wait queue、Completion、mutex、rwsem与I/O等待,并建立Waiter到Owner/Producer的依赖链。


Linux hung_task故障分析:khungtaskd检测源码
https://cj0510.github.io/2026/10/09/Linux Kernel/Debug/Linux-hung-task故障分析-第2章-khungtaskd检测源码/
作者
CJ1018
发布于
2026年10月9日
许可协议
CC BY-NC-SA 4.0