Linux 2.6中斷下半部機制分析

 http://hi.baidu.com/starmyth/blog/item/5ee2f6ceb80f730093457e70.html

Linux 2.6中斷下半部機制分析

作者:流星


摘要    本文主要從使用者的角度對Linux 2.6內核的下半部機制softirq、tasklet和workqueue進行分析,對於這三種機制在內核中的具體實現並未進行深入分析,倘若讀者有興趣瞭解,可以直接閱讀Linux內核源代碼的相關部分。

說明    本文檔由流星自網上收集整理,按照自由軟件開放源代碼的精神發佈,任何人可以免費獲得、使用和重新發布,但是你沒有限制別人重新發布你發佈內容的權利。發佈本文的目的是希望它能對讀者有用,但沒有任何擔保,甚至沒有適合特定目的的隱含的擔保。更詳細的情況請參閱GNU通用公共許可證(GPL),以及GNU自由文檔協議(GFDL)。


                  目 錄
1 概述
2 Linux 2.6內核中斷下半部機制
    2.1 softirq機制
    2.2 tasklet機制
    2.3 workqueue機制
3 幾種下半部機制的比較
4 下半部機制的選擇
5 Linux與NGSA的下半部機制比較
    5.1 NGSA中斷下半部機制分析
    5.2 NGSA下半部機制缺陷分析


1 概述
中斷服務程序往往都需要在CPU關中斷的情況下運行,以避免中斷嵌套而使控制複雜化,但是關中斷的時間又不能太長,否則會造成中斷信號的丟失。爲此,在Linux中,將中斷處理程序分爲兩部分,即上半部和下半部。上半部通常用於執行跟硬件關係密切的關鍵程序,這部分執行時間非常短,而且是在關中斷的環境下運行的。對時間要求不是很嚴格,而且通常比較耗時的一些操作,則交給下半部來執行,這部分代碼是在開中斷中執行的。上半部處理硬件相關,稱爲硬件中斷,這通常需要立即執行。下半部則可以延遲一定時間,在內核合適的時間段來執行程序,這就是我們這裏要討論的軟中斷。
本文以目前最新版本的Linux內核2.6.22爲例,來討論Linux的中斷下半部機制。在2.6版本的內核中,下半部機制主要由softirq、tasklet和workqueue來實現,下面着重對這3種機制進行分析。


2 Linux 2.6內核中斷下半部機制
老版本的Linux內核中,下半部是以一種叫做Bottom Half(簡稱爲BH)的機制來實現的,最初它是藉助中斷向量來實現的,在系統中用一組(共32個)函數指針,分別表示32箇中斷向量,這種實現方式目前在2.4版本的內核中還可以看到它的身影。但是目前在2.6版本的內核中已經看不到它了。現在的Linux內核,一般以一種稱爲softirq的軟中斷機制來實現下半部。

