1. 程式人生 > >linux中斷處理的上半部和下半部

linux中斷處理的上半部和下半部

裝置中斷會打斷核心中程序的正常排程和執行,系統對更高吞吐率的追求勢必要求中斷服務儘可能的短小精悍。但是,在大多數真實的系統中,當中斷到來時,要完成的工作往往並不會是短小的,它可能要進行較大量的耗時處理。

Linux核心中,為了在中斷執行時間儘可能短和中斷處理需完成大量工作之間找到一個平衡點,Linux將中斷處理程式分為兩個部分:上半部(top half)和下半部(bottom half)。中斷處理程式的上半部在接收到一箇中斷時就立即執行,但只做比較緊急的工作,這些工作都是在所有中斷被禁止的情況下完成的,所以要快,否則其它的中斷就得不到及時的處理。那些耗時又不緊急的工作被推遲到下半部去。中斷處理程式的下半部分(如果有的話)幾乎做了中斷處理程式所有的事情。它們最大的不同是上半部分不可中斷,而下半部分可中斷。在理想的情況下,最好是中斷處理程式上半部分將所有工作都交給下半部分執行,這樣的話在中斷處理程式上半部分中完成的工作就很少,也就能儘可能快地返回。但是,中斷處理程式上半部分一定要完成一些工作,例如,通過操作硬體對中斷的到達進行確認,還有一些從硬體拷貝資料等對時間比較敏感的工作。剩下的其他工作都可由下半部分執行。

對於上半部分和下半部分之間的劃分沒有嚴格的規則,靠驅動程式開發人員自己的程式設計習慣來劃分,不過還是有一些習慣供參考:

如果該任務對時間比較敏感,將其放在上半部中執行。

如果該任務和硬體相關,一般放在上半部中執行。

如果該任務要保證不被其他中斷打斷,放在上半部中執行(因為這是系統關中斷)。

其他不太緊急的任務,一般考慮在下半部執行。

下半部分並不需要指明一個確切時間,只要把這些任務推遲一點,讓它們在系統不太忙併且中斷恢復後執行就可以了。通常下半部分在中斷處理程式一返回就會馬上執行。核心中實現下半部的手段不斷演化,目前已經從最原始的BHbottom half)衍生出BH(在2.5

中去除)、軟中斷(softirq2.3引入)、tasklet(在2.3引入)、工作佇列(work queue2.5引入)。稍後筆者將介紹後兩種方式。

儘管上半部和下半部的結合能夠改善系統的響應能力,但是,Linux裝置驅動中的中斷處理並不一定要分成兩個半部。如果中斷要處理的工作本身就很少,則完全可以直接在上半部全部完成。

前面說過,Linux實現下半部的機制主要有tasklet、工作佇列和軟中斷等。下面對tasklet機制和工作佇列機制在Linux系統中的實現作簡單介紹。

tasklet機制

tasklet可以理解為軟體中斷的派生,所以它的排程時機和軟中斷一樣。對於核心中需要延遲執行的多數任務都可以用

tasklet來完成,由於同類tasklet本身已經進行了同步保護,所以使用tasklet比軟中斷要簡單的多,而且效率也不錯。tasklet把任務延遲到安全時間執行的一種方式,在中斷期間執行,即使被排程多次,tasklet也只執行一次。

軟中斷和tasklet都是執行在中斷上下文中,它們與任一程序無關,沒有支援的程序完成重新排程。所以軟中斷和tasklet不能睡眠、不能阻塞,它們的程式碼中不能含有導致睡眠的動作,如減少訊號量、從使用者空間拷貝資料或手工分配記憶體等。

tasklet的使用相當簡單,我們只需要定義tasklet及其處理函式並將二者關聯:

void my_tasklet_func(unsigned long);

DECLARE_TASKLET(my_tasklet,my_tasklet_func,data);

其中my_tasklet_func(unsigned long)定義了tasklet的處理函式;DECLARE_TASKLET (my_tasklet,my_tasklet_func,data)實現了將名稱為my_tasklettaskletmy_tasklet_func()函式相關聯。然後,在需要排程tasklet的時候引用下面的的API就能使系統在適當的時候進行排程執行:

tasklet_schedule(&my_tasklet);

此外,Linux還提供了另外一些其它的控制tasklet排程與執行的API

DECLARE_TASKLET_DISABLED(name,function,data); //DECLARE_TASKLET類似,但等待tasklet被使能

tasklet_enable(struct tasklet_struct *); //使能

tasklet tasklet_disble(struct tasklet_struct *); //禁用

tasklet tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long); //類似DECLARE_TASKLET()

tasklet_kill(struct tasklet_struct *); // 清除指定tasklet的可排程位,即不允許排程該tasklet

tasklet的具體實現程式碼如下:

#include

//定義與繫結tasklet函式
void test_tasklet_action(unsigned long t);
DECLARE_TASKLET(test_tasklet, test_tasklet_action, 0);

void test_tasklet_action(unsigned long t)
{
printk("tasklet is executing\n");
}



ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{

if (copy_from_user(&global_var, buf, sizeof(int)))
{
return - EFAULT;
}

//排程tasklet執行
tasklet_schedule(&test_tasklet);
return sizeof(int);
}

它的功能是:在globalvar被寫入一次後,就排程一個tasklet,函式中輸出"tasklet is executing"。

工作佇列機制

工作佇列是Linux 2.6 核心中新增加的一種下半部機制。它與其它幾種下半部分機制最大的區別就是它可以把工作推後,交由一個核心執行緒去執行。核心執行緒只在核心空間執行,沒有自己的使用者空間,它和普通程序一樣可以被排程,也可以被搶佔。該工作佇列總是會在程序上下文執行。這樣,通過工作佇列執行的程式碼能佔盡程序上下文的所有優勢,最重要的就是工作佇列允許重新排程甚至是睡眠。因此,如果推後執行的任務需要睡眠,那麼就選擇工作佇列;如果推後執行的任務不需要睡眠,那麼就選擇tasklet。另外,如果需要獲得大量的記憶體、需要獲取訊號量或者需要執行阻塞式的I/O操作時,使用工作佇列的方式將非常有用。

工作佇列的使用方法和tasklet非常相似,下面的程式碼用於定義一個工作佇列和一個底半部執行函式:

struct work_struct my_wq; //定義一個工作佇列

void my_wq_func(unsigned long); //定義一個處理函式

通過INIT_WORK()可以初始化該工作佇列並將工作佇列與處理函式繫結,如下所示:

INIT_WORK(&my_wq, (void(*) (void *))my_wq_func, NULL);

tasklet_schedule()對應的用於排程工作佇列執行的函式為schedule_work(),如下:

schedule_work(&my_wq); //排程工作佇列執行