Linux 커널의 hungtask 감지 메커니즘 분석

기능 개요

Linux 커널 5.10.0 버전에서 hungtask 기능은 시스템 내 모든 프로세스 스레드를 주기적으로 점검하여, TASK_UNINTERRUPTIBLE 상태에 지속적으로 머무르는 비정상적인 작업을 탐지합니다. 이 상태가 설정된 시간(기본값: 120초) 이상 지속되면 경고를 출력하거나 시스템을 패닉시킵니다.

초기화 과정

hung_task_init() 함수는 커널 부팅 시 실행되며, khungtaskd라는 백그라운드 내부 스레드를 생성하고, 패닉 발생 시 중복 검사를 방지하기 위해 panic_notifier_list에 등록합니다. 또한 시스템 절전 모드 전후에 감지를 일시 정지/재개할 수 있도록 pm_notifier를 통해 이벤트 핸들러를 등록합니다.

static int __init hung_task_init(void)
{
    atomic_notifier_chain_register(&panic_notifier_list, &panic_block);
    pm_notifier(hungtask_pm_notify, 0);
    watchdog_task = kthread_run(watchdog, NULL, "khungtaskd");
    return 0;
}
subsys_initcall(hung_task_init);

감지 스레드 동작 흐름

khungtaskd 스레드는 watchdog() 함수를 반복 실행하며, 설정된 체크 간격(sysctl_hung_task_check_interval_secs)마다 상태를 점검합니다. 단, 다음 조건이 충족될 경우 점검을 건너뜁니다:

  • reset_hung_task 플래그가 활성화된 경우 (외부에서 재설정 요청)
  • 시스템이 절전 상태일 때 (hung_detector_suspended가 참)
static int watchdog(void *dummy)
{
    unsigned long hung_last_checked = jiffies;
    set_user_nice(current, 0);

    for ( ; ; ) {
        unsigned long timeout = sysctl_hung_task_timeout_secs;
        unsigned long interval = sysctl_hung_task_check_interval_secs;

        if (interval == 0)
            interval = timeout;
        interval = min_t(unsigned long, interval, timeout);

        long t = hung_timeout_jiffies(hung_last_checked, interval);
        if (t <= 0) {
            if (!atomic_xchg(&reset_hung_task, 0) && !hung_detector_suspended)
                check_hung_uninterruptible_tasks(timeout);
            hung_last_checked = jiffies;
            continue;
        }
        schedule_timeout_interruptible(t);
    }
    return 0;
}

작업 상태 점검 로직

check_hung_uninterruptible_tasks() 함수는 RCU 잠금을 안전하게 유지하면서 모든 스레드를 순회하며 TASK_UNINTERRUPTIBLE 상태인 항목만 대상으로 합니다. 최대 sysctl_hung_task_check_count개의 스레드까지만 점검하여 시스템 성능 저하를 방지합니다.

static void check_hung_uninterruptible_tasks(unsigned long timeout)
{
    int max_count = sysctl_hung_task_check_count;
    unsigned long last_break = jiffies;
    struct task_struct *g, *t;

    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;
        if (time_after(jiffies, last_break + HUNG_TASK_LOCK_BREAK)) {
            if (!rcu_lock_break(g, t))
                goto unlock;
            last_break = jiffies;
        }
        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_show_all_bt) {
        hung_task_show_all_bt = false;
        trigger_all_cpu_backtrace();
    }

    if (hung_task_call_panic)
        panic("hung_task: blocked tasks");
}

비정상 상태 판단 기준

check_hung_task()는 특정 스레드의 컨텍스트 스위치 횟수 변화 여부를 기반으로 판단합니다. 만약 현재 스위치 카운트와 이전 값이 동일하고, 그 이후 시간이 timeout 초를 초과하면, 해당 스레드는 장시간 블록 상태라고 판단됩니다.

  • 주동 스위치: I/O 대기, 세마포어/락 대기, schedule() 호출 등
  • 수동 스위치: 시간 슬라이스 소진, 다른 프로세스에 의해 강제 중단됨
static void check_hung_task(struct task_struct *t, unsigned long timeout)
{
    unsigned long switch_count = t->nvcsw + t->nivcsw;

    if (unlikely(t->flags & (PF_FROZEN | PF_FREEZER_SKIP)))
        return;

    if (unlikely(!switch_count))
        return;

    if (switch_count != t->last_switch_count) {
        t->last_switch_count = switch_count;
        t->last_switch_time = jiffies;
        return;
    }

    if (time_is_after_jiffies(t->last_switch_time + timeout * HZ))
        return;

    trace_sched_process_hang(t);

    if (sysctl_hung_task_panic) {
        console_verbose();
        hung_task_show_lock = true;
        hung_task_call_panic = true;
    }

    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, (jiffies - t->last_switch_time) / HZ);
        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);
        hung_task_show_lock = true;

        if (sysctl_hung_task_all_cpu_backtrace)
            hung_task_show_all_bt = true;
    }

    touch_nmi_watchdog();
}

보조 기능 및 처리

경고 메시지 출력, 데드락 정보 표시, 전체 CPU 백트레이스 수집 등의 기능은 hung_task_show_lock, hung_task_show_all_bt 플래그를 통해 제어됩니다. 최종적으로 sysctl_hung_task_panic가 켜져 있으면 시스템을 패닉 시켜 오류를 강제 종료합니다.

또한 touch_nmi_watchdog() 호출을 통해 인터럽트 없는 시스템 감시자(예: NMI 워치독)의 타임아웃을 리셋함으로써, 실제 하드웨어 고장과 혼동되는 상황을 피합니다.

태그: Linux kernel hungtask TASK_UNINTERRUPTIBLE context switch watchdog

5월 28일 05:07에 게시됨