2.1 softirq機制
原來的BH機制有兩個明顯的缺陷:一是系統中一次只能有一個CPU可以執行BH代碼,二是BH函數不允許嵌套。這在單處理器系統中或許沒關係,但在SMP系統中卻是致命的缺陷。但是軟中斷機制就不一樣了。Linux的softirq機制與SMP是緊密相連的,整個softirq機制的設計與實現始終貫穿着一個思想:“誰觸發,誰執行”(Who marks, who runs),也就是說,每個CPU都單獨負責它所觸發的軟中斷,互不干擾。這就有效地利用了SMP系統的性能和特點,極大地提高了處理效率。
Linux在include/linux/interrupt.h中定義了一個softirq_action結構來描述一個softirq請求,如下所示:
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
其中,函數指針action指向軟中斷請求的服務函數,而data則指向由服務函數自行解釋的參數數據。
基於上述結構,系統在kernel/softirq.c中定義了一個全局的softirq軟中斷向量表softirq_vec[32],對應32個softirq_action結構表示的軟中斷描述符。但實際上,Linux並沒有使用到32個軟中斷向量,內核預定義了一些軟中斷向量的含義供我們使用:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
#ifdef CONFIG_HIGH_RES_TIMERS
HRTIMER_SOFTIRQ,
#endif
};
其中HI_SOFTIRQ用於實現高優先級的軟中斷,比如高優先級的hi_tasklet,而TASKLET_SOFTIRQ則用於實現諸如tasklet這樣的一般性軟中斷。關於tasklet,我們在後面會進行介紹。我們不需要使用到32個軟中斷向量,事實上,內核預定義的軟中斷向量已經可以滿足我們絕大多數應用的需求。其他向量保留給今後內核擴展使用,我們不應去使用它們。
要使用softirq,我們必須先初始化它。我們使用open_softirq()函數來開啓一個指定的軟中斷向量nr,初始化nr對應的描述符softirq_vec[nr],設置所有CPU的軟中斷掩碼的相應位爲1。函數do_softirq()負責執行數組softirq_vec[32]中設置的軟中斷服務函數。每個CPU都是通過執行這個函數來執行軟中斷服務的。由於同一個CPU上的軟中斷服務例程不允許嵌套,因此,do_softirq()函數一開始就檢查當前CPU是否已經正處在中斷服務中,如果是則立即返回。在同一個CPU上,do_softirq()是串行執行的。
使用open_softirq()註冊完一個軟中斷之後,我們需要觸發它。內核使用函數raise_softirq()來觸發一個軟中斷。對於一個指定的softirq來說,只會有一個處理函數,這個處理函數是所有CPU共享的。由於同一個softirq的處理函數可能在不同的CPU上同時執行,併產生競爭條件,處理函數本身的同步機制是非常重要的。激活一個軟中斷一般在中斷的上半部中執行。當一箇中斷處理程序想要激活一個軟中斷時,raise_softirq()就會被調用。在後來的某個時刻,當do_softirq()在某個CPU上運行時,就會調用相關的軟中斷處理函數。
需要注意的是,在softirq機制中,還包含有一個很小的內核線程ksoftirqd。這是爲了平衡系統負載而設的。試想,如果系統一直不斷觸發軟中斷請求,CPU就會不斷地去處理軟中斷,因爲至少每次時鐘中斷都會執行一次do_softirq()。這樣一來,系統中其他重要任務不是要因長期得不到CPU而一直處於飢餓狀態嗎?在系統繁忙的時候,這個小小的內核線程就顯得特別有用了,過多的軟中斷請求會被放到系統合適的時間段執行,給其他進程更多的執行機會。
在2.6內核中,do_softirq()被放到irq_exit()中執行。在中斷上半部的處理中,只在irq_exit()中才調用do_softirq()進行軟中斷的處理,這非常有利於軟中斷模塊的升級和移植。如果需要在我們的NGSA中移植Linux的軟中斷,這樣的處理確實給了我們許多便利,因爲我們只需要對我們的中斷上半部的執行作很小的改動。如果在中斷上半部有許多軟中斷調用的入口,那我們的移植豈不是會很痛苦?
可能有人會產生這樣的疑問:系統中最多可以有32個softirq,那麼這麼多softirq,CPU是如何查找的呢?顯然,我們在執行raise_softirq()對軟中斷進行觸發時,必須要有一個很好的機制保證這個觸發動作能夠快速準確地進行。在Linux中,我們使用一種結構irq_cpustat_t來組織軟中斷。它在include/asm-xxx/hardirq.h中定義,其中xxx表示相應的處理器體系結構。比如對於PowerPC處理器,這個結構在include/asm-powerpc/hardirq.h中定義如下:
typedef struct{
unsigned int __softirq_pending; /* set_bit is used on this */
unsigned int __last_jiffy_stamp;
} ____cacheline_aligned irq_cpustat_t;

extern irq_cpustat_t irq_stat[];  /* defined in asm/hardirq.h */
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
其中,__softirq_pending成員使用bit map的方式來指示相應的softirq是否激活(即是否處於pending狀態)。raise_softirq的主要工作就是在__softirq_pending中設置softirq的相應位,它的實現如下:
void fastcall raise_softirq(unsigned int nr)
{
unsigned long flags;

local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}

inline fastcall void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);

if (!in_interrupt())
   wakeup_softirqd();    /* 喚醒內核線程ksoftirqd */
}

#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)

#define or_softirq_pending(x) (local_softirq_pending() |= (x))

