關鍵詞: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);
