Linux中斷(interrupt)子系統之五:軟件中斷(softIRQ)

軟件中斷(softIRQ)是內核提供的一種延遲執行機制,它完全由軟件觸發,雖然說是延遲機制,實際上,在大多數情況下,它與普通進程相比,能得到更快的響應時間。軟中斷也是其他一些內核機制的基礎,比如tasklet,高分辨率timer等。

/*****************************************************************************************************/
聲明:本博內容均由http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/

1.  軟件中斷的數據結構

1.1  struct softirq_action

        內核用softirq_action結構管理軟件中斷的註冊和激活等操作,它的定義如下:

點擊(此處)摺疊或打開

  1. struct softirq_action
  2. {
  3.     void    (*action)(struct softirq_action *);
  4. };

非常簡單,只有一個用於回調的函數指針。軟件中斷的資源是有限的,內核目前只實現了10種類型的軟件中斷,它們是:

點擊(此處)摺疊或打開

  1. enum
  2. {
  3.     HI_SOFTIRQ=0,
  4.     TIMER_SOFTIRQ,
  5.     NET_TX_SOFTIRQ,
  6.     NET_RX_SOFTIRQ,
  7.     BLOCK_SOFTIRQ,
  8.     BLOCK_IOPOLL_SOFTIRQ,
  9.     TASKLET_SOFTIRQ,
  10.     SCHED_SOFTIRQ,
  11.     HRTIMER_SOFTIRQ,
  12.     RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */

  13.     NR_SOFTIRQS
  14. };

內核的開發者們不建議我們擅自增加軟件中斷的數量,如果需要新的軟件中斷,儘可能把它們實現爲基於軟件中斷的tasklet形式。與上面的枚舉值相對應,內核定義了一個softirq_action的結構數組,每種軟中斷對應數組中的一項:

點擊(此處)摺疊或打開

  1. static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

1.2  irq_cpustat_t

        多個軟中斷可以同時在多個cpu運行,就算是同一種軟中斷,也有可能同時在多個cpu上運行。內核爲每個cpu都管理着一個待決軟中斷變量(pending),它就是irq_cpustat_t:

點擊(此處)摺疊或打開

  1. typedef struct {
  2.     unsigned int __softirq_pending;
  3. } ____cacheline_aligned irq_cpustat_t;

點擊(此處)摺疊或打開

  1. irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;

__softirq_pending字段中的每一個bit,對應着某一個軟中斷,某個bit被置位,說明有相應的軟中斷等待處理。

1.3  軟中斷的守護進程ksoftirqd

        在cpu的熱插拔階段,內核爲每個cpu創建了一個用於執行軟件中斷的守護進程ksoftirqd,同時定義了一個per_cpu變量用於保存每個守護進程的task_struct結構指針:

點擊(此處)摺疊或打開

  1. DEFINE_PER_CPU(struct task_struct *, ksoftirqd);

大多數情況下,軟中斷都會在irq_exit階段被執行,在irq_exit階段沒有處理完的軟中斷纔有可能會在守護進程中執行。

2.  觸發軟中斷

        要觸發一個軟中斷,只要調用api:raise_softirq即可,它的實現很簡單,先是關閉本地cpu中斷,然後調用:raise_softirq_irqoff

點擊(此處)摺疊或打開

  1. void raise_softirq(unsigned int nr)
  2. {
  3.     unsigned long flags;

  4.     local_irq_save(flags);
  5.     raise_softirq_irqoff(nr);
  6.     local_irq_restore(flags);
  7. }

再看看raise_softirq_irqoff:

點擊(此處)摺疊或打開

  1. inline void raise_softirq_irqoff(unsigned int nr)
  2. {
  3.     __raise_softirq_irqoff(nr);

  4.         ......
  5.     if (!in_interrupt())
  6.         wakeup_softirqd();
  7. }