#define local_softirq_pending() \
__IRQ_STAT(smp_processor_id(), __softirq_pending)
這裏有一個宏函數local_softirq_pending(),其實就是用於返回當前cpu的相應irq_cpustat_t結構irq_stat[cpu]的__softirq_pending成員值。因此__raise_softirq_irqoff(nr)的作用就是把要觸發的softirq在__softirq_pending中的相應位置1,在do_softirq()中則通過檢查irq_stat[cpu]中相應的pending位是否設置來執行該softirq。

2.2 tasklet機制
tasklet實際上是一種較爲特殊的軟中斷,軟中斷向量HI_SOFTIRQ和TASKLET_SOFTIRQ均是用tasklet機制來實現的。tasklet一詞原意爲“小片任務”,在這裏指一小段可執行的代碼。從某種程度上來講,tasklet機制是Linux內核對BH機制的一種擴展,但是它和BH不同,不同的tasklet代碼在同一時刻可以在多個CPU上並行執行。同時,它又和一般的softirq軟中斷不一樣,一段tasklet代碼在同一時刻只能在一個CPU上運行,而softirq中註冊的軟中斷服務函數(即softirq_action結構中的action函數指針)在同一時刻可以被多個CPU併發地執行。
Linux內核用tasklet_struct結構來描述一個tasklet,該結構也是定義在include/linux/interrupt.h中的,如下所示:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
其中,各個成員的含義如下:
(1)next指針指向下一個tasklet,它用於將多個tasklet連接成一個單向循環鏈表。爲此,內核還專門在softirq.c中定義了一個tasklet_head結構用來表示tasklet隊列:
struct tasklet_head
{
struct tasklet_struct *list;
};
(2)state定義了tasklet的當前狀態,這是一個32位無符號整數,不過目前只使用了bit 0和bit 1,bit 0爲1表示tasklet已經被調度去執行了,而bit 1是專門爲SMP系統設置的,爲1時表示tasklet當前正在某個CPU上執行,這是爲了防止多個CPU同時執行一個tasklet的情況。內核對這兩個位的含義也進行了預定義:
enum
{
TASKLET_STATE_SCHED,/* Tasklet is scheduled for execution */
TASKLET_STATE_RUN     /* Tasklet is running (SMP only) */
};

(3)count是一個原子計數,對tasklet的引用進行計數。需要注意的是,只有當count的值爲0的時候,tasklet代碼段才能執行,即這個時候該tasklet纔是enable的;如果count值非0,則該tasklet是被禁止的(disable)。因此,在執行tasklet代碼段之前,必須先檢查其原子值count是否爲0。
(4)func是一個函數指針,指向一個可執行的tasklet代碼段,data是func函數的參數。

tasklet的使用其實很簡單:先定義一個tasklet執行函數,然後用該函數去初始化一個tasklet描述符,接着使用tasklet的軟中斷觸發函數去登記定義好的tasklet,以便讓系統在適當的時候調度它運行。
內核爲tasklet準備了兩個宏定義用於聲明並初始化一個tasklet描述符:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = {NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = {NULL, 0, ATOMIC_INIT(1), func, data }
從上面的定義可以看出,DECLARE_TASKLET在初始化一個tasklet之後,該tasklet是enable的,而DECLARE_TASKLET_DISABLED則用於初始化並disable一個tasklet。
tasklet的enable和disable操作總是成對出現,分別使用tasklet_enable()函數和tasklet_disable()函數實現。
初始化指定tasklet描述符的一般操作是用tasklet_init()來實現的,而tasklet_kill()則用來將一個tasklet殺死,即恢復到未調度的狀態。如果tasklet還未執行完,內核會先等待它執行完畢。需要注意的是,由於調用該函數可能會導致休眠,所以禁止在中斷上下文中調用它。
儘管tasklet機制是特定於軟中斷向量HI_SOFTIRQ和TASKLET_SOFTIRQ的一種實現,但是tasklet機制仍然屬於softirq機制的整體框架範圍內的,因此,它的設計與實現仍然必須堅持“誰觸發,誰執行”的思想。爲此,Linux爲系統中的每一個CPU都定義了一個tasklet隊列頭部,來表示應該由各個CPU負責執行的tasklet隊列。
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec) = {NULL };
static DEFINE_PER_CPU(structtasklet_head, tasklet_hi_vec) = { NULL };
其中,軟中斷向量TASKLET_SOFTIRQ和HI_SOFTIRQ的執行分別由各自的軟中斷服務程序tasklet_action()函數和tasklet_hi_action()函數來實現,這是在softirq_init()函數中指定的。前面講到tasklet初始化完畢必須使用觸發函數去登記,系統才能在適當的時候執行它們,這兩個軟中斷的觸發,分別是由函數tasklet_schedule()和tasklet_hi_schedule()來執行的。

