声明
本文著作权非笔者独有,
xiaoguang.hu
享有共同著作权。
本文章节7. hung task 问题的常见应对措施
由xiaoguang.hu
贡献。
文章目录
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 分析背景
本文分析基于 linux-4.14.132
内核代码分析,运行环境 Ubuntu 16.04.4 LTS
+ QEMU
+ ARM vexpress-a9
,rootfs
基于 ubuntu-base-16.04-core-armhf.tar.gz
制作。
3. hung task 机制分析
3.1 什么是 hung task ?
内核监测 TASK_UNINTERRUPTIBLE 状态
的进程(top 显示为 D 态
),如果超过 sysctl_hung_task_timeout_secs
设定的秒数,TASK_UNINTERRUPTIBLE 状态
的进程期间没有被唤醒调度,则内核日志会报警该进程为 hung task
,如果开启了 hung task panic
,还会产生内核 panic
。sysctl_hung_task_timeout_secs
默认配置为 120 秒
,用户空间可以通过 /proc/sys/kernel/hung_task_timeout_secs
进行调整;hung task
是否产生内核 panic
,用户空间可通过 /proc/sys/kernel/hung_task_panic
查看。
3.2 hung task 例子
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/kthread.h>
#include <linux/delay.h>
static struct task_struct *hung_task;
static int hung_task_fn(void *ignored)
{
int ret = 0;
while (!kthread_should_stop())
msleep((CONFIG_DEFAULT_HUNG_TASK_TIMEOUT + 8) * 1000);
return ret;
}
static int __init hung_task_demo_init(void)
{
int ret = 0;
hung_task = kthread_run(hung_task_fn, NULL, "hung_task_demo");
if (IS_ERR(hung_task)) {
ret = PTR_ERR(hung_task);
printk(KERN_ERR "%s: Failed to create kernel thread, ret = [%d]\n", __func__, ret);
}
printk(KERN_INFO "hung task example module loaded.\n");
return ret;
}
static void __exit hung_task_demo_exit(void)
{
if (hung_task) {
kthread_stop(hung_task);
hung_task = NULL;
}
printk(KERN_INFO "hung task example module exited.\n");
}
module_init(hung_task_demo_init);
module_exit(hung_task_demo_exit);
MODULE_LICENSE("GPL");
其中,msleep()
将内核线程置于 TASK_UNINTERRUPTIBLE 状态
长达 CONFIG_DEFAULT_HUNG_TASK_TIMEOUT + 8
秒,在默认 hung task
超时时间配置下,系统将此内核线程鉴定为 hung task
。完整代码见 这里 或 这里 。如果要在内核代码之外构建测试模块(out-of-tree module
),可参考博文 Linux: 编写一个简单的内核模块。
编译 hung task 测试代码,首先开启配置项:
CONFIG_DEBUG_KERNEL=y
CONFIG_DETECT_HUNG_TASK=y
CONFIG_SAMPLE_HUNG_TASK=m
然后运行下列指令:
cd linux-4.14.132
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- -j8 O=output
将在内核目录生成测试模块 output/samples/hung_task/hung_task_example.ko
。将该文件安装到根文件系统:
sudo mount rootfs.img temp
cd linux-4.14.132
sudo make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- O=output INSTALL_MOD_PATH=/path/to/temp modules_install
cd -
sudo umount temp
用QEMU启动内核和根系统:
sudo qemu-system-arm \
-M vexpress-a9 \
-smp 4 \
-m 512M \
-kernel /path/to/zImage \
-dtb /path/to/vexpress-v2p-ca9.dtb \
-nographic \
-append "root=/dev/mmcblk0 rw rootfstype=ext4 console=ttyAMA0" \
-sd rootfs.img
系统启动完成后,进入特权用户,运行:
# insmod /lib/modules/4.14.132/kernel/samples/hung_task/hung_task_example.ko
# ps -ef | grep hung
root 282 2 0 13:43 ? 00:00:00 [khungtaskd]
root 932 2 0 13:44 ? 00:00:00 [hung_task_demo]
我们观察到,用于监测 hung task
的内核线程 [khungtaskd]
,和测试模块创建的内核线程 [hung_task_demo]
。测试模块加载一段时间后,我们看到了 hung task
的内核日志:
[ 156.562951] hung task example module loaded.
[ 1285.085942] INFO: task hung_task_demo:935 blocked for more than 120 seconds.
[ 1285.091698] Not tainted 4.14.132 #32
[ 1285.092283] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
[ 1285.094722] hung_task_demo D 0 935 2 0x00000000
[ 1285.116899] [<806f3be8>] (__schedule) from [<806f4098>] (schedule+0x50/0xa8)
[ 1285.126001] [<806f4098>] (schedule) from [<806f7a84>] (schedule_timeout+0x190/0x2ac)
[ 1285.126103] [<806f7a84>] (schedule_timeout) from [<80187dfc>] (msleep+0x3c/0x48)
[ 1285.128376] [<80187dfc>] (msleep) from [<7f00001c>] (hung_task_fn+0x1c/0x34 [hung_task_example])
[ 1285.134498] [<7f00001c>] (hung_task_fn [hung_task_example]) from [<80143ac8>] (kthread+0x144/0x174)
[ 1285.134731] [<80143ac8>] (kthread) from [<80107ee8>] (ret_from_fork+0x14/0x2c)
3.3 hung task 检测机制实现
static int __init hung_task_init(void)
{
atomic_notifier_chain_register(&panic_notifier_list, &panic_block);
/* 创建用来监视 hung task 的内核线程 "khungtaskd" */
watchdog_task = kthread_run(watchdog, NULL, "khungtaskd");
return 0;
}
static int watchdog(void *dummy)
{
unsigned long hung_last_checked = jiffies;
set_user_nice(current, 0);
for ( ; ; ) {
unsigned long timeout = sysctl_hung_task_timeout_secs;
long t = hung_timeout_jiffies(hung_last_checked, timeout);
if (t <= 0) { /* hung task 检测超时时间到了,检查 TASK_UNINTERRUPTIBLE 的进程是否有 hung 起的 */
if (!atomic_xchg(&reset_hung_task, 0))
check_hung_uninterruptible_tasks(timeout);
hung_last_checked = jiffies; /* 上次进行 hung task 检测的时间点 */
continue;
}
schedule_timeout_interruptible(t); /* 调度出去一段时间后再来做 hung task 检查 */
}
return 0;
}
static void check_hung_uninterruptible_tasks(unsigned long timeout)
{
/* 最多检测的进程数目:
* /proc/sys/kernel/hung_task_check_count
*/
int max_count = sysctl_hung_task_check_count;
...
for_each_process_thread(g, t) {
if (!max_count--) /* 已经达到了限定的进程检测数目 */
goto unlock;
...
if (t->state == TASK_UNINTERRUPTIBLE) /* 仅检查 TASK_UNINTERRUPTIBLE 状态的进程 */
check_hung_task(t, timeout); /* 看进程是否成为了 hung task */
}
unlock:
...
if (hung_task_show_lock)
debug_show_all_locks();
if (hung_task_call_panic) { /* hung task 会导致内核 panic */
trigger_all_cpu_backtrace();
panic("hung_task: blocked tasks");
}
}
static void check_hung_task(struct task_struct *t, unsigned long timeout)
{
unsigned long switch_count = t->nvcsw + t->nivcsw; /* 进程诞生以来,总共发生切换次数 */
if (switch_count != t->last_switch_count) { /* 上次 hung 检查过后,又发生过了切换,进程 @t 不是 hung task */
t->last_switch_count = switch_count;
return;
}
/*
* 上次 hung 检查过后 @timeout 时间之内,
* 进程 @t 再无发生调度切花,表明进程 @t 已经 hung 了。
*/
/*
* 初始值为 CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE (0),
* 可通过 /proc/sys/kernel/hung_task_panic 修改。
*/
if (sysctl_hung_task_panic) {
console_verbose();
hung_task_show_lock = true;
hung_task_call_panic = true;
}
/*
* sysctl_hung_task_warnings 为 hung task 最多告警次数,默认为 10 次。
* 可通过 /proc/sys/kernel/hung_task_warnings 修改。
*/
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, timeout);
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;
}
touch_nmi_watchdog();
}
4. hung task 机制的不足
由于 khungtaskd
按一定的周期检测 hung task
,这意味它可能漏掉超过 sysctl_hung_task_timeout_secs
时间没有发生调度、但在 khungtaskd
休眠期间发生过调度的进程,这种情况是经常发生的。前面测试例子中的日志,恰好可以看到这一点:模块在时间点 156.562951
加载,内核线程 hung_task_demo
也将在该时间点附近启动,而直到时间点 1285.085942
,才报告了一次 hung task
,中间相隔整整大约 1128 秒
,远超过配置值 sysctl_hung_task_timeout_secs
的 12 秒。像这种撞大运的方式,有点靠天收的意思,经常会很长时间都监测不到一次异常。
5. hung task 机制的改善?
调小 sysctl_hung_task_timeout_secs
的值,缩短扫描周期,看起来是一个可能的方法。但其实不大现实,我们看扫描的代码片段:
static void check_hung_uninterruptible_tasks(unsigned long timeout)
{
rcu_read_lock();
for_each_process_thread(g, t) {
...
}
unlock:
rcu_read_unlock();
...
}
对于这样一个遍历系统中进程的冗长循环,期间还持有 rcu 锁,频次过高进行这些操作显然是不合适的。对于 hung task
检测机制自身的改进,社区好像没什么动力。内核有更多的挂起任务检测机制,如 soft lockup,hard lockup
,将会是更好的选择。事实上,hung task 检测功能是从 soft lockup 功能中解耦分离出来的,专门用来监测 TASK_UNINTERRUPTIBLE
状态的进程。
6. hung task 机制用户空间接口
/proc/sys/kernel/hung_task_timeout_secs # 判定D态进程为 hung task 的阈值,单位为秒
/proc/sys/kernel/hung_task_check_count # hung task 检测进程的最大数,包括非D态进程
/proc/sys/kernel/hung_task_panic # hung task 是否导致内核 panic
/proc/sys/kernel/hung_task_warnings # hung task 报警信息最大次数
7. hung task 问题的常见应对措施
khungtaskd
会输出长时间处于TASK_UNINTERRUPTIBLE
状态(常说的D
态)进程的调用栈
。在通过/proc/sys/kernel/hung_task_panic
开启了hung task panic
的情形下,如果同时启用了 pstore ,则在pstore
转储信息也可以发现D 态进程的调用栈
信息。- 对于
驱动中长时间处于 D 态
的情形,一般是由于等待的资源无法获取导致,需要对具体业务场景进行分析;而对于资源获取时间不可预测的情形,可以让进程进入TASK_INTERRUPTIBLE
态、或 通过*_timeout()
调用进行优化,让进程时不时的被唤醒,查询资源状态,不至于一直睡眠,从而引发 hung task 问题。 - 另一类由于
死锁导致的 hung task
,需要分析代码找到死锁的根源,可以根据内核日志中输出的所有D
态进程栈 和 所有运行态进程栈进行综合分析;对于大量D
态进程复杂死锁问题,可抓取内核 kdump 转储 (如高通的 ramdump) 进行分析。
8. 参考文献
[1] Linux pstore 实现自动“抓捕”内核崩溃日志
[2] pstore block oops/panic logger
[3] Add basic Minidump kernel driver support
[4] Add Qualcomm Minidump kernel driver related support