HungTask检测

基于msm-4.4

一、简介

处于D状态的进程不能接收信号,kill不掉,又可能导致永远卡住,因此才需要检测它。检测逻辑是若一个任务持续120秒都是D状态,则认为是hung住了,然后根据配置决定是否进行打印和panic。会打印出D状态hung住的任务的内核栈,若使能了Lockdep还会打印其持有的所有内核锁。

Kconfig 配置文件 lib/Kconfig.debug,配置与使能:CONFIG_DETECT_HUNG_TASK

config DETECT_HUNG_TASK
    bool "Detect Hung Tasks"
    depends on DEBUG_KERNEL
    default LOCKUP_DETECTOR
    help
在此处选择 Y 即可启用内核检测“hung tasks”的功能。这些错误会导致任务无限期地卡在不可中断的“D”状态。
检测到hung住的任务时,内核会打印当前堆栈跟踪(您应该报告),但该任务仍将保持不可中断状态。如果启用了 lockdep,则持有的所有锁也将被报告。此功能的开销几乎可以忽略不计。


同文件中所有相关配置:

CONFIG_DETECT_HUNG_TASK: 使能hungtask检测功能。

CONFIG_DEFAULT_HUNG_TASK_TIMEOUT: 此宏的值配置hung task的默认超时时间(单位秒),默认值是120. 此超时时间还可以通过 /proc/sys/kernel/hung_task_timeout_secs 文件进行配置。若设置为0则关闭hungtask检测功能。

CONFIG_BOOTPARAM_HUNG_TASK_PANIC: 当检测到hungtask时,配置是否panic.

CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE: 其值依赖于 CONFIG_BOOTPARAM_HUNG_TASK_PANIC 是否使能,若使能则为1否则为0,会默认赋值给 sysctl_hung_task_panic 变量,当检测到 hung task 时进行panic。

实现文件: kernel/hung_task.c


二、相关结构

1. struct task_struct

struct task_struct {
    unsigned long last_switch_count;
};

last_switch_count: 记录上次检测时此D状态任务的切换次数。若过120秒后再次检测还是D状态,且切换次数没变化,就认为是一直卡在这个D状态了,就认为是 HangTask 了。


二、实现

1. 初始化

static int __init hung_task_init(void)
{
    /* 注册通知,当内核panic时在 panic()中回调,回调函数中只是将 did_panic=1 */
    atomic_notifier_chain_register(&panic_notifier_list, &panic_block);
    /* 运行内核线程 khungtaskd, 其执行方法体为 watchdog() CFS-120 默认不绑核 */
    watchdog_task = kthread_run(watchdog, NULL, "khungtaskd");

    return 0;
}
subsys_initcall(hung_task_init);


2. 检测逻辑

2.1 watchdog()

static int watchdog(void *dummy)
{
    /* 设置当前 khungtaskd 内核线程的nice为0,即普通优先级,为了不影响业务运行 */
    set_user_nice(current, 0);

    for ( ; ; ) {
        /* 轮询周期可通过 /proc/sys/kernel/hung_task_timeout_secs 控制,默认为 120s */
        unsigned long timeout = sysctl_hung_task_timeout_secs;

        /* 先休眠120秒后再醒过来,若timeout为0就永久休眠不检测了 */
        while (schedule_timeout_interruptible(timeout_jiffies(timeout)))
            timeout = sysctl_hung_task_timeout_secs; //120

        /*
         * 原子地将 reset_hung_task=0. 并返回 reset_hung_task 的旧值,若为1就放弃此轮检测,
         * 检索整个内核,没有设1的位置,其值恒为0。
         */
        if (atomic_xchg(&reset_hung_task, 0))
            continue;

        /* 醒来后执行实际的检测操作 */
        check_hung_uninterruptible_tasks(timeout);
    }

    return 0;
}


2.2 check_hung_uninterruptible_tasks()

/* 检查 TASK_UNINTERRUPTIBLE 是否在很长一段时间(120 秒)内未被唤醒。*/
static void check_hung_uninterruptible_tasks(unsigned long timeout)
{
    int max_count = sysctl_hung_task_check_count; //默认值是 PID_MAX_LIMIT=4194304
    unsigned long last_break = jiffies;
    struct task_struct *g, *t;

    /* 如果系统已经崩溃,就不要报告hung tasks. did_panic 在 panic() 中的回调中设置为1. */
    if (test_taint(TAINT_DIE) || did_panic)
        return;

    hung_task_cnt = 0;
    rcu_read_lock();
    /* 遍历系统中的所有线程 */
    for_each_process_thread(g, t) {
        /* 可以配置每次RCU持锁遍历的任务个数, 默认不起作用 */
        if (!max_count--)
            goto unlock;
        /* 默认 HZ/10 即0.1秒,即还有一个周期100ms的释放RCU锁的操作 */
        if (time_after(jiffies, last_break + HUNG_TASK_LOCK_BREAK)) {
            /* 释放一下RCU锁并主动将自己以被抢占的状态切走,以免较长时间阻塞RCU宽限期 */
            if (!rcu_lock_break(g, t))
                goto unlock;
            /* 更新时间,以便下轮100ms后检测 */
            last_break = jiffies;
        }
        /* 只检测纯D状态,不包括 TASK_KILLABLE 和 TASK_IDLE #### */
        if (t->state == TASK_UNINTERRUPTIBLE)
            check_hung_task(t, timeout);
    }
 unlock:
    rcu_read_unlock();
}


