死鎖:就是多個程式(≥2)因為爭奪資源而相互等待的一種現象,若無外力推動,將無法繼續執行下去。注意,只有在多程式或者多執行緒之間或者他們與中斷之間相互通訊或者共享資源才有可能發生死鎖,單執行緒或者程式之間沒有聯絡的話,一般不會發生死鎖。鎖的種類比較多,這裡主要說自旋鎖和訊號量。兩者的差別就在於前者獲得不到資源時的動作是不斷的資源(即忙轉浪費cpu的cycles)而後者則表現為睡眠等待。
死鎖的基本情況如下:
(一)、自旋鎖
1 遞迴使用:同一個程式或執行緒中,申請自旋鎖,但沒有釋放之前又再次申請,一定產生死鎖。
2 程式得到自旋鎖後阻塞,睡眠:在獲得自旋鎖之後呼叫copy_from_user()、copy_to_ser()、和kmalloc()等有可能引起阻塞的函式。
3 中斷中沒有關中斷,或著因為申請未釋放的自旋鎖:在中斷中使用自旋鎖是可以的,應該在進入中斷的時候關閉中斷,不然中斷再次進入的時候,中斷處理函式會自旋等待自旋鎖可以再次使用。或者在程式中申請了自旋鎖,釋放前進入中斷處理函式,中斷處理函式又申請同樣的自旋鎖,這將導致死鎖。
4 中斷與中斷下半部共享資源和中斷與程式共享資源死鎖出現的情況類似。
5 中斷下半部與程式共享資源和中斷與程式共享資源死鎖出現的情況類似。
自旋鎖三種狀態:
自旋鎖保持期間是搶佔失效的(核心不允許被搶佔)。
1 單CPU且核心不可搶佔:
自旋鎖的所有操作都是空。不會引起死鎖,核心程式間不存在併發操作程式,程式與中斷仍然可能共享資料,存在併發操作,此時核心自旋鎖已經失去效果。
2 單CPU且核心可搶佔:
當獲得自旋鎖的時候,禁止核心搶佔直到釋放鎖為止。此時可能存在死鎖的情況是參考自旋鎖可能死鎖的一般情況。
禁止核心搶佔並不代表不會進行核心排程,如果在獲得自旋鎖後阻塞或者主動排程,核心會排程其他程式執行,被排程的核心程式返回使用者空間時,會進行使用者搶佔,此時呼叫的程式再次申請上次未釋放的自旋鎖時,會一直自旋。但是核心被禁止搶佔,從而造成死鎖。
核心被禁止搶佔,但此時中斷並沒被禁止,核心程式可能因為中斷申請自旋鎖而死鎖。
3 多CPU且核心可搶佔:
這才是是真正的SMP的情況。當獲得自旋鎖的時候,禁止核心搶佔直到釋放鎖為止。
(二)訊號量
1 遞迴使用: 同理,在同一個程式或執行緒中,申請了訊號量,但沒有釋放之前又再次申請,程式會一直睡眠,這種情況一定死鎖。
2 程式得到訊號量後阻塞,睡眠:由於獲取到訊號量的程式阻塞或者隨眠,其他在獲取不到後訊號量也會進入睡眠等待,這種情況可能造成死鎖。
3 中斷中申請訊號量:由於訊號量在獲取不到自旋鎖後會進入睡眠等待,中斷處理函式不允許睡眠,如果睡眠,中斷將無法返回。
4 中斷下半部申請訊號量:中斷下半部允許睡眠,這種情況不會造成死鎖。
5 倆個程式相互等待資源:程式1獲得訊號量A,需要訊號量B,在程式1需要訊號量B之前程式2獲得訊號量B,需要訊號量A。程式1、2因相互等待資源而死鎖。
上面是死鎖的基本情況和型別。linux本身就提供了檢測死鎖的機制,如下:
(一)D狀態死鎖檢測
所謂D狀態死鎖:程式長時間(系統預設配置120秒)處於TASK_UNINTERRUPTIBLE 睡眠狀態,這種狀態下程式不響應非同步訊號。如:程式與外設硬體的互動(如read),通常使用這種狀態來保證程式與裝置的互動過程不被打斷,否則裝置可能處於不可控的狀態。
對於這種死鎖的檢測linux提供的是hungtask機制,主要內容集中在Hung_task.c檔案中。具體實現原理如下:
1)、系統建立normal級別的khungtaskd核心執行緒,核心執行緒每120秒檢查一次,檢查的內容:遍歷所有的執行緒連結串列,發現D狀態的任務,就判斷自最近一次切換以來是否還有切換髮生,若是有,則返回。若沒有發生切換,則把任務的所有呼叫棧等資訊列印出來。
2)、具體實現過程如下:
首先,hung_task_init建立一個名為khungtaskd的核心執行緒,核心執行緒的工作由watchdog來完成。
1 2 3 4 5 6 7 |
static int __init hung_task_init(void) { atomic_notifier_chain_register(&panic_notifier_list, &panic_block); watchdog_task = kthread_run(watchdog, NULL, "khungtaskd"); return 0; } |
其次,我們看watchdog的實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static int watchdog(void *dummy) { //將核心執行緒設定為normal級別 set_user_nice(current, 0); for ( ; ; ) { //設定hungtask的校驗時間間隔,使用者可以修改這個時間,預設為120秒 unsigned long timeout = sysctl_hung_task_timeout_secs; while (schedule_timeout_interruptible(timeout_jiffies(timeout))) timeout = sysctl_hung_task_timeout_secs; //核心的檢查程式碼在下面的函式中實現。 check_hung_uninterruptible_tasks(timeout); } return 0; } |
最後,我們分析一下hungtask的核心實現check_hung_uninterruptible_tasks,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
static void check_hung_uninterruptible_tasks(unsigned long timeout) { int max_count = sysctl_hung_task_check_count; 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: */ //判斷系統是否已經die、oops或者panic了,若是系統已經crash了,就無需再做hungtask了。 if (test_taint(TAINT_DIE) || did_panic) return; rcu_read_lock(); //檢查程式的列表,尋找D狀態的任務 do_each_thread(g, t) { //判斷使用者是否設定了檢查程式的數量,若是已經達到使用者設定的限制,就跳出迴圈。 if (!max_count--) goto unlock; //判斷是否到達批處理的個數,做這個批處理的目的就是因為整個檢查是在關搶佔的前提下進行的,可能程式列表的程式數很多,為了防止hungtask壟斷cpu,所以,做了一個批處理的限制,到達批處理的數量後,就放一下權,給其他的程式執行的機會。 if (!--batch_count) { batch_count = HUNG_TASK_BATCHING; rcu_lock_break(g, t); /* Exit if t or g was unhashed during refresh. */ if (t->state == TASK_DEAD || g->state == TASK_DEAD) goto unlock; } /* use "==" to skip the TASK_KILLABLE tasks waiting on NFS */ //如果程式處於D狀態,就開始把相關的資訊顯示出來了。 if (t->state == TASK_UNINTERRUPTIBLE) check_hung_task(t, timeout); } while_each_thread(g, t); unlock: rcu_read_unlock(); } |
1 2 3 4 5 6 7 8 9 10 |
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, when a freshly created task is scheduled once, changes * its state to TASK_UNINTERRUPTIBLE without having ever been * switched out once, it musn |
怎麼樣,經過上面的分析,可以發現其實原理很簡單。
(二)R狀態死鎖檢測
所謂R狀態死鎖:程式長時間(系統預設配置60秒)處於TASK_RUNNING 狀態壟斷cpu而不發生切換,一般情況下是程式關搶佔後長時候幹活,有時候可能程式關搶佔後處於死迴圈或者睡眠後,這樣就造成系統異常。
對於這種死鎖的檢測機制linux提供的機制是softlockup。主要集中在softlockup.c檔案中。
1)、系統建立一個fifo的程式,此程式週期性的清一下時間戳(per cpu),而系統的時鐘中斷中會被softlockup掛入一個鉤子(softlockup_tick),這個鉤子就是每個時鐘中斷到來的時候都檢查是否每cpu的時間戳被touch了,若在閥值60秒內都沒有被touch,系統就列印除錯資訊。
2)、讓我們分析一下具體的實現:
首先,系統初始化的時候為每個cpu建立一個watchdog執行緒,這個執行緒是fifo的。具體實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
static int __init spawn_softlockup_task(void) { void *cpu = (void *)(long)smp_processor_id(); int err; //可以通過啟動引數禁止softlockup if (nosoftlockup) return 0; //下面兩個回撥函式就是為每個cpu建立一個watchdog執行緒 err = cpu_callback(&cpu_nfb, CPU_UP_PREPARE, cpu); if (err == NOTIFY_BAD) { BUG(); return 1; } cpu_callback(&cpu_nfb, CPU_ONLINE, cpu); register_cpu_notifier(&cpu_nfb); atomic_notifier_chain_register(&panic_notifier_list, &panic_block); return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
static int __cpuinit cpu_callback(struct notifier_block *nfb, unsigned long action, void *hcpu) { int hotcpu = (unsigned long)hcpu; struct task_struct *p; switch (action) { case CPU_UP_PREPARE: case CPU_UP_PREPARE_FROZEN: BUG_ON(per_cpu(softlockup_watchdog, hotcpu)); //建立watchdog核心執行緒 p = kthread_create(watchdog, hcpu, "watchdog/%d", hotcpu); if (IS_ERR(p)) { printk(KERN_ERR "watchdog for %i failed\n", hotcpu); return NOTIFY_BAD; } //將時間戳清零 per_cpu(softlockup_touch_ts, hotcpu) = 0; //設定watchdog per_cpu(softlockup_watchdog, hotcpu) = p; //繫結cpu kthread_bind(p, hotcpu); break; case CPU_ONLINE: case CPU_ONLINE_FROZEN: //喚醒watchdog這個核心執行緒 wake_up_process(per_cpu(softlockup_watchdog, hotcpu)); break; ...... return NOTIFY_OK; } |
其次,我們看一下核心執行緒watchdog的實現,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
static int watchdog(void *__bind_cpu) { //將watchdog設定為fifo struct sched_param param = { .sched_priority = MAX_RT_PRIO-1 }; sched_setscheduler(current, SCHED_FIFO, ?m); //清狗,即touch時間戳 /* initialize timestamp */ __touch_softlockup_watchdog(); //當前程式可以被訊號打斷 set_current_state(TASK_INTERRUPTIBLE); /* * Run briefly once per second to reset the softlockup timestamp. * If this gets delayed for more than 60 seconds then the * debug-printout triggers in softlockup_tick(). */ while (!kthread_should_stop()) { //核心實現就是touch時間戳,讓出cpu __touch_softlockup_watchdog(); schedule(); if (kthread_should_stop()) break; set_current_state(TASK_INTERRUPTIBLE); } __set_current_state(TASK_RUNNING); return 0; } |
最後,softlockup在時鐘中斷中掛上一個鉤子softlockup_tick,每個時鐘中斷都來檢查watchdog這個fifo程式是否有touch過時間戳,若是60秒都沒有touch過,就向系統上報異常資訊了,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void softlockup_tick(void) { //取得當前的cpu int this_cpu = smp_processor_id(); //獲得當前cpu的時間戳 unsigned long touch_ts = per_cpu(softlockup_touch_ts, this_cpu); unsigned long print_ts; //獲得程式當前的暫存器組 struct pt_regs *regs = get_irq_regs(); unsigned long now; //如果沒有開啟softlockup,就將這個cpu對應的時間戳清零 /* Is detection switched off? */ if (!per_cpu(softlockup_watchdog, this_cpu) || softlockup_thresh <= 0) { /* Be sure we don |
怎麼樣,說白了,也不難,就是兩條線並行工作:一條線是fifo級別的核心執行緒負責清時間戳,另一條線是時鐘中斷定期檢查時間戳是否有被清過,若是到了閥值都沒有被請過,則列印softlockup的資訊。
(三)長時間關中斷檢測
長時間關中斷檢測可以有幾種實現機制,而利用nmi watchdog來檢查這種長時間關中斷情況,是比較簡單的。其原理是需要軟硬體配合,硬體通常提供一個計數器(可以遞增也可以遞減),當記數到某個值得時候,系統就硬體復位。而nmi watchdog就定期(小於這個計數到達系統復位的時間)的去清一下系統的計數,若是某個程式長時間關中斷,則可能導致nmi watchdog得不到清,最終系統復位。