Linux: hung task 检测机制简析

声明

本文著作权非笔者独有,xiaoguang.hu 享有共同著作权。
本文章节 7. hung task 问题的常见应对措施xiaoguang.hu 贡献。

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 分析背景

本文分析基于 linux-4.14.132 内核代码分析,运行环境 Ubuntu 16.04.4 LTS + QEMU + ARM vexpress-a9rootfs 基于 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,还会产生内核 panicsysctl_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 lockuphard 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

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值