7下半部分

我們在前面的“中斷處理”博文中提到,在由內核執行的幾個中斷任務之間有些不是緊急的:在必要情況下它們可以延遲一段時間。回憶一下,一箇中斷處理程序是急迫的,調用do_IRQ將幾個中斷服務例程串行執行,並且通常在一箇中斷的處理程序結束前,不應該再次出現這個中斷,我們叫它“上半部分”。相反,可延遲中斷可以在開中斷的情況下執行。把可延遲中斷從中斷處理程序中抽出來有助於使內核保持較短的響應時間。

 

Linux 2.6對後者的處理叫做下半部分(“bottom half”),是通過兩種非緊迫、可中斷內核函數:所謂的可延遲函數(包括軟中斷與tasklets)和通過工作隊列來執行的函數。

 

軟中斷和tasklet有密切的關係,tasklet是在軟中斷之上實現。這裏先清理一些概念,出現在內核代碼中的術語“軟中斷(softirq)”常常表示可延遲函數的所有種類。另外一種被廣泛使用的術語是“中斷上下文”:表示內核當前正在執行一箇中斷處理程序或一個可延遲的函數。

 

軟中斷的分配是靜態的(即在編譯時定義),而tasklet的分配和初始化可以在運行時進行(例如:安裝一個內核模塊時)。軟中斷(即便是同一種類型的軟中斷)可以併發地運行在多個CPU上。因此,軟中斷是可重入函數而且必須明確地使用自旋鎖保護其數據結構。tasklet不必擔心這些問題,因爲內核對tasklet的執行進行了更加嚴格的控制。相同類型的tasklet總是被串行地執行,換句話說就是:不能在兩個CPU上同時運行相同類型的tasklet。但是,類型不同的tasklet可以在幾個CPU上併發執行。tasklet的串行化使tasklet函數不必是可重入的,因此簡化了設備驅動程序開發者的工作。

 

一般而言,在可延遲函數上可以執行四種操作:

 

初始化(initialization)
定義一個新的可延遲函數;這個操作通常在內核自身初始化或加載模塊時進行。

 

激活(activation)
標記一個可延遲函數爲“掛起”(在可延遲函數的下一輪調度中執行)。激活可以在任何時候進行(即使正在處理中斷)。

 

屏蔽(masking)
有選擇地屏蔽一個可延遲函數,這樣,即使它被激活,內核也不執行它。

 

技行(execution)
執行一個掛起的可延遲函數和同類型的其他所有掛起的可延遲函數;執行是在特定的時間進行的。

 

軟中斷

 

Linux 2.6使用有限個軟中斷。其實,Linux更傾向於用tasklet,因爲大多數場合tasklet是足夠用的,且更容易編寫,因爲tasklet不必是可重入的。如下表所示,目前只定義了六種軟中斷。

 

 

軟中斷

下標 (優先級)

說明

HI_SOFTIRQ

0

處理高優先級的tasklet

TIMER_SOFTIRQ

1

和時鐘中斷相關的tasklet

NET_TX_SOFTIRQ

2

把數據包傳送到網卡

NET_RX_SOFTIRQ

3

從網卡接收數據包

SCSI_SOFTIRQ

4

SCSI命令的後臺中斷處理

TASKLET_SOFTIRQ

5

處理常規tasklet

 

一個軟中斷的下標決定了它的優先級:低下標意味着高優先級,因爲軟中斷函數將從下標0開始執行。

 

軟中斷所使用的數據結構

 

表示軟中斷的主要數據結構是softirq_vec數組,該數組包含類型爲softirq_action的32個元素。一個軟中斷的優先級是相應的softirq_action元素在數組內的下標。如上表所示,只有數組的前六個元素被有效地使用。softirq_action數據結構包括兩個字段:指向軟中斷函數的一個action指針和指向軟中斷函數需要的通用數據結構的data指針。

 

