linux中斷--中斷下半部機制的使用&中斷編程

linux中斷--中斷下半部機制的使用&中斷編程
2014-04-15      0 個評論    來源:linux中斷--中斷下半部機制的使用 & 中斷編程  
收藏    我要投稿

中斷程序一般會包含在某個設備的驅動程序中,因此,中斷處理程序本質上還是一個內核模塊。在上篇文章中也看到了一個簡單的中斷處理流程和內核模板的寫法非常相似。但是那個中斷是最簡單的中斷,沒有用到中斷處理的下半部的處理機制,在別的文章中也講述了下半部的處理機制,這裏簡單的使用介紹下!

上文中我們通過一個簡單的例子分析了一箇中斷程序的基本結構。可以看到,中斷處理程序在處理中斷時起到了關鍵作用,也是一箇中斷程序必不可少的部分。不過,現如今的中斷處理流程都會分爲兩部分:上半部分(top half)和下半部分(bottom half)。爲什麼要將一箇中斷分爲如此兩部分?下面的幾個經典原因可以很好的詮釋這個問題。

1.中斷可以隨時的打斷處理機對其他程序的執行,如果被打斷的代碼對系統很重要,那麼此時中斷處理程序的執行時間應該是越短越好。

2.通過上文我們知道,中斷處理程序正在執行時,會屏蔽同條中斷線上的中斷請求;而更嚴重的是,如果設置了IRQF_DISABLED,那麼該中斷服務程序執行是會屏蔽所有其他的中斷請求。那麼此時應該讓中斷處理程序執行的越快越好。

上面的幾個例子都要求中斷服務程序的執行時間越短越好。一般的,中斷處理程序會在上半部分執行。而事實上,幾乎所有的情況,上半部分就只執行中斷處理程序。因此,我們可以這樣認爲:一個完整的中斷處理流程是由中斷處理程序和下半部分共同完成的。

這樣劃分是有一定原因的,因爲我們必須有一個快速、異步而且簡單的處理程序專門來負責對硬件的中斷請求做出快速響應,與此同時也要完成那些對時間要求很嚴格的操作。而那些對時間要求相對寬鬆,其他的剩餘工作則會在稍候的任意時間執行,也就是在所謂的下半部分去執行。

總之,這樣劃分一箇中斷處理過程主要是希望減少中斷處理程序的工作量(當然了,理想情況是將全部工作都拋給下半段。但是中斷處理程序至少應該完成對中斷請求的相應。),因爲在它運行期間至少會使得同級的中斷請求被屏蔽,這些都直接關係到整個系統的響應能力和性能。而在下半段執行期間,則會允許響應所有的中斷。

和上半段只能通過中斷處理程序實現不同的是,下半部可以通過多種機制來完成:小任務(tasklet),工作隊列,軟中斷。在本博客後續的文章當中你會看到,不管是那種機制,它們均爲下半部提供了一種執行機制,比上半部靈活多了。至於何時執行,則由內核負責。

以上是上下部分劃分的基本概述,通過tasklet工作隊列機制,你可以更深刻的理解下部分的執行。

tasklet的實現

tasklet(小任務)機制是中斷處理下半部分最常用的一種方法,其使用也是非常簡單的。正如在前文中你所知道的那樣,一個使用tasklet的中斷程序首先會通過執行中斷處理程序來快速完成上半部分的工作,接着通過調用tasklet使得下半部分的工作得以完成。可以看到,下半部分被上半部分所調用,至於下半部分何時執行則屬於內核的工作。對應到我們此刻所說的tasklet就是,在中斷處理程序中,除了完成對中斷的響應等工作,還要調用tasklet,如下圖示。

\

tasklet由tasklet_struct結構體來表示,每一個這樣的結構體就表示一個tasklet。在<linux/interrupt.h>中可以看到如下的定義:

1 tasklet_struct
2 {
3 structtasklet_struct *next;
4 unsigned long state;
5 atomic_t count;
6 void(*func)(unsignedlong);
7 unsigned long data;
8 };

在這個結構體中,第一個成員代表鏈表中的下一個tasklet。第二個變量代表此刻tasklet的狀態,一般爲TASKLET_STATE_SCHED,表示此tasklet已被調度且正準備運行;此變量還可取TASKLET_STATE_RUN,表示正在運行,但只用在多處理器的情況下。count成員是一個引用計數器,只有當其值爲0時候,tasklet纔會被激活;否則被禁止,不能被執行。而接下來的func變量很明顯是一個函數指針,它指向tasklet處理函數,這個處理函數的唯一參數爲data。

