Linux内核的Softirq机制
生活随笔
收集整理的這篇文章主要介紹了
Linux内核的Softirq机制
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
前言?
中斷服務程序往往都是在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)?|=?(1<<nr);?
}?
為了保證“原子”性地完成軟中斷的觸發過程,Linux在interrupt.h頭文件中對上述內聯函數又作了高層封裝,也即函數raise_softirq()。該函數向下通過調用__cpu_raise_softirq()函數來實現軟中斷的觸發,但在調用該函數之前,它先通過local_irq_save()函數來關閉當前CPU的中斷并保存標志寄存器的內容,如下所示:?
/*?I?do?not?want?to?use?atomic?variables?now,?so?that?cli/sti?*/?
static?inline?void?raise_softirq(int?nr)?
{?
unsigned?long?flags;?
local_irq_save(flags);?
__cpu_raise_softirq(smp_processor_id(),?nr);?
local_irq_restore(flags);?
}?
6.1.3?Linux對軟中斷的預定義分類?
在軟中斷向量0~31中,Linux內核僅僅使用了軟中斷向量0~3,其余被留待系統以后擴展。Linux在頭文件include/linux/interrupt.h中對軟中斷向量0~3進行了預定義:?
/*?PLEASE,?avoid?to?allocate?new?softirqs,?if?you?need?not?_really_?high?
frequency?threaded?job?scheduling.?For?almost?all?the?purposes?
tasklets?are?more?than?enough.?F.e.?all?serial?device?BHs?et?
al.?should?be?converted?to?tasklets,?not?to?softirqs.?
*/?
enum?
{?
HI_SOFTIRQ=0,?
NET_TX_SOFTIRQ,?
NET_RX_SOFTIRQ,?
TASKLET_SOFTIRQ?
};?
其中,軟中斷向量0(即HI_SOFTIRQ)用于實現高優先級的軟中斷,如:高優先級的tasklet(將在后面詳細描述)。軟中斷向量1和2則分別用于網絡數據的發送與接收。軟中斷向量3(即TASKLET_SOFTIRQ)則用于實現諸如tasklet這樣的一般性軟中斷。關于tasklet我們將在后面詳細描述。NOTE!Linix內核并不鼓勵一般用戶擴展使用剩余的軟中斷向量,因為它認為其預定義的軟中斷向量HI_SOFTIRQ和TASKLET_SOFTIRQ已經足夠應付絕大多數應用。?
6.1.4?軟中斷機制的初始化?
函數softirq_init()完成softirq機制的初始化。該函數由內核啟動例程start_kernel()所調用。函數源碼如下所示(kernel/softirq.c):?
void?__init?softirq_init()?
{?
int?i;?
for?(i=0;?i<32;?i++)?
tasklet_init(bh_task_vec+i,?bh_action,?i);?
open_softirq(TASKLET_SOFTIRQ,?tasklet_action,?NULL);?
open_softirq(HI_SOFTIRQ,?tasklet_hi_action,?NULL);?
}?
初始化的過程如下:?
(1)先用一個for循環來初始化用于實現BH機制的bh_task_vec[32]數組。這一點我們將在后面詳細解釋。?
(2)調用open_softirq()函數開啟使用軟中斷向量TASKLET_SOFTIRQ和HI_SOFTIRQ,并將它們的軟中斷服務函數指針分別指向tasklet_action()函數和tasklet_hi_action()函數。函數open_softirq()的主要作用是初始化設置軟中斷請求描述符softirq_vec[nr]。?
6.1.5?開啟一個指定的軟中斷向量?
函數open_softirq()用于開啟一個指定的軟中斷向量nr,也即適當地初始化軟中斷向量nr所對應的軟中斷描述符softirq_vec[nr]。它主要做兩件事情:(1)初始化設置軟中斷向量nr所對應的軟中斷描述符softirq_vec[nr]。(2)將所有CPU的軟中斷屏蔽掩碼變量__softirq_mask中的對應位設置為1,以使能該軟中斷向量。該函數的源碼如下所示(kernel/softirq.c):?
void?open_softirq(int?nr,?void?(*action)(struct?softirq_action*),?void?*data)?
{?
unsigned?long?flags;?
int?i;?
spin_lock_irqsave(&softirq_mask_lock,?flags);?
softirq_vec[nr].data?=?data;?
softirq_vec[nr].action?=?action;?
for?(i=0;?i<NR_CPUS;?i++)?
softirq_mask(i)?|=?(1<<nr);?
spin_unlock_irqrestore(&softirq_mask_lock,?flags);?
}?
6.1.6?軟中斷服務的執行函數do_softirq()?
函數do_softirq()負責執行數組softirq_vec[32]中設置的軟中斷服務函數。每個CPU都是通過執行這個函數來執行軟中斷服務的。由于同一個CPU上的軟中斷服務例程不允許嵌套,因此,do_softirq()函數一開始就檢查當前CPU是否已經正出在中斷服務中,如果是則do_softirq()函數立即返回。舉個例子,假設CPU0正在執行do_softirq()函數,執行過程產生了一個高優先級的硬件中斷,于是CPU0轉去執行這個高優先級中斷所對應的中斷服務程序。總所周知,所有的中斷服務程序最后都要跳轉到do_IRQ()函數并由它來依次執行中斷服務隊列中的ISR,這里我們假定這個高優先級中斷的ISR請求觸發了一次軟中斷,于是do_IRQ()函數在退出之前看到有軟中斷請求,從而調用do_softirq()函數來服務軟中斷請求。因此,CPU0再次進入do_softirq()函數(也即do_softirq()函數在CPU0上被重入了)。但是在這一次進入do_softirq()函數時,它馬上發現CPU0此前已經處在中斷服務狀態中了,因此這一次do_softirq()函數立即返回。于是,CPU0回到該開始時的do_softirq()函數繼續執行,并為高優先級中斷的ISR所觸發的軟中斷請求補上一次服務。從這里可以看出,do_softirq()函數在同一個CPU上的執行是串行的。?
函數源碼如下(kernel/softirq.c):?
asmlinkage?void?do_softirq()?
{?
int?cpu?=?smp_processor_id();?
__u32?active,?mask;?
if?(in_interrupt())?
return;?
local_bh_disable();?
local_irq_disable();?
mask?=?softirq_mask(cpu);?
active?=?softirq_active(cpu)?&?mask;?
if?(active)?{?
struct?softirq_action?*h;?
restart:?
/*?Reset?active?bitmask?before?enabling?irqs?*/?
softirq_active(cpu)?&=?~active;?
local_irq_enable();?
h?=?softirq_vec;?
mask?&=?~active;?
do?{?
if?(active?&?1)?
h->action(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();?
}?
}
.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;?i<32;?i++)?
tasklet_init(bh_task_vec+i,?bh_action,?i);?
……?
}?
因此,bh_action()函數將負責相應地調用參數所指定的bh函數。該函數是連接tasklet機制與Bottom?Half機制的關鍵所在。?
6.2.3?bh_action()函數?
該函數的源碼如下(kernel/softirq.c):?
static?void?bh_action(unsigned?long?nr)?
{?
int?cpu?=?smp_processor_id();?
if?(!spin_trylock(&global_bh_lock))?
goto?resched;?
if?(!hardirq_trylock(cpu))?
goto?resched_unlock;?
if?(bh_base[nr])?
bh_base[nr]();?
hardirq_endlock(cpu);?
spin_unlock(&global_bh_lock);?
return;?
resched_unlock:?
spin_unlock(&global_bh_lock);?
resched:?
mark_bh(nr);?
}?
對該函數的注釋如下:?
①首先,調用spin_trylock()函數試圖對自旋鎖global_bh_lock進行加鎖,同時該函數還將返回自旋鎖global_bh_lock的原有值的非。因此,如果global_bh_lock已被某個CPU上鎖而為非0值(那個CPU肯定在執行某個BH函數),那么spin_trylock()將返回為0表示上鎖失敗,在這種情況下,當前CPU是不能執行BH函數的,因為另一個CPU正在執行BH函數,于是執行goto語句跳轉到resched程序段,以便在當前CPU上再一次調度該BH函數。?
②調用hardirq_trylock()函數鎖定當前CPU,確保當前CPU不是處于硬件中斷請求服務中,如果鎖定失敗,跳轉到resched_unlock程序段,以便先對global_bh_lock解鎖,在重新調度一次該BH函數。?
③此時,我們已經可以放心地在當前CPU上執行BH函數了。當然,對應的BH函數指針bh_base[nr]必須有效才行。?
④從BH函數返回后,先調用hardirq_endlock()函數(實際上它什么也不干,調用它只是為了保此加、解鎖的成對關系),然后解除自旋鎖global_bh_lock,最后函數就可以返回了。?
⑤resched_unlock程序段:先解除自旋鎖global_bh_lock,然后執行reched程序段。?
⑥resched程序段:當某個CPU正在執行BH函數時,當前CPU就不能通過bh_action()函數來調用執行任何BH函數,所以就通過調用mark_bh()函數在當前CPU上再重新調度一次,以便將這個BH函數留待下次軟中斷服務時執行。?
6.3.4?Bottom?Half的原有接口函數?
(1)init_bh()函數?
該函數用來在bh_base[]數組登記一個指定的bh函數,如下所示(kernel/softirq.c):?
void?init_bh(int?nr,?void?(*routine)(void))?
{?
bh_base[nr]?=?routine;?
mb();?
}?
(2)remove_bh()函數?
該函數用來在bh_base[]數組中注銷指定的函數指針,同時將相對應的tasklet殺掉。如下所示(kernel/softirq.c):?
void?remove_bh(int?nr)?
{?
tasklet_kill(bh_task_vec+nr);?
bh_base[nr]?=?NULL;?
}?
(3)mark_bh()函數?
該函數用來向當前CPU標記由一個BH函數等待去執行。它實際上通過調用tasklet_hi_schedule()函數將相應的tasklet加入到當前CPU的tasklet隊列tasklet_hi_vec[cpu]中,然后觸發軟中斷請求HI_SOFTIRQ,如下所示(include/linux/interrupt.h):?
static?inline?void?mark_bh(int?nr)?
{?
tasklet_hi_schedule(bh_task_vec+nr);?
}?
6.3.5?預定義的BH函數?
在32個BH函數指針中,大多數已經固定用于一些常見的外設,比如:第0個BH函數就固定地用于時鐘中斷。Linux在頭文件include/linux/interrupt.h中定義了這些已經被使用的BH函數所引,如下所示:?
enum?{?
TIMER_BH?=?0,?
TQUEUE_BH,?
DIGI_BH,?
SERIAL_BH,?
RISCOM8_BH,?
SPECIALIX_BH,?
AURORA_BH,?
ESP_BH,?
SCSI_BH,?
IMMEDIATE_BH,?
CYCLADES_BH,?
CM206_BH,?
JS_BH,?
MACSERIAL_BH,?
ISICOM_BH?
};
6.4?任務隊列Task?Queue?
任務隊列是與Bottom?Half機制緊密相連的。因為Bottom?Half機制只有有限的32個函數指針,而且大部分都已被系統預定義使用,所以早期版本的Linux內核為了擴展Bottom?Half機制,就設計了任務隊列機制。?
所謂任務隊列就是指以雙向隊列形式連接起來的任務鏈表,每一個鏈表元數都描述了一個可執行的任務(以函數的形式表現)。如下圖所示:?
任務隊列機制實現在include/linux/tqueue.h頭文件中。?
6.4.1?數據結構的定義?
Linux用數據結構tq_struct來描述任務隊列中的每一個鏈表元數(即一個可執行的任務):?
struct?tq_struct?{?
struct?list_head?list;?/*?linked?list?of?active?bh's?*/?
unsigned?long?sync;?/*?must?be?initialized?to?zero?*/?
void?(*routine)(void?*);?/*?function?to?call?*/?
void?*data;?/*?argument?to?function?*/?
};?
這個數據結構很簡單,在此就不詳述。?
然后,Linux定義了數據結構task_queue來描述任務隊列的頭部,其實task_queue就是結構類型list_head,如下:?
typedef?struct?list_head?task_queue;?
但是Linux又定義了一個宏DECLARE_TASK_QUEUE()來輔助我們更方便地定義任務隊列的鏈表表頭:?
#define?DECLARE_TASK_QUEUE(q)?LIST_HEAD(q)?
一個任務隊列是否處于active狀態主要取決于其鏈表表頭(即task_queue結構)是否為空,因此Linux定義宏TQ_ACTIVE()來判斷一個任務隊列是否有效:?
#define?TQ_ACTIVE(q)?(!list_empty(&q))?
顯然,只要任務隊列表頭q不為空,該任務隊列就是有效的。?
6.4.2?向任務隊列中插入一個新任務?
(1)保護自旋鎖?
由于任務隊列是系統全局的共享資源,所以面臨競爭的問題。為了實現對任務隊列鏈表的互斥訪問,Linux在kernel/timer.c文件中定義了一個任務隊列保護自旋鎖tqueue_lock,如下:?
spinlock_t?tqueue_lock?=?SPIN_LOCK_UNLOCKED;?
該自旋鎖在tqueue.h頭文件中也有原型聲明:?
extern?spinlock_t?tqueue_lock;?
任何想要訪問任務隊列的代碼都首先必須先持有該自旋鎖。?
(2)queue_task()函數?
實現在tqueue.h頭文件中的內聯函數queue_task()用來將一個指定的任務添加到某指定的任務隊列的尾部,如下:?
/*?
*?Queue?a?task?on?a?tq.?Return?non-zero?if?it?was?successfully?
*?added.?
*/?
static?inline?int?queue_task(struct?tq_struct?*bh_pointer,?task_queue?*bh_list)?
{?
int?ret?=?0;?
if?(!test_and_set_bit(0,&bh_pointer->sync))?{?
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);?
}
中斷服務程序往往都是在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)?|=?(1<<nr);?
}?
為了保證“原子”性地完成軟中斷的觸發過程,Linux在interrupt.h頭文件中對上述內聯函數又作了高層封裝,也即函數raise_softirq()。該函數向下通過調用__cpu_raise_softirq()函數來實現軟中斷的觸發,但在調用該函數之前,它先通過local_irq_save()函數來關閉當前CPU的中斷并保存標志寄存器的內容,如下所示:?
/*?I?do?not?want?to?use?atomic?variables?now,?so?that?cli/sti?*/?
static?inline?void?raise_softirq(int?nr)?
{?
unsigned?long?flags;?
local_irq_save(flags);?
__cpu_raise_softirq(smp_processor_id(),?nr);?
local_irq_restore(flags);?
}?
6.1.3?Linux對軟中斷的預定義分類?
在軟中斷向量0~31中,Linux內核僅僅使用了軟中斷向量0~3,其余被留待系統以后擴展。Linux在頭文件include/linux/interrupt.h中對軟中斷向量0~3進行了預定義:?
/*?PLEASE,?avoid?to?allocate?new?softirqs,?if?you?need?not?_really_?high?
frequency?threaded?job?scheduling.?For?almost?all?the?purposes?
tasklets?are?more?than?enough.?F.e.?all?serial?device?BHs?et?
al.?should?be?converted?to?tasklets,?not?to?softirqs.?
*/?
enum?
{?
HI_SOFTIRQ=0,?
NET_TX_SOFTIRQ,?
NET_RX_SOFTIRQ,?
TASKLET_SOFTIRQ?
};?
其中,軟中斷向量0(即HI_SOFTIRQ)用于實現高優先級的軟中斷,如:高優先級的tasklet(將在后面詳細描述)。軟中斷向量1和2則分別用于網絡數據的發送與接收。軟中斷向量3(即TASKLET_SOFTIRQ)則用于實現諸如tasklet這樣的一般性軟中斷。關于tasklet我們將在后面詳細描述。NOTE!Linix內核并不鼓勵一般用戶擴展使用剩余的軟中斷向量,因為它認為其預定義的軟中斷向量HI_SOFTIRQ和TASKLET_SOFTIRQ已經足夠應付絕大多數應用。?
6.1.4?軟中斷機制的初始化?
函數softirq_init()完成softirq機制的初始化。該函數由內核啟動例程start_kernel()所調用。函數源碼如下所示(kernel/softirq.c):?
void?__init?softirq_init()?
{?
int?i;?
for?(i=0;?i<32;?i++)?
tasklet_init(bh_task_vec+i,?bh_action,?i);?
open_softirq(TASKLET_SOFTIRQ,?tasklet_action,?NULL);?
open_softirq(HI_SOFTIRQ,?tasklet_hi_action,?NULL);?
}?
初始化的過程如下:?
(1)先用一個for循環來初始化用于實現BH機制的bh_task_vec[32]數組。這一點我們將在后面詳細解釋。?
(2)調用open_softirq()函數開啟使用軟中斷向量TASKLET_SOFTIRQ和HI_SOFTIRQ,并將它們的軟中斷服務函數指針分別指向tasklet_action()函數和tasklet_hi_action()函數。函數open_softirq()的主要作用是初始化設置軟中斷請求描述符softirq_vec[nr]。?
6.1.5?開啟一個指定的軟中斷向量?
函數open_softirq()用于開啟一個指定的軟中斷向量nr,也即適當地初始化軟中斷向量nr所對應的軟中斷描述符softirq_vec[nr]。它主要做兩件事情:(1)初始化設置軟中斷向量nr所對應的軟中斷描述符softirq_vec[nr]。(2)將所有CPU的軟中斷屏蔽掩碼變量__softirq_mask中的對應位設置為1,以使能該軟中斷向量。該函數的源碼如下所示(kernel/softirq.c):?
void?open_softirq(int?nr,?void?(*action)(struct?softirq_action*),?void?*data)?
{?
unsigned?long?flags;?
int?i;?
spin_lock_irqsave(&softirq_mask_lock,?flags);?
softirq_vec[nr].data?=?data;?
softirq_vec[nr].action?=?action;?
for?(i=0;?i<NR_CPUS;?i++)?
softirq_mask(i)?|=?(1<<nr);?
spin_unlock_irqrestore(&softirq_mask_lock,?flags);?
}?
6.1.6?軟中斷服務的執行函數do_softirq()?
函數do_softirq()負責執行數組softirq_vec[32]中設置的軟中斷服務函數。每個CPU都是通過執行這個函數來執行軟中斷服務的。由于同一個CPU上的軟中斷服務例程不允許嵌套,因此,do_softirq()函數一開始就檢查當前CPU是否已經正出在中斷服務中,如果是則do_softirq()函數立即返回。舉個例子,假設CPU0正在執行do_softirq()函數,執行過程產生了一個高優先級的硬件中斷,于是CPU0轉去執行這個高優先級中斷所對應的中斷服務程序。總所周知,所有的中斷服務程序最后都要跳轉到do_IRQ()函數并由它來依次執行中斷服務隊列中的ISR,這里我們假定這個高優先級中斷的ISR請求觸發了一次軟中斷,于是do_IRQ()函數在退出之前看到有軟中斷請求,從而調用do_softirq()函數來服務軟中斷請求。因此,CPU0再次進入do_softirq()函數(也即do_softirq()函數在CPU0上被重入了)。但是在這一次進入do_softirq()函數時,它馬上發現CPU0此前已經處在中斷服務狀態中了,因此這一次do_softirq()函數立即返回。于是,CPU0回到該開始時的do_softirq()函數繼續執行,并為高優先級中斷的ISR所觸發的軟中斷請求補上一次服務。從這里可以看出,do_softirq()函數在同一個CPU上的執行是串行的。?
函數源碼如下(kernel/softirq.c):?
asmlinkage?void?do_softirq()?
{?
int?cpu?=?smp_processor_id();?
__u32?active,?mask;?
if?(in_interrupt())?
return;?
local_bh_disable();?
local_irq_disable();?
mask?=?softirq_mask(cpu);?
active?=?softirq_active(cpu)?&?mask;?
if?(active)?{?
struct?softirq_action?*h;?
restart:?
/*?Reset?active?bitmask?before?enabling?irqs?*/?
softirq_active(cpu)?&=?~active;?
local_irq_enable();?
h?=?softirq_vec;?
mask?&=?~active;?
do?{?
if?(active?&?1)?
h->action(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();?
}?
}
.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;?i<32;?i++)?
tasklet_init(bh_task_vec+i,?bh_action,?i);?
……?
}?
因此,bh_action()函數將負責相應地調用參數所指定的bh函數。該函數是連接tasklet機制與Bottom?Half機制的關鍵所在。?
6.2.3?bh_action()函數?
該函數的源碼如下(kernel/softirq.c):?
static?void?bh_action(unsigned?long?nr)?
{?
int?cpu?=?smp_processor_id();?
if?(!spin_trylock(&global_bh_lock))?
goto?resched;?
if?(!hardirq_trylock(cpu))?
goto?resched_unlock;?
if?(bh_base[nr])?
bh_base[nr]();?
hardirq_endlock(cpu);?
spin_unlock(&global_bh_lock);?
return;?
resched_unlock:?
spin_unlock(&global_bh_lock);?
resched:?
mark_bh(nr);?
}?
對該函數的注釋如下:?
①首先,調用spin_trylock()函數試圖對自旋鎖global_bh_lock進行加鎖,同時該函數還將返回自旋鎖global_bh_lock的原有值的非。因此,如果global_bh_lock已被某個CPU上鎖而為非0值(那個CPU肯定在執行某個BH函數),那么spin_trylock()將返回為0表示上鎖失敗,在這種情況下,當前CPU是不能執行BH函數的,因為另一個CPU正在執行BH函數,于是執行goto語句跳轉到resched程序段,以便在當前CPU上再一次調度該BH函數。?
②調用hardirq_trylock()函數鎖定當前CPU,確保當前CPU不是處于硬件中斷請求服務中,如果鎖定失敗,跳轉到resched_unlock程序段,以便先對global_bh_lock解鎖,在重新調度一次該BH函數。?
③此時,我們已經可以放心地在當前CPU上執行BH函數了。當然,對應的BH函數指針bh_base[nr]必須有效才行。?
④從BH函數返回后,先調用hardirq_endlock()函數(實際上它什么也不干,調用它只是為了保此加、解鎖的成對關系),然后解除自旋鎖global_bh_lock,最后函數就可以返回了。?
⑤resched_unlock程序段:先解除自旋鎖global_bh_lock,然后執行reched程序段。?
⑥resched程序段:當某個CPU正在執行BH函數時,當前CPU就不能通過bh_action()函數來調用執行任何BH函數,所以就通過調用mark_bh()函數在當前CPU上再重新調度一次,以便將這個BH函數留待下次軟中斷服務時執行。?
6.3.4?Bottom?Half的原有接口函數?
(1)init_bh()函數?
該函數用來在bh_base[]數組登記一個指定的bh函數,如下所示(kernel/softirq.c):?
void?init_bh(int?nr,?void?(*routine)(void))?
{?
bh_base[nr]?=?routine;?
mb();?
}?
(2)remove_bh()函數?
該函數用來在bh_base[]數組中注銷指定的函數指針,同時將相對應的tasklet殺掉。如下所示(kernel/softirq.c):?
void?remove_bh(int?nr)?
{?
tasklet_kill(bh_task_vec+nr);?
bh_base[nr]?=?NULL;?
}?
(3)mark_bh()函數?
該函數用來向當前CPU標記由一個BH函數等待去執行。它實際上通過調用tasklet_hi_schedule()函數將相應的tasklet加入到當前CPU的tasklet隊列tasklet_hi_vec[cpu]中,然后觸發軟中斷請求HI_SOFTIRQ,如下所示(include/linux/interrupt.h):?
static?inline?void?mark_bh(int?nr)?
{?
tasklet_hi_schedule(bh_task_vec+nr);?
}?
6.3.5?預定義的BH函數?
在32個BH函數指針中,大多數已經固定用于一些常見的外設,比如:第0個BH函數就固定地用于時鐘中斷。Linux在頭文件include/linux/interrupt.h中定義了這些已經被使用的BH函數所引,如下所示:?
enum?{?
TIMER_BH?=?0,?
TQUEUE_BH,?
DIGI_BH,?
SERIAL_BH,?
RISCOM8_BH,?
SPECIALIX_BH,?
AURORA_BH,?
ESP_BH,?
SCSI_BH,?
IMMEDIATE_BH,?
CYCLADES_BH,?
CM206_BH,?
JS_BH,?
MACSERIAL_BH,?
ISICOM_BH?
};
6.4?任務隊列Task?Queue?
任務隊列是與Bottom?Half機制緊密相連的。因為Bottom?Half機制只有有限的32個函數指針,而且大部分都已被系統預定義使用,所以早期版本的Linux內核為了擴展Bottom?Half機制,就設計了任務隊列機制。?
所謂任務隊列就是指以雙向隊列形式連接起來的任務鏈表,每一個鏈表元數都描述了一個可執行的任務(以函數的形式表現)。如下圖所示:?
任務隊列機制實現在include/linux/tqueue.h頭文件中。?
6.4.1?數據結構的定義?
Linux用數據結構tq_struct來描述任務隊列中的每一個鏈表元數(即一個可執行的任務):?
struct?tq_struct?{?
struct?list_head?list;?/*?linked?list?of?active?bh's?*/?
unsigned?long?sync;?/*?must?be?initialized?to?zero?*/?
void?(*routine)(void?*);?/*?function?to?call?*/?
void?*data;?/*?argument?to?function?*/?
};?
這個數據結構很簡單,在此就不詳述。?
然后,Linux定義了數據結構task_queue來描述任務隊列的頭部,其實task_queue就是結構類型list_head,如下:?
typedef?struct?list_head?task_queue;?
但是Linux又定義了一個宏DECLARE_TASK_QUEUE()來輔助我們更方便地定義任務隊列的鏈表表頭:?
#define?DECLARE_TASK_QUEUE(q)?LIST_HEAD(q)?
一個任務隊列是否處于active狀態主要取決于其鏈表表頭(即task_queue結構)是否為空,因此Linux定義宏TQ_ACTIVE()來判斷一個任務隊列是否有效:?
#define?TQ_ACTIVE(q)?(!list_empty(&q))?
顯然,只要任務隊列表頭q不為空,該任務隊列就是有效的。?
6.4.2?向任務隊列中插入一個新任務?
(1)保護自旋鎖?
由于任務隊列是系統全局的共享資源,所以面臨競爭的問題。為了實現對任務隊列鏈表的互斥訪問,Linux在kernel/timer.c文件中定義了一個任務隊列保護自旋鎖tqueue_lock,如下:?
spinlock_t?tqueue_lock?=?SPIN_LOCK_UNLOCKED;?
該自旋鎖在tqueue.h頭文件中也有原型聲明:?
extern?spinlock_t?tqueue_lock;?
任何想要訪問任務隊列的代碼都首先必須先持有該自旋鎖。?
(2)queue_task()函數?
實現在tqueue.h頭文件中的內聯函數queue_task()用來將一個指定的任務添加到某指定的任務隊列的尾部,如下:?
/*?
*?Queue?a?task?on?a?tq.?Return?non-zero?if?it?was?successfully?
*?added.?
*/?
static?inline?int?queue_task(struct?tq_struct?*bh_pointer,?task_queue?*bh_list)?
{?
int?ret?=?0;?
if?(!test_and_set_bit(0,&bh_pointer->sync))?{?
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机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 信用卡全额还款
- 下一篇: 村镇银行是私人银行吗?和信用社比哪个安全