另外一個關鍵的字段是32位的 preempt_count字段,用它來跟蹤內核搶佔和內核控制路徑的嵌套,該字段存放在每個進程描述符的thread_info字段中。如下表所示,preempt_count字段的編碼表示三個不同的計數器和一個標誌。

 

說明

0~7

Preemption counter搶佔計數器 (max value = 255)

8~15

Softirq counter軟中斷計數器 (max value = 255).

16~27

Hardirq counter硬中斷計數器 (max value = 4096)

28

PREEMPT_ACTIVE 標誌

 

第一個計數器記錄顯式禁用本地CPU內核搶佔的次數,值等於0表示允許內核搶佔。第二個計數器表示可延遲函數被禁用的程度(值爲0表示可延遲函數處於激活狀態)。第三個計數器表示在本地CPU上中斷處理程序的嵌套數(irq_enter()宏遞增它的值,irq_exit()宏遞減它的值)。

 

給preempt_count字段起這個名字的理由是很充分的:當內核代碼明確不允許發生搶佔(搶佔計數器不等於0)或當內核正在中斷上下文中運行時,必須禁用內核的搶佔功能。因此,爲了確定是否能夠搶佔當前進程,內核快速檢查preempt_count字段中的相應值是否等於0。

 

宏in_interrupt()檢查current_thread_info()->preempt_count字段的硬中斷計數器和軟中斷計數器,只要這兩個計數器中的一個值爲正數,該宏就產生一個非零值,否則產生一個零值。如果內核不使用多內核棧,則該宏只檢查當前進程的thread_info描述符的preempt_count字段。但是,如果內核使用多內核棧,則該宏可能還要檢查本地CPU的irq_ctx聯合體中thread_info描述符的preempt_count字段。在這種情況下,由於該字段總是正數值,所以宏返回非零值。

 

實現軟中斷的最後一個關鍵的數據結構是每個CPU都有的32位掩碼(描述掛起的軟中斷),它存放在irq_cpustat_t數據結構的__softirq_pending字段中。爲了獲取或設置位掩碼的值,內核使用宏local_softirq_pending(),它選擇本地CPU的軟中斷位掩碼。

 

處理軟中斷

 

open_softirq()函數處理軟中斷的初始化。它使用三個參數:軟中斷下標、指向要執行的軟中斷函數的指針及指向可能由軟中斷函數使用的數據結構的指針。open_softirq()限制自己初始化softirq_vec數組中適當的元素。

 

raise_softirq()函數用來激活軟中斷,它接受軟中斷下標作爲參數。

 

1. 執行local_irq_save宏以保存eflags寄存器IF標誌的狀態值並禁用本地CPU上的中斷。

2. 把軟中斷標記爲掛起狀態,這是通過設置本地CPU的軟中斷掩碼中與下標nr相關的位來實現的。

3. 如果in_interrupt()產生爲1的值,則跳轉到第5步。這種情況說明:要麼已經在中斷上下文中調用了raise_softirq(),要麼當前禁用了軟中斷。

4. 否則,就在需要的時候去調用wakeup_softirqd()以喚醒本地CPU的ksoftirqd內核線程(見後面)。

5. 執行local_irq_restore宏,恢復在第1步保存的IF標誌的狀態值。

 

應該週期性地(但又不能太頻繁地)檢查活動(掛起)的軟中斷,檢查是在內核代碼的幾個點上進行的。這在下列幾種情況下進行(注意,檢查點的個數和位置隨內核版本和所支持的硬件結構而變化):

 

- 當內核調用local_bh_enable()函數激活本地CPU的軟中斷時。
- 當do_IRQ()完成了I/O中斷的處理時或調用irq_exit()宏時。
- 如果系統使用I/O APIC,則當smp_apic_timer_interrupt()函數處理完本地定時器中斷時。
- 在多處理器系統中,當CPU處理完被CALL_FUNCTION_VECTOR處理器間中斷所觸發的函數時。
- 當一個特殊的ksoftirqd/n內核線程被喚醒時(見後面)。

 