2.3 workqueue機制
由於BH機制本身的侷限性,早在2.0內核中就開始使用task queue(任務隊列)機制對其進行了擴充。而在2.6內核中,則使用了另外一種機制workqueue(工作隊列)來替換任務隊列。
workqueue看起來有點兒類似於tasklet,它也允許內核代碼請求在將來某個時間調用一個函數,所不同的是,workqueue是運行於一個特殊的內核進程上下文中的,而tasklet是運行於中斷上下文中的,它的執行必須是短暫的,而且是原子態的。另外一個和tasklet不同的是,你可以請求工作隊列函數被延後一個明確的時間間隔後再執行。workqueue通常用來處理不是很緊急的事件,因此它往往有比tasklet更高的執行週期,但不需要是原子操作,而且允許睡眠。
workqueue機制在include/linux/workqueue.h和kernel/workqueue.c中定義和實現。工作隊列由workqueue_struct結構來維護,定義如下:
struct workqueue_struct {
struct cpu_workqueue_struct *cpu_wq;
struct list_head list;
const char *name;
int singlethread;
int freezeable;   /* Freeze threads during suspend */
};
其中,cpu_workqueue_struct結構是針對每個CPU定義的。對於每一個CPU,內核都爲它掛接一個工作隊列,這樣就可以將新的工作動態放入到不同的CPU下的工作隊列中去,以此體現對“負載平衡”的支持(將work分配到各個CPU)。該結構定義如下:
struct cpu_workqueue_struct {

spinlock_t lock; /* 結構鎖 */

struct list_head worklist;     /* 工作列表 */
wait_queue_head_t more_work;        /* 要進行處理的等待隊列 */
struct work_struct *current_work;  
/* 處理完畢的等待隊列   */

struct workqueue_struct *wq;   /* 工作隊列節點 */
struct task_struct *thread;    
/* 工作者線程指針 */

int run_depth;   /* Detect run_workqueue() recursion depth */
} ____cacheline_aligned;
我們看到,在上面有一個work_struct結構,稱作工作節點結構。要提交一個任務給一個工作隊列,你必須填充一個工作節點。該結構定義如下:
struct work_struct {
atomic_long_t data;
#define WORK_STRUCT_PENDING 0   /* T if work item pending execution */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
struct list_head entry;   /* 連接所有工作的鏈表節點 */
work_func_t func;         /* 工作隊列函數指針,指向具體需要處理的工作 */
};
爲了方便對工作隊列的維護,內核創建了一個工作隊列鏈表,所有的工作隊列都可以掛接到這個鏈表上來:
static LIST_HEAD(workqueues);
工作隊列任務可以靜態或動態地創建,它創建時需要填充一個work_struct結構。內核提供了一個宏定義用來方便地聲明並初始化一個工作隊列任務:
#define DECLARE_WORK(n, f)      \
struct work_struct n = __WORK_INITIALIZER(n, f)
如果你想在運行時動態地初始化工作隊列任務,或者重新建立一個工作任務結構,你需要下面2個接口:
#define PREPARE_WORK(_work, _func)     \
do {        \
   (_work)->func = (_func);    \
} while (0)
#define INIT_WORK(_work, _func)       \
do {         \
   (_work)->data = (atomic_long_t) WORK_DATA_INIT(); \
   INIT_LIST_HEAD(&(_work)->entry);    \
   PREPARE_WORK((_work), (_func));     \
} while (0)
其實只要用到INIT_WORK即可,PREPARE_WORK在INIT_WORK中調用。
工作隊列的使用,其實也很簡單。首先你需要建立一個工作隊列,這一般通過函數create_workqueue(name)來實現,其中name是工作隊列的名字。它會爲每個CPU創建一個工作線程。當然,如果你覺得單線程用來處理你的工作已經足夠,你也可以使用函數create_singlethread_workqueue(name)來創建單線程的工作隊列。然後你需要把你所要做的工作提交給該工作隊列。首先創建工作隊列的任務,這在上面已經講過了,接着使用函數queue_work(wq, work)把創建好的任務提交給工作隊列,其中wq是要提交任務的工作隊列,work是一個work_struct結構,就是你所要提交的任務。當你想要延後一段時間再提交你的任務,那麼你可以使用queue_delayed_work(wq, work, delay)來提交,delay是你要延後的時間,以tick爲單位,delay保證你的任務至少在指定的最小延遲之後纔可能得到執行。當然了,由於delay任務的提交需要用到timer,因此你應當用另外一個結構delayed_work來替代work_struct,它實際上是在work_struct結構的基礎上再增加一個timer而已:
struct delayed_work {
struct work_struct work;
struct timer_list timer;
};
相應地,初始化工作任務的接口應該改爲DECLARE_DELAYED_WORK和INIT_DELAYED_WORK:
#define DECLARE_DELAYED_WORK(n, f)     \
struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f)

