hungtask 机制分析

今天遇到了这样的一个log, 于是就看了下hung_task的原理!!但是目前只是看到了相关信息,并没有出现panic vmcroe等信息。也无法跟踪到最开始出现此log的上下文。console也卡死。貌似无解了,只能抱着学习态度看下。
首先要明白怎样分析问题:
也就是哪些会是是D进程? =》 内核哪些行为会置任务为D状态?
- 用户态和底层驱动交互,比如IO交互,一般是等待硬件响应。
- 死锁,mutex spinlock等;
- 竞争激烈,导致120s都无法拿到资源,比如内存耗尽或大内核锁;
有哪些信息可以看??
- 分析挂死调用栈,一般第1类问题调用栈或cat /proc/PID/stack
- 谁拿到了锁?什么原因不释放?是不是死锁问题?
Perf sched record [-p D进程PID] –g
Perf record [-p D进程PID] –g
分析进程D状态sched_switch在等什么
- 竞争激烈,那就是要对内核整体的全局锁分析了
长期以来,处于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。
代码分析
初始化函数hung_task_init
该函数初始化了一个内核线程来检测系统中是否有D状态超过120s的进程
static int __init hung_task_init(void) { // 注册通知,当内核panic时,调用函数panic_block atomic_notifier_chain_register(&panic_notifier_list, &panic_block); // 运行内核线程khungtaskd,其执行方法体为watchdog watchdog_task = kthread_run(watchdog, NULL, "khungtaskd"); return 0; } subsys_initcall(hung_task_init);
内核线程处理:watchdog
该函数时hung task机制中khungtaskd内核线程的处理函数
/* * kthread which checks for tasks stuck in D state */ static int watchdog(void *dummy) { unsigned long hung_last_checked = jiffies; /*设置当前khungtaskd内核线程的nice为0,即普通优先级,为了不影响业务运行 */ set_user_nice(current, 0); for ( ; ; ) { /*进程处于D状态的时间上线可通过sysctl控制,默认为120s 其值为0 表示disable watchdog*/ unsigned long timeout = sysctl_hung_task_timeout_secs; /* timeout 计算timeout*/ long t = hung_timeout_jiffies(hung_last_checked, timeout); if (t <= 0) { if (!atomic_xchg(&reset_hung_task, 0)) { /*醒来后执行实际的检测操作*/ check_hung_uninterruptible_tasks(timeout); } hung_last_checked = jiffies; continue; } /*检测线程(watchdog)sleep 120s(默认)后,再次唤醒。*/ schedule_timeout_interruptible(t); } return 0; }
check_hung_uninterruptible_tasks
每隔120s,由check_hung_uninterruptible_tasks来遍历所有的进程,看其是否为D状态,且120s没有被调度。
/* * 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) { /*hung task检测是检查的最大进程数,默认为最大的进程号*/ int max_count = sysctl_hung_task_check_count; /* * 每次遍历进程数的上限,默认为1024,这样做的目的是为了: * 1、防止rcu_read_lock的占用时间太长。 * 2、hung task的watchdog占用CPU时间太长。如果没开内核抢占,则如果内核线程不主动调度的话,是不能发生进程切换的? */ /* *Fixme:如果系统中的进程数比较多,那么就可能检测不到部分D状态进程了?不会,因为这里只是会调度一次,调度回来 *后,会继续遍历后面的进程*/ int batch_count = HUNG_TASK_BATCHING; struct task_struct *g, *t; /* If the system crashed already then all bets are off, * do not report extra hung tasks: *//*如果系统已经处于crash状态了,就不在报hung task了。*/ if (test_taint(TAINT_DIE) || did_panic) return; rcu_read_lock(); /*这里的遍历使用for_each_process_thread,遍历系统中所有的thread, 它每次都是从init_task进行检测。 所以内核demsg中打印出的第一个hung_tasks信息,不一定是第一个成为D状态的进程信息。*/ for_each_process_thread(g, t) { if (!max_count--) goto unlock; /*如果每次检测的进程数量超过1024了,则需要发起调度,结束rcu优雅周期*/ if (!--batch_count) { batch_count = HUNG_TASK_BATCHING; /*释放rcu,并主动调度,调度回来后检查相应进程是否还在,如果不在了,则退出遍历,否则继续*/ if (!rcu_lock_break(g, t)) goto unlock; } /*检测进程状态是否为D*/ /* use "==" to skip the TASK_KILLABLE tasks waiting on NFS */ if (t->state == TASK_UNINTERRUPTIBLE) check_hung_task(t, timeout);/*检测进程处于D状态的时间是否超过120s。*/ } unlock: rcu_read_unlock(); }
#define __for_each_thread(signal, t) \ list_for_each_entry_rcu(t, &(signal)->thread_head, thread_node) #define for_each_thread(p, t) \ __for_each_thread((p)->signal, t) /* Careful: this is a double loop, 'break' won't work as expected. */ #define for_each_process_thread(p, t) \ for_each_process(p) for_each_thread(p, t)
check_hung_task()
check_hung_task() 用来对给定进程,判断其是否在120s内一值为D状态:
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内核线程的唤醒周期来控制的。 */ if (switch_count != t->last_switch_count) {////这一句是核心,如果120s内被调度。则这个if条件成立,则在这里就返回了 /*更新last_switch_count计数,只在这里更新,该计数专用于hung task的检测。*/ t->last_switch_count = switch_count; return; } trace_sched_process_hang(t); /* * hung task错误打印次数限制,防止dos攻击。默认为10次,由于是全局变量, * 表示系统运行期间最多打印10次,超过后就不打印了。该参数应该可以 * 通过sysctl修改 */ if (!sysctl_hung_task_warnings) return; if (sysctl_hung_task_warnings > 0) sysctl_hung_task_warnings--; /* * Ok, the task did not get scheduled for more than 2 minutes, * complain:
如果定义了sysctl_hung_task_warnings。则显示下面的log 显示警告,并调用sched_show_task显示当前task的pid,ppid,stack等信息 */ 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); /*如果开启了debug_lock,则打印锁的占用情况*/ debug_show_held_locks(t); touch_nmi_watchdog(); if (sysctl_hung_task_panic) { /*检测是否配置了/proc/sys/kernel/hung_task_panic,如果配置则直接触发panic*/ trigger_all_cpu_backtrace();//如果定义了sysctl_hung_task_panic,则通过trigger_all_cpu_backtrace触发让所有的cpu都挂掉 panic("hung_task: blocked tasks"); } }

浙公网安备 33010602011771号