内核hung检测机制
Hung检测为内核提供的debug机制,用来检测系统是否存在长期处于TASK_UNINTERRUPTIBLE的进程。
Hung检测原理
内核进程 定期扫描状态为TASK_UNINTERRUPTIBLE的进程,如果在sysctl_hung_task_timeout_secs时间内,进程没有发生调度行为,则标识该进程处于hung状态,打印此进程堆栈等相关信息。
具体实现
static int __init hung_task_init(void)
{
atomic_notifier_chain_register(&panic_notifier_list, &panic_block);
/* Disable hung task detector on suspend */
pm_notifier(hungtask_pm_notify, 0);
watchdog_task = kthread_run(watchdog, NULL, "khungtaskd");
return 0;
}
subsys_initcall(hung_task_init);
通过subsys_initall(hung_task_init) 注册hung_task_init到启动阶段回调函数段中,
内核初始化阶段触发hung_task_init()回调完成hungtaskd初始化流程。
- 注册原子通知链回调,panic时回调被执行。注册panic回调的目的是当panic发生时,置did_panic。该值被khungtaskd判断是否需要执行hung check操作。见
check_hung_uninterruptible_tasks() : if (test_taint(TAINT_DIE) || did_panic) return;
- 创建内核进程khungtaskd执行watchdog(),循环遍历所有进程,根据一定周期内进程是否被调度来识别进程是否hung住。
/*
* kthread which checks for tasks stuck in D state
*/
static int watchdog(void *dummy)
{
//本次hung check开始的时刻
unsigned long hung_last_checked = jiffies;
set_user_nice(current, 0); //hungtaskd进程开始执行时,设置优先级为普通优先级。
//为普通优先级的khungtaskd也能被调度出去,遍历process的过程可能会被中断说明khungtaskd不是严格准确的检测机制,但即便不是严格准确的检查机制,可能也已经足够实用了。kmemleak的设计与此类似。
for ( ; ; ) {
//sysctl_hung_task_timeout_secs 超时时长,进程两次切换间隙时长如果超过此值,那么
//被认为处于hung状态,可通过sys节点调整。默认为120秒
unsigned long timeout = sysctl_hung_task_timeout_secs;
//khungtaskd进程两次执行hung检查间的时间间隔
//可通过sys节点调整
unsigned long interval = sysctl_hung_task_check_interval_secs;
long t;
//如果没有设置check间隔,则默认同进程超时时长相同,timeout默认为120秒
if (interval == 0)
interval = timeout;
//internal为khungtaskd检查的时长间隙,所以必然要比进程被判断为hung状态的阈值要小才行。
//举例:进程设定为10秒不切换就识别为hung,那么check的间隙最好要在10秒内,否则无法及时检测到处于hung状态的进程,此时sysctl_hung_task_timeout_secs已经失去了意义,因为已经hung住了sysctl_hung_task_timeout_secs秒,但却没有及时被系统发现已经hung住了。
interval = min_t(unsigned long, interval, timeout);
//如果sysctl_hung_task_timeout_secs+interval 超过当前时刻,则t>0
//说明下一轮hung 检查的时间点还没来,不用执行hung检查。
t = hung_timeout_jiffies(hung_last_checked, interval);
// t<=0 说明上一次检查到现在已经超过interval了,需要进行检查
if (t <= 0) {
//把0赋值给reset_hung_task,并返回reset_hung_task的旧值
//如果reset_hung_task值为0 并且 hung_detecotr_suspend为0,执行检查
if (!atomic_xchg(&reset_hung_task, 0) &&
!hung_detector_suspended)
check_hung_uninterruptible_tasks(timeout);
hung_last_checked = jiffies;
continue;
}
//t>0 说明还需要等待t,才到下一次hung 检查的时机,因此khungtaskd休眠等待t时长
schedule_timeout_interruptible(t);
}
return 0;
}
/*
* Check whether a TASK_UNINTERRUPTIBLE does not get woken up for
* a really long time (120 seconds). If that happens, print out
* a warning.
*/
//由khungtaskd内核线程执行hung检查
static void check_hung_uninterruptible_tasks(unsigned long timeout)
{
//设置一轮hung检查可以检查多少个进程
int max_count = sysctl_hung_task_check_count;
unsigned long last_break = jiffies;
//g 进程 t 线程
//每个进程和线程都有唯一的pid,其中线程通过tgid标识所属的进程
struct task_struct *g, *t;
/*
* If the system crashed already then all bets are off,
* do not report extra hung tasks:
*/
//如果处于die状态 或者panic状态,那么hung检查已经没有任何意义了。
//还记得吗?我们在初始化khungtaskd之前注册了panic的通知链panic_block,其中的回调会置did_panic为1
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;
//不知道是干嘛的,2023年1月6日 01:04:12
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 */
// 注意这里的条件是严格等于,而不是包含
//timeout即为sysctl_hung_task_timeout_secs,
//用户可配置的 确定进程hung住的阈值
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_call_panic) {
trigger_all_cpu_backtrace();
panic("hung_task: blocked tasks");
}
}
//检查某进程是否hung住的核心函数
static void check_hung_task(struct task_struct *t, unsigned long timeout)
{
//struct task_struct成员,记录进程切换次数
/* Context switch counts: */
// unsigned long nvcsw;
// unsigned long nivcsw;
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.
*/
//对于处于frozen状态 或者 不能被frozen的进程,不做检查。
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.
*/
//从没发生切换,当然不需要做检查,这种情况很少见,因为如果被设置为
//TASK_UNINTERRUPTIBLE状态后,那么通常会马上切换进程,因此
//switch_count为0的可能性很低
//猜想switch_count为0的一种可能性:切换为TASK_UNINTERRUPTIBLE后,
//正要执行schedule()时,khungtaskd对进程进行了扫描
if (unlikely(!switch_count))
return;
// 本次检查发现切换次数与上次检查不相同
//说明这期间进程有被调度过,说明没有hung住
//更新当前被扫描进程的(上次khungtaskd)“检查点的时刻”以及(khungtaskd扫描时)“上下文切换次数”
if (switch_count != t->last_switch_count) {
t->last_switch_count = switch_count;
t->last_switch_time = jiffies;
return;
}
//走到这里,说明本次检查时发现切换次数与上次检查时上下文切换次数相同
//但我们还得再看一下距此进程上一次被khungtaskd检查的间隔
//是否已经过了timeout,如果到了,说明进程hung住了。
// if为真,说明当前jiffer小于超时时刻,进程还不能被识别为hung住,因此返回。
if (time_is_after_jiffies(t->last_switch_time + timeout * HZ))
return;
//到这里说明进程在timeout周期内,没有发生调度。需要将进程标识为hung状态
//静态tracepoint点。
trace_sched_process_hang(t);
// 如果设定了panic_on_hung,检测到有hung状态进程时会直接触发panic(在遍历进程之后,不是立刻panic)
// 则置hung_task_show_lock hung_task_call_panic
//当遍历进程流程结束后会根据这两个变量触发具体动作
if (sysctl_hung_task_panic) {
console_verbose();
// 有hung状态进程时 打印锁信息
hung_task_show_lock = true;
// 有hung状态进程时 ,打印所有cpu堆栈,panic
hung_task_call_panic = true;
}
/*
* Ok, the task did not get scheduled for more than 2 minutes,
* complain:
*/
// sysctl_hung_task_warnings设置的是khungtaskd记录处于hung状态
//进程的个数,输出多少个hung状态进程信息后不再输出相关信息。
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%s\n",
print_tainted(), init_utsname()->release,
(int)strcspn(init_utsname()->version, " "),
init_utsname()->version,
LINUX_PACKAGE_ID);
pr_err("\"echo 0 > /proc/sys/kernel/hung_task_timeout_secs\""
" disables this message.\n");
sched_show_task(t);
hung_task_show_lock = true;
}
//不太懂为啥这里要更新nmi的watchdog value 2023年1月6日 01:28:35
touch_nmi_watchdog();
}