一、相关知识
长期以来,处于D状态(TASK_UNINTERRUPTIBLE状态)的进程都是让人比较烦恼的问题,处于D状态的进程不能接收信号,kill不掉。在一些场景下,常见到进程长期处于D状态,用户对此无能为力,也不知道原因,只能重启恢复。
其实进程长期处于D状态肯定是不正常的,内核中设计D状态的目的是为了让进程等待IO完成,正常情况下IO应该会顺利完成,然后唤醒相应的D状态进程,即使在异常情况下(比如磁盘离或损坏、磁阵链路断开等),IO处理也是有超时机制的,原理上不会存在永久处于D状态的进程。但是就是因为内核代码流程中可能存在一些bug,或者用户内核模块中的相关机制不合理,可能导致进程长期处于D状态,无法唤醒,类似于死锁状态。
针对这种情况,内核中提供了hung task机制用于检测系统中是否存在处于D状态超过120s(时长可以设置)的进程,如果存在,则打印相关警告和进程堆栈。如果配置了hung_task_panic(proc或内核启动参数),则直接发起panic,结合kdump可以搜集到vmcore。从内核的角度看,如果有进程处于D状态的时间超过了120s,那肯定已经出现异常了,以此机制来收集相关的异常信息,用于分析定位问题。
二、基本原理
hung task机制的实现很简单,其基本原理为:
创建一个内核线程(khungtaskd),定期(120)唤醒后,遍历系统中的所有进程,检查是否存在处于D状态超过120s(时长可以设置)的进程,如果存在,则打印相关警告和进程堆栈。如果配置了hung_task_panic(proc或内核启动参数),则直接发起panic。
三、代码实现
1、初始化
static int __init hung_task_init(void)
{
//注册panic通知链,在panic时执行相关的操作
atomic_notifier_chain_register(&panic_notifier_list, &panic_block);
/* Disable hung task detector on suspend */
pm_notifier(hungtask_pm_notify, 0);
//创建内核线程khungtaskd,执行函数为watchdog
watchdog_task = kthread_run(watchdog, NULL, "khungtaskd");
return 0;
}
2、内核线程处理
static int watchdog(void *dummy)
{
unsigned long hung_last_checked = jiffies;//初始化一个变量hung_last_checked,用于记录上次检查的时间
set_user_nice(current, 0);//设置内核线程的nice为0,即普通优先级,不影响业务运行
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);
//如果时间间隔小于等于0,表示已经超过了设定的检查间隔,需要进行任务的检查和处理
if (t <= 0) {
if (!atomic_xchg(&reset_hung_task, 0) &&
!hung_detector_suspended)
check_hung_uninterruptible_tasks(timeout);
hung_last_checked = jiffies;
continue;
}
//让线程进入睡眠状态,直到下次唤醒时长为t的时间过去
schedule_timeout_interruptible(t);
}
return 0;
}
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;
//如果距离上次中断的时间超过了预定的时间间隔,则退出,防止rcu_read_lock占用时间太长。
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();
if (hung_task_show_lock)
debug_show_all_locks();
if (hung_task_show_all_bt) {
hung_task_show_all_bt = false;
trigger_all_cpu_backtrace();
}
if (hung_task_call_panic)
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;
/*
* 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.
*/
if (unlikely(!switch_count))
return;
/*
* 如果当前switch_count不等于last_switch_count,则说明在khungtaskd进程被唤醒期间,该进程没有发生过调度。
* 也就是说,该进程一直处于D状态,因为last_switch_count只在这里更新,其它地方不会。
* hung task机制中的120s其实是通过khungtaskd内核线程的唤醒周期来控制的,不是通过per task其它计数。
*/
if (switch_count != t->last_switch_count) {
t->last_switch_count = switch_count;
t->last_switch_time = jiffies;
return;
}
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;
}
/*
* Ok, the task did not get scheduled for more than 2 minutes,
* complain:
*/
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();
}