使用tasklet

在使用tasklet前,必須首先創建一個tasklet_struct類型的變量。通常有兩種方法:靜態創建和動態創建。這樣官方的說法仍然使我們不能理解這兩種創建到底是怎麼一回事。不夠透過源碼來分析倒是可以搞明白。

在<linux/interrupt.h>中的兩個宏:

1 464#define DECLARE_TASKLET(name, func, data) \
2 465struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
3 466
4 467#define DECLARE_TASKLET_DISABLED(name, func, data) \
5 468struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

就是我們進行靜態創建tasklet的兩種方法。通過第一個宏創建的tasklet處於激活狀態,再通過調度函數被掛起盡而被內核執行;而通過第二個宏創建的tasklet處於禁止狀態。從兩個宏的定義可以看到,所謂的靜態創建就是直接定義個一個名爲name的tasklet_struct類型的變量,並將宏中各個參數相應的賦值給這個name變量的各個成員。注意,兩個宏在功能上差異就在於對name變量count成員的賦值上,具體原因在第一部分已經說明。也許你對ATOMIC_INIT這樣的初始化方式感到疑惑,那麼看完定義後,你就會一目瞭然:

1 //在arch/x86/include/asm/atomic.h中
2 15#define ATOMIC_INIT(i) { (i) }
3 //在linux/types.h中
4 190typedef struct{
5 191 intcounter;
6 192} atomic_t;

與靜態創建相對的是動態創建,通過給tasklet_init函數傳遞一個事先定義的指針,來動態創建一個tasklet。這個函數源碼如下。

1 470void tasklet_init(structtasklet_struct *t,
2 471 void(*func)(unsignedlong), unsignedlongdata)
3 472{
4 473 t->next = NULL;
5 474 t->state = 0;
6 475 atomic_set(&t->count, 0);
7 476 t->func = func;
8 477 t->data = data;
9 478}

相信你在閱讀上面的代碼是基本上沒有什麼難以理解的地方,不過這裏還是要特別說明一下atomic_set函數:

1 //在arch/x86/include/asm/atomic.h中
2 35static inlinevoidatomic_set(atomic_t *v,inti)
3 36{
4 37 v->counter = i;
5 38}

首先tasklet_init當中,將&t->count傳遞給了此函數。也就是說將atomic_t類型的成員count的地址傳遞給了atomic_set函數。而我們在此函數中卻要爲count變量中的成員counter賦值。如果說我們當前要使用i,那麼應該是如下的引用方法:t-》count.i。明白了嗎?

ok,通過上述兩種方法就可以創建一個tasklet了。同時,你應該注意到不管是上述那種創建方式都有func參數。透過上述分析的源碼,我們可以看到func參數是一個函數指針,它指向的是這樣的一個函數:

1 void tasklet_handler(unsignedlongdata);

如同上半部分的中斷處理程序一樣,這個函數需要我們自己來實現。

創建好之後,我們還要通過如下的方法對tasklet進行調度:

1 tasklet_schedule(&my_tasklet)

通過此函數的調用,我們的tasklet就會被掛起,等待機會被執行

一個舉例

在此只分析上下兩部分的調用關係,完整代碼在這裏查看。

01 //define a argument of tasklet struct
02 static struct tasklet_struct mytasklet;
03  
04 static void mytasklet_handler(unsigned longdata)
05 {
06 printk("This is tasklet handler..\n");
07 }
08  
09 static irqreturn_t myirq_handler(int irq,void* dev)
10 {
11 staticintcount=0;
12 if(count<10)
13 {
14 printk("-----------%d start--------------------------\n",count+1);
15 printk("The interrupt handeler is working..\n");
16 printk("The most of interrupt work will be done by following tasklet..\n");
17 tasklet_init(&mytasklet,mytasklet_handler,0);
18 tasklet_schedule(&mytasklet);
19 printk("The top half has been done and bottom half will be processed..\n");
20 }
21 count++;
22 returnIRQ_HANDLED;
23 }

從代碼中可以看到,在上半部中通過調用tasklet,使得對時間要求寬鬆的那部分中斷程序推後執行。