#define PREPARE_DELAYED_WORK(_work, _func)    \
PREPARE_WORK(&(_work)->work, (_func))
#define INIT_DELAYED_WORK(_work, _func)     \
do {        \
   INIT_WORK(&(_work)->work, (_func));   \
   init_timer(&(_work)->timer);    \
} while (0)
工作隊列中的任務由相關的工作線程執行,可能是在一個無法預期的時間段內執行,這要取決於系統的負載、中斷等等因素,或者至少要在延遲一段時間以後執行。如果你的任務在一個工作隊列中等待了無限長的時間都無法得到運行,那麼你可以用下面的方法取消它:
int cancel_delayed_work(structdelayed_work *work);
如果當一個取消操作的調用返回時任務正在執行,那麼這個任務將會繼續執行下去,不會因爲你的取消而終止,但是它不會再加入到工作隊列中來。你可以使用下面的方法清除工作隊列中的所有任務:
void flush_workqueue(struct workqueue_struct *wq);
如果工作隊列中還有已經提交的任務還沒執行完,那麼內核會進入等待,直到所有提交的任務都執行完畢爲止。flush_workqueue確保所有提交的任務都能執行完,這在設備驅動關閉時候的處理程序中特別有用。
當你用完了一個工作隊列,你可以銷燬它:
void destroy_workqueue(struct workqueue_struct *queue);
需要注意的是,destroy一個workqueue時,如果隊列上還有未完成的任務,該函數首先會執行它們。destroy操作保證所有未處理的任務在工作隊列被銷燬之前都能順利完成,所以你不必擔心,當你想要銷燬工作隊列時,是否還有工作未完成。

由於工作隊列運行在內核進程的上下文中,執行過程可能休眠,因此,工作隊列處理的應該是那些不是很緊急的任務,通常在系統空閒時執行。
在workqueue的初始化函數中,定義了一個針對內核中所有線程可用的事件工作隊列keventd_wq,其他內核線程建立的事件工作結構就都掛到該隊列上來:
static struct workqueue_struct *keventd_wq __read_mostly;

void __init init_workqueues(void)
{
/* …… */
keventd_wq = create_workqueue("events");
/* …… */
}
使用內核提供的事件工作隊列keventd_wq,事實上,你提交工作任務只需要使用schedule_work(work)或schedule_delayed_work(work)即可。
我們在編寫設備驅動的時候,並非所有驅動程序都需要有自己的工作隊列的。事實上,一個工作隊列,在許多情況下,都不需要建立自己的工作隊列。如果只偶爾提交任務給工作隊列,簡單地使用內核提供的共享的缺省工作隊列,或許會更有效。不過,由於這個工作隊列可能是由很多驅動程序共享的,任務可能會需要比較長的一段時間後才能開始執行。爲了解決這個問題,工作函數的延遲應該保持最小,或者乾脆不要。