先是通過__raise_softirq_irqoff設置cpu的軟中斷pending標誌位(irq_stat[NR_CPUS] ),然後通過in_interrupt判斷現在是否在中斷上下文中,或者軟中斷是否被禁止,如果都不成立,則喚醒軟中斷的守護進程,在守護進程中執行軟中斷的回調函數。否則什麼也不做,軟中斷將會在中斷的退出階段被執行。

3.  軟中斷的執行

        基於上面所說,軟中斷的執行既可以守護進程中執行,也可以在中斷的退出階段執行。實際上,軟中斷更多的是在中斷的退出階段執行(irq_exit),以便達到更快的響應,加入守護進程機制,只是擔心一旦有大量的軟中斷等待執行,會使得內核過長地留在中斷上下文中。

3.1  在irq_exit中執行
        看看irq_exit的部分:

點擊(此處)摺疊或打開

  1. void irq_exit(void)
  2. {
  3.         ......
  4.     sub_preempt_count(IRQ_EXIT_OFFSET);
  5.     if (!in_interrupt() && local_softirq_pending())
  6.         invoke_softirq();
  7.         ......
  8. }

如果中斷髮生嵌套,in_interrupt()保證了只有在最外層的中斷的irq_exit階段,invoke_interrupt纔會被調用,當然,local_softirq_pending也會實現判斷當前cpu有無待決的軟中斷。代碼最終會進入__do_softirq中,內核會保證調用__do_softirq時,本地cpu的中斷處於關閉狀態,進入__do_softirq:

點擊(此處)摺疊或打開

  1. asmlinkage void __do_softirq(void)
  2. {
  3.         ......
  4.     pending = local_softirq_pending();

  5.     __local_bh_disable((unsigned long)__builtin_return_address(0),
  6.                 SOFTIRQ_OFFSET);
  7. restart:
  8.     /* Reset the pending bitmask before enabling irqs */
  9.     set_softirq_pending(0);

  10.     local_irq_enable();

  11.     h = softirq_vec;

  12.     do {
  13.         if (pending & 1) {
  14.      ......
  15.             trace_softirq_entry(vec_nr);
  16.             h->action(h);
  17.             trace_softirq_exit(vec_nr);
  18.                         ......
  19.         }
  20.         h++;
  21.         pending >>= 1;
  22.     } while (pending);

  23.     local_irq_disable();

  24.     pending = local_softirq_pending();
  25.     if (pending && --max_restart)
  26.         goto restart;

  27.     if (pending)
  28.         wakeup_softirqd();

  29.     lockdep_softirq_exit();

  30.     __local_bh_enable(SOFTIRQ_OFFSET);
  31. }

  • 首先取出pending的狀態;
  • 禁止軟中斷,主要是爲了防止和軟中斷守護進程發生競爭;
  • 清除所有的軟中斷待決標誌;
  • 打開本地cpu中斷;
  • 循環執行待決軟中斷的回調函數;
  • 如果循環完畢,發現新的軟中斷被觸發,則重新啓動循環,直到以下條件滿足,才退出:
    • 沒有新的軟中斷等待執行;
    • 循環已經達到最大的循環次數MAX_SOFTIRQ_RESTART,目前的設定值時10次;
  • 如果經過MAX_SOFTIRQ_RESTART次循環後還未處理完,則激活守護進程,處理剩下的軟中斷;
  • 推出前恢復軟中斷;

3.2  在ksoftirqd進程中執行
        從前面幾節的討論我們可以看出,軟中斷也可能由ksoftirqd守護進程執行,這要發生在以下兩種情況下:
  • 在irq_exit中執行軟中斷,但是在經過MAX_SOFTIRQ_RESTART次循環後,軟中斷還未處理完,這種情況雖然極少發生,但畢竟有可能;
  • 內核的其它代碼主動調用raise_softirq,而這時正好不是在中斷上下文中,守護進程將被喚醒;
守護進程最終也會調用__do_softirq執行軟中斷的回調,具體的代碼位於run_ksoftirqd函數中,內核會關閉搶佔的情況下執行__do_softirq,具體的過程這裏不做討論。

