hungtask问题处理

750 阅读3分钟

1.hungtask问题现象

内核打印“INFO: task xxx:xxx blocked for more than 120 seconds.”

2.hungtask问题原因

内核某进程/线程长期处于D状态(TASK_UNINTERRUPTIBLE),不能接收信号,只能在资源满足条件时进行唤醒。但是资源超过120s不能满足,即发生hungtask。

3.hungtask基本原理

3.1 task_strcut中hung task相关成员

在进行hung task分析之前,需要了解struct task_strcut中的state、nvcsw、nivcsw、last_switch_count几个成员含义。

struct task_struct {
...
    /* -1 unrunnable, 0 runnable, >0 stopped */
    volatile long state; 
...
    //nvcsw表示进程主动切换次数,nivcsw表示进程被动切换次数,两者之和就是进程总的切换次数
    unsigned long nvcsw, nivcsw; 
#ifdef CONFIG_DETECT_HUNG_TASK
    //这个变量只有两个地方修改,一是在新建进程的时候设置初始值last_switch_count=nvcsw+nivcsw。
    //另一个是在khungtaskd中进行更新。
    unsigned long last_switch_count;
#endif
...
};

3.2 khungtaskd线程创建

watchdog()是khuangtaskd线程主函数,线程每隔sysctl_hung_task_timeout_secs醒来一次,调用check_hung_uninterruptible_tasks()检查所有进程。

static int watchdog(void *dummy)
{
    unsigned long hung_last_checked = jiffies;
    //设置当前进程nice为0,即普通优先级
    set_user_nice(current, 0);

    for ( ; ; ) {
        //获取进程hung时间上限
        unsigned long timeout = sysctl_hung_task_timeout_secs;
        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;
        }
        //休眠sysctl_hung_task_timeout_secs秒
        schedule_timeout_interruptible(t);
    }

    return 0;
}

static int __init hung_task_init(void)
{
    //注册panic通知链,在panic时执行相关操作
    atomic_notifier_chain_register(&panic_notifier_list, &panic_block);
    //创建内核线程khungtaskd
    watchdog_task = kthread_run(watchdog, NULL, "khungtaskd");

    return 0;
}
subsys_initcall(hung_task_init);

panic_block注册到panic_notifier_list通知链表上,如果系统产生panic,那么did_panic就会被置1。

static int
hung_task_panic(struct notifier_block *this, unsigned long event, void *ptr)
{
    did_panic = 1;

    return NOTIFY_DONE;
}

static struct notifier_block panic_block = {
    .notifier_call = hung_task_panic,
};

3.3 检查进程是否hung

check_hung_uninterruptible_tasks()遍历内核中所有task_struct,首先判断状态是否是TASK_UNINTERRUPTIBLE。

static void check_hung_uninterruptible_tasks(unsigned long timeout)
{
    //检测最大进程数,默认为最大进程号
    int max_count = sysctl_hung_task_check_count;
    //每次遍历进程数上限1024
    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:
     */
    if (test_taint(TAINT_DIE) || did_panic)
        return;

    rcu_read_lock();
    for_each_process_thread(g, t) {
        if (!max_count--)
            goto unlock;
        if (!--batch_count) {
            batch_count = HUNG_TASK_BATCHING;
            //防止rcu_read_lock占用过长时间。释放rcu,并主动调度。
            //调度回来后检查响应进程是否还在,不在则退出遍历,否则继续
            if (!rcu_lock_break(g, t))
                goto unlock;
        }
        // khungtaskd只监控TASK_UNINTERRUPTIBLE状态的进程线程
        if (t->state == TASK_UNINTERRUPTIBLE)
            check_hung_task(t, timeout);
    }
 unlock:
    rcu_read_unlock();
}


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;
    //如果总切换次数和last_switch_count不等,表示在上次khungtaskd更新last_switch_count之后就发生了进程切换;
    //反之,相等则表示120s时间内没有发生切换。
    if (switch_count != t->last_switch_count) {
        //更新last_switch_count
        t->last_switch_count = switch_count;
        return;
    }

    trace_sched_process_hang(t);
    //如果不使能warning和panic,返回
    if (!sysctl_hung_task_warnings && !sysctl_hung_task_panic)
        return;

    //hung task错误打印次数限制,默认为10次,整个系统运行期间最多打印10次
    if (sysctl_hung_task_warnings) {
        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\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");
        //显示进程ID、名称、状态以及栈等信息
        sched_show_task(t);
        //如果使能debug_locks,则打印进程持有的锁
        debug_show_all_locks();
    }

    touch_nmi_watchdog();

    if (sysctl_hung_task_panic) {
        trigger_all_cpu_backtrace();
        panic("hung_task: blocked tasks");
    }
}

4. 对khungtaskd的配置

通过sysctl或者在/proc/sys/kernel/中进行配置:

  • hung_task_panic------------------------是否在检测到hung后panic,默认值0
  • hung_task_check_count---------------最大检查task数量,默认值32768
  • hung_task_timeout_secs--------------超时时间,默认值120
  • hung_task_warnings--------------------打印hung warning的次数,默认值10

5.hungtask问题如何处理

5.1 可能出现问题的情况

  1. 处于io schedule
  2. 内存不足,导致多次持有shrink_node相关的锁
  3. 等待各种uninterruptable的锁

5.2 如何分析

  1. 通过log找到出现问题的cpu的栈
  • 如果是io schedule,分析具体的存储过程,判断是否io异常导致
  • 如果是shrink_node相关,则可能是内存不足导致
  • 如果是等待锁,可能当前线程栈只是个受害者
  1. 分析hung住的线程的线程栈的代码
  2. 使用其他工具进行分析:例如kdump、打印锁的持有者信息