do_softirq()函數:如果在這樣的一個檢查點(local_softirq_pending()不爲0)檢測到掛起的軟中斷,內核就調用do_softirq()來處理它們。具體代碼不去分析了。

 

ksoftirq內核線程

 

在最近的內核版本中,每個CPU都有自己的ksoftirqd/n內核線程(這裏,n爲CPU的邏輯號)。每個ksoftirqd/n內核線程都運行ksoftirqd()函數,這個函數實際上執行下列循環:

    for(;;) {
        set_current_state(TASK_INTERRUPTIBLE );
        schedule( );
        /* now in TASK_RUNNING state */
        while (local_softirq_pending( )) {
            preempt_disable();
            do_softirq( );
            preempt_enable();
            cond_resched( );
        }
    }

 

當內核線程被喚醒時,就檢查local_softirq_pending()中的軟中斷位掩碼並在必要時調用do_softirq( )。如果沒有掛起的軟中斷,函數把當前進程狀態置爲TASK_INTERRUPTIBLE,隨後,如果當前進程需要就調用cond_resched()函數來實現進程切換。(當前thread_info的TIF_NEED_RESCHED標誌被設置)

 

ksoftirqd/n內核線程的目的是:爲重要而難以平衡的多CPU負載問題提供瞭解決方案。如果感興趣的兄弟們可以深入研究一下,這裏還是很有意思的。

 

tasklet

 

tasklet是I/O驅動程序中實現可延遲函數的首選方法。

 

如前所述,tasklet建立在兩個叫做HI_SOFTIRQ和TASKLET_SOFTIRQ的軟中斷之上。幾個tasklet可以與同一個軟中斷相關聯,每個tasklet執行自己的函數。兩個軟中斷之間沒有真正的區別,只不過do_softirq()先執行HI_SOFTIRQ的tasklet,後執行TASKLET_SOFTIRQ的tasklet。

 

tasklet和高優先級的tasklet分別存放在tasklet_vec和tasklet_hi_vec數組中。二者都包含類型爲tasklet_head的NR_CPUS個元素,每個元素都由一個指向tasklet描述符鏈表的指針組成。tasklet描述符是一個tasklet_struct類型的數據結構。

 

tasklet是個很重要的概念,特別對於那些編寫驅動程序的兄弟們。讓我們假定,你正在寫一個設備驅動程序,且想使用tasklet,應該做些什麼呢?首先,你應該分配一個新的tasklet_struct數據結構,並調用tasklet_init()初始化它;該函數接收的參數爲tasklet描述符的地址、tasklet函數的地址和它的可選整型參數。

 

隨後,我們可以調用tasklet_disable_nosync()或tasklet_disable()可以選擇性地禁止tasklet。這兩個函數都增加tasklet描述符的count字段,但是後一個函數只有在tasklet函數己經運行的實例結束後才返回。爲了重新激活你的tasklet,可以調用tasklet_enable()。

 

爲了激活tasklet,你應該根據自己tasklet需要的優先級,調用tasklet_schedule()函數或tasklet_hi_schedule()函數。這兩個函數非常類似,每個都執行下列操作:

 

1. 檢查TASKLET_STATE_SCHED標誌;如果設置則返回(tasklet已經被調度)。
2. 調用local_irq_save保存IF標誌的狀態並禁用本地中斷。
3. 在tasklet_vec[n]或tasklet_hi_vec[n]指向的鏈表的起始處增加tasklet描述符(n表示本地CPU的邏輯號)。
4. 調用raise_softirq_irqoff()激活TASKLET_SOFTIRQ或HI_SOFTIRQ類型的軟中斷。(這個函數與raise softirq()函數類似,只是raise_softirq_irqoff()函數假設已經禁用了本地中斷。)
5. 調用local_irq_restore恢復IF標誌的狀態。

 

最後,讓我們看一下tasklet如何被執行。我們從前一節知道,軟中斷函數一旦被激活,就由do_softirq()函數執行。與HI_SOFTIRQ軟中斷相關的軟中斷函數叫做tasklet_hi_action () ,而與TASKLET_SOFTIRQ相關的函數叫做tasklet_action()。這兩個函數非常相似,它們都執行下列操作:

 