4.  tasklet

       因爲內核已經定義好了10種軟中斷類型,並且不建議我們自行添加額外的軟中斷,所以對軟中斷的實現方式,我們主要是做一個簡單的瞭解,對於驅動程序的開發者來說,無需實現自己的軟中斷。但是,對於某些情況下,我們不希望一些操作直接在中斷的handler中執行,但是又希望在稍後的時間裏得到快速地處理,這就需要使用tasklet機制。 tasklet是建立在軟中斷上的一種延遲執行機制,它的實現基於TASKLET_SOFTIRQ和HI_SOFTIRQ這兩個軟中斷類型。 4.1  tasklet_struct        

在軟中斷的初始化函數softirq_init的最後,內核註冊了TASKLET_SOFTIRQ和HI_SOFTIRQ這兩個軟中斷:

點擊(此處)摺疊或打開

  1. void __init softirq_init(void)
  2. {
  3.         ......
  4.     open_softirq(TASKLET_SOFTIRQ, tasklet_action);
  5.     open_softirq(HI_SOFTIRQ, tasklet_hi_action);
  6. }

內核用一個tasklet_struct來表示一個tasklet,它的定義如下:

點擊(此處)摺疊或打開

  1. struct tasklet_struct
  2. {
  3.     struct tasklet_struct *next;
  4.     unsigned long state;
  5.     atomic_t count;
  6.     void (*func)(unsigned long);
  7.     unsigned long data;
  8. };

next用於把同一個cpu的tasklet鏈接成一個鏈表,state用於表示該tasklet的當前狀態,目前只是用了最低的兩個bit,分別用於表示已經準備被調度執行和已經在另一個cpu上執行:

點擊(此處)摺疊或打開

  1. enum
  2. {
  3.     TASKLET_STATE_SCHED,    /* Tasklet is scheduled for execution */
  4.     TASKLET_STATE_RUN    /* Tasklet is running (SMP only) */
  5. };

原子變量count用於tasklet對tasklet_disable和tasklet_enable的計數,count爲0時表示允許tasklet執行,否則不允許執行,每次tasklet_disable時,該值加1,tasklet_enable時該值減1。func是tasklet被執行時的回調函數指針,data則用作回調函數func的參數。 4.2  初始化一個tasklet

有兩種辦法初始化一個tasklet,第一種是靜態初始化,使用以下兩個宏,這兩個宏定義一個tasklet_struct結構,並用相應的參數對結構中的字段進行初始化:
  • DECLARE_TASKLET(name, func, data);定義名字爲name的tasklet,默認爲enable狀態,也就是count字段等於0。
  • DECLARE_TASKLET_DISABLED(name, func, data);定義名字爲name的tasklet,默認爲enable狀態,也就是count字段等於1。
第二個是動態初始化方法:先定義一個tasklet_struct,然後用tasklet_init函數進行初始化,該方法默認tasklet處於enable狀態:

點擊(此處)摺疊或打開

  1. struct tasklet_struct tasklet_xxx;
  2. ......
  3. tasklet_init(&tasklet_xxx, func, data);

4.3  tasklet的使用方法

使能和禁止tasklet,使用以下函數:
  • tasklet_disable()  通過給count字段加1來禁止一個tasklet,如果tasklet正在運行中,則等待運行完畢才返回(通過TASKLET_STATE_RUN標誌)。
  • tasklet_disable_nosync()  tasklet_disable的異步版本,它不會等待tasklet運行完畢。
  • tasklet_enable()  使能tasklet,只是簡單地給count字段減1。
調度tasklet的執行,使用以下函數:
  • tasklet_schedule(struct tasklet_struct *t)  如果TASKLET_STATE_SCHED標誌爲0,則置位TASKLET_STATE_SCHED,然後把tasklet掛到該cpu等待執行的tasklet鏈表上,接着發出TASKLET_SOFTIRQ軟件中斷請求。
  • tasklet_hi_schedule(struct tasklet_struct *t)  效果同上,區別是它發出的是HI_SOFTIRQ軟件中斷請求。
