歡迎您光臨本站 註冊首頁

Linux內核的Softirq機制

←手機掃碼閱讀     火星人 @ 2014-03-26 , reply:0

中斷服務程序往往都是在CPU關中斷的條件下執行的,以避免中斷嵌套而使控制複雜化。但是CPU關中斷的時間不能太長,否則容易丟失中斷信號。為此,Linux將中斷服務程序一分為二,各稱作「Top Half」和「Bottom Half」。前者通常對時間要求較為嚴格,必須在中斷請求發生后立即或至少在一定的時間限制內完成。因此為了保證這種處理能原子地完成,Top Half通常是在CPU關中斷的條件下執行的。具體地說,Top Half的範圍包括:從在IDT中登記的中斷入口函數一直到驅動程序註冊在中斷服務隊列中的ISR。而Bottom Half則是Top Half根據需要來調度執行的,這些操作允許延遲到稍後執行,它的時間要求並不嚴格,因此它通常是在CPU開中斷的條件下執行的。
但是,Linux的這種Bottom Half(以下簡稱BH)機制有兩個缺點,也即:(1)在任意一時刻,系統只能有一個CPU可以執行Bottom Half代碼,以防止兩個或多個CPU同時來執行Bottom Half函數而相互干擾。因此BH代碼的執行是嚴格「串列化」的。(2)BH函數不允許嵌套。
這兩個缺點在單CPU系統中是無關緊要的,但在SMP系統中卻是非常致命的。因為BH機制的嚴格串列化執行顯然沒有充分利用SMP系統的多CPU特點。為此,Linux2.4內核在BH機制的基礎上進行了擴展,這就是所謂的「軟中斷請求」(softirq)機制。
6.1 軟中斷請求機制
Linux的softirq機制是與SMP緊密不可分的。為此,整個softirq機制的設計與實現中自始自終都貫徹了一個思想:「誰觸發,誰執行」(Who marks,Who runs),也即觸發軟中斷的那個CPU負責執行它所觸發的軟中斷,而且每個CPU都由它自己的軟中斷觸發與控制機制。這個設計思想也使得softirq機制充分利用了SMP系統的性能和特點。
6.1.1 軟中斷描述符
Linux在include/linux/interrupt.h頭文件中定義了數據結構softirq_action,來描述一個軟中斷請求,如下所示:
/* softirq mask and active fields moved to irq_cpustat_t in
* asm/hardirq.h to get better cache usage. KAO
*/
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
其中,函數指針action指向軟中斷請求的服務函數,而指針data則指向由服務函數自行解釋的數據。
基於上述軟中斷描述符,Linux在kernel/softirq.c文件中定義了一個全局的softirq_vec[32]數組:
static struct softirq_action softirq_vec[32] __cacheline_aligned;
在這裡系統一共定義了32個軟中斷請求描述符。軟中斷向量i(0≤i≤31)所對應的軟中斷請求描述符就是softirq_vec[i]。這個數組是個系統全局數組,也即它被所有的CPU所共享。這裡需要注意的一點是:每個CPU雖然都由它自己的觸發和控制機制,並且只執行他自己所觸發的軟中斷請求,但是各個CPU所執行的軟中斷服務常式卻是相同的,也即都是執行softirq_vec[]數組中定義的軟中斷服務函數。
6.1.2 軟中斷觸發機制
要實現「誰觸發,誰執行」的思想,就必須為每個CPU都定義它自己的觸發和控制變數。為此,Linux在include/asm-i386/hardirq.h頭文件中定義了數據結構irq_cpustat_t來描述一個CPU的中斷統計信息,其中就有用於觸發和控制軟中斷的成員變數。數據結構irq_cpustat_t的定義如下:
/* entry.S is sensitive to the offsets of these fields */
typedef struct {
unsigned int __softirq_active;
unsigned int __softirq_mask;
unsigned int __local_irq_count;
unsigned int __local_bh_count;
unsigned int __syscall_count;
unsigned int __nmi_count; /* arch dependent */
} ____cacheline_aligned irq_cpustat_t;
結構中每一個成員都是一個32位的無符號整數。其中__softirq_active和__softirq_mask就是用於觸發和控制軟中斷的成員變數。
①__softirq_active變數:32位的無符號整數,表示軟中斷向量0~31的狀態。如果bit[i](0≤i≤31)為1,則表示軟中斷向量i在某個CPU上已經被觸發而處於active狀態;為0表示處於非活躍狀態。
②__softirq_mask變數:32位的無符號整數,軟中斷向量的屏蔽掩碼。如果bit[i](0≤i≤31)為1,則表示使能(enable)軟中斷向量i,為0表示該軟中斷向量被禁止(disabled)。
根據系統中當前的CPU個數(由宏NR_CPUS表示),Linux在kernel/softirq.c文件中為每個CPU都定義了它自己的中斷統計信息結構,如下所示:
/* No separate irq_stat for s390, it is part of PSA */
#if !defined(CONFIG_ARCH_S390)
irq_cpustat_t irq_stat[NR_CPUS];
#endif /* CONFIG_ARCH_S390 */
這樣,每個CPU都只操作它自己的中斷統計信息結構。假設有一個編號為id的CPU,那麼它只能操作它自己的中斷統計信息結構irq_stat[id](0≤id≤NR_CPUS-1),從而使各CPU之間互不影響。這個數組在include/linux/irq_cpustat.h頭文件中也作了原型聲明。
l 觸發軟中斷請求的操作函數
函數__cpu_raise_softirq()用於在編號為cpu的處理器上觸發軟中斷向量nr。它通過將相應的__softirq_active成員變數中的相應位設置為1來實現軟中斷觸發。如下所示(include/linux/interrupt.h):
static inline void __cpu_raise_softirq(int cpu, int nr)
{
softirq_active(cpu) |= (1action(h);
h++;
active >>= 1;
} while (active);
local_irq_disable();
active = softirq_active(cpu);
if ((active &= mask) != 0)
goto retry;
}
local_bh_enable();
/* Leave with locally disabled hard irqs. It is critical to close
* window for infinite recursion, while we help local bh count,
* it protected us. Now we are defenceless.
*/
return;
retry:
goto restart;
}
結合上述源碼,我們可以看出軟中斷服務的執行過程如下:
(1)調用宏in_interrupt()來檢測當前CPU此次是否已經處於中斷服務中。該宏定義在hardirq.h,請參見5.7節。
(2)調用local_bh_disable()宏將當前CPU的中斷統計信息結構中的__local_bh_count成員變數加1,表示當前CPU已經處在軟中斷服務狀態。
(3)由於接下來要讀寫當前CPU的中斷統計信息結構中的__softirq_active變數和__softirq_mask變數,因此為了保證這一個操作過程的原子性,先用local_irq_disable()宏(實際上就是cli指令)關閉當前CPU的中斷。
(4)然後,讀當前CPU的__softirq_active變數值和__softirq_mask變數值。當某個軟中斷向量被觸發時(即__softirq_active變數中的相應位被置1),只有__softirq_mask變數中的相應位也為1時,它的軟中斷服務函數才能得到執行。因此,需要將__softirq_active變數和__softirq_mask變數作一次「與」邏輯操作。
(5)如果active變數非0,說明需要執行軟中斷服務函數。因此:①先將當前CPU的__softirq_active中的相應位清零,然後用local_irq_enable()宏(實際上就是sti指令)打開當前CPU的中斷。②將局部變數mask中的相應位清零,其目的是:讓do_softirq()函數的這一次執行不對同一個軟中斷向量上的再次軟中斷請求進行服務,而是將它留待下一次do_softirq()執行時去服務,從而使do_sottirq()函數避免陷入無休止的軟中斷服務中。③用一個do{}while循環來根據active的值去執行相應的軟中斷服務函數。④由於接下來又要檢測當前CPU的__softirq_active變數,因此再一次調用local_irq_disable()宏關閉當前CPU的中斷。⑤讀取當前CPU的__softirq_active變數的值,並將它與局部變數mask進行與操作,以看看是否又有其他軟中斷服務被觸發了(比如前面所說的那種情形)。如果有的話,那就跳轉到entry程序段(實際上是跳轉到restart程序段)重新執行軟中斷服務。如果沒有的話,那麼此次軟中斷服務過程就宣告結束。
(6)最後,通過local_bh_enable()宏將當前CPU的__local_bh_count變數值減1,表示當前CPU已經離開軟中斷服務狀態。宏local_bh_enable()也定義在include/asm-i386/softirq.h頭文件中。
6.2 tasklet機制
Tasklet機制是一種較為特殊的軟中斷。Tasklet一詞的原意是「小片任務」的意思,這裡是指一小段可執行的代碼,且通常以函數的形式出現。軟中斷向量HI_SOFTIRQ和TASKLET_SOFTIRQ均是用tasklet機制來實現的。
從某種程度上講,tasklet機制是Linux內核對BH機制的一種擴展。在2.4內核引入了softirq機制后,原有的BH機制正是通過tasklet機制這個橋樑來納入softirq機制的整體框架中的。正是由於這種歷史的延伸關係,使得tasklet機制與一般意義上的軟中斷有所不同,而呈現出以下兩個顯著的特點:
1. 與一般的軟中斷不同,某一段tasklet代碼在某個時刻只能在一個CPU上運行,而不像一般的軟中斷服務函數(即softirq_action結構中的action函數指針)那樣——在同一時刻可以被多個CPU併發地執行。
2. 與BH機制不同,不同的tasklet代碼在同一時刻可以在多個CPU上併發地執行,而不像BH機制那樣必須嚴格地串列化執行(也即在同一時刻系統中只能有一個CPU執行BH函數)。
6.2.1 tasklet描述符
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的指針。
(2)state:定義了這個tasklet的當前狀態。這一個32位的無符號長整數,當前只使用了bit[1]和bit[0]兩個狀態位。其中,bit[1]=1表示這個tasklet當前正在某個CPU上被執行,它僅對SMP系統才有意義,其作用就是為了防止多個CPU同時執行一個tasklet的情形出現;bit[0]=1表示這個tasklet已經被調度去等待執行了。對這兩個狀態位的宏定義如下所示(interrupt.h):
enum
{
TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */
TASKLET_STATE_RUN /* Tasklet is running (SMP only) */
};
(3)原子計數count:對這個tasklet的引用計數值。NOTE!只有當count等於0時,tasklet代碼段才能執行,也即此時tasklet是被使能的;如果count非零,則這個tasklet是被禁止的。任何想要執行一個tasklet代碼段的人都首先必須先檢查其count成員是否為0。
(4)函數指針func:指向以函數形式表現的可執行tasklet代碼段。
(5)data:函數func的參數。這是一個32位的無符號整數,其具體含義可供func函數自行解釋,比如將其解釋成一個指向某個用戶自定義數據結構的地址值。
Linux在interrupt.h頭文件中又定義了兩個用來定義tasklet_struct結構變數的輔助宏:
#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在初始化時是被使能的(enabled),因為其count成員為0。而用DECLARE_TASKLET_DISABLED宏定義的tasklet在初始時是被禁止的(disabled),因為其count等於1。
6.2.2 改變一個tasklet狀態的操作
在這裡,tasklet狀態指兩個方面:(1)state成員所表示的運行狀態;(2)count成員決定的使能/禁止狀態。
(1)改變一個tasklet的運行狀態
state成員中的bit[0]表示一個tasklet是否已被調度去等待執行,bit[1]表示一個tasklet是否正在某個CPU上執行。對於state變數中某位的改變必須是一個原子操作,因此可以用定義在include/asm/bitops.h頭文件中的位操作來進行。
由於bit[1]這一位(即TASKLET_STATE_RUN)僅僅對於SMP系統才有意義,因此Linux在Interrupt.h頭文件中顯示地定義了對TASKLET_STATE_RUN位的操作。如下所示:
#ifdef CONFIG_SMP
#define tasklet_trylock(t) (!test_and_set_bit(TASKLET_STATE_RUN, &(t)->state))
#define tasklet_unlock_wait(t) while (test_bit(TASKLET_STATE_RUN, &(t)->state)) { /* NOTHING */ }
#define tasklet_unlock(t) clear_bit(TASKLET_STATE_RUN, &(t)->state)
#else
#define tasklet_trylock(t) 1
#define tasklet_unlock_wait(t) do { } while (0)
#define tasklet_unlock(t) do { } while (0)
#endif
顯然,在SMP系統同,tasklet_trylock()宏將把一個tasklet_struct結構變數中的state成員中的bit[1]位設置成1,同時還返回bit[1]位的非。因此,如果bit[1]位原有值為1(表示另外一個CPU正在執行這個tasklet代碼),那麼tasklet_trylock()宏將返回值0,也就表示上鎖不成功。如果bit[1]位的原有值為0,那麼tasklet_trylock()宏將返回值1,表示加鎖成功。而在單CPU系統中,tasklet_trylock()宏總是返回為1。
任何想要執行某個tasklet代碼的程序都必須首先調用宏tasklet_trylock()來試圖對這個tasklet進行上鎖(即設置TASKLET_STATE_RUN位),且只能在上鎖成功的情況下才能執行這個tasklet。建議!即使你的程序只在CPU系統上運行,你也要在執行tasklet之前調用tasklet_trylock()宏,以便使你的代碼獲得良好可移植性。
在SMP系統中,tasklet_unlock_wait()宏將一直不停地測試TASKLET_STATE_RUN位的值,直到該位的值變為0(即一直等待到解鎖),假如:CPU0正在執行tasklet A的代碼,在此期間,CPU1也想執行tasklet A的代碼,但CPU1發現tasklet A的TASKLET_STATE_RUN位為1,於是它就可以通過tasklet_unlock_wait()宏等待tasklet A被解鎖(也即TASKLET_STATE_RUN位被清零)。在單CPU系統中,這是一個空操作。
宏tasklet_unlock()用來對一個tasklet進行解鎖操作,也即將TASKLET_STATE_RUN位清零。在單CPU系統中,這是一個空操作。
(2)使能/禁止一個tasklet
使能與禁止操作往往總是成對地被調用的,tasklet_disable()函數如下(interrupt.h):
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
}
函數tasklet_disable_nosync()也是一個靜態inline函數,它簡單地通過原子操作將count成員變數的值減1。如下所示(interrupt.h):
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
atomic_inc(&t->count);
}
函數tasklet_enable()用於使能一個tasklet,如下所示(interrupt.h):
static inline void tasklet_enable(struct tasklet_struct *t)
{
atomic_dec(&t->count);
}
6.2.3 tasklet描述符的初始化與殺死
函數tasklet_init()用來初始化一個指定的tasklet描述符,其源碼如下所示(kernel/softirq.c):
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->func = func;
t->data = data;
t->state = 0;
atomic_set(&t->count, 0);
}
函數tasklet_kill()用來將一個已經被調度了的tasklet殺死,即將其恢復到未調度的狀態。其源碼如下所示(kernel/softirq.c):
void tasklet_kill(struct tasklet_struct *t)
{
if (in_interrupt())
printk("Attempt to kill tasklet from interrupt\n");
while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
current->state = TASK_RUNNING;
do {
current->policy |= SCHED_YIELD;
schedule();
} while (test_bit(TASKLET_STATE_SCHED, &t->state));
}
tasklet_unlock_wait(t);
clear_bit(TASKLET_STATE_SCHED, &t->state);
}
6.2.4 tasklet對列
多個tasklet可以通過tasklet描述符中的next成員指針鏈接成一個單向對列。為此,Linux專門在頭文件include/linux/interrupt.h中定義了數據結構tasklet_head來描述一個tasklet對列的頭部指針。如下所示:
struct tasklet_head
{
struct tasklet_struct *list;
} __attribute__ ((__aligned__(SMP_CACHE_BYTES)));
儘管tasklet機制是特定於軟中斷向量HI_SOFTIRQ和TASKLET_SOFTIRQ的一種實現,但是tasklet機制仍然屬於softirq機制的整體框架範圍內的,因此,它的設計與實現仍然必須堅持「誰觸發,誰執行」的思想。為此,Linux為系統中的每一個CPU都定義了一個tasklet對列頭部,來表示應該有各個CPU負責執行的tasklet對列。如下所示(kernel/softirq.c):
struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned;
struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;
其中,tasklet_vec[]數組用於軟中斷向量TASKLET_SOFTIRQ,而tasklet_hi_vec[]數組則用於軟中斷向量HI_SOFTIRQ。也即,如果CPUi(0≤i≤NR_CPUS-1)觸發了軟中斷向量TASKLET_SOFTIRQ,那麼對列tasklet_vec[i]中的每一個tasklet都將在CPUi服務於軟中斷向量TASKLET_SOFTIRQ時被CPUi所執行。同樣地,如果CPUi(0≤i≤NR_CPUS-1)觸發了軟中斷向量HI_SOFTIRQ,那麼隊列tasklet_vec[i]中的每一個tasklet都將CPUi在對軟中斷向量HI_SOFTIRQ進行服務時被CPUi所執行。
隊列tasklet_vec[I]和tasklet_hi_vec[I]中的各個tasklet是怎樣被所CPUi所執行的呢?其關鍵就是軟中斷向量TASKLET_SOFTIRQ和HI_SOFTIRQ的軟中斷服務程序——tasklet_action()函數和tasklet_hi_action()函數。下面我們就來分析這兩個函數。
6.2.5 軟中斷向量TASKLET_SOFTIRQ和HI_SOFTIRQ
Linux為軟中斷向量TASKLET_SOFTIRQ和HI_SOFTIRQ實現了專用的觸發函數和軟中斷服務函數。其中,tasklet_schedule()函數和tasklet_hi_schedule()函數分別用來在當前CPU上觸發軟中斷向量TASKLET_SOFTIRQ和HI_SOFTIRQ,並把指定的tasklet加入當前CPU所對應的tasklet隊列中去等待執行。而tasklet_action()函數和tasklet_hi_action()函數則分別是軟中斷向量TASKLET_SOFTIRQ和HI_SOFTIRQ的軟中斷服務函數。在初始化函數softirq_init()中,這兩個軟中斷向量對應的描述符softirq_vec[0]和softirq_vec[3]中的action函數指針就被分別初始化成指向函數tasklet_hi_action()和函數tasklet_action()。
(1)軟中斷向量TASKLET_SOFTIRQ的觸發函數tasklet_schedule()
該函數實現在include/linux/interrupt.h頭文件中,是一個inline函數。其源碼如下所示:
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
int cpu = smp_processor_id();
unsigned long flags;
local_irq_save(flags);
t->next = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
}
該函數的參數t指向要在當前CPU上被執行的tasklet。對該函數的NOTE如下:
①調用test_and_set_bit()函數將待調度的tasklet的state成員變數的bit[0]位(也即TASKLET_STATE_SCHED位)設置為1,該函數同時還返回TASKLET_STATE_SCHED位的原有值。因此如果bit[0]為的原有值已經為1,那就說明這個tasklet已經被調度到另一個CPU上去等待執行了。由於一個tasklet在某一個時刻只能由一個CPU來執行,因此tasklet_schedule()函數什麼也不做就直接返回了。否則,就繼續下面的調度操作。
②首先,調用local_irq_save()函數來關閉當前CPU的中斷,以保證下面的步驟在當前CPU上原子地被執行。
③然後,將待調度的tasklet添加到當前CPU對應的tasklet隊列的首部。
④接著,調用__cpu_raise_softirq()函數在當前CPU上觸發軟中斷請求TASKLET_SOFTIRQ。
⑤最後,調用local_irq_restore()函數來開當前CPU的中斷。
(2)軟中斷向量TASKLET_SOFTIRQ的服務程序tasklet_action()
函數tasklet_action()是tasklet機制與軟中斷向量TASKLET_SOFTIRQ的聯繫紐帶。正是該函數將當前CPU的tasklet隊列中的各個tasklet放到當前CPU上來執行的。該函數實現在kernel/softirq.c文件中,其源代碼如下:
static void tasklet_action(struct softirq_action *a)
{
int cpu = smp_processor_id();
struct tasklet_struct *list;
local_irq_disable();
list = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = NULL;
local_irq_enable();
while (list != NULL) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (atomic_read(&t->count) == 0) {
clear_bit(TASKLET_STATE_SCHED, &t->state);
t->func(t->data);
/*
* talklet_trylock() uses test_and_set_bit that imply
* an mb when it returns zero, thus we need the explicit
* mb only here: while closing the critical section.
*/
#ifdef CONFIG_SMP
smp_mb__before_clear_bit();
#endif
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
local_irq_enable();
}
}
註釋如下:
①首先,在當前CPU關中斷的情況下,「原子」地讀取當前CPU的tasklet隊列頭部指針,將其保存到局部變數list指針中,然後將當前CPU的tasklet隊列頭部指針設置為NULL,以表示理論上當前CPU將不再有tasklet需要執行(但最後的實際結果卻並不一定如此,下面將會看到)。
②然後,用一個while{}循環來遍歷由list所指向的tasklet隊列,隊列中的各個元素就是將在當前CPU上執行的tasklet。循環體的執行步驟如下:
l 用指針t來表示當前隊列元素,即當前需要執行的tasklet。
l 更新list指針為list->next,使它指向下一個要執行的tasklet。
l 用tasklet_trylock()宏試圖對當前要執行的tasklet(由指針t所指向)進行加鎖,如果加鎖成功(當前沒有任何其他CPU正在執行這個tasklet),則用原子讀函數atomic_read()進一步判斷count成員的值。如果count為0,說明這個tasklet是允許執行的,於是:(1)先清除TASKLET_STATE_SCHED位;(2)然後,調用這個tasklet的可執行函數func;(3)執行barrier()操作;(4)調用宏tasklet_unlock()來清除TASKLET_STATE_RUN位。(5)最後,執行continue語句跳過下面的步驟,回到while循環繼續遍歷隊列中的下一個元素。如果count不為0,說明這個tasklet是禁止運行的,於是調用tasklet_unlock()清除前面用tasklet_trylock()設置的TASKLET_STATE_RUN位。
l 如果tasklet_trylock()加鎖不成功,或者因為當前tasklet的count值非0而不允許執行時,我們必須將這個tasklet重新放回到當前CPU的tasklet隊列中,以留待這個CPU下次服務軟中斷向量TASKLET_SOFTIRQ時再執行。為此進行這樣幾步操作:(1)先關CPU中斷,以保證下面操作的原子性。(2)把這個tasklet重新放回到當前CPU的tasklet隊列的首部;(3)調用__cpu_raise_softirq()函數在當前CPU上再觸發一次軟中斷請求TASKLET_SOFTIRQ;(4)開中斷。
l 最後,回到while循環繼續遍歷隊列。
(3)軟中斷向量HI_SOFTIRQ的觸發函數tasklet_hi_schedule()
該函數與tasklet_schedule()幾乎相同,其源碼如下(include/linux/interrupt.h):
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
int cpu = smp_processor_id();
unsigned long flags;
local_irq_save(flags);
t->next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = t;
__cpu_raise_softirq(cpu, HI_SOFTIRQ);
local_irq_restore(flags);
}
}
(4)軟中斷向量HI_SOFTIRQ的服務函數tasklet_hi_action()
該函數與tasklet_action()函數幾乎相同,其源碼如下(kernel/softirq.c):
static void tasklet_hi_action(struct softirq_action *a)
{
int cpu = smp_processor_id();
struct tasklet_struct *list;
local_irq_disable();
list = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = NULL;
local_irq_enable();
while (list != NULL) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (atomic_read(&t->count) == 0) {
clear_bit(TASKLET_STATE_SCHED, &t->state);
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = t;
__cpu_raise_softirq(cpu, HI_SOFTIRQ);
local_irq_enable();
}
}
6.3 Bottom Half機制
Bottom Half機制在新的softirq機制中被保留下來,並作為softirq框架的一部分。其實現也似乎更為複雜些,因為它是通過tasklet機制這個中介橋樑來納入softirq框架中的。實際上,軟中斷向量HI_SOFTIRQ是內核專用於執行BH函數的。
6.3.1 數據結構的定義
原有的32個BH函數指針被保留,定義在kernel/softirq.c文件中:
static void (*bh_base[32])(void);
但是,每個BH函數都對應有一個tasklet,並由tasklet的可執行函數func來負責調用相應的bh函數(func函數的參數指定調用哪一個BH函數)。與32個BH函數指針相對應的tasklet的定義如下所示(kernel/softirq.c):
struct tasklet_struct bh_task_vec[32];
上述tasklet數組使系統全局的,它對所有的CPU均可見。由於在某一個時刻只能有一個CPU在執行BH函數,因此定義一個全局的自旋鎖來保護BH函數,如下所示(kernel/softirq.c):
spinlock_t global_bh_lock = SPIN_LOCK_UNLOCKED;
6.3.2 初始化
在softirq機制的初始化函數softirq_init()中將bh_task_vec[32]數組中的每一個tasklet中的func函數指針都設置為指向同一個函數bh_action,而data成員(也即func函數的調用參數)則被設置成該tasklet在數組中的索引值,如下所示:
void __init softirq_init()
{
……
for (i=0; isync)) {
unsigned long flags;
spin_lock_irqsave(&tqueue_lock, flags);
list_add_tail(&bh_pointer->list, bh_list);
spin_unlock_irqrestore(&tqueue_lock, flags);
ret = 1;
}
return ret;
}
6.4.3 運行任務隊列
函數run_task_queue()用於實現指定的任務隊列。它只有一個參數:指針list——指向待運行的任務隊列頭部task_queue結構變數。該函數實現在tqueue.h頭文件中:
static inline void run_task_queue(task_queue *list)
{
if (TQ_ACTIVE(*list))
__run_task_queue(list);
}
顯然,函數首先調用宏TQ_ACTIVE()來判斷參數list指定的待運行任務隊列是否為空。如果不為空,則調用__run_task_queue()函數來實際運行這個有效的任務隊列。
函數__run_task_queue()實現在kernel/softirq.c文件中。該函數將依次遍歷任務隊列中的每一個元數,並調用執行每一個元數的可執行函數。其源碼如下:
void __run_task_queue(task_queue *list)
{
struct list_head head, *next;
unsigned long flags;
spin_lock_irqsave(&tqueue_lock, flags);
list_add(&head, list);
list_del_init(list);
spin_unlock_irqrestore(&tqueue_lock, flags);
next = head.next;
while (next != &head) {
void (*f) (void *);
struct tq_struct *p;
void *data;
p = list_entry(next, struct tq_struct, list);
next = next->next;
f = p->routine;
data = p->data;
wmb();
p->sync = 0;
if (f)
f(data);
}
}
對該函數的註釋如下:
(1)首先,用一個局部的表頭head來代替參數list所指向的表頭。這是因為:在__run_task_queue()函數的運行期間可能還會有新的任務加入到list任務隊列中來,但是__run_task_queue()函數顯然不想陷入無休止的不斷增加的任務處理中,因此它用局部的表頭head來代替參數list所指向的表頭,以使要執行的任務個數固定化。為此:①先對全局的自旋鎖tqueue_lock進行加鎖,以實現對任務隊列的互斥訪問;②將局部的表頭head加在表頭(*list)和第一個元數之間。③將(*list)表頭從隊列中去除,並將其初始化為空。④解除自旋鎖tqueue_lock。
(2)接下來,用一個while循環來遍歷整個隊列head,並調用執行每一個隊列元素中的函數。注意!任務隊列是一個雙向循環隊列。
6.4.4 內核預定義的任務隊列
Bottom Half機制與任務隊列是緊密相連的。大多數BH函數都是通過調用run_task_queue()函數來執行某個預定義好的任務隊列。最常見的內核預定義任務隊列有:
l tq_timer:對應於TQUEUE_BH。
l tq_immediate:對應於IMMEDIATE_BH。
l tq_disk:用於塊設備任務。
任務隊列tq_timer和tq_immediate都定義在kernel/timer.c文件中,如下所示:
DECLARE_TASK_QUEUE(tq_timer);
DECLARE_TASK_QUEUE(tq_immediate);
BH向量TQUEUE_BH和IMMEDIATE_BH的BH函數分別是:queue_bh()函數和immediate_bh()函數,它們都僅僅是簡單地調用run_task_queue()函數來分別運行任務隊列tq_timer和tq_immediate,如下所示(kernel/timer.c):
void tqueue_bh(void)
{
run_task_queue(&tq_timer);
}
void immediate_bh(void)
{
run_task_queue(&tq_immediate);
}

[火星人 ] Linux內核的Softirq機制已經有5322次圍觀

http://coctec.com/docs/linux/show-post-185662.html