對於工作隊列,有必要補充說明的一點是,工作隊列是在2.5內核開發版本中引入的用來替代任務隊列的,它的數據結構比較複雜。或許到現在,你還對上面3個數據結構的關係感到混亂,理不出頭緒來。在這裏,我們把3個數據結構放在一起,對它們的關係進行一點說明。這3個數據結構的關係如下圖所示:

從上面的圖可以看出,位於最高一層的是工作者線程(worker_thread),就是我們在cpu_workqueue_struct結構中看到的thread成員。內核爲每個CPU創建了一個工作者線程,關聯一個cpu_workqueue_struct結構。每個工作者線程都是一個特定的內核線程,它們都會執行worker_thread()函數,它初始化完畢後,就開始執行一個死循環並休眠。當有任務提交給工作隊列時,線程會被喚醒,以便執行這些任務,否則就繼續休眠。
工作處於最底層,用work_struct結構來描述。這個結構體最重要的一個部分是一個指針,它指向一個函數,正是該函數負責處理需要延後執行的具體任務。工作被提交給工作隊列後,實際上是提交給某個具體的工作者線程,然後該線程會被喚醒並執行提交的工作。
我們編寫設備驅動的時候,通常大部分的驅動程序都是使用系統默認的工作者線程,它們使用起來簡單、方便。但是在有些要求更嚴格的情況下,驅動程序需要使用自己的工作者線程。在這種情況下,系統允許驅動程序根據需要來創建工作者線程。也就是說,系統允許有多個類型的工作者線程存在,對於每種類型,系統在每個CPU上都有一個該類的工作者線程,對應於一個cpu_workqueue_struct結構。而workqueue_struct結構則用於表示給定類型的所有工作者線程。這樣,在一個CPU上就可能存在多個工作隊列,每一個工作隊列維護一個cpu_workqueue_struct結構,也就是關聯一種類型的工作者線程。
舉個例子,我們的驅動在系統已有的默認工作者events類型(這是在init_workqueues中創建的系統默認工作者)的基礎上,再自己加入一個falcon工作者類型:
struct workqueue_struct *mydriver_wq;
mydriver_wq = create_workqueue("falcon");
並且我們在一臺具有4個處理器的計算機上工作。那麼現在系統中就有4個events類型的線程和4個falcon類型的線程(相應的,就有8個cpu_workqueue_struct結構體,分別對應2種類型的工作者。同時,會有一個對應events類型的workqueue_struct和一個對應falcon類型的workqueue_struct。在提交工作的時候,我們的工作會提交給一個特殊的falcon線程,由它進行處理。


3 幾種下半部機制的比較
Linux內核提供的幾種下半部機制都用來推後執行你的工作,但是它們在使用上又有諸多差異,各自有不同的適用範圍,使用時應該加以區分。
Linux 2.6內核提供的幾種軟中斷機制都貫穿着“誰觸發,誰執行”的思想,但是它們各自有不同的特點。softirq是整個軟中斷框架體系的核心,是最底層的一種機制,內核程序員很少直接使用它,大部分應用,我們只需要使用tasklet就行了。內核提供了32個softirq,但是僅僅使用了其中的幾個。softirq是在編譯期間靜態分配的,它不像tasklet那樣能夠動態地創建和刪除。softirq的軟中斷向量通過枚舉對其含義進行預定義,這我們在前面2.1節中可以看到。其中,HI_SOFTIRQ和TASKLET_SOFTIRQ這兩個軟中斷都是通過tasklet來實現的,而且也是用得最普遍的軟中斷。在SMP系統中,不同的tasklet可以在多個CPU上並行執行,但是同一個tasklet在同一時刻只能在一個CPU上執行,這一點和softirq不一樣,softirq都可以在多個CPU上同時執行,不管是不同的softirq還是同一softirq的不同實例。tasklet是利用軟中斷來實現的,它和softirq在本質上非常相近,行爲表現也很接近,但是它的接口更簡單,鎖保護的要求也較低,因而也獲得了更廣泛的用途。通常,只有在那些執行頻率很高和連續性要求很高的情況下,我們才需要使用softirq。
HI_SOFTIRQ和TASKLET_SOFTIRQ兩個軟中斷依靠tasklet來實現,它們的差別僅僅在於HI_SOFTIRQ的優先級高於TASKLET_SOFTIRQ,因此它會優先執行。前者稱爲高優先級的tasklet,而後者則稱爲一般的tasklet。
workqueue是另外一種能夠使你的工作延後執行的機制。實際上它不是一種軟中斷機制,因爲它和前面的兩種機制都不一樣,softirq和tasklet通常運行於中斷上下文中,而workqueue則運行於內核進程的上下文中。之所以把它們放在一起討論,是因爲它們都是用於把中斷處理剩下的工作推後執行的一種下半部機制。工作隊列可以把工作推後,交由一個內核線程來執行,因此它允許重新調度,甚至是睡眠,這在softirq和tasklet一般都是不允許的。如果你推後執行的任務不需要睡眠,那麼你可以選擇softirq或者tasklet,但是如果你需要一個可以重新調度的實體來執行你的下半部處理,你應該使用工作隊列。這是一種唯一能在進程上下文中運行的下半部實現機制,也只有它纔可以睡眠。除了上面所說的差異,工作隊列和軟中斷還有一點明顯的不同,就是它可以指定一個明確的時間間隔,用來告訴內核你的工作至少要延遲到指定的時間間隔之後才能開始執行。另外,工作隊列在默認情況下和軟中斷一樣,由最初提交工作的處理器負責執行延後的工作,但是它另外提供了一個接口queue_delayed_work_on(cpu, wq, work, delay)用來提交任務給一個特定的處理器(如果是使用默認的工作隊列,相應的可以使用schedule_delayed_work_on(cpu, work, delay)來提交)。這一點,也是工作隊列和軟中斷不一樣的地方。


4 下半部機制的選擇
在各種下半部實現機制之間作出選擇是很重要的。在目前的2.6版本內核中,有3種可能的選擇,就是本文討論的3種機制:softirq,tasklet,以及工作隊列。tasklet基於softirq實現,因此兩者非常相近,而workqueue則不一樣,它依靠內核線程來實現。
從設計的角度考慮,softirq提供的執行序列化保障是最少的,兩個甚至更多個相同類別的softirq可能在不同的處理器上同時執行,因此你必須格外小心地採取一些步驟確保共享數據的安全。如果被考察的代碼本身多線索化的工作就做得非常好,比如網絡子系統,它完全使用單處理器變量,那麼softirq就是一個非常好的選擇。對於時間要求嚴格和執行頻率很高的應用來說,它執行得也最快。如果代碼多線索化考慮得並不充分,那麼選擇tasklet或許會更好一些,它的接口非常簡單,而且由於同一類型的tasklet不能同時在多個CPU上執行,所以它實現起來也比較簡單一些。驅動程序開發者應儘可能選擇tasklet而非softirq。tasklet是有效的軟中斷,但是它不能併發運行。如果你可以確保軟中斷能夠在多個處理器上安全運行,那麼,你還是選擇softirq比較合適。
當你需要將任務推遲到進程上下文中完成,毫無疑問,你只能使用工作隊列。工作隊列的開銷太大,因爲它牽涉到內核線程甚至是上下文切換。所以如果進程上下文不是必須的,更確切地說,如果不需要睡眠,那麼工作隊列就應該儘量避免,softirq和tasklet或許會更合適。這並不是說工作隊列的工作效率就低,在大部分情況下,工作隊列都能夠提供足夠的支持。只是,在諸如網絡子系統這樣的環境中,時常經歷的每秒鐘幾千次的中斷,那麼採用softirq或者tasklet機制可能會更合適一些。
當然,從易於使用的角度來考慮,首推工作隊列,其次纔是tasklet。最後纔是softirq,它必須靜態地創建,並且需要慎重地考慮其實現,確保共享數據的安全。
一般來說,驅動程序編寫者經常需要做兩個選擇:首先,你是不是需要一個可調度的實體來執行需要推後的工作——從根本上來講,你有休眠的需要嗎?如果有,那麼,工作隊列將是你唯一的選擇。否則最好用tasklet。其次,如果你必須專注於性能的提高,那麼就考慮用softirq吧。這個時候,你還要考慮的一點是,該如何採取有效的措施,才能保證共享數據的安全。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章