本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/
在上一篇文章中,我們看到中斷實際分為了兩個部分,俗稱就是一部分是硬中斷,一部分是軟中斷。軟中斷是專門用於處理中斷過程中費時費力的操作,而為什么系統要分硬中斷和軟中斷呢?問得明白點就是為什么需要軟中斷。我們可以試着想想,如果只有硬中斷的情況下,我們需要在中斷處理過程中執行一些耗時的操作,比如浮點數運算,復雜算法的運算時,其他的外部中斷就不能得到及時的響應,因為在硬中斷過程中中斷是關閉着的,甚至一些很緊急的中斷會得不到響應,系統穩定性和及時性都受到很大影響。所以linux為了解決上述這種情況,將中斷處理分為了兩個部分,硬中斷和軟中斷。首先一個外部中斷得到響應時,會先關中斷,並進入到硬中斷完成較為緊急的操作,然后開中斷,並在軟中斷執行那些非緊急、可延時執行的操作;在這種情況下,緊急操作可以立即執行,而其他的外部中斷也可以獲得一個較為快速的響應。這也是軟中斷存在的必要性。在軟中斷過程中是不可以被搶占也不能被阻塞的,也不能在一個給定的CPU上交錯執行。
軟中斷
軟中斷是在中斷框架中專門用於處理非緊急操作的,在SMP系統中,軟中斷可以並發地運行在多個CPU上,但在一些路徑在需要使用自旋鎖進行保護。在系統中,很多東西都分優先級,軟中斷也不例外,有些軟中斷要求更快速的響應運行,在內核中軟中斷一共分為10個,同時也代表着10種不同的優先級,系統用一個枚舉變量表示:
enum { HI_SOFTIRQ=0, /* 高優先級tasklet */ /* 優先級最高 */ TIMER_SOFTIRQ, /* 時鍾相關的軟中斷 */ NET_TX_SOFTIRQ, /* 將數據包傳送到網卡 */ NET_RX_SOFTIRQ, /* 從網卡接收數據包 */ BLOCK_SOFTIRQ, /* 塊設備的軟中斷 */ BLOCK_IOPOLL_SOFTIRQ, /* 支持IO輪詢的塊設備軟中斷 */ TASKLET_SOFTIRQ, /* 常規tasklet */ SCHED_SOFTIRQ, /* 調度程序軟中斷 */ HRTIMER_SOFTIRQ, /* 高精度計時器軟中斷 */ RCU_SOFTIRQ, /* RCU鎖軟中斷,該軟中斷總是最后一個軟中斷 */ /* 優先級最低 */ NR_SOFTIRQS /* 軟中斷數,為10 */ };
注釋中的tasklet我們之后會說明,這里先無視它。每一個優先級的軟中斷都使用一個struct softirq_action結構來表示,在這個結構中,只有一個成員變量,就是action函數指針,因為不同的軟中斷它的處理方式可能不同,從優先級表中就可以看出來,有塊設備的,也有網卡處理的。系統將這10個軟中斷用softirq_vec[10]的數組進行保存。
/* 用於描述一個軟中斷 */ struct softirq_action { /* 此軟中斷的處理函數 */ void (*action)(struct softirq_action *); }; /* 10個軟中斷描述符都保存在此數組 */ static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
系統一般使用open_softirq()函數進行軟中斷描述符的初始化,主要就是將action函數指針指向該軟中斷應該執行的函數。在start_kernel()進行系統初始化中,就調用了softirq_init()函數對HI_SOFTIRQ和TASKLET_SOFTIRQ兩個軟中斷進行了初始化
void __init softirq_init(void) { int cpu; for_each_possible_cpu(cpu) { per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; } /* 開啟常規tasklet */ open_softirq(TASKLET_SOFTIRQ, tasklet_action); /* 開啟高優先級tasklet */ open_softirq(HI_SOFTIRQ, tasklet_hi_action); } /* 開啟軟中斷 */ void open_softirq(int nr, void (*action)(struct softirq_action *)) { softirq_vec[nr].action = action; }
可以看到,TASKLET_SOFTIRQ的action操作使用了tasklet_action()函數,HI_SOFTIRQ的action操作使用了tasklet_hi_action()函數,這兩個函數我們需要結合tasklet進行說明。我們也可以看看其他的軟中斷使用了什么函數:
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
open_softirq(BLOCK_SOFTIRQ, blk_done_softirq);
open_softirq(BLOCK_IOPOLL_SOFTIRQ, blk_iopoll_softirq);
open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);
open_softirq(HRTIMER_SOFTIRQ, run_hrtimer_softirq);
open_softirq(RCU_SOFTIRQ, rcu_process_callbacks);
其實很明顯可以看出,除了TASKLET_SOFTIRQ和HI_SOFTIRQ,其他的軟中斷更多地是用於特定的設備和環境,對於我們普通的IO驅動和設備而已,使用的軟中斷幾乎都是TASKLET_SOFTIRQ和HI_SOFTIRQ,而系統為了對這些不同IO設備進行統一的處理,就在TASKLET_SOFTIRQ和HI_SOFTIRQ的action函數中使用到了tasklet。
對於每個CPU,都有一個irq_cpustat_t的數據結構,里面有一個__softirq_pending變量,這個變量很重要,用於表示該CPU的哪個軟中斷處於掛起狀態,在軟中斷處理時可以根據此值跳過不需要處理的軟中斷,直接處理需要處理的軟中斷。內核使用local_softirq_pending()獲取此CPU的__softirq_pending的值。
當使用open_softirq設置好某個軟中斷的action指針后,該軟中斷就會開始可以使用了,其實更明了地說,從中斷初始化完成開始,即使所有的軟中斷都沒有使用open_softirq()進行初始化,軟中斷都已經開始使用了,只是所有軟中斷的action都為空,系統每次執行到軟中斷都沒有軟中斷需要執行罷了。
在每個CPU上一次軟中斷處理的一個典型流程是:
- 硬中斷執行完畢,開中斷。
- 檢查該CPU是否處於嵌套中斷的情況,如果處於嵌套中,則不執行軟中斷,也就是在最外層中斷才執行軟中斷。
- 執行軟中斷,設置一個軟中斷執行最多使用時間和循環次數(10次)。
- 進入循環,獲取CPU的__softirq_pending的副本。
- 執行此__softirq_pending副本中所有需要執行的軟中斷。
- 如果軟中斷執行完畢,退出中斷上下文。
- 如果還有軟中斷需要執行(在軟中斷期間又發發生了中斷,產生了新的軟中斷,新的軟中斷記錄在CPU的__softirq_pending上,而我們的__softirq_pending只是個副本)。
- 檢查此次軟中斷總共使用的時間和循環次數,條件允許繼續執行軟中斷,循環次數減一,並跳轉到第4步。
我們具體看一下代碼,首先在irq_exit()中會檢查是否需要進行軟中斷處理:
void irq_exit(void) { #ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED local_irq_disable(); #else WARN_ON_ONCE(!irqs_disabled()); #endif account_irq_exit_time(current); /* 減少preempt_count的硬中斷計數器 */ preempt_count_sub(HARDIRQ_OFFSET); /* in_interrupt()會檢查preempt_count上的軟中斷計數器和硬中斷計數器來判斷是否處於中斷嵌套中 */ /* local_softirq_pending()則會檢查該CPU的__softirq_pending變量,是否有軟中斷掛起 */ if (!in_interrupt() && local_softirq_pending()) invoke_softirq(); tick_irq_exit(); rcu_irq_exit(); trace_hardirq_exit(); /* must be last! */ }
我們再進入到invoke_softirq():
static inline void invoke_softirq(void) { if (!force_irqthreads) { #ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK /* * We can safely execute softirq on the current stack if * it is the irq stack, because it should be near empty * at this stage. */ /* 軟中斷處理函數 */ __do_softirq(); #else /* * Otherwise, irq_exit() is called on the task stack that can * be potentially deep already. So call softirq in its own stack * to prevent from any overrun. */ do_softirq_own_stack(); #endif } else { /* 如果強制使用軟中斷線程進行軟中斷處理,會通知調度器喚醒軟中斷線程ksoftirqd */ wakeup_softirqd(); } }
重頭戲就在__do_softirq()中,我已經注釋好了,方便大家看:
1 asmlinkage __visible void __do_softirq(void) 2 { 3 /* 為了防止軟中斷執行時間太長,設置了一個軟中斷結束時間 */ 4 unsigned long end = jiffies + MAX_SOFTIRQ_TIME; 5 /* 保存當前進程的標志 */ 6 unsigned long old_flags = current->flags; 7 /* 軟中斷循環執行次數: 10次 */ 8 int max_restart = MAX_SOFTIRQ_RESTART; 9 /* 軟中斷的action指針 */ 10 struct softirq_action *h; 11 bool in_hardirq; 12 __u32 pending; 13 int softirq_bit; 14 15 /* 16 * Mask out PF_MEMALLOC s current task context is borrowed for the 17 * softirq. A softirq handled such as network RX might set PF_MEMALLOC 18 * again if the socket is related to swap 19 */ 20 current->flags &= ~PF_MEMALLOC; 21 22 /* 獲取此CPU的__softirq_pengding變量值 */ 23 pending = local_softirq_pending(); 24 /* 用於統計進程被軟中斷使用時間 */ 25 account_irq_enter_time(current); 26 27 /* 增加preempt_count軟中斷計數器,也表明禁止了調度 */ 28 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); 29 in_hardirq = lockdep_softirq_start(); 30 31 /* 循環10次的入口,每次循環都會把所有掛起需要執行的軟中斷執行一遍 */ 32 restart: 33 /* 該CPU的__softirq_pending清零,當前的__softirq_pending保存在pending變量中 */ 34 /* 這樣做就保證了新的軟中斷會在下次循環中執行 */ 35 set_softirq_pending(0); 36 37 /* 開中斷 */ 38 local_irq_enable(); 39 40 /* h指向軟中斷數組頭 */ 41 h = softirq_vec; 42 43 /* 每次獲取最高優先級的已掛起軟中斷 */ 44 while ((softirq_bit = ffs(pending))) { 45 unsigned int vec_nr; 46 int prev_count; 47 /* 獲取此軟中斷描述符地址 */ 48 h += softirq_bit - 1; 49 50 /* 減去軟中斷描述符數組首地址,獲得軟中斷號 */ 51 vec_nr = h - softirq_vec; 52 /* 獲取preempt_count的值 */ 53 prev_count = preempt_count(); 54 55 /* 增加統計中該軟中斷發生次數 */ 56 kstat_incr_softirqs_this_cpu(vec_nr); 57 58 trace_softirq_entry(vec_nr); 59 /* 執行該軟中斷的action操作 */ 60 h->action(h); 61 trace_softirq_exit(vec_nr); 62 63 /* 之前保存的preempt_count並不等於當前的preempt_count的情況處理,也是簡單的把之前的復制到當前的preempt_count上,這樣做是防止最后軟中斷計數不為0導致系統不能夠執行調度 */ 64 if (unlikely(prev_count != preempt_count())) { 65 pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n", 66 vec_nr, softirq_to_name[vec_nr], h->action, 67 prev_count, preempt_count()); 68 preempt_count_set(prev_count); 69 } 70 /* h指向下一個軟中斷,但下個軟中斷並不一定需要執行,這里只是配合softirq_bit做到一個處理 */ 71 h++; 72 pending >>= softirq_bit; 73 } 74 75 rcu_bh_qs(); 76 /* 關中斷 */ 77 local_irq_disable(); 78 79 /* 循環結束后再次獲取CPU的__softirq_pending變量,為了檢查是否還有軟中斷未執行 */ 80 pending = local_softirq_pending(); 81 /* 還有軟中斷需要執行 */ 82 if (pending) { 83 /* 在還有軟中斷需要執行的情況下,如果時間片沒有執行完,並且循環次數也沒到10次,繼續執行軟中斷 */ 84 if (time_before(jiffies, end) && !need_resched() && 85 --max_restart) 86 goto restart; 87 /* 這里是有軟中斷掛起,但是軟中斷時間和循環次數已經用完,通知調度器喚醒軟中斷線程去執行掛起的軟中斷,軟中斷線程是ksoftirqd,這里只起到一個通知作用,因為在中斷上下文中是禁止調度的 */ 88 wakeup_softirqd(); 89 } 90 91 lockdep_softirq_end(in_hardirq); 92 /* 用於統計進程被軟中斷使用時間 */ 93 account_irq_exit_time(current); 94 /* 減少preempt_count中的軟中斷計數器 */ 95 __local_bh_enable(SOFTIRQ_OFFSET); 96 WARN_ON_ONCE(in_interrupt()); 97 /* 還原進程標志 */ 98 tsk_restore_flags(current, old_flags, PF_MEMALLOC); 99 }
流程就和上面所說的一致,如果還有不懂,可以去內核代碼目錄/kernel/softirq.c查看源碼。
tasklet
軟中斷有多種,部分種類有自己特殊的處理,如從NET_TX_SOFTIRQ和NET_RT_SOFTIRQ、BLOCK_SOFTIRQ等,而如HI_SOFTIRQ和TASKLET_SOFTIRQ則是專門使用tasklet。它是在I/O驅動程序中實現可延遲函數的首選方法,如上一句所說,它建立在HI_SOFTIRQ和TASKLET_SOFTIRQ這兩種軟中斷之上,多個tasklet可以與同一個軟中斷相關聯,系統會使用一個鏈表組織他們,而每個tasklet執行自己的函數處理。而HI_SOFTIRQ和TASKLET_SOFTIRQ這兩個軟中斷並沒有什么區別,他們只是優先級上的不同而已,系統會先執行HI_SOFTIRQ的tasklet,再執行TASKLET_SOFTIRQ的tasklet。同一個tasklet不能同時在幾個CPU上執行,一個tasklet在一個時間上只能在一個CPU的軟中斷鏈上,不能同時在多個CPU的軟中斷鏈上,並且當這個tasklet正在執行時,其他CPU不能夠執行這個tasklet。也就是說,tasklet不必要編寫成可重入的函數。
系統會為每個CPU維護兩個鏈表,用於保存HI_SOFTIRQ的tasklet和TASKLET_SOFTIRQ的tasklet,這兩個鏈表是tasklet_vec和tasklet_hi_vec,它們都是雙向鏈表,如下:
struct tasklet_head { struct tasklet_struct *head; struct tasklet_struct **tail; }; static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec); static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
在softirq_init()函數中,會將每個CPU的tasklet_vec鏈表和tasklet_hi_vec鏈表進行初始化,將他們的頭尾相連,實現為一個空鏈表。由於tasklet_vec和tasklet_hi_vec處理方式幾乎一樣,只是軟中斷的優先級別不同,我們只需要理解系統如何對tasklet_vec進行處理即可。需要注意的是,tasklet_vec鏈表都是以順序方式執行,並不會出現后一個先執行,再到前一個先執行(在軟中斷期間被中斷的情況),之后的代碼我們詳細說明。
介紹完tasklet_vec和tasklet_hi_vec鏈表,我們來看看tasklet,tasklet簡單來說,就是一個處理函數的封裝,類似於硬中斷中的irqaction結構。一般來說,在一個驅動中如果需要使用tasklet進行軟中斷的處理,只需要一個中斷對應初始化一個tasklet,它可以在每次中斷產生時重復使用。系統使用tasklet_struct結構進行描述一個tasklet,而且對於同一個tasklet_struct你可以選擇放在tasklet_hi_vec鏈表或者tasklet_vec鏈表上。我們來看看:
struct tasklet_struct { struct tasklet_struct *next; /* 指向鏈表下一個tasklet */ unsigned long state; /* tasklet狀態 */ atomic_t count; /* 禁止計數器,調用tasklet_disable()會增加此數,tasklet_enable()減少此數 */ void (*func)(unsigned long); /* 處理函數 */ unsigned long data; /* 處理函數使用的數據 */ };
tasklet狀態主要分為以下兩種:
- TASKLET_STATE_SCHED:這種狀態表示此tasklet處於某個tasklet鏈表之上(可能是tasklet_vec也可能是tasklet_hi_vec)。
- TASKLET_STATE_RUN:表示此tasklet正在運行中。
這兩個狀態主要就是用於防止tasklet同時在幾個CPU上運行和在同一個CPU上交錯執行。
而func指針就是指向相應的處理函數。在編寫驅動時,我們可以使用tasklet_init()函數或者DECLARE_TASKLET宏進行一個task_struct結構的初始化,之后可以使用tasklet_schedule()或者tasklet_hi_schedule()將其放到相應鏈表上等待CPU運行。我們使用一張圖描述一下軟中斷和tasklet結合運行的情況:

我們知道,每個軟中斷都有自己的action函數,在HI_SOFTIRQ和TASKLET_SOFTIRQ的action函數中,就用到了它們對應的TASKLET_HI_VEC鏈表和TASKLET_VEC鏈表,並依次順序執行鏈表中的每個tasklet結點。
在SMP系統中,我們會遇到一個問題:兩個CPU都需要執行同一個tasklet的情況,雖然一個tasklet只能放在一個CPU的tasklet_vec鏈表或者tasklet_hi_vec鏈表上,但是這種情況是有可能發生的,我們設想一下,中斷在CPU1上得到了響應,並且它的tasklet放到了CPU1的tasklet_vec上進行執行,而當中斷的tasklet上正在執行時,此中斷再次發生,並在CPU2上進行了響應,此時CPU2將此中斷的tasklet放到CPU2的tasklet_vec上,並執行到此中斷的tasklet。
實際上,為了處理這種情況,在HI_SOFTIRQ和TASKLET_SOFTIRQ的action函數中,會先將對應的tasklet鏈表取出來,並把對應的tasklet鏈表的head和tail清空,如果在執行過程中,某個tasklet的state為TASKLET_STATE_RUN狀態,說明其他CPU正在處理這個tasklet,這時候當前CPU則會把此tasklet加入到當前CPU已清空的tasklet鏈表的末尾,然后設置__softirq_pending變量,這樣,在下次循環軟中斷的過程中,會再次檢查這個tasklet。也就是如果其他CPU的這個tasklet一直不退出,當前CPU就會不停的置位tasklet的pending,然后不停地循環檢查。
我們可以看看TASKLET_SOFTIRQ的action處理:
1 static void tasklet_action(struct softirq_action *a) 2 { 3 struct tasklet_struct *list; 4 5 local_irq_disable(); 6 /* 將tasklet鏈表從該CPU中拿出來 */ 7 list = __this_cpu_read(tasklet_vec.head); 8 /* 將該CPU的此軟中斷的tasklet鏈表清空 */ 9 __this_cpu_write(tasklet_vec.head, NULL); 10 __this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head)); 11 local_irq_enable(); 12 13 /* 鏈表已經處於list中,並且該CPU的tasklet_vec鏈表為空 */ 14 while (list) { 15 struct tasklet_struct *t = list; 16 17 list = list->next; 18 19 /* 檢查並設置該tasklet為TASKLET_STATE_RUN狀態 */ 20 if (tasklet_trylock(t)) { 21 /* 檢查是否被禁止 */ 22 if (!atomic_read(&t->count)) { 23 /* 清除其TASKLET_STATE_SCHED狀態 */ 24 if (!test_and_clear_bit(TASKLET_STATE_SCHED, 25 &t->state)) 26 BUG(); 27 /* 執行該tasklet的func處理函數 */ 28 t->func(t->data); 29 /* 清除該tasklet的TASKLET_STATE_RUN狀態 */ 30 tasklet_unlock(t); 31 continue; 32 } 33 tasklet_unlock(t); 34 } 35 36 /* 以下為tasklet為TASKLET_STATE_RUN狀態下的處理 */ 37 /* 禁止中斷 */ 38 local_irq_disable(); 39 /* 將此tasklet添加的該CPU的tasklet_vec鏈表尾部 */ 40 t->next = NULL; 41 *__this_cpu_read(tasklet_vec.tail) = t; 42 __this_cpu_write(tasklet_vec.tail, &(t->next)); 43 /* 設置該CPU的此軟中斷處於掛起狀態,設置irq_cpustat_t的__sofirq_pending變量,這樣在軟中斷的下次執行中會再次執行此tasklet */ 44 __raise_softirq_irqoff(TASKLET_SOFTIRQ); 45 /* 開啟中斷 */ 46 local_irq_enable(); 47 } 48 }
軟中斷處理線程
當有過多軟中斷需要處理時,為了保證進程能夠得到一個滿意的響應時間,設計時給定軟中斷一個時間片和循環次數,當時間片和循環次數到達但軟中斷又沒有處理完時,就會把剩下的軟中斷交給軟中斷處理線程進行處理,這個線程是一個內核線程,其作為一個普通進程,優先級是120。其核心處理函數是run_ksoftirqd(),其實此線程的處理也很簡單,就是調用了上面的__do_softirq()函數,我們可以具體看看:
1 /* 在smpboot_thread_fun的一個死循環中被調用 */ 2 static void run_ksoftirqd(unsigned int cpu) 3 { 4 /* 禁止中斷,在__do_softirq()中會開啟 */ 5 local_irq_disable(); 6 /* 檢查該CPU的__softirq_pending是否有軟中斷被掛起 */ 7 if (local_softirq_pending()) { 8 /* 9 * We can safely run softirq on inline stack, as we are not deep 10 * in the task stack here. 11 */ 12 /* 執行軟中斷 */ 13 __do_softirq(); 14 rcu_note_context_switch(cpu); 15 /* 開中斷 */ 16 local_irq_enable(); 17 /* 檢查是否需要調度 */ 18 cond_resched(); 19 return; 20 } 21 /* 開中斷 */ 22 local_irq_enable(); 23 }