銷燬tasklet,使用以下函數:
  • tasklet_kill(struct tasklet_struct *t)  如果tasklet處於TASKLET_STATE_SCHED狀態,或者tasklet正在執行,則會等待tasklet執行完畢,然後清除TASKLET_STATE_SCHED狀態。

4.4  tasklet的內部執行機制

內核爲每個cpu用定義了一個tasklet_head結構,用於管理每個cpu上的tasklet的調度和執行:

點擊(此處)摺疊或打開

  1. struct tasklet_head
  2. {
  3.     struct tasklet_struct *head;
  4.     struct tasklet_struct **tail;
  5. };

  6. static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
  7. static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

回到4.1節,我們知道,tasklet是利用TASKLET_SOFTIRQ和HI_SOFTIRQ這兩個軟中斷來實現的,兩個軟中斷只是有優先級的差別,所以我們只討論TASKLET_SOFTIRQ的實現,TASKLET_SOFTIRQ的中斷回調函數是tasklet_action,我們看看它的代碼:

點擊(此處)摺疊或打開

  1. static void tasklet_action(struct softirq_action *a)
  2. {
  3.     struct tasklet_struct *list;

  4.     local_irq_disable();
  5.     list = __this_cpu_read(tasklet_vec.head);
  6.     __this_cpu_write(tasklet_vec.head, NULL);
  7.     __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);
  8.     local_irq_enable();

  9.     while (list) {
  10.         struct tasklet_struct *= list;

  11.         list = list->next;

  12.         if (tasklet_trylock(t)) {
  13.             if (!atomic_read(&t->count)) {
  14.                 if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
  15.                     BUG();
  16.                 t->func(t->data);
  17.                 tasklet_unlock(t);
  18.                 continue;
  19.             }
  20.             tasklet_unlock(t);
  21.         }

  22.         local_irq_disable();
  23.         t->next = NULL;
  24.         *__this_cpu_read(tasklet_vec.tail) = t;
  25.         __this_cpu_write(tasklet_vec.tail, &(t->next));
  26.         __raise_softirq_irqoff(TASKLET_SOFTIRQ);
  27.         local_irq_enable();
  28.     }
  29. }

解析如下:
  • 關閉本地中斷的前提下,移出當前cpu的待處理tasklet鏈表到一個臨時鏈表後,清除當前cpu的tasklet鏈表,之所以這樣處理,是爲了處理當前tasklet鏈表的時候,允許新的tasklet被調度進待處理鏈表中。
  • 遍歷臨時鏈表,用tasklet_trylock判斷當前tasklet是否已經在其他cpu上運行,而且tasklet沒有被禁止:
    • 如果沒有運行,也沒有禁止,則清除TASKLET_STATE_SCHED狀態位,執行tasklet的回調函數。
    • 如果已經在運行,或者被禁止,則把該tasklet重新添加會當前cpu的待處理tasklet鏈表上,然後觸發TASKLET_SOFTIRQ軟中斷,等待下一次軟中斷時再次執行。
分析到這了我有個疑問,看了上面的代碼,如果一個tasklet被tasklet_schedule後,在沒有被執行前被tasklet_disable了,豈不是會無窮無盡地引發TASKLET_SOFTIRQ軟中斷?
通過以上的分析,我們需要注意的是,tasklet有以下幾個特徵:
  • 同一個tasklet只能同時在一個cpu上執行,但不同的tasklet可以同時在不同的cpu上執行;
  • 一旦tasklet_schedule被調用,內核會保證tasklet一定會在某個cpu上執行一次;
  • 如果tasklet_schedule被調用時,tasklet不是出於正在執行狀態,則它只會執行一次;
  • 如果tasklet_schedule被調用時,tasklet已經正在執行,則它會在稍後被調度再次被執行;
  • 兩個tasklet之間如果有資源衝突,應該要用自旋鎖進行同步保護;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章