爲什麼還需要工作隊列?

工作隊列(work queue)是另外一種將中斷的部分工作推後的一種方式,它可以實現一些tasklet不能實現的工作,比如工作隊列機制可以睡眠。這種差異的本質原因是,在工作隊列機制中,將推後的工作交給一個稱之爲工作者線程(worker thread)的內核線程去完成(單核下一般會交給默認的線程events/0)。因此,在該機制中,當內核在執行中斷的剩餘工作時就處在進程上下文(process context)中。也就是說由工作隊列所執行的中斷代碼會表現出進程的一些特性,最典型的就是可以重新調度甚至睡眠。

對於tasklet機制(中斷處理程序也是如此),內核在執行時處於中斷上下文(interrupt context)中。而中斷上下文與進程毫無瓜葛,所以在中斷上下文中就不能睡眠。

因此,選擇tasklet還是工作隊列來完成下半部分應該不難選擇。當推後的那部分中斷程序需要睡眠時,工作隊列毫無疑問是你的最佳選擇;否則,還是用tasklet吧。

中斷上下文

在瞭解中斷上下文時,先來回顧另一個熟悉概念:進程上下文(這個中文翻譯真的不是很好理解,用“環境”比它好很多)。一般的進程運行在用戶態,如果這個進程進行了系統調用,那麼此時用戶空間中的程序就進入了內核空間,並且稱內核代表該進程運行於內核空間中。由於用戶空間和內核空間具有不同的地址映射,並且用戶空間的進程要傳遞很多變量、參數給內核,內核也要保存用戶進程的一些寄存器、變量等,以便系統調用結束後回到用戶空間繼續執行。這樣就產生了進程上下文。

所謂的進程上下文,就是一個進程在執行的時候,CPU的所有寄存器中的值、進程的狀態以及堆棧中的內容。當內核需要切換到另一個進程時(上下文切換),它需要保存當前進程的所有狀態,即保存當前進程的進程上下文,以便再次執行該進程時,能夠恢復切換時的狀態繼續執行。上述所說的工作隊列所要做的工作都交給工作者線程來處理,因此它可以表現出進程的一些特性,比如說可以睡眠等。

對於中斷而言,是硬件通過觸發信號,導致內核調用中斷處理程序,進入內核空間。這個過程中,硬件的一些變量和參數也要傳遞給內核,內核通過這些參數進行中斷處理,中斷上下文就可以理解爲硬件傳遞過來的這些參數和內核需要保存的一些環境,主要是被中斷的進程的環境。因此處於中斷上下文的tasklet不會有睡眠這樣的特性。

工作隊列的使用

內核中通過下述結構體來表示一個具體的工作:

1 struct work_struct
2 {
3 unsigned long pending;//這個工作是否正在等待處理
4 structlist_head entry;//鏈接所有工作的鏈表,形成工作隊列
5 void(*func)(void*);//處理函數
6 void*data;//傳遞給處理函數的參數
7 void*wq_data;//內部使用數據
8 structtimer_list timer;//延遲的工作隊列所用到的定時器
9 };

而這些工作(結構體)鏈接成的鏈表就是所謂的工作隊列。工作者線程會在被喚醒時執行鏈表上的所有工作,當一個工作被執行完畢後,相應的work_struct結構體也會被刪除。當這個工作鏈表上沒有工作時,工作線程就會休眠。

通過如下宏可以創建一個要推後的完成的工作:

1 DECLARE_WORK(name,void(*func)(void*),void*data);

也可以通過下述宏動態創建一個工作:

1 INIT_WORK(structwork_struct *work,void(*func)(void*),void*data);

與tasklet類似,每個工作都有具體的工作隊列處理函數,原型如下:

1 void work_handler(void*data)

將工作隊列機制對應到具體的中斷程序中,即那些被推後的工作將會在func所指向的那個工作隊列處理函數中被執行。

實現了工作隊列處理函數後,就需要schedule_work函數對這個工作進行調度,就像這樣:

1 schedule_work(&work);

這樣work會馬上就被調度,一旦工作線程被喚醒,這個工作就會被執行(因爲其所在工作隊列會被執行)。

(PS;在前面很多篇文章中從理論的角度分析了中斷機制的處理流程,分爲上下兩部完成中斷處理,其實就是爲了滿足各個條件才分兩步的。用實際的例子講述怎麼使用下半部
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章