之前学习了lockup,除了这个之外,还有一种异常也经常遇到,就是hung task
学习
概念:
首先需要明白,hung task是什么?它指的是一个线程长时间处于D(TASK_UNINTERRUPTIBLE)状态不能被调度
与lockup的区别:
hung task容易和lockup搞混淆,这俩其实本质上是不一样的:lockup是会定期检测系统还是否有调度的能力,而hung task检测的是线程,定期检测线程是否被调度执行。
但是这俩并不是完全独立的,经常一块出现。比如内核lockup了,一直不能调度,而此时正好有线程在io处于D状态,那它就一直不能被调度从而hung死;又或者一个线程hung了,而它正好持有某个锁,而内核此时正好要获取这个锁,一直获取不到,也不调度,就lockup了。
相关参数:
config:
CONFIG_BLK_IO_HUNG_TASK_CHECK //是否检测块设备的io hung,这种情况下,io慢(但并没有hang)可能也会被检测到
CONFIG_DETECT_HUNG_TASK //是否检测hang
CONFIG_DEFAULT_HUNG_TASK_TIMEOUT //超时时间,进程超过这个这个时间没有被调度就认为hang
CONFIG_BOOTPARAM_HUNG_TASK_PANIC //检测到hang时是否触发panic
内核参数:
/proc/sys/kernel/hung_task_all_cpu_backtrace //发生hang时是否对cpu栈进行回溯
/proc/sys/kernel/hung_task_panic //是否触发panic
/proc/sys/kernel/hung_task_check_count //检查的最大进程数
/proc/sys/kernel/hung_task_timeout_secs //超过多少时间没有调度认为hang。可以覆盖CONFIG_DEFAULT_HUNG_TASK_TIMEOUT
/proc/sys/kernel/hung_task_check_interval_secs //检测间隔
/proc/sys/kernel/hung_task_warnings //控制内核报告hung task的次数
原理:
设置CONFIG_DETECT_HUNG_TASK为y后,会启用hung task检测
subsys_initcall(hung_task_init);
static int __init hung_task_init(void)
{
//注册一个通知链,,这个之前还真没见过,后续研究一下
atomic_notifier_chain_register(&panic_notifier_list, &panic_block);
/* Disable hung task detector on suspend */
//系统suspend的时候也禁用hung task检测,也是用通知链搞的,后面研究一下
pm_notifier(hungtask_pm_notify, 0);
//起一个khungtaskd内核线程
watchdog_task = kthread_run(watchdog, NULL, "khungtaskd");
hung_task_sysctl_init();
return 0;
}
//这就是起的khungtaskd内核线程要执行的函数
static int watchdog(void *dummy)
{
unsigned long hung_last_checked = jiffies;
//set khungtaskd的nice值为高优先级
set_user_nice(current, 0);
//死循环,一直检测
for ( ; ; ) {
//这个timeout和interval就是上面那俩sysctl参数控制的
unsigned long timeout = sysctl_hung_task_timeout_secs;
unsigned long interval = sysctl_hung_task_check_interval_secs;
long t;
//如果没设置interval,就是timeout的时间,也就是每隔timeout时间检测一下
//如果设置了,就使用最小值
if (interval == 0)
interval = timeout;
interval = min_t(unsigned long, interval, timeout);
//这里就是计算一下,last是上次检查时间,jiffies是当前时间
//如果interval是0的话就表示不开启hung task检测
//如果不是的话,那就计算last - jiffies + interval(没转换单位,知道意思就好)
//也就是计算下一次检测的时候(其实感觉这里叫timeout不太好,应该是检测时间,而不是timeout吧?)
t = hung_timeout_jiffies(hung_last_checked, interval);
//处理小于0的情况,真正的检测hang也是在这的
//因为总会走到t<=0的情况的
//比如下一次唤醒的时候,hung_timeout_jiffies中的last没更新,interval没更新,但是jiffies一直在增长
//理想的一种情况是,last更新完,interval就是timeout,那么sched出去下一次唤醒的时候t应该是正好小于等于0
if (t <= 0) {
//要保证没有reset_hung_task且hungtask没有挂起
if (!atomic_xchg(&reset_hung_task, 0) &&
!hung_detector_suspended)
//真正的检测hang的函数
check_hung_uninterruptible_tasks(timeout);
hung_last_checked = jiffies;
continue;
}
//sched出去,等到t时刻唤醒
schedule_timeout_interruptible(t);
}
return 0;
}
//看主要流程
static void check_hung_uninterruptible_tasks(unsigned long timeout)
{
hung_task_show_lock = false;
rcu_read_lock();
//遍历所有线程
for_each_process_thread(g, t) {
unsigned int state;
//这块就是hung_task_check_count控制的
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;
}
/*
* skip the TASK_KILLABLE tasks -- these can be killed
* skip the TASK_IDLE tasks -- those are genuinely idle
*/
//对D进程执行check_hung_task,也就是检测是否hang
state = READ_ONCE(t->__state);
if ((state & TASK_UNINTERRUPTIBLE) &&
!(state & TASK_WAKEKILL) &&
!(state & TASK_NOLOAD))
check_hung_task(t, timeout);
}
。。。。
}
//真正对D进程检查是否hang
static void check_hung_task(struct task_struct *t, unsigned long timeout)
{
//自愿和非自愿上下文切换的次数
unsigned long switch_count = t->nvcsw + t->nivcsw;
//如果上次switch_count不等于当前switch_count,跳出
//因为这就意味着进行过上下文切换了,最起码就是已经调度过了
if (switch_count != t->last_switch_count) {
t->last_switch_count = switch_count;
t->last_switch_time = jiffies;
return;
}
//如果当前的jiffies是在last_switch_time加timeout之前的
//这就说明还没到hang的时间限制,也返回
if (time_is_after_jiffies(t->last_switch_time + timeout * HZ))
return;
trace_sched_process_hang(t);
if (sysctl_hung_task_panic) {
console_verbose();
hung_task_show_lock = true;
hung_task_call_panic = true;
}
//上面那些都不满足的话,也就是hang了
//下面其实就是hang的处理了
。。。。。。
touch_nmi_watchdog();
}
所以,其实是怎么检测的呢?主要就是检测D进程的swicth次数在timeout时间没没有改变(也就是这段时间没有上下文切换),last_switch_time在timeout时间也没有改变(也就是这段时间没有被调度执行),就认为hang死了
那怎么关闭hung task的检测呢?很简单,在不改变config重新编译内核的情况下,可以把hung_task_timeout_secs设置为0
hung task原因:
一般而言,产生hung的原因有以下几点(个人最常遇到的是前两种):
- 申请io,等待读写完成时间太长(或者设备有问题)
- 调用mutex等待访问某个共享资源
- 调用wait_event io_wait_event wait_for_completion等待某个事件的发生
- 内核中其他任务设置为task_uninterruptible的场景
模拟hungtask:
首先写用户态写死循环脚本是行不通的,因为用户态的死循环没什么用啊。他不会真的一直占着cpu,时间片一完,tick一到应该也会强行调度出去了
- 结文件系统然后向里面写入文件
fsfreeze -f /XXXX //冻结对文件系统的访问,比如ext4 jfs xfs等文件系统
fsfreeze -u /XXXX //解冻
冻结后向该文件系统下的一个文件写入内容,就会出现hung task。就是利用了冻结后不能io的原理吧
(另外,经过实验,这时候读写虽然会hung住,但是-u解冻之后,之前hung住的进程会继续执行完成)
(另外,不能用tmpfs这些文件系统,所以可以创建一个文件系统然后挂载上再冻结解冻之类)
- 应该可以使用大量的fio压力,导致系统处理不过来,导致hung task。不过没成功过,可能压力不够大?
后记
- 所以看hung task的时候,直接ps aux |grep D找不到D进程也是正常的。因为之前都看到了,在check_hung_uninterruptible_tasks函数中使用的是for_each_process_thread函数遍历的是每一个线程,所以要遍历每个线程看有没有D住的
- 还有个参数呢CONFIG_BLK_IO_HUNG_TASK_CHECK。这个简单看了下,其实也就是如果开了这个的话在blk_io_schedule中就用io_schedule_timeout代替io_schedule。另外,即使不在config中指定CONFIG_BLK_IO_HUNG_TASK_CHECK打开io hung task,也可以使用类似echo Y > /sys/module/blk_core/parameters/io_hung_task_check的方式打开io hung task。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 857879363@qq.com