Linux Hung Task分析


關鍵詞:khungtaskd、TASK_UNINTERRUPTIBLE、nvcsw、nivcsw、last_switch_count等等。

 

經常會遇到內核打印“INFO: task xxx:xxx blocked for more than 120 seconds.”這樣的log信息,這是內核的hung task機制在起作用。

hung task機制通過內核線程khungtaskd來實現的,khungtaskd監控TASK_UNINTERRUPTIBLE狀態的進程,如果在120s周期內沒有切換,就會打印詳細信息。

 

1. hung task背景

處於D狀態,即TASK_UNINTERRUPTIBLE狀態的進程,不能接收kill信號。

如果一個進程長期處於D狀態,用戶往往無能為力。

進程處於長期處於D狀態是不正常的,內核設計D狀態目的是為了讓進程等待IO完成,正常情況下IO應該會瞬息完成,然后喚醒響應D裝固態進程。

即使在異常情況下,IO處理也有超時機制,原則上不應是進程長期處於D狀態。

如果進程長期處於D狀態,一是IO設備損壞,或者是內核中存在bug或機制不合理,導致進程長期處於D狀態,無法喚醒。

針對這種情況,內核提供了hung task機制用於檢測系統中是否有處於D狀態進程超過120s沒有切換過;如果存在則打印相關警告和堆棧。

2. hung task基本原理

hung task的實現通過創建khungtaskd內核線程,定期120s喚醒一次;

然后遍歷內核所有進程,需要滿足兩個條件:進程處於TASK_UNINTERRUPTIBLE,並且nvcsw+nivcsw==last_switch_count;

最后打印進程信息和堆棧。

3. hung task代碼分析

3.1 task_strcut中hung task相關成員

在進行hung task分析之前,需要了解struct task_strcut中的state、nvcsw、nivcsw、last_switch_count幾個成員含義。

struct task_struct {
...
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */---------------當前進程狀態,TASK_UNINTERRUPTIBLE表示進程不會被打斷。
...
    unsigned long nvcsw, nivcsw; /* context switch counts */--------------------------nvcsw表示進程主動切換次數,nivcsw表示進程被動切換次數,兩者之和就是進程總的切換次數。...
#ifdef CONFIG_DETECT_HUNG_TASK
/* hung task detection */
    unsigned long last_switch_count;--------------------------------------------------這個變量只有兩個地方修改,一是在新建進程的時候設置初始值last_switch_count=nvcsw+nivcsw。另一個是在khungtaskd中進行更新。
#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;

    set_user_nice(current, 0);---------------------------------------------------設置當前進程nice為0,即普通優先級。

    for ( ; ; ) {
        unsigned long timeout = sysctl_hung_task_timeout_secs;-------------------獲取進程hung時間上限。
        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;
        }
        schedule_timeout_interruptible(t);-----------------------------------------休眠sysctl_hung_task_timeout_secs秒。
    }

    return 0;
}

static int __init hung_task_init(void)
{
    atomic_notifier_chain_register(&panic_notifier_list, &panic_block);------------注冊panic通知鏈,在panic時執行相關操作。
    watchdog_task = kthread_run(watchdog, NULL, "khungtaskd");---------------------創建內核線程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_UNINTERRUPTIBLE。

static void check_hung_uninterruptible_tasks(unsigned long timeout)
{
    int max_count = sysctl_hung_task_check_count;-------------------檢測最大進程數,默認為最大進程號。 int batch_count = HUNG_TASK_BATCHING;---------------------------每次遍歷進程數上限1024。 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;
            if (!rcu_lock_break(g, t))--------------------------------防止rcu_read_lock占用過長時間。釋放rcu,並主動調度。調度回來后檢查響應進程是否還在,不在則退出遍歷,否則繼續。 goto unlock;
        }
        /* use "==" to skip the TASK_KILLABLE tasks waiting on NFS */
        if (t->state == TASK_UNINTERRUPTIBLE)-------------------------khungtaskd只監控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;

    if (switch_count != t->last_switch_count) {-------------------------如果總切換次數和last_switch_count不等,表示在上次khungtaskd更新last_switch_count之后就發生了進程切換;反之,相等則表示120s時間內沒有發生切換。
        t->last_switch_count = switch_count;----------------------------更新last_switch_count。 return;
    }

    trace_sched_process_hang(t);

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

    /*
     * Ok, the task did not get scheduled for more than 2 minutes,
     * complain:
     */
    if (sysctl_hung_task_warnings) {------------------------------------hung task錯誤打印次數限制,默認為10次,整個系統運行期間最多打印10次。
        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");
        sched_show_task(t);----------------------------------------------顯示進程ID、名稱、狀態以及棧等信息。
        debug_show_all_locks();------------------------------------------如果使能debug_locks,則打印進程持有的鎖。
    }

    touch_nmi_watchdog();

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

 下面看一下進程詳細信息:

void sched_show_task(struct task_struct *p)
{
    unsigned long free = 0;
    int ppid;
    unsigned long state = p->state;

    if (!try_get_task_stack(p))
        return;
    if (state)
        state = __ffs(state) + 1;
    printk(KERN_INFO "%-15.15s %c", p->comm,
        state < sizeof(stat_nam) - 1 ? stat_nam[state] : '?');------------------進程名稱和狀態,這里應該是D。 if (state == TASK_RUNNING)
        printk(KERN_CONT "  running task    ");
#ifdef CONFIG_DEBUG_STACK_USAGE
    free = stack_not_used(p);
#endif
    ppid = 0;
    rcu_read_lock();
    if (pid_alive(p))
        ppid = task_pid_nr(rcu_dereference(p->real_parent));
    rcu_read_unlock();
    printk(KERN_CONT "%5lu %5d %6d 0x%08lx\n", free,
        task_pid_nr(p), ppid,
        (unsigned long)task_thread_info(p)->flags);------------------------------free表示棧空閑量;第二個表示線程/進程pid;第三個表示父進程pid;最后一個表示進程的flags。

    print_worker_info(KERN_INFO, p);
    show_stack(p, NULL);
    put_task_stack(p);
}

 

 如下log可以得到recvComm進程,pid為175,父進程為148,當前狀態是D;當前hung的棧是read調用,卡在usb_sourceslink_read()函數。

 

 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

還可以通過bootargs對hung后是否panic進行設置。

/*
 * Should we panic (and reboot, if panic_timeout= is set) when a
 * hung task is detected:
 */
unsigned int __read_mostly sysctl_hung_task_panic =
                CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE;

static int __init hung_task_panic_setup(char *str)
{
    int rc = kstrtouint(str, 0, &sysctl_hung_task_panic);

    if (rc)
        return rc;
    return 1;
}
__setup("hung_task_panic=", hung_task_panic_setup);

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM