又是一篇精彩的文章,強力轉貼。
Linux softirq執行分析 Author: sinister Email: sinister@whitecell.org Homepage:http://www.whitecell.org Date: 2007-01-11 本文對 Linux 內核軟中斷的執行流程進行了分析,並盡可能的結合當前運行環境詳細地寫出我的理解, 但這並不表明我的理解一定正確。這本是論壇裏的一篇帖子,發出來是為了抛磚引玉,如果您在閱讀本文 時發現了我的錯誤,還望得到您的指正。 今天無意中看了眼 2.6 內核的軟中斷實現,發現和以前我看到的大不相同(以前也是走馬觀花,不大仔 細),可以說改動很大。連 softirq 的調用點都不一樣了,以前是三個調用點,今天搜索了一下源代 碼,發現在多出了ksoftirqd 後,softirq 在系統中的調用點僅是在 ISR 返回時和使用了 local_bh_enable() 函數後被調用了。網卡部分的顯示調用,我覺得應該不算是系統中的調用點。 ksoftirqd 返回去調用 do_softirq() 函數應該也只能算是其中的一個分支,因為其本身從源頭上 來講也還是在 ISR 返回時 irq_exit() 調用的。這樣一來就和前些日子寫的那份筆記 (Windows/Linux/Solaris 軟中斷機制)裏介紹的 Linux 內核部分的軟中斷有出處了,看來以後 討論 Linux kernel 代碼一定要以內核版本為前題,要不非亂了不可。得買本 Linux 方面的書了, 每次上來直接看相關代碼也不是回事,時間也不允許。 // // do_IRQ 函數執行完硬體 ISR 後退出時調用此函數。 // void irq_exit(void) { account_system_vtime(current); trace_hardirq_exit(); sub_preempt_count(IRQ_EXIT_OFFSET); // // 判斷當前是否有硬體中斷嵌套,並且是否有軟中斷在 // pending 狀態,注意:這裏只有兩個條件同時滿足 // 時,才有可能調用 do_softirq() 進入軟中斷。也就是 // 說確認當前所有硬體中斷處理完成,且有硬體中斷安裝了 // 軟中斷處理時理時才會進入。 // if (!in_interrupt() && local_softirq_pending()) // // 其實這裏就是調用 do_softirq() 執行 // invoke_softirq(); preempt_enable_no_resched(); } #ifndef __ARCH_HAS_DO_SOFTIRQ asmlinkage void do_softirq(void) { __u32 pending; unsigned long flags; // // 這個函數判斷,如果當前有硬體中斷嵌套,或者 // 有軟中斷正在執行時候,則馬上返回。在這個 // 入口判斷主要是為了與 ksoftirqd 互斥。 // if (in_interrupt()) return; // // 關中斷執行以下代碼 // local_irq_save(flags); // // 判斷是否有 pending 的軟中斷需要處理。 // pending = local_softirq_pending(); // // 如果有則調用 __do_softirq() 進行實際處理 // if (pending) __do_softirq(); // // 開中斷繼續執行 // local_irq_restore(flags); } // // 最大軟中斷調用次數為 10 次。 // #define MAX_SOFTIRQ_RESTART 10 asmlinkage void __do_softirq(void) { // // 軟體中斷處理結構,此結構中包括了 ISR 中 // 註冊的回調函數。 // struct softirq_action *h; __u32 pending; int max_restart = MAX_SOFTIRQ_RESTART; int cpu; // // 得到當前所有 pending 的軟中斷。 // pending = local_softirq_pending(); account_system_vtime(current); // // 執行到這裏要遮罩其他軟中斷,這裏也就證明了 // 每個 CPU 上同時運行的軟中斷只能有一個。 // __local_bh_disable((unsigned long)__builtin_return_address(0)); trace_softirq_enter(); // // 針對 SMP 得到當前正在處理的 CPU // cpu = smp_processor_id(); // // 迴圈標誌 // restart: // // 每次迴圈在允許硬體 ISR 強佔前,首先重置軟中斷 // 的標誌位元。 // /* Reset the pending bitmask before enabling irqs */ set_softirq_pending(0); // // 到這裏才開中斷運行,注意:以前運行狀態一直是關中斷 // 運行,這時當前處理軟中斷才可能被硬體中斷搶佔。也就 // 是說在進入軟中斷時不是一開始就會被硬體中斷搶佔。只有 // 在這裏以後的代碼才可能被硬體中斷搶佔。 // local_irq_enable(); // // 這裏要注意,以下代碼運行時可以被硬體中斷搶佔,但 // 這個硬體 ISR 執行完成後,它的所註冊的軟中斷無法馬上運行, // 別忘了,現在雖是開硬體中斷執行,但前面的 __local_bh_disable() // 函數遮罩了軟中斷。所以這種環境下只能被硬體中斷搶佔,但這 // 個硬中斷註冊的軟中斷回調函數無法運行。要問為什麼,那是因為 // __local_bh_disable() 函數設置了一個標誌當作互斥量,而這個 // 標誌正是上面的 irq_exit() 和 do_softirq() 函數中的 // in_interrupt() 函數判斷的條件之一,也就是說 in_interrupt() // 函數不僅檢測硬中斷而且還判斷了軟中斷。所以在這個環境下觸發 // 硬中斷時註冊的軟中斷,根本無法重新進入到這個函數中來,只能 // 是做一個標誌,等待下面的重複迴圈(最大 MAX_SOFTIRQ_RESTART) // 才可能處理到這個時候觸發的硬體中斷所註冊的軟中斷。 // // // 得到軟中斷向量表。 // h = softirq_vec; // // 迴圈處理所有 softirq 軟中斷註冊函數。 // do { // // 如果對應的軟中斷設置 pending 標誌則表明 // 需要進一步處理它所註冊的函數。 // if (pending & 1) { // // 在這裏執行了這個軟中斷所註冊的回調函數。 // h->action(h); rcu_bh_qsctr_inc(cpu); } // // 繼續找,直到把軟中斷向量表中所有 pending 的軟 // 中斷處理完成。 // h++; // // 從代碼裏可以看出按位操作,表明一次迴圈只 // 處理 32 個軟中斷的回調函數。 // pending >>= 1; } while (pending); // // 關中斷執行以下代碼。注意:這裏又關中斷了,下面的 // 代碼執行過程中硬體中斷無法搶佔。 // local_irq_disable(); // // 前面提到過,在剛才開硬體中斷執行環境時只能被硬體中斷 // 搶佔,在這個時候是無法處理軟中斷的,因為剛才開中 // 斷執行過程中可能多次被硬體中斷搶佔,每搶佔一次就有可 // 能註冊一個軟中斷,所以要再重新取一次所有的軟中斷。 // 以便下面的代碼進行處理後跳回到 restart 處重複執行。 // pending = local_softirq_pending(); // // 如果在上面的開中斷執行環境中觸發了硬體中斷,且每個都 // 註冊了一個軟中斷的話,這個軟中斷會設置 pending 位, // 但在當前一直遮罩軟中斷的環境下無法得到執行,前面提 // 到過,因為 irq_exit() 和 do_softirq() 根本無法進入到 // 這個處理過程中來。這個在上面詳細的記錄過了。那麼在 // 這裏又有了一個執行的機會。注意:雖然當前環境一直是 // 處於遮罩軟中斷執行的環境中,但在這裏又給出了一個執行 // 剛才在開中斷環境過程中觸發硬體中斷時所註冊的軟中斷的 // 機會,其實只要理解了軟中斷機制就會知道,無非是在一些特 // 定環境下調用 ISR 註冊到軟中斷向量表裏的函數而已。 // // // 如果剛才觸發的硬體中斷註冊了軟中斷,並且重複執行次數 // 沒有到 10 次的話,那麼則跳轉到 restart 標誌處重複以上 // 所介紹的所有步驟:設置軟中斷標誌位元,重新開中斷執行... // 注意:這裏是要兩個條件都滿足的情況下才可能重複以上步驟。 // if (pending && --max_restart) goto restart; // // 如果以上步驟重複了 10 次後還有 pending 的軟中斷的話, // 那麼系統在一定時間內可能達到了一個峰值,為了平衡這點。 // 系統專門建立了一個 ksoftirqd 線程來處理,這樣避免在一 // 定時間內負荷太大。這個 ksoftirqd 線程本身是一個大循環, // 在某些條件下為了不負載過重,它是可以被其他進程搶佔的, // 但注意,它是顯示的調用了 preempt_xxx() 和 schedule() // 才會被搶佔和切換的。這麼做的原因是因為在它一旦調用 // local_softirq_pending() 函數檢測到有 pending 的軟中斷 // 需要處理的時候,則會顯示的調用 do_softirq() 來處理軟中 // 斷。也就是說,下面代碼喚醒的 ksoftirqd 線程有可能會回 // 到這個函數當中來,尤其是在系統需要回應很多軟中斷的情況 // 下,它的調用入口是 do_softirq(),這也就是為什麼在 do_softirq() // 的入口處也會用 in_interrupt() 函數來判斷是否有軟中斷 // 正在處理的原因了,目的還是為了防止重入。ksoftirqd 實現 // 看下面對 ksoftirqd() 函數的分析。 // if (pending) // // 此函數實際是調用 wake_up_process() 來喚醒 ksoftirqd // wakeup_softirqd(); trace_softirq_exit(); account_system_vtime(current); // // 到最後才開軟中斷執行環境,允許軟中斷執行。注意:這裏 // 使用的不是 local_bh_enable(),不會再次觸發 do_softirq() // 的調用。 // _local_bh_enable(); } static int ksoftirqd(void * __bind_cpu) { // // 顯示調用此函數設置當前進程的靜態優先順序。當然, // 這個優先順序會隨調度器策略而變化。 // set_user_nice(current, 19); // // 設置當前進程不允許被掛啟 // current->flags |= PF_NOFREEZE; // // 設置當前進程狀態為可中斷的狀態,這種睡眠狀 // 態可回應信號處理等。 // set_current_state(TASK_INTERRUPTIBLE); // // 下面是一個大循環,迴圈判斷當前進程是否會停止, // 不會則繼續判斷當前是否有 pending 的軟中斷需 // 要處理。 // while (!kthread_should_stop()) { // // 如果可以進行處理,那麼在此處理期間內禁止 // 當前進程被搶佔。 // preempt_disable(); // // 首先判斷系統當前沒有需要處理的 pending 狀態的軟中斷 // if (!local_softirq_pending()) { // // 沒有的話在主動放棄 CPU 前先要允許搶佔,因為 // 一直是在不允許搶佔狀態下執行的代碼。 // preempt_enable_no_resched(); // // 顯示調用此函數主動放棄 CPU 將當前進程放入睡眠佇列, // 並切換新的進程執行(調度器相關不記錄在此) // schedule(); // // 注意:如果當前顯示調用 schedule() 函數主動切換的進 // 程再次被調度執行的話,那麼將從調用這個函數的下一條 // 語句開始執行。也就是說,在這裏當前進程再次被執行的 // 話,將會執行下面的 preempt_disable() 函數。 // // // 當進程再度被調度時,在以下處理期間內禁止當前進程被搶佔。 // preempt_disable(); } // // 設置當前進程為運行狀態。注意:已經設置了當前進程不可搶佔 // 在進入迴圈後,以上兩個分支不論走哪個都會執行到這裏。一是 // 進入迴圈時就有 pending 的軟中斷需要執行時。二是進入迴圈時 // 沒有 pending 的軟中斷,當前進程再次被調度獲得 CPU 時繼續 // 執行時。 // __set_current_state(TASK_RUNNING); // // 迴圈判斷是否有 pending 的軟中斷,如果有則調用 do_softirq() // 來做具體處理。注意:這裏又是一個 do_softirq() 的入口點, // 那麼在 __do_softirq() 當中迴圈處理 10 次軟中斷的回調函數 // 後,如果還有 pending 的話,會又調用到這裏。那麼在這裏則 // 又會有可能去調用 __do_softirq() 來處理軟中斷回調函數。在前 // 面介紹 __do_softirq() 時已經提到過,處理 10 次還處理不完的 // 話說明系統正處於繁忙狀態。根據以上分析,我們可以試想如果在 // 系統非常繁忙時,這個進程將會與 do_softirq() 相互交替執行, // 這時此進程佔用 CPU 應該會很高,雖然下面的 cond_resched() // 函數做了一些處理,它在處理完一輪軟中斷後當前處理進程可能會 // 因被調度而減少 CPU 負荷,但是在非常繁忙時這個進程仍然有可 // 能大量佔用 CPU。 // while (local_softirq_pending()) { /* Preempt disable stops cpu going offline. If already offline, we'll be on wrong CPU: don't process */ if (cpu_is_offline((long)__bind_cpu)) // // 如果當前被關聯的 CPU 無法繼續處理則跳轉 // 到 wait_to_die 標記出,等待結束並退出。 // goto wait_to_die; // // 執行 do_softirq() 來處理具體的軟中斷回調函數。注 // 意:如果此時有一個正在處理的軟中斷的話,則會馬上 // 返回,還記得前面介紹的 in_interrupt() 函數麼。 // do_softirq(); // // 允許當前進程被搶佔。 // preempt_enable_no_resched(); // // 這個函數有可能間接的調用 schedule() 來切換當前 // 進程,而且上面已經允許當前進程可被搶佔。也就是 // 說在處理完一輪軟中斷回調函數時,有可能會切換到 // 其他進程。我認為這樣做的目的一是為了在某些負載 // 超標的情況下不至於讓這個進程長時間大量的佔用 CPU, // 二是讓在有很多軟中斷需要處理時不至於讓其他進程 // 得不到回應。 // cond_resched(); // // 禁止當前進程被搶佔。 // preempt_disable(); // // 處理完所有軟中斷了嗎?沒有的話繼續迴圈以上步驟 // } // // 待一切都處理完成後,允許當前進程被搶佔,並設置 // 當前進程狀態為可中斷狀態,繼續迴圈以上所有過程。 // preempt_enable(); set_current_state(TASK_INTERRUPTIBLE); } // // 如果將會停止則設置當前進程為運行狀態後直接返回。 // 調度器會根據優先順序來使當前進程運行。 // __set_current_state(TASK_RUNNING); return 0; // // 一直等待到當前進程被停止 // wait_to_die: // // 允許當前進程被搶佔。 // preempt_enable(); /* Wait for kthread_stop */ // // 設置當前進程狀態為可中斷的狀態,這種睡眠狀 // 態可回應信號處理等。 // set_current_state(TASK_INTERRUPTIBLE); // // 判斷當前進程是否會被停止,如果不是的話 // 則設置進程狀態為可中斷狀態並放棄當前 CPU // 主動切換。也就是說這裏將一直等待當前進程 // 將被停止時候才結束。 // while (!kthread_should_stop()) { schedule(); set_current_state(TASK_INTERRUPTIBLE); } // // 如果將會停止則設置當前進程為運行狀態後直接返回。 // 調度器會根據優先順序來使當前進程運行。 // __set_current_state(TASK_RUNNING); return 0; } 參考: linux kernel source 2.6.19.1 /kernel/softirq.c WSS(Whitecell Security Systems),一個非營利性民間技術組織,致力於各種系統安全技術的研究。 堅持傳統的hacker精神,追求技術的精純。 WSS 主頁:http://www.whitecell.org/ WSS 論壇:http://www.whitecell.org/forums/