文章目錄
1.下半部
下半部的任務就是執行與中斷處理密切相關但中斷處理程序本身不執行的工作。
ps:在理想情況下,最好的是中斷處理程序將所有工作都交給下半部分執行,因爲中斷處理程序中完成的工作越少越好(越快越好)。
對於上半部和下半部之間劃分工作,儘管不存在某種嚴格的規則,但還是有一些提示可供借鑑:
- 如果一個任務對事件非常敏感,將其放在中斷處理程序中執行;(上半部)
- 如果一個任務和硬件相關,將其放在中斷處理程序中執行;(上半部)
- 如果一個任務要保證不被其他中斷(特別是相同的中斷)打斷,將其放在中斷處理程序中執行;(上半部)
- 其他所有任務,考慮放在下半部執行。
1.1 爲什麼要用下半部
Q:爲什麼要用下半部?
A:在中斷上下文運行的時候,當前的中斷線在所有處理器上都會被屏蔽。更糟糕的是,如果一個處理程序是IRQF_DISABLED類型,它執行的時候會禁止所有本地中斷(而且把本地中斷線全局地屏蔽掉)。而縮短中斷被屏蔽的時間對系統的響應能力和性能都至關重要。再加上中斷處理程序要與其他程序(甚至是其他的中斷處理程序)異步執行。必須儘量減少中斷處理的執行,解決方法就是把一些工作放到”以後“去做。(這個以後僅僅說明不是馬上)
通常下半部在中斷處理程序一返回就會馬上運行。下半部執行的關鍵在於當它們運行的時候,允許響應所有的中斷。
1.2 下半部的環境
和上半部只能通過中斷處理程序實現不同,下半部可以通過多種機制實現。
這些用來實現下半部的機制分別由不同的接口和子系統組成。
被棄用的一些機制:BH,任務隊列(Tsk queues)。
ps:不要錯誤的把所有的下半部機制都叫做”軟中斷“。
當前,由三種機制可以用來實現將工作推後執行:軟中斷、tasklet和工作隊列。
下表揭示了下半部機制的演化例程:
2.軟中斷
實際的下半部實現–軟中斷方法開始。軟中斷使用得比較少;而tasklet是下半部更常用的一種形式。(tasklet是通過軟中斷實現的),軟中斷代碼位於kernel/softirq.c文件中。
2.1 軟中斷的實現
軟中斷是在編譯期間靜態分配的。它不像tasklet那樣能被動態的註冊或註銷。
軟中斷由softirq_action結構標識,它定義在<linux/interrupt.h>中:
struct softirq_action {
void (*action)(struct softirq_action *);
};
kernel/softirq.c中定義了一個包含由32個該結構體的數組。
static struct softirq_action softirq_vec[NR_SOFTIRQS];
每個被註冊的軟中斷都佔據該數組的一項,因此最多可能由32個軟中斷。
ps:NR_SOFTIRQS是一個定值–註冊的軟中斷數目的最大值無法動態改變。
軟中斷處理程序:
軟中斷處理程序action的函數原型如下:
void softirq_handler(struct softirq_action *);
當內核運行一個軟中斷處理程序的時候,它就會執行這個action函數,其唯一參數爲指向相應softirq_action結構體的指針。如:如果my_softirq指向softirq_vec數組的某項,那麼內核會用如下的方式調用軟中斷處理程序中的函數:
my_softirq->action(my_softirq);
notice:這裏內核把整個結構體都傳遞給軟中斷處理程序而不是僅僅傳遞數據值。這是因爲可以保證將來在結構體加入新的域時,無須對所有軟中斷處理程序都進行變動。或者如有需要,軟中斷處理程序可以方便地解析它的參數,從數據成員中提取整數。
ps:一個軟中斷不會搶佔另外一個軟中斷。實際上,唯一可以搶佔軟中斷的是中斷處理程序。不過,其他的軟中斷(甚至是相同類型的軟中斷)可以在其它處理器上同時執行。
執行軟中斷:
觸發軟中斷(raising the softirq):一個註冊的軟中斷必須在標記後纔會執行。
通常,中斷處理程序在返回前標記它的軟中斷,使其在稍後被執行。
在下列地方,待處理的軟中斷會被檢查和執行:
- 從一個硬件中斷代碼處返回時;
- 在ksoftirqd內核線程中;
- 在那些顯式檢查和執行待處理的軟中斷的代碼中,如網絡子系統中。
在軟中斷被喚醒後,軟中斷都要在do_softirq()中執行。
關於do_softirq函數的解析可閱讀源碼或者參考該書8.2.1章節。
2.2 使用軟中斷
軟中斷保留給系統中對時間要求最嚴格以及最重要的下半部使用。目前,只有兩個子系統(網絡和SCSI)直接使用軟中斷。
ps:在想實現一個軟中斷之前,先想想用tasklet能不能滿足要求。另外,對於時間要求嚴格並能自己高效的完成加鎖工作的應用,軟中斷會是正確的選擇。
Q:如何使用軟中斷?
A:
1.分配索引
在編譯期間,通過在<linux/interrupt.h>中定義的一個枚舉類型來靜態聲明軟中斷。
建立一個新的軟中斷必須在此枚舉類型中加入新的項。
索引號小的軟中斷在索引號大的軟中斷之前執行。
加入新項的時候必須根據希望賦予它的優先級來決定加入的位置。
ps:習慣上HI_SOFTIRQ通常作爲第一項,而RCU_SOFTIRQ作爲最後一項。
新項可能插在BLOCK_SOFTIRQ和TASKLET_SOFTIRQ之間。
2.註冊處理程序
在運行時通過open_softirq()註冊軟中斷處理程序,該函數接受兩個參數:軟中斷的索引號和處理函數。
eg: 網絡子系統 net/coreldev.c通過以下方式註冊自己的軟中斷:
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
notice:
軟中斷處理程序執行的時候,允許響應中斷,但它自己不能休眠。
在一個處理程序運行的時候,當前處理器上的軟中斷被禁止。
如果同一個軟中斷在它被執行的同時被再次觸發了,那麼另一個處理器可以同時運行其處理程序。(可重入),這裏就要考慮到併發和資源競爭的問題,而同一個處理程序的多個實例能在多個處理器上同時運行又是軟中斷的特色,如果單純靠鎖避免併發競爭,那麼這二者就衝突了。
ps:
大部分的軟中斷處理程序,都是通過採取單處理器數據(僅屬於一個處理器的數據,因此根本不需要加鎖)或其他一些技巧來避免顯示加鎖,從而提供更出色的性能。
引入軟中斷的主要原因是其可擴展性。如果不需要擴展到多個處理器,那麼,可使用tasklet.
tasklet本質上也是軟中斷,只不過同一個處理程序的多個實例不能再多個處理器上同時運行。
3.觸發軟中斷–raise_sofrirq()
raise_sofrirq()函數可以將一個軟中斷設置爲掛起狀態,讓它在下次調用do_softirq()函數時投入運行。
ps:
raise_sofrirq()函數在觸發一個軟中斷之前先要禁止中斷,觸發後再恢復原來的狀態。如果中斷本來就已經禁止了,可以調用另一函數raise_softirq_irqoff()會帶來一些優化效果。
在中斷處理程序中觸發軟中斷是最常見的形式。
通常,中斷處理程序執行硬件設備的相關操作,然後觸發相應的軟中斷,最後退出。內核在執行完中斷處理程序後,馬上就會調用do_softirq()函數。這樣軟中斷開始執行中斷處理程序留給它去完成的剩餘任務。(上半部和下半部的含義就此顯現)
3.tasklet
tasklet是利用軟中斷實現的一種下半部機制。它的接口更簡單,鎖保護也要求較低。
Q:選擇tasklet還是軟中斷?
A:通常應該選用tasklet,軟中斷的使用者屈指可數,它只在那些執行頻率很高和連續性要求很高的情況才需要使用。
3.1 tasklet的實現–本身也是軟中斷
tasklet有兩類軟中斷代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。
這兩者之間唯一的實際區別在於,HI_SOFTIRQ類型的軟中斷先於TASKLET_SOFTIRQ類型的軟中斷執行。
1.tasklet由tasklet_struct結構表示。每個結構體單獨代表一個tasklet,它在<linux/interrupt.h>中定義爲:
struct tasklet_struct {
struct tasklet_struct *next; /* 鏈表中的下一個tasklet */
unsigned long state; /* tasklet的狀態 */
atomic_t count; /* 引用計數器 */
void (*func)(unsigned long); /* tasklet處理函數 */
unsigned long data; /* 給tasklet處理函數的參數 */
};
state成員只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之間取值。
TASKLET_STATE_SCHED表明tasklet已經被調度,正準備投入運行;
TASKLET_STATE_RUN表明該tasklet正在運行。
count成員是tasklet的引用計數器。如果爲0,則tasklet被禁止,不允許執行;只有爲0時,tasklet才被激活,並且在被設置爲掛起狀態時,該tasklet才能夠執行。
ps:TASKLET_STATE_RUN只有在多處理器的系統上纔會作爲一種優化來使用,單處理器系統任何時候都清楚單個tasklet是不是正在運行(要麼正在運行,要麼不是)。
2.調度tasklet
已調度的tasklet(等同於被觸發的軟中斷)存放在兩個單處理器數據結構:tasklet_vec(普通tasklet)和tasklet_hi_vec(高優先級的tasklet)。
這兩個數據結構都是由tasklet_struct結構體構成的鏈表。鏈表中的每個tasklet_struct代表一個不同的tasklet。
tasklet由tasklet_schedule()和tasklet_hi_schedule()函數進行調度,它們接受一個指向tasklet_struct結構的指針作爲參數。
這二個杉樹區別在於一個使用TASKLET_SOFTIRQ而另一個使用HI_SOFTIRQ.
其餘的皆類似。
tasklet_schedule的執行步驟:
- 檢查tasklet的狀態是否爲TASKLET_STATE_SCHED。如果是,說明tasklet已經被調度過了(也可能是已經調度單還沒來得及執行),函數立即返回;
- 調用_tasklet_schedule();
- 保存中斷狀態,然後禁止本地中斷。這麼做的原因是在執行tasklet代碼時候,能夠保證當tasklet_schedule處理這些tasklet時,處理器上的數據不會弄亂;
- 把需要調度的tasklet加到每個處理器一個的tasklet_vec鏈表或tasklet_hi_vec鏈表的表頭上去;
- 喚起TASKLET_SOFTIRQ或HI_SOFTIRQ軟中斷,下次調用do_softirq時就會執行該tasklet;
- 恢復中斷到原狀態返回。
do_softirq會儘可能地在下一個何時的時機執行,由於大部分tasklet和軟中斷都是在中斷處理程序中被設置成待處理狀態,所以最近一箇中斷返回的時候看起來就是執行do_softirq的最佳時機。
因爲TASKLET_SOFTIRQ和HI_SOFTIRQ已經被觸發了,所以do_softirq會執行相應的軟中斷處理程序。也就是tasklet處理的核心–tasklet_action()和tasklet_hi_action()
tasklet處理的核心執行流程如下:
- 禁止中斷(沒有必要首先保存其狀態,這裏的代碼總是作爲軟中斷被調用,而且中斷總是被激活的),並未當前處理器檢索tasklet_vec或tasklet_hi_vec鏈表;
- 將當前處理器上的該鏈表設置爲NULL,達到清空的效果;
- 允許響應中斷。沒有必要再恢復它們回原狀態,因爲這段程序本身就是作爲軟中斷處理程序被調用的,所以中斷是應該被允許的;
- 循環遍歷獲得鏈表上的每一個待處理的tasklet;
- 如果是多處理器系統,通過檢查TASKLET_STATE_RUN來判斷這個tasklet是否正在其他處理器上運行。如果它正在允許,那麼現在就不要執行,跳到下一個待處理的tasklet去–同一時間裏,相同類型的tasklet只能由一個執行;
- 如果當前這個tasklet沒有執行,將其狀態設置爲TASKLET_STATE_RUN,這樣別的處理器就不會再去執行它了;
- 檢查count值是否爲0,確保tasklet沒有被禁止。如果tasklet被禁止了,則跳到下一個掛起的tasklet去;
- 目前已經清楚的知道這個tasklet沒有再其他地方執行,並且被設置成執行狀態,這樣它再其他部分就不會被執行,而且引用計數爲0,現在可以執行tasklet的處理程序了;
- 重複執行下一個tasklet,直至沒有剩餘的等待處理的tasklet。
ps:tasklet接口的使用保證了同一時間裏只有一個給定類型的tasklet會被執行(但其他不同類型的tasklet可以同時執行) 。
3.2 使用tasklet
Q:如何使用tasklet?
A:
1.聲明tasklet
tasklet的聲明支持靜態或動態,創建方式取決於需要一個對tasklet的直接引用(靜態)還是簡介引用(動態)。
方式 | 宏 | 說明 |
---|---|---|
靜態 | DECLARE_TASKLET(name, func, data); DECLARE_TASK_DISABLED(name, func, data); |
這兩個宏都能根據給定的名稱 靜態的創建一個tasklet_struct結構. 當該tasklet被調度後, 給定的函數func回被執行, 它的參數由data給出 它們的區別在於 引用計數的初始值不同。 一個是0,一個是1; 則前者是激活,後者是禁止。 |
動態 | tasklet_int(t, tasklet_handler, dev); |
將一個間接引用(一個指針) 賦給一個動態創建的tasklet_struct結構 的方式來初始化一個tasklet_init() |
2.編寫tasklet處理程序
tasklet處理程序必須符號規定的函數類型(回調):
void tasklet_handler(unsigned long data);
因爲靠軟中斷實現,所以tasklet不能睡眠。也就是說不能在tasklet中使用信號量或者其他什麼阻塞式的函數。
ps:由於tasklet運行時允許響應中斷,必須做好資源的保護工作。
3.調度tasklet
調用tasklet_schedule()函數並傳遞給它相應的tasklet_struct的指針,該tasklet就會被調度以便執行:
tasklet_schedule(&my_tasklet); /* 把 my_tasklet 標記爲掛起 */
在tasklet被調度以後,只要有機會它就會儘可能早地運行。
在它還沒有得到運行機會之前,如果有一個相同的tasklet又被調度了,那麼它仍然只會運行依次。
而如果這時它已經開始運行了,比如說在另一個處理器上,那麼這個新的tasklet會被重新調度並再次運行。(疑問:這樣的話,豈不是也要做鎖保護?) 作爲一種優化措施,一個tasklet總在調度它的處理器上執行–希望更好的利用處理器的高速緩存。
tasklet相關操作函數如下表所示:
函數 | 說明 |
---|---|
tasklet_disable() | 禁止某個指定的tasklet(阻塞式) ,會等tasklet執行完畢在返回 |
tasklet_disable_nosync() | 禁止某個指定的tasklet(非阻塞式) |
tasklet_enable() | 激活一個tasklet(包括靜態創建的tasklet) |
tasklet_kill() | 從掛起的隊列中去掉一個tasklet,該函數的參數是一個指向某個tasklet_struct的長指針 |
ps:tasklet_kill函數在處理一個經常重新調度它自身的tasklet的時候,從掛起的隊列中移去已調度的tasklet會很有用。這個函數首先等待該tasklet執行完畢,然後再將它移去。
注意:沒有什麼可以阻止其他地方的代碼重新調度該tasklet。由於該函數可能會引起休眠,所以禁止再中斷上下文中使用它。
4.ksoftirqd–輔助處理軟中斷(包括tasklet)的內核線程
每個處理器都有一組輔助處理軟中斷(和tasklet)的內核線程。
當內核線程中出現大量軟中斷的時候,這些內核線程就會輔助處理它們。
Q:爲什麼需要ksoftirqd這個線程?
A:對於軟中斷,內核會選擇在幾個特殊時機進行處理。而在中斷處理程序返回時處理是最常見的。
軟中斷被觸發的頻率有時可能很高(像在進行大流量的網絡通信期間–這樣就會中斷很頻繁,根據前面的分析,會導致軟中斷觸發的頻率很高)。
更不利的是,處理函數有時還會自行重複觸發。也就是說,當一個軟中斷執行的時候,它可以重新觸發自己以便再次得到執行。如果軟中斷本身出現的頻率就高,再加上它們又有將自己重新設置爲可執行狀態的能力,那麼就會導致用戶空間進程無法獲得足夠的處理器實際,因而處於飢餓狀態。
因此提出以下兩種最容易直觀的方案(具體方案可參考本書8.3.2章節),因爲這兩種方案要不是會讓用戶空間的進程處於飢餓狀態就是會讓軟中斷處理程序處於飢餓狀態。得不到一個好的利用處理器的處理方案。引出了一個這種並且同時兼顧不讓用戶空間進程飢餓和軟中斷處理程序飢餓的處理方式–引入ksoftirqd線程。
在大量軟中斷出現的時候,內核會喚醒一組內核線程(ksoftirqd)來處理這些負載。
這些線程在最低的優先級上運行(nice值是19),這能避免它們跟其他重要的任務搶奪資源。但它們最終肯定會被執行,所以,這個折中的方案能夠保證在軟中斷負擔很重的時候,用戶程序不會因爲得不到處理時間而處於飢餓狀態,也能保證”過量“的軟中斷終究會得到處理。
ps:在空閒系統上(負載很低的情況),這個方案也同樣表現良好,軟中斷處理得非常迅速,因爲i僅存的內核線程肯定會馬上調度。
notice:
每個處理器都有一個這樣的線程。
所有線程的名字都叫做ksoftirqd/n,區別在於n,它對應的是處理器的編號。
如下所示:
該線程會執行類似如下步驟:
for(;;) {
if(!softirq_pending(cpu))
schedule();
set_current_state(TASK_RUNNING);
while(softirq_pending(cpu)) {
do_softirq();
if(need_resched())
schedule();
}
set_current_state(TASK_INTERRUPTIBLE);
}
只要有待處理的軟中斷(由softirq_pending()函數負責發現),ksoftirq就會調用do_softirq()去處理它們。通過重複執行這樣的操作,重新觸發的軟中斷也會被執行。如果有必要,每次迭代後都會調用schedule()以便讓更重要的進程得到處理機會。當所有需要執行的操作都完成以後,該內核線程將自己設置爲TASK_INTERRUPTIBLE狀態,喚起調度程序選擇其他可執行進程投入運行。
只要do_softirq()函數發現已經執行過的內核線程重新觸發了它自己,軟中斷內核線程就會被喚醒。
3.3 老的BH機制
因爲是丟棄了的機制,不多學習,詳細可參考本書8.3.3章節。這裏簡單提下丟棄的原因,隨着多處理器的發展,老的BH機制不利於多處理器的可擴展性,也不利於大型SMP的性能。使用BH的驅動程序很難從多個處理器受益,特別是網絡層,可以說是飽受困擾。
4.工作隊列
工作隊列(work queue)是另外一種將工作推後執行的形式。
工作隊列可以把工作推後,交由一個內核線程去執行--這個下半部總是會在進程上下文中執行。
通過工作隊列執行的代碼能佔盡進程上下文的所有優勢,特別是工作隊列允許重新調度甚至是睡眠。
工作隊列通常可以用內核線程替換,但內核開發者們非常反對創建新的內核線程(因爲在有些場合,可能會吃到苦頭)。
當需要用一個可以重新調度的實體來執行你的下半部處理,應該使用工作隊列。
工作隊列是唯一能在進程上下文中運行的下半部實現機制,下半部實現機制也只有它纔可以睡眠。
在需要獲得大量內存時,在需要獲取信號量的時候,在需要執行阻塞式的I/O操作時,它都會非常有用。
ps:如果不需要用一個內核線程來推後執行工作,考慮tasklet。
4.1 工作隊列的實現
工作隊列子系統是一個用於創建內核線程(工作者線程(woker thread))的接口,通過它創建的進程負責執行由內核其他部分排到隊列裏的任務。
ps:工作隊列子系統可以提供了兩種工作者線程:驅動創建專門的工作者線程和由工作隊列子系統提供的一個缺省的工作者線程
由上述可知工作隊列最基本的表現形式,就轉變成了一個把需要推後執行的任務交給特定的通用線程的這樣一種接口。
缺省的工作者線程叫做events/n,同前面見過的ksoftirqd的n一個意思,標識處理器的編號。
缺省的工作者線程會從多個地方得到被推後的工作。
ps:
許多內核驅動程序都把它們的下半部交給缺省的工作者線程去做。除非一個驅動程序或者子系統必須建立屬於它自己的內核線程,否則最好使用缺省線程。
處理器密集型和性能要求嚴格的任務會因爲擁有自己的工作者線程而獲得好處。此時這麼做也有助於減輕缺省線程的負擔,避免工作隊列中其他需要完成的工作處於飢餓狀態。
工作者線程用workqueue_struct結構標識:
/*
* 外部可見的工作隊列抽象是
* 由每個CPU的工作隊列組成的數組
*/
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS]; /* 數組中的每一項對應系統中的一個處理器 定義在kernel/workqueue.c中 */
struct list_head list;
const char *name;
int singlethread;
int freezeable;
int re;
};
由於系統中每個處理器對應一個工作者線程,所以對於給定的計算機,每個處理器,每個工作者線程都對應這樣一個cpu_workqueue_struct結構體。
cpu_workqueue_struct是kernle/workqueue.c中的核心數據結構:
struct cpu_workqueue_struct {
spinlock_t lock; /* 鎖保護這種結構 */
struct list_head worklist; /* 工作列表 */
wait_queue_head_t more_work;
struct work_struct *current_struct;
struct workqueue_struct *wq; /* 關聯工作隊列結構 */
task_t *thread; /* 關聯線程 */
};
表示工作的數據結構:
工作用<linux/workqueue.h>中定義的work_struct結構體表示:
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
};
所有的工作者線程都是用普通的內核線程實現的,它們都要執行worker_thread()函數。
在它初始化完成後,這個函數執行一個死循環並開始休眠;
當有操作被插入到隊列裏的時候,線程就會被喚醒,以便執行這些操作;
當沒有剩餘的操作時,又繼續休眠。
ps:work_thread函數的工作分析可參考本書8.4.1章節。
3.工作隊列實現機制的總結
每個工作者線程都由一個cpu_workqueue_struct結構體表示。
workqueue_struct結構體則表示給定類型的所有工作者線程。
工作用work_struct結構表示,該結構體中最重要的是一個指針,指向一個函數,該函數負責處理需要推後指向的具體任務。
工作被提交給某個具體的工作者線程後,這個工作者線程就會被喚醒並執行該函數。
4.2 使用工作隊列
1.創建推後的工作
方式 | 聲明 | 說明 |
---|---|---|
靜態 | DECLARE_WORK(name, void (*func) (void *), void *data); | 靜態的創建一個名爲name, 處理函數爲func, 參數爲data的work_struct結構體 |
動態 | INIT_WORK(struct work_struct *work, void(*func) (void *), void *data); | 動態地初始化一個 由work指向的工作, 處理函數爲func,參數爲data |
2.工作隊列處理函數
原型如下:
void work_handler(void *data);
該函數會由一個工作中線程執行,因此,函數會運行在進程上下文中。
默認情況下,允許響應中斷,並不持有任何鎖。如有需要,函數可以睡眠。
ps:
儘管操作處理函數運行在進程上下文中,但它不能訪問用戶空間,因爲內核線程在用戶空間沒有相關的內存映射。因此在工作隊列和內核其他部分之間使用鎖機制就像在其他進程上下文中使用鎖機制一樣方便。
通常在發生系統調用時,內核會代表用戶空間的進程運行,此時它才能訪問用戶空間,也只有在此時它纔會映射用戶空間的內存。
3.對工作進行調度
Q:如何把給定工作的處理函數提交給缺省的events工作線程?
A:如下表所示:
方法函數 | 說明 |
---|---|
schedule_work(&work) | work馬上就會被調度, 一旦其所在的處理器上的工作者線程被喚醒,它就會被執行 |
schedule_delay_work(&work, delay) | &work指向的work_struct直到delay指定的時鐘節拍用完以後纔會執行 |
4.刷新操作
排入隊列的工作會在工作者線程下一次被喚醒的時候執行。
考慮到如下情況:有時在繼續下一步工作之前,必須保證一些操作已經執行完畢。如在模塊卸載之前,就需要保證有些操作已經執行完畢。而在內核的其他部分,爲了防止競爭條件的出現,也可能需要確保不再有待處理的工作。
因此,內核準備了一個用於刷新指定工作隊列的函數:
void flush_scheduled_work(void);
該函數會一直等待,直到隊列中所有對象都被執行以後才返回。
ps:在等待所有待處理的工作執行的時候,該函數會進入休眠狀態–只能在進程上下文中使用。
刷新函數並不取消任何延遲執行的工作–任何通過schedule_delayed_work()調度的工作,如果其延時時間未結束,它並不會因爲調用flush_scheduled_work()而被刷新掉。
取消延時執行的工作應調用如下:
int cancel_delayed_work(struct work_struct *work); /* 取消任何與work相關的掛起工作 */
Q:前面提到工作者線程可用系統提供的缺省的events,也可以是驅動對應自己的,如何做?
A:創建新的工作隊列,如果缺省的隊列不能滿足需要,那麼應該創建一個新的工作隊列和與之相應的工作者線程。由於這麼做會在每個處理器上都創建一個工作者線程,所以只有你在明確了必須要靠自己的一套線程來提高性能的情況下,再創建自己的工作隊列。
創建一個新的任務隊列和與之相關的工作者線程,需要使用如下函數:
struct workqueue_struct *create_workqueue(const char *name); /* name參數用於該內核線程的命名 */
該函數調用後會創建所有的工作者線程(系統中的每個處理器都有一個),並且做好所有開始處理工作之前的準備工作。
新的工作隊列操作函數如下所示(前面的都是針對缺省的events隊列):
函數 | 說明 |
---|---|
int queue_work(struct workqueue_struct *wq, struct work_struct *work); | 同schedule_work() |
int queue_delayed_work(struct workqueue_struct *wq, struct work_struct *work, unsigned long delay); |
同schedule_delayed_work() |
flush_workqueue(struct workqueue_struce *wq); | 同flush_scheduled_work() |
4.3 老的任務隊列(現在是工作隊列)機制
因爲是丟棄了的機制,不多學習,詳細可參考本書8.4.3章節。簡單介紹下:任務隊列機制通過定義一組隊列來實現其功能,每個隊列都有自己的名字,如調度程序隊列、立即隊列和定時器隊列。
5.下半部機制的選擇
目前的可選擇中有三種:軟中斷、tasklet和工作隊列。
tasket基於軟中斷實現。
工作隊列機制靠內核線程實現。
三種下半部機制的比較如下:
Q:如何選擇下半部機制?
A:
1.軟中斷:
從設計的角度考慮,軟中斷提供的執行序列化的保障最少。這就要求軟中斷處理函數必須格外小心地採取一些步驟確保共享數據的安全,兩個甚至更多相同類別的軟中斷有可能在不同的處理器上同時執行。
如果被考察的代碼本身多線索化的工作就做得非常好,如網絡子系統,它完全使用單處理器變量,那麼軟中斷就是非常好的選擇。對於時間要求嚴格和執行頻率很高(內核會啓動ksofrirqd線程輔助)的應用來說,它執行得也快。
2.tasklet:
如果代碼多線索化考慮得並不充分,那麼選擇tasklet意義更大。它的接口非常簡單,而且,由於兩個同種類型的tasklet不能同時執行,所以實現起來也會簡單一些。tasklet是有效的軟中斷,但不能併發運行。驅動程序開發者應當儘可能選擇tasklet而不是軟中斷,當然,如果準備利用每一處理器上的變量或者類似的情形,以確保軟中斷能安全地在多個處理器上併發的運行,那麼還是選擇軟中斷。
3.工作隊列:
如果需要把任務推後到進程上下文中(可能睡眠),那麼在這三者中就只能選擇工作隊列了。如果進程上下文並不是必須的條件(不需要睡眠),那麼軟中斷和tasklet可能更合適。工作隊列造成的開銷最大,因爲它要牽扯到內核線程甚至是上下文切換。這並不是說工作隊列的效率低,如果每秒鐘有幾千次中斷,像網絡子系統時常經歷的那樣,那麼採用其他的機制可能更合適些。儘管如此,針對大部分情況,工作隊列都能夠提供足夠的支持。
上述簡單來說,是否需要一個可調度的實體(是否有休眠需要)來執行需要推後完成的工作–有,工作隊列就是唯一選擇。
否則最好使用tasklet,若是必須專注於性能的提高,那麼就考慮軟中斷。
6.在下半部之間加鎖
在使用下半部機制時,即使是在單處理器的系統上,避免共享數據被同時訪問也是至關重要的。
記住,一個下半部實際上可能在任何時候執行。
使用tasklet的一個好處在於,它自己負責執行的序列化保障:兩個相同類型的tasklet不允許同時執行,即使在不同的處理器上也不行。
Q:通常在哪些地方枷鎖?
A:
- 如果進程上下文和一個下半部共享數據,在訪問這些數據之前,需要禁止下半部的處理並得到鎖的使用權。是爲了本地和SMP的保護並且防止死鎖的出現;
- 如果中斷上下文和一個下半部共享數據,在訪問數據之前,需要禁止中斷並得到鎖的使用權。爲了本地和SMP的保護並防止死鎖的出現;
- 任何在工作隊列中被共享的數據也需要使用鎖機制,其中有關鎖的要點和一般內核代碼中沒什麼區別,工作隊列本來就是在進程上下文中執行的。
7.禁止下半部
一般如果需要禁止下半部,那麼單純禁止下半部的處理是不夠的的。
爲了保證共享數據的安全,更常見的做法是,先得到一個鎖然後在禁止下半部的處理。
下半部機制控制函數如下所示:
上述函數有可能被嵌套使用–因此最後被調用的local_bh_enable()最後激活下半部。
並且它們通過preempt_count(內核搶佔的時候也是它)爲每個進程維護一個計數器。
當計數器變爲0時,下半部才能夠被處理。因爲下半部的處理已經被禁止,所以local_bh_enable()還需要檢查所有現存的待處理的下半部並執行它們。
這些函數與硬件體系結構相關,位於<asm/softirq.h>中,通常由一些複雜的宏實現。
ps:這些函數並不能禁止工作隊列的執行(工作隊列是在進程上下文中運行的,不會涉及異步執行的問題,所以也就不需要禁止工作隊列執行。而軟中斷和tasklet是異步發生的–在中斷處理返回的時候,所以內核要禁止它們)