Linux 2.6中斷下半部機制分析

來源:互聯網
上載者:User

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下的工作隊列中去,以此體現對“Server Load Balancer”的支援(將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吧。這個時候,你還要考慮的一點是,該如何採取有效措施,才能保證共用資料的安全。

查看原文:http://blog.csdn.net/yicao821/article/details/6670683

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.