i. 下半部
1. 下半部
執行與中斷處理密切相關但中斷處理程序本身不執行的工作。
2. 爲什麼用下半部
中斷處理流程(上半部)的侷限性:
a) 中斷處理程序以異步方式執行並且可能會打斷重要代碼(也有可能是其他中斷處理程序)的執行。爲了避免被打斷的代碼停止時間過長,中斷處理程序執行得越快越好。
b) 如果當前有一箇中斷處理程序正在執行,在最好的情況下,該中斷同級的其他中斷會被屏蔽,在最壞的情況下,當前處理器上所有其他中斷都會被屏蔽。因此,讓其執行得越快越好。
c) 由於中斷處理程序要對硬件操作,它們通常有很高的時限要求。
d) 中斷處理程序不能運行在進程上下文,不能阻塞。限制了其所作的事情。
中斷處理流程(上半部)負責對硬件迅速響應並完成時間要求嚴格的操作。對時間要求寬鬆的任務,推遲到中斷被激活以後再去運行。
下半部的優勢:推遲運行強調的是不是馬上運行,不需要指明一個確切的時間,只要把這些任務推遲一點,讓它們在系統不太繁忙併且中斷恢復後執行。通常下半部在中斷處理程序一返回馬上運行。下半部執行的關鍵在於當它們運行的時候會,允許響應所有的中斷。
3. 上半部,下半部任務劃分
對於上半部和下半部之間劃分工作,儘管不存在某種嚴格的規則,但還是有一些提示:
a) 一個任務對時間非常敏感,將其放在上半部。
b) 一個任務和硬件相關,將其放在上半部。
c) 一個任務保證不被其他中斷(特別是相同的中斷)打斷,將其放在上半部。
d) 其他所有任務,考慮放在下半部執行。
4. 下半部實現機制
下半部實現:軟中斷,tasklet,工作隊列。
Softirq
Tasklet
Work Queue
執行上下文
延後的工作,運行於中斷上下文
延後的工作,運行於中斷上下文
延後的工作,運行於進程上下文
可重用
可以在不同的CPU上同時運行
不能在不同的CPU上同時運行,
但是不同的CPU可以運行不同的tasklet
可以在不同的CPU上同時運行
睡眠
不能睡眠
不能睡眠
可以睡眠
搶佔
不能搶佔/調度
不能搶佔/調度
可以搶佔/調度
易用性
不容易使用
容易使用
容易使用
何時使用
如果延後的工作不會睡眠,
而且有嚴格的可擴展性或速度要求
如果延後的工作不會睡眠
如果延後的工作會睡眠
ii. 軟中斷
軟中斷保留給系統中對時間要求最嚴格以及最重要的下半部使用。網絡和SCSI直接使用軟中斷。內核定時器和tasklet都是建立在軟中斷上面。下面以網絡子系統爲例說明如何使用軟中斷。
1. 軟中斷處理程序
static void net_tx_action(struct softirq_action *h);
static void net_rx_action(struct softirq_action *h)
2. 註冊軟中斷處理程序
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
3. 觸發軟中斷
raise_softirq(NET_TX_SOFTIRQ);
raise_softirq(NET_RX_SOFTIRQ);
raise_softirq()函數將一個軟中斷設置爲掛起狀態,讓它在下次調用do_softirq()函數時運行處理程序。
4. 執行軟中斷
在合適的時刻,軟中斷會運行。在下列地方,待處理的軟中斷會被檢查和執行:
a) 從一個硬件中斷代碼返回時。
b) 在ksoftirqd內核線程中。
c) 顯式檢查和執行待處理的軟中斷代碼中,如網絡子系統。
軟中斷在do_softirq()中執行。如果有待處理的軟中斷,此函數會循環遍歷每一個,調用它們的處理函數。核心代碼如下:
h->action(h)執行實際的軟中斷處理函數
iii. Tasklet
大多數情況下,爲了控制一個尋常的硬件設備,tasklet機制都是實現下半部的最佳選擇。Tasklet可以動態創建,使用方便,執行起來還算快。
1. Tasklet的實現
a) 註冊tasklet
在softirq_init()函數中調用open_softirq(TASKLET_SOFTIRQ, tasklet_action);和open_softirq(HI_SOFTIRQ,tasklet_hi_action);函數註冊tasklet處理函數。
b) 觸發tasklet action函數
Tasklet_schedule()和task_hi_schedule函數進行調度tasklet。下面爲兩個函數實現的簡單說明:
1) 檢查tasklet的狀態是否爲TASK_STATE_SCHED.如果是,說明tasklet已經被調度過。函數立即返回。
2) 保存中斷狀態, 然後禁止本地中斷。
3) 把需要調度的tasklet加到每個處理器的tasklet_vec鏈表或tasklet_hi_vec鏈表的表頭上。
4) 喚起TASKLET_SOFTIRQ 或HI_SOFTIRQ軟中斷,這樣在下一次調用do_softirq()函數時執行此tasklet
5) 恢復中斷到原狀態返回。
c) asklet_action()和tasklet_hi_action()軟中斷處理函數
這兩個函數是tasklet處理的核心。下面是簡單實現:
1) 禁止中斷,並檢索保存當前處理器tasklet_vec和tasklet_hi_vec鏈表。
2) 清空當前處理器的鏈表。
3) 允許響應中斷。
4) 循環遍歷獲得鏈表上每一個待處理的tasklet.
5) 如果是多處理器系統。檢查TASKLET_STATE_RUN來判斷這個tasklet是否真正運行在其他處理器上。如果真正運行,就不要執行,跳到下一個待處理的tasklet去。(同一時間,相同類型的tasklet只能有一個執行)。
6) 如果當前tasklet沒有執行,將其狀態標誌設置爲TASKLET_STATE_RUN,這樣其它處理器不會再去執行它。
7) 檢查count是否爲0,確保tasklet沒有被禁止。如果被禁止,跳到下一個tasklet。
8) 執行tasklet的處理程序。
9) Tasklet執行完畢,清除tasklet的state域的TASKLET_STATE_RUN狀態標誌。
10) 重複執行下一個tasklet,直到沒有剩餘的等待處理的tasklet。
核心代碼如下:
2. 使用tasklet
a) asklet處理程序
void short_do_tasklet(unsigned long);
b) 聲明tasklet
靜態創建:DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);
動態創建:tasklet_init(t, short_do_tasklet, dev);
c) 調度tasklet
tasklet_schedule(&short_tasklet);
d) 禁用或激活tasklet
Tasklet_disable(&short_tasklet);
如果該tasklet正在運行,這個函數會等到它執行完畢再返回。
Tasklet_disable_nosync(&short_tasklet);
無須在返回前等待tasklet執行完畢。
Tasklet_enable(&short_tasklet);
e) 去掉tasklet
從掛起的隊列中去掉一個tasklet。這個函數等待該tasklet執行完畢,然後將它移去。該函數可能會引起休眠,禁止在中斷上下文中使用。
iv. Ksoftirqd
Problem:如果軟中斷出現的頻率很高,再加上它們又有將自己重新設置爲可執行狀態的能力,就會導致用戶空間進程無法獲得足夠的處理器時間,處於飢餓狀態。如果單純的對重新觸發的軟中斷採取不立即處理的策略,也無法接受。
Solution: 1. 只要還有被觸發並等待處理的軟中斷,本次執行就負責處理,重新觸發的軟中斷也在本次執行返回前處理。這樣可以保證即時處理。但系統可能一直處理軟中斷,不能完成用戶任務。這樣,用戶空間根本不能容忍有明顯的停頓出現。2. 選擇不處理重新觸發的軟中斷。從中斷返回的時候,檢查掛起的軟中斷並處理,但重新觸發的軟中斷不馬上處理,等一段時間,到下一個軟中斷執行時處理。但比較空閒的系統中,立即處理軟中斷纔是比較好的做法。
結論:設計軟中斷時,做一個折中。當大量軟中斷出現的時候,內核會喚醒一組內核線程來處理這些負載。這些線程在最低的優先級上運行,這能避免和其他重要的任務搶奪資源。
每個處理器都有一個這樣的線程。此線程的名字叫ksoftirqd/n。一旦該線程被初始化,它就會執行類似下面的死循環:
v. 工作隊列
工作隊列是另外一種將工作推後執行的形式。它把工作推後,交給內核線程去執行—運行在進程上下文。由於執行於進程上下文,它允許重新調度或睡眠。
1. 工作隊列的實現
a) 數據結構間的關係
b) worker_thread()函數
初始化結束後,這個函數執行一個死循環並開始休眠。當有操作被插入到隊列後,線程被喚醒,以便執行下半部操作。此函數核心流程如下:
c) run_workqueue()函數
2. 使用工作隊列
a) 靜態創建: DECLARE_WORK(name, void(*func)(void *)data);
動態創建:INIT_WORK(struct work_struct *work,void(*func)(void*), void *data);
b) 工作隊列處理函數
Void work_handler(void *data);
該函數運行在進程上下文中,允許響應中斷,不持有任何鎖。
c) 對工作隊列進行調度
Schedule_work(&work);
Schedule_delayed_work(&work, delay);
d) 刷新操
排入隊列的工作會在工作者線程下一次被喚醒的時候執行。但在繼續下一步工作之前,有可能需要保證一些操作已經執行完畢(卸載)。基於以上 考慮,內核提供了用於刷新指定工作隊列的函數。
Void flush_scheduled_work(void);
e) 創建新的隊列
Struct workqueue_struct *create_workqueue(const char * name);
Queue_work(&wq, &work) à Schedule_work(&work);
Queue_delayed_work(&wq,&work,delay) àSchedule_delayed_work(&work,delay);
Flush_workqueue(&wq) -> flush_scheduled_work(void);
vi. 在下半部加鎖
1. asklet之間同步:加鎖
2. 軟中斷之間共享數據:加鎖
3. 進程上下文和下半部共享數據:加鎖並禁止下半部的處理
本地和SMP的保護並且防止死鎖
禁止下半部:void local_bh_diable()
激活下半部:void local_bh_enable()
4. 中斷上下文和下半部共享數據:加鎖並禁止中斷。
本地和SMP的保護並且防止死鎖