2.3 check_hung_task()

/* 进来之前,已经将 hung_task_cnt=0 了。对系统中的每一个D状态的任务都进行调用 */
static void check_hung_task(struct task_struct *t, unsigned long timeout)
{
    /* 进程上下文切换计数,以此来判断该进程是否发生过调度 */
    unsigned long switch_count = t->nvcsw + t->nivcsw;

    /*
     * 确保任务未被冻结。此外,跳过 vfork 和 freezer 需要跳过的用户进程。
     * freezer1.0任务被冻结后长时间处于D状态可能会误报。
     */
    if (unlikely(t->flags & (PF_FROZEN | PF_FREEZER_SKIP)))
        return;

    /* 跳过新建的还一次都没切换过的任务 */
    if (unlikely(!switch_count))
        return;

    /* 虽然间隔120秒的两次检测都是D状态,但是中途任务有切换过,也不能说明长时间卡D状态,退出对此任务的继续检测 */
    if (switch_count != t->last_switch_count) {
        t->last_switch_count = switch_count;
        return;
    }

    /* 只打印了 t->comm 和 t->pid */
    trace_sched_process_hang(t);

    /*
     * hung_task_cnt 是每次检测时设置为0(周期120s),只有D状态的任务才会进到这个函数,即每轮检测最多只允
     * 许报2个任务. #### 
     */
    hung_task_cnt++;
    if (hung_task_cnt < 3)
        return;

    /*
     * 若通过 /proc/sys/kernel/hung_task_warnings(默认是3) 若允许报警告的次数用完了,####
     * 即使检测到长时间D状态的任务也不报警告,这个配置每次重启机器最大允许报的次数。####
     * TODO: 这里可以加个tain标志,以免报告次数用完了,发现不了问题了。
     */
    if (!sysctl_hung_task_warnings)
        return;
    if (sysctl_hung_task_warnings > 0)
        sysctl_hung_task_warnings--;

    /* 下面开始打印持续120s都是D状态的任务了 */

    pr_err("hung_task_cnt:%d\n", hung_task_cnt); //此轮检测报的第几个长时间D状态的任务
    pr_err("INFO: task %s:%d blocked for more than %ld seconds.\n", t->comm, t->pid, timeout); //默认timeout=120s
    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");
    /* 打印这个休眠状态的任务的堆栈,实测即使t运行在其它cpu上,也能打印出来(比较奇怪, 打印curr打印不出来) */
    sched_show_task(t);
    /* 若使能了 Lockdep(一般默认不使能), 会打印进程持有的所有内核锁 #### */
    debug_show_held_locks(t);
    /* 防止在此过程中触发 nmi_watchdog */
    touch_nmi_watchdog();

    /* 检测如果配置了panic则直接触发panic, 可通过 /proc/sys/kernel/hung_task_panic 进行配置 */
    if (sysctl_hung_task_panic) {
        /* 向所有的 oneline cpus 发送 IPI_CPU_BACKTRACE 中断,然后忙等这些cpu执行完栈回溯。然后在每个CPU上执行 show_regs(regs) 获取栈回溯 */
        trigger_all_cpu_backtrace();
        panic("hung_task: blocked tasks");
    }
}


三、调试接口

1. 启动参数

1.1 hung_task_panic

static int __init hung_task_panic_setup(char *str)
{
    kstrtouint(str, 0, &sysctl_hung_task_panic);
}
__setup("hung_task_panic=", hung_task_panic_setup);

用于决定检测到 hung task 后是否报panic.


2. 文件节点

2.1 /proc/sys/kernel/hung_task_check_count

用于配置 sysctl_hung_task_check_count, 表示每轮检测遍历的最大任务数量。这个功能没实现好,若配置的较小可能会导致有些任务始终遍历不到。需要保持默认值不要改。

2.2 /proc/sys/kernel/hung_task_panic

用于配置 sysctl_hung_task_panic 的值,表示检测到 hung task 后是否触发panic重启。

2.3 /proc/sys/kernel/hung_task_timeout_secs

用于配置 sysctl_hung_task_timeout_secs 的值,表示检测时间间隔,默认120秒,若持续这个时间都恒为D状态

2.4 /proc/sys/kernel/hung_task_warnings

用于配置 sysctl_hung_task_warnings 的值,默认是3,它是个计数值,表示每次重启机器最多支持检测多少个hung task,若是次数用完了,即使检测到也不再有任何动作了。

 

posted on 2025-07-17 16:00  Hello-World3  阅读(49)  评论(0)    收藏  举报

导航