1. hung task 的含义
hung 本意是 "挂起",hung task 本意即 "任务被挂起,得不到执行"。
但是如果我们对 linux kernel 的任务调度机制有所了解的话,应该会知道 "任务被挂起,得不到执行" 是很正常的事情。
而在 linux kernel 中,hung task 是指长时间处于 D 状态(TASK_UNINTERRUPTIBLE,即 uninterruptible sleep)的进程。
2. 检测 hung task 的机制
linux kernel 检测 hung task 的机制是 khungtaskd。
khungtaskd 是 kthreadd 的子进程。
yudi:/ # ps -A|grep khungtaskd
root 80 2 0 0 watchdog 0 S [khungtaskd]
通过 sysctl 或者 /proc/sys/kernel/hung_task_xxx 节点可以读、写 khungtaskd 配置。
3. khungtaskd
3.1 配置
3.1.1 首先是 hung task 超时时间
缺省的超时时间由宏 DEFAULT_HUNG_TASK_TIMEOUT 配置,默认是 120 秒。
// lib/Kconfig.debug
config DEFAULT_HUNG_TASK_TIMEOUT
int "Default timeout for hung task detection (in seconds)"
depends on DETECT_HUNG_TASK
default 120
help
This option controls the default timeout (in seconds) used
to determine when a task has become non-responsive and should
be considered hung.
It can be adjusted at runtime via the kernel.hung_task_timeout_secs
sysctl or by writing a value to
/proc/sys/kernel/hung_task_timeout_secs.
A timeout of 0 disables the check. The default is two minutes.
Keeping the default should be fine in most cases.
下面是本地 Android 设备的 DEFAULT_HUNG_TASK_TIMEOUT 值,也是默认值 120(单位是秒)。
yudi:/ # zcat /proc/config.gz |grep T_HUNG_TASK_TIMEOUT
CONFIG_DEFAULT_HUNG_TASK_TIMEOUT=120
表示 hungtask 超时时间的变量是 sysctl_hung_task_timeout_secs。
/*
* Zero means infinite timeout - no checking done:
*/
unsigned long __read_mostly sysctl_hung_task_timeout_secs = CONFIG_DEFAULT_HUNG_TASK_TIMEOUT;
sysctl_hung_task_timeout_secs 可以通过 sysctl 或者写 /proc/sys/kernel/hung_task_timeout_secs 节点来改变。
本地 Android 设备的 /proc/sys/kernel/hung_task_timeout_secs 值是 0。
yudi:/ # cat /proc/sys/kernel/hung_task_timeout_secs
0
Android 设备的 /proc/sys/kernel/hung_task_timeout_secs 值为 0,应该是 init.rc 中的默认配置。
// system/core/rootdir/init.rc
97 on init
...
302 write /proc/sys/kernel/panic_on_oops 1
303 write /proc/sys/kernel/hung_task_timeout_secs 0
304 write /proc/cpu/alignment 4
llkd.rc 也会配置 hung_task_timeout_secs 的值,不过是在 khungtask.enable 使能时设置。
本地 Android 设备并未使能 khungtask.enable。
// system/core/llkd/llkd.rc
24 # Configure [khungtaskd]
25 on property:khungtask.enable=true
26 write /proc/sys/kernel/hung_task_timeout_secs ${ro.khungtask.timeout:-720}
27 write /proc/sys/kernel/hung_task_warnings 65535
28 write /proc/sys/kernel/hung_task_check_count 65535
29 write /proc/sys/kernel/hung_task_panic 1
30
31 on property:khungtask.enable=false
32 write /proc/sys/kernel/hung_task_panic 0
yudi:/ # getprop |grep khungtask.enable
[khungtask.enable]: [false]
3.1.2 然后是 check 周期
表示 check 周期的变量是 sysctl_hung_task_check_interval_secs,缺省值是 0。
sysctl_hung_task_check_interval_secs 可以通过 sysctl 或者写 /proc/sys/kernel/hung_task_check_interval_secs 节点来改变。
本地 Android 设备的 /proc/sys/kernel/hung_task_check_interval_secs 值是也 0
yudi:/ # cat /proc/sys/kernel/hung_task_check_interval_secs
0
3.1.3 检查到 hung task 时,是否触发 panic
变量:sysctl_hung_task_panic
缺省值由宏 CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE 设置。
/*
* Should we panic (and reboot, if panic_timeout= is set) when a
* hung task is detected:
*/
unsigned int __read_mostly sysctl_hung_task_panic =
CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE;
本地 Android 设备的 CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE 值为 0。
yudi:/ # zcat /proc/config.gz |grep CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE
CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE=0
sysctl_hung_task_panic 可以由 sysctl 或 /proc/sys/kernel/hung_task_panic 读、写。
本地 Android 设备的 /proc/sys/kernel/hung_task_panic 值也是 0。
yudi:/ # cat /proc/sys/kernel/hung_task_panic
0
3.1.4 hung task 的最大警告次数
变量:sysctl_hung_task_warnings
缺省值是 10。
int __read_mostly sysctl_hung_task_warnings = 10;
sysctl_hung_task_warnings 可以由 sysctl 或 /proc/sys/kernel/hung_task_warnings 读、写。
本地 Android 设备的 /proc/sys/kernel/hung_task_warnings 值也是 10。
yudi:/ # cat /proc/sys/kernel/hung_task_warnings
10
每检测到一次 hung task,就会将 sysctl_hung_task_warnings 值减一,sysctl_hung_task_warnings 为 0 后不再警告。
sysctl_hung_task_warnings 和 sysctl_hung_task_panic 可以同时开启。
3.1.5 hung task 警告时是否打印每个 CPU 上正在运行 task 的调用栈
变量:sysctl_hung_task_all_cpu_backtrace
缺省值是 0.
#ifdef CONFIG_SMP
/*
* Should we dump all CPUs backtraces in a hung task event?
* Defaults to 0, can be changed via sysctl.
*/
unsigned int __read_mostly sysctl_hung_task_all_cpu_backtrace;
#endif /* CONFIG_SMP */
sysctl_hung_task_all_cpu_backtrace 可以由 sysctl 或 /proc/sys/kernel/hung_task_all_cpu_backtrace 读、写。
本地 Android 设备的 /proc/sys/kernel/hung_task_all_cpu_backtrace 值也是 0。
yudi:/ # cat /proc/sys/kernel/hung_task_all_cpu_backtrace
0
注意,sysctl_hung_task_all_cpu_backtrace 要跟 sysctl_hung_task_warnings 结合使用,即只有在 hung task 警告使能时,sysctl_hung_task_all_cpu_backtrace 才有效。
3.2 huang task 检查
huang task 检查的核心是 check_hung_task 方法。
// kernel/hung_task.c
static void check_hung_task(struct task_struct *t, unsigned long timeout)
{
// 目标 task 状态切换次数
unsigned long switch_count = t->nvcsw + t->nivcsw;
/*
* Ensure the task is not frozen.
* Also, skip vfork and any other user process that freezer should skip.
*/
// 忽略被冻结的进程
if (unlikely(t->flags & (PF_FROZEN | PF_FREEZER_SKIP)))
return;
/*
* When a freshly created task is scheduled once, changes its state to
* TASK_UNINTERRUPTIBLE without having ever been switched out once, it
* musn't be checked.
*/
// 如果目标 task 状态切换次数为 0,也忽略(说明该 task 应该是刚创建的)
if (unlikely(!switch_count))
return;
// 如果目标 task 状态切换次数相比上一次 check 时发生了变化,也忽略(说明 task 唤醒过了,不是一直是 D 状态)
if (switch_count != t->last_switch_count) {
t->last_switch_count = switch_count;
t->last_switch_time = jiffies;
return;
}
// time_is_after_jiffies 检查目标时间是否在当前时间之后,
// 这里即表示检查当前是否还没到 hungtask 超时时间,如果没到超时的话,也忽略
if (time_is_after_jiffies(t->last_switch_time + timeout * HZ))
return;
// 到这里就表示目标 task 已经被确定为 hungtask 了!
trace_sched_process_hang(t);
// 如果 sysctl_hung_task_panic 使能了,则将变量 hung_task_show_lock 和 hung_task_call_panic 置为 true。
// 1. 设置变量 hung_task_show_lock 为 true,会在后面(check_hung_task 方法的调用函数中)控制打印所有 task 的持锁信息
// 2. 设置变量 hung_task_call_panic 为 true,会在后面(check_hung_task 方法的调用函数中)控制触发 panic
if (sysctl_hung_task_panic) {
console_verbose(); // 设置向控制台输出日志的等级
hung_task_show_lock = true;
hung_task_call_panic = true;
}
/*
* Ok, the task did not get scheduled for more than 2 minutes,
* complain:
*/
// sysctl_hung_task_warnings 表示 hungtask 警告次数!
// 如果 sysctl_hung_task_warnings 使能了(数值 > 0),则
// 1. 将 sysctl_hung_task_warnings 减 1
// 2. 打印 error log,记录 hungtask 的 task 名、task pid,以及持续了多长时间的 block 状态
// 3. 打印 error log,提示用户可以通过 echo 0 > /proc/sys/kernel/hung_task_timeout_secs 关闭 hungtask 检查
// 4. 设置变量 hung_task_show_lock 值为 true
// 5. 调用 sched_show_task 方法打印 hungtask 任务的状态、内核态调用栈、调度信息(调度策略等)
// 6. 如果 sysctl_hung_task_all_cpu_backtrace 使能,则设置变量 hung_task_show_all_bt 值为 true,这个变量会在后面(check_hung_task 方法的调用函数中)控制打印所有 cpu 上正在执行的任务的调用栈
if (sysctl_hung_task_warnings) {
if (sysctl_hung_task_warnings > 0)
sysctl_hung_task_warnings--;
pr_err("INFO: task %s:%d blocked for more than %ld seconds.\n",
t->comm, t->pid, (jiffies - t->last_switch_time) / HZ);
pr_err(" %s %s %.*s\n",
print_tainted(), init_utsname()->release,
(int)strcspn(init_utsname()->version, " "),
init_utsname()->version);
pr_err("\"echo 0 > /proc/sys/kernel/hung_task_timeout_secs\""
" disables this message.\n");
sched_show_task(t);
hung_task_show_lock = true;
if (sysctl_hung_task_all_cpu_backtrace)
hung_task_show_all_bt = true;
}
touch_nmi_watchdog();
}
下面是一些需要注意的点。
1. time_is_after_jiffies 方法的作用是检查目标时间是否在当前时间之后。
2. sched_show_task 方法的作用是打印目标 task 的状态、内核态调用栈、调度信息(调度策略等)
3. console_verbose 方法设置向控制台输出日志的等级
4. 变量 hung_task_show_lock 控制打印所有 task 的持锁信息;
变量 hung_task_call_panic 控制触发 panic;
变量 hung_task_show_all_bt 控制打印所有 cpu 上正在执行的任务的调用栈。
这些信息都是在 check_hung_task 的调用者 check_hung_uninterruptible_tasks 函数中打印的。
sysctl_hung_task_panic 使能时,会使能 hung_task_show_lock 和 hung_task_call_panic;
sysctl_hung_task_warnings 使能时,会使能 hung_task_show_lock,可能会使能 hung_task_show_all_bt。
5. 为什么每次 check_hung_task 最后都要执行一次 touch_nmi_watchdog ?
check_hung_task 对一个目标 task 进行 hungtask 检查,也算是耗时动作;遍历所有 task 调用 check_hung_task 的话耗时很长,所以每次检查完一个 task 就调用一次 touch_nmi_watchdog 忽略 hardlockup。
类似的还有 lockdep 模块的 debug_show_all_locks 方法,它在遍历所有 task 打印持锁信息时,每次遍历都会执行 touch_nmi_watchdog 忽略 hardlockup 检查,以及执行 touch_all_softlockup_watchdogs 忽略 softlockup 检查。
3.3 打印 hungtask debug 信息
// kernel/hung_task.c
/*
* Check whether a TASK_UNINTERRUPTIBLE does not get woken up for
* a really long time (120 seconds). If that happens, print out
* a warning.
*/
static void check_hung_uninterruptible_tasks(unsigned long timeout)
{
int max_count = sysctl_hung_task_check_count;
unsigned long last_break = jiffies;
struct task_struct *g, *t;
/*
* If the system crashed already then all bets are off,
* do not report extra hung tasks:
*/
if (test_taint(TAINT_DIE) || did_panic)
return;
hung_task_show_lock = false;
rcu_read_lock();
for_each_process_thread(g, t) {
if (!max_count--)
goto unlock;
if (time_after(jiffies, last_break + HUNG_TASK_LOCK_BREAK)) {
if (!rcu_lock_break(g, t))
goto unlock;
last_break = jiffies;
}
/* use "==" to skip the TASK_KILLABLE tasks waiting on NFS */
if (t->state == TASK_UNINTERRUPTIBLE)
check_hung_task(t, timeout);
}
unlock:
rcu_read_unlock();
// 打印所有 task 的持锁信息
if (hung_task_show_lock)
debug_show_all_locks();
// 打印各 CPU 上正在运行 task 的调用栈
if (hung_task_show_all_bt) {
hung_task_show_all_bt = false;
trigger_all_cpu_backtrace();
}
// 触发 panic
if (hung_task_call_panic)
panic("hung_task: blocked tasks");
}
需要注意,lockdep 模块的 debug_show_all_locks 方法会打印所有 task 的持锁信息。
3.4 enhance
大米对 hung task 检查做了 enhance,有机会再专门讲。
yudi:/ # ls -l /proc/sys/hung_task_enh/
total 0
-rw-r--r-- 1 root root 0 2024-07-10 11:10 global_detect_mode
-rw-r--r-- 1 root root 0 2024-07-10 11:10 max_iowait_task_cnt
-rw-r--r-- 1 root root 0 2024-07-10 11:10 max_iowait_timeout_cnt
-rw-r--r-- 1 root root 0 2024-07-10 11:10 per_task_detect_mode
-rw-r--r-- 1 root root 0 2024-07-10 11:10 read_pid