1. 禁用本地中斷。
2. 獲得本地CPU的邏輯號n。
3. 把tasklet_vec[n]或tasklet_hi_vec[n]指向的鏈表的地址存入局部變量list。
4. 把tasklet_vec[n]或tasklet_hi_vec[n]的值賦爲NULL,因此,已調度的tasklet描述符的鏈表被清空。
5. 打開本地中斷。
6. 對於list指向的鏈表中的每個tasklet描述符:


a) 在多處理器系統上,檢查tasklet的TASKLET_STATE_RUN標誌。


i. 如果該標誌被設置,說明同類型的一個tasklet正在另一個CPU上運行,因此,就把任務描述符重新插入到由tasklet_vec[n]或tasklet_hi_vec[n]指向的鏈表中,並再次激活TASKLET_SOFTIRQ或HI SOFTIRQ軟中斷。這樣,當同類型的其他tasklet在其他CPU上運行時,這個tasklet就被延遲。
ii. 如果TASKLET_STATE_RUN標誌未被設置,tasklet就沒有在其他CPU上運行,就需要設置這個標誌,以便tasklet函數不能在其他CPU上執行。


b) 通過查看tasklet描述符的count字段,檢查tasklet是否被禁止。如果是,就清TASKLET_STATE_RUN標誌,並把任務描述符重新插入到由tasklet_vec[n]或tasklet_hi_vec[n]指向的鏈表中,然後函數再次激活TASKLET_SOFTIRQ或HI_SOFTIRQ軟中斷。
c) 如果tasklet被激活,清TASKLET_STATE-SCHED標誌,並執行tasklet函數。

 

注意,除非tasklet函數重新激活自己,否則,tasklet的每次激活至多觸發tasklet函數的一次執行。

 

工作隊列

 

在Linux 2.6中引入了工作隊列。它允許內核函數被激活,而且稍後由一種叫做工作者線程(worker thread)的特殊內核線程來執行。

 

注意!千萬不要混淆,工作隊列不是可延遲函數。可延遲函數和工作隊列非常相似,但是它們的區別還是很大的。主要區別在於:可延遲函數運行在中斷上下文中,而工作隊列中的函數運行在進程上下文中。那麼,爲什麼要發明工作隊列這個東西呢,因爲有一些執行可阻塞的函數(例如:需要訪問磁盤數據塊的函數)的唯一方式是在進程上下文中運行。因爲在中斷上下文中不可能發生進程切換。可延遲函數和工作隊列中的函數都不能訪問進程的用戶態地址空間。事實上,一方面,可延遲函數被執行時不可能有任何正在運行的進程。另一方面,工作隊列中的函數是由內核線程來執行的,因此,根本不存在它要訪問的用戶態地址空間。

 

工作隊列的數據結構

 

與工作隊列相關的主要數據結構是名爲workqueue_struct的描述符,它包括一個有NR_CPUS個元素的數組,NR_CPUS是系統中CPU的最大數量(在多處理器系統中複製工作隊列數據結構的原因是每CPU本地數據結構產生更有效的代碼)。每個元素都是cpu_workqueue_struct類型的描述符,cpu_workqueue_struct結構的worklist字段是雙向鏈表的頭,鏈表集中了工作隊列中的所有掛起函數。work_struct數據結構用來表示每一個掛起函數,它的字段如下所示:


pending:如果函數已經在工作隊列鏈表中,該字段值設爲1,否則設爲0
entry:指向掛起函數鏈表前一個或後一個元素的指針
func:掛起函數的地址
data:傳遞給掛起函數的參數,是一個指針
wq_data:通常是指向cpu_workqueue_struct描述符的父結點的指針
timer:用於延遲掛起函數執行的軟定時器

 

工作隊列函數

 

create_workqueue("foo")函數接收一個字符串作爲參數,返回新創建工作隊列的workqueue_struct描述符的地址。該函數還創建n個工作者線程(n是當前系統中有效運行的CPU的個數),並根據傳遞給函數的字符串爲工作者線程命名,如:foo/0, foo/1等等。

 

create_singlethread_workqueue()函數與之相似,但不管系統中有多少個CPU,create_singlethread_workqueue()函數都只創建一個工作者線程。內核調用destroy_workqueue()函數撤消工作隊列,它接收指向workqueue_struct數組的指針作爲參數。

 

queue_work()(封裝在work_struct描述符中)把函數插入工作隊列,它接收wq和work兩個指針。wq指向workqueue_struct描述符,work指向work_struct描述符。queue_work()主要執行下面的步驟:

 

1.檢查要插入的函數是否已經在工作隊列中(work->pending字段等於1),如果是就結束。
2.把work_struct描述符加到工作隊列鏈表中,然後把work->pending置爲1。
3.如果工作者線程在本地CPU的cpu_workqueue_struct描述符的more_work等待隊列上睡眠,該函數喚醒這個線程。

 

queue_delayed_work()函數和queued_work()幾乎是相同的,只是queue_delayed_work()函數多接收一個以系統滴答數來表示時間延遲的參數,它用於確保掛起函數在執行前的等待時間儘可能短。事實上,queue_delayed_work()依靠軟定時器(work_struct描述符的timer字段)把work_struct描述符插入工作隊列鏈表的實際操作向後推遲了。如果相應的work_struct描述符還沒有插入工作隊列鏈表,cancel_delayed_work()就刪除曾被調度過的工作隊列函數。

 

每個工作者線程在worker_thread()函數內部不斷地執行循環操作,因而,線程在絕大多數時間裏處於睡眠狀態並等待某些工作被插入隊列。工作線程一旦被喚醒就調用run_workqueue()函數,該函數從工作者線程的工作隊列鏈表中刪除所有work_struct描述符並執行相應的掛起函數。由於工作隊列函數可以阻塞,因此,可以讓工作者線程睡眠,甚至可以讓它遷移到另一個CPU上恢復執行。

 

有些時候,內核必須等待工作隊列中的所有掛起函數執行完畢。flush_workqueue()函數接收workqueue_struct描述符的地址,並且在工作隊列中的所有掛起函數結束之前使調用進程一直處於阻塞狀態。但是該函數不會等待在調用flush_workqueue()之後新加入工作隊列的掛起函數,每個cpu_workqueue_struct描述符的remove_sequence字段和insert_sequence字段用於識別新增加的掛起函數。

 

預定義工作隊列

 

在絕大多數情況下,爲了運行一個函數而創建整個工作者線程開銷太大了。因此,內核引入叫做events的預定義工作隊列,所有的內核開發者都可以隨意使用它。預定義工作隊列只是一個包括不同內核層函數和I/O驅動程序的標準工作隊列,它的workqueue_struct描述符存放在keventd_wq數組中。爲了使用預定義工作隊列,內核提供下表中列出的函數:

 

 

預定義工作隊列函數

等價的標準工作隊列函數

schedule_work(w)

queue_work(keventd_wq,w)

schedule_delayed_work(w,d)

queue_delayed_work(keventd_wq,w,d) (在任何CPU上)

schedule_delayed_work_on(cpu,w,d)

queue_delayed_work(keventd_wq,w,d) (在任何 CPU上)

flush_scheduled_work( )

flush_workqueue(keventd_wq)

 

當函數很少被調用時,預定義工作隊列節省了重要的系統資源。另一方面,不應該使在預定義工作隊列中執行的函數長時間處於阻塞狀態。因爲工作隊列鏈表中的掛起函數是在每個CPU上以串行的方式執行的,而太長的延遲對預定義工作隊列的其他用戶會產生不良影響。

 

除了一般的events隊列,在Linux2.6中你還會發現一些專用的工作隊列。其中最重要的是塊設備層使用的kblockd工作隊列。


發佈了0 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章