1. 程式人生 > >tasklet與workqueue區別

tasklet與workqueue區別

轉自 http://blog.csdn.net/av_geek/article/details/41278801

一、中斷處理的tasklet(小任務)機制

中斷服務程式一般都是在中斷請求關閉的條件下執行的,以避免巢狀而使中斷控制複雜化。但是,中斷是一個隨機事件,它隨時會到來,如果關中斷的時間太長,CPU就不能及時響應其他的中斷請求,從而造成中斷的丟失。因此,Linux核心的目標就是儘可能快的處理完中斷請求,盡其所能把更多的處理向後推遲。例如,假設一個數據塊已經達到了網線,當中斷控制器接受到這個中斷請求訊號時,Linux核心只是簡單地標誌資料到來了,然後讓處理器恢復到它以前執行的狀態,其餘的處理稍後再進行(如把資料移入一個緩衝區,接受資料的程序就可以在緩衝區找到資料)。因此,核心把中斷處理分為兩部分:上半部(tophalf)和下半部(bottomhalf),上半部(就是中斷服務程式)核心立即執行,而下半部(就是一些核心函式)留著稍後處理,

首先,一個快速的“上半部”來處理硬體發出的請求,它必須在一個新的中斷產生之前終止。通常,除了在裝置和一些記憶體緩衝區(如果你的裝置用到了DMA,就不止這些)之間移動或傳送資料,確定硬體是否處於健全的狀態之外,這一部分做的工作很少。

下半部執行時是允許中斷請求的,而上半部執行時是關中斷的,這是二者之間的主要區別。

但是,核心到底什時候執行下半部,以何種方式組織下半部?這就是我們要討論的下半部實現機制,這種機制在核心的演變過程中不斷得到改進,在以前的核心中,這個機制叫做bottomhalf(簡稱bh),在2.4以後的版本中有了新的發展和改進,改進的目標使下半部可以在多處理機上並行執行,並有助於驅動程式的開發者進行驅動程式的開發。下面主要介紹常用的小任務(Tasklet)機制及2.6核心中的工作佇列機制。


小任務機制

這裡的小任務是指對要推遲執行的函式進行組織的一種機制。其資料結構為tasklet_struct,每個結構代表一個獨立的小任務,其定義如下:

  1. <SPAN style="FONT-WEIGHT: normal">struct tasklet_struct {  
  2. struct tasklet_struct *next; /*指向連結串列中的下一個結構*/
  3. unsignedlong state; /* 小任務的狀態*/
  4. atomic_tcount; /* 引用計數器*/
  5. void(*func) (unsigned long); /* 要呼叫的函式*/
  6. unsignedlong data; 
    /* 傳遞給函式的引數*/
  7. };</SPAN>  
  1. <span style="FONT-WEIGHT: normal">struct tasklet_struct {  
  2. struct tasklet_struct *next; /*指向連結串列中的下一個結構*/
  3. unsignedlong state; /* 小任務的狀態*/
  4. atomic_tcount; /* 引用計數器*/
  5. void(*func) (unsigned long); /* 要呼叫的函式*/
  6. unsignedlong data; /* 傳遞給函式的引數*/
  7. };</span>  


結構中的func域就是下半部中要推遲執行的函式,data是它唯一的引數。
State域的取值為TASKLET_STATE_SCHED或TASKLET_STATE_RUN。TASKLET_STATE_SCHED表示小任務已被排程,正準備投入執行,TASKLET_STATE_RUN表示小任務正在執行。TASKLET_STATE_RUN只有在多處理器系統上才使用,單處理器系統什麼時候都清楚一個小任務是不是正在執行(它要麼就是當前正在執行的程式碼,要麼不是)。
Count域是小任務的引用計數器。如果它不為0,則小任務被禁止,不允許執行;只有當它為零,小任務才被啟用,並且在被設定為掛起時,小任務才能夠執行。
1. 宣告和使用小任務大多數情況下,為了控制一個尋常的硬體裝置,小任務機制是實現下半部的最佳選擇。小任務可以動態建立,使用方便,執行起來也比較快。
我們既可以靜態地建立小任務,也可以動態地建立它。選擇那種方式取決於到底是想要對小任務進行直接引用還是一個間接引用。如果準備靜態地建立一個小任務(也就是對它直接引用),使用下面兩個巨集中的一個:
DECLARE_TASKLET(name,func, data)
DECLARE_TASKLET_DISABLED(name,func, data)
這兩個巨集都能根據給定的名字靜態地建立一個tasklet_struct結構。當該小任務被排程以後,給定的函式func會被執行,它的引數由data給出。這兩個巨集之間的區別在於引用計數器的初始值設定不同。第一個巨集把建立的小任務的引用計數器設定為0,因此,該小任務處於啟用狀態。另一個把引用計數器設定為1,所以該小任務處於禁止狀態。例如:
DECLARE_TASKLET(my_tasklet,my_tasklet_handler, dev);
這行程式碼其實等價於
structtasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0),
tasklet_handler,dev};
這樣就建立了一個名為my_tasklet的小任務,其處理程式為tasklet_handler,並且已被啟用。當處理程式被呼叫的時候,dev就會被傳遞給它。
2. 編寫自己的小任務處理程式小任務處理程式必須符合如下的函式型別:
voidtasklet_handler(unsigned long data)
由於小任務不能睡眠,因此不能在小任務中使用訊號量或者其它產生阻塞的函式。但是小任務執行時可以響應中斷。
3. 排程自己的小任務通過呼叫tasklet_schedule()函式並傳遞給它相應的tasklt_struct指標,該小任務就會被排程以便適當的時候執行:
tasklet_schedule(&my_tasklet); /*把my_tasklet標記為掛起 */
在小任務被排程以後,只要有機會它就會盡可能早的執行。在它還沒有得到執行機會之前,如果一個相同的小任務又被排程了,那麼它仍然只會執行一次。
可以呼叫tasklet_disable()函式來禁止某個指定的小任務。如果該小任務當前正在執行,這個函式會等到它執行完畢再返回。呼叫tasklet_enable()函式可以啟用一個小任務,如果希望把以DECLARE_TASKLET_DISABLED()建立的小任務啟用,也得呼叫這個函式,如:
tasklet_disable(&my_tasklet); /*小任務現在被禁止,這個小任務不能執行*/
tasklet_enable(&my_tasklet); /* 小任務現在被啟用*/
也可以呼叫tasklet_kill()函式從掛起的佇列中去掉一個小任務。該函式的引數是一個指向某個小任務的tasklet_struct的長指標。在小任務重新排程它自身的時候,從掛起的佇列中移去已排程的小任務會很有用。這個函式首先等待該小任務執行完畢,然後再將它移去。
4.tasklet的簡單用法
下面是tasklet的一個簡單應用,以模組的形成載入。
  1. <SPAN style="FONT-WEIGHT: normal">#include <linux/module.h>  
  2. #include<linux/init.h> 
  3. #include<linux/fs.h> 
  4. #include<linux/kdev_t.h> 
  5. #include <linux/cdev.h> 
  6. #include <linux/kernel.h> 
  7. #include<linux/interrupt.h> 
  8. staticstruct t asklet_struct my_tasklet;  
  9. staticvoid tasklet_handler (unsigned longd ata)  
  10. {  
  11. printk(KERN_ALERT,"tasklet_handler is running./n");  
  12. }  
  13. staticint __init test_init(void)  
  14. {  
  15. tasklet_init(&my_tasklet,tasklet_handler,0);  
  16. tasklet_schedule(&my_tasklet);  
  17. return0;  
  18. }  
  19. staticvoid __exit test_exit(void)  
  20. {  
  21. tasklet_kill(&tasklet);  
  22. printk(KERN_ALERT,"test_exit is running./n");  
  23. }  
  24. MODULE_LICENSE("GPL");  
  25. module_init(test_init);  
  26. module_exit(test_exit);</SPAN>  
  1. <span style="FONT-WEIGHT: normal">#include <linux/module.h>  
  2. #include<linux/init.h>
  3. #include<linux/fs.h>
  4. #include<linux/kdev_t.h>
  5. #include <linux/cdev.h>
  6. #include <linux/kernel.h>
  7. #include<linux/interrupt.h>
  8. staticstruct t asklet_struct my_tasklet;  
  9. staticvoid tasklet_handler (unsigned longd ata)  
  10. {  
  11. printk(KERN_ALERT,"tasklet_handler is running./n");  
  12. }  
  13. staticint __init test_init(void)  
  14. {  
  15. tasklet_init(&my_tasklet,tasklet_handler,0);  
  16. tasklet_schedule(&my_tasklet);  
  17. return0;  
  18. }  
  19. staticvoid __exit test_exit(void)  
  20. {  
  21. tasklet_kill(&tasklet);  
  22. printk(KERN_ALERT,"test_exit is running./n");  
  23. }  
  24. MODULE_LICENSE("GPL");  
  25. module_init(test_init);  
  26. module_exit(test_exit);</span>  

從這個例子可以看出,所謂的小任務機制是為下半部函式的執行提供了一種執行機制,也就是說,推遲處理的事情是由tasklet_handler實現,何時執行,經由小任務機制封裝後交給核心去處理。

二、中斷處理的工作佇列機制

工作佇列(work queue)是另外一種將工作推後執行的形式,它和前面討論的tasklet有所不同。工作佇列可以把工作推後,交由一個核心執行緒去執行,也就是說,這個下半部分可以在程序上下文中執行。這樣,通過工作佇列執行的程式碼能佔盡程序上下文的所有優勢。最重要的就是工作佇列允許被重新排程甚至是睡眠。

那麼,什麼情況下使用工作佇列,什麼情況下使用tasklet。如果推後執行的任務需要睡眠,那麼就選擇工作佇列。如果推後執行的任務不需要睡眠,那麼就選擇tasklet。另外,如果需要用一個可以重新排程的實體來執行你的下半部處理,也應該使用工作佇列。它是唯一能在程序上下文執行的下半部實現的機制,也只有它才可以睡眠。這意味著在需要獲得大量的記憶體時、在需要獲取訊號量時,在需要執行阻塞式的I/O操作時,它都會非常有用。如果不需要用一個核心執行緒來推後執行工作,那麼就考慮使用tasklet。

  1. 工作、工作佇列和工作者執行緒

如前所述,我們把推後執行的任務叫做工作(work),描述它的資料結構為work_struct,這些工作以佇列結構組織成工作佇列(workqueue),其資料結構為workqueue_struct,而工作執行緒就是負責執行工作佇列中的工作。系統預設的工作者執行緒為events,自己也可以建立自己的工作者執行緒。

  1. 表示工作的資料結構

工作用<linux/workqueue.h>中定義的work_struct結構表示:

  1. <SPAN style="FONT-WEIGHT: normal">struct work_struct{  
  2. unsigned long pending; /* 這個工作正在等待處理嗎?*/
  3. struct list_head entry; /* 連線所有工作的連結串列 */
  4. void (*func) (void *); /* 要執行的函式 */
  5. void *data; /* 傳遞給函式的引數 */
  6. void *wq_data; /* 內部使用 */
  7. struct timer_list timer; /* 延遲的工作佇列所用到的定時器 */
  8. };</SPAN>  
  1. <span style="FONT-WEIGHT: normal">struct work_struct{  
  2. unsigned long pending; /* 這個工作正在等待處理嗎?*/
  3. struct list_head entry; /* 連線所有工作的連結串列 */
  4. void (*func) (void *); /* 要執行的函式 */
  5. void *data; /* 傳遞給函式的引數 */
  6. void *wq_data; /* 內部使用 */
  7. struct timer_list timer; /* 延遲的工作佇列所用到的定時器 */
  8. };</span>  

這些結構被連線成連結串列。當一個工作者執行緒被喚醒時,它會執行它的連結串列上的所有工作。工作被執行完畢,它就將相應的work_struct物件從連結串列上移去。當連結串列上不再有物件的時候,它就會繼續休眠。

3. 建立推後的工作

要使用工作佇列,首先要做的是建立一些需要推後完成的工作。可以通過DECLARE_WORK在編譯時靜態地建該結構:

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

這樣就會靜態地建立一個名為name,待執行函式為func,引數為data的work_struct結構。

同樣,也可以在執行時通過指標建立一個工作:

INIT_WORK(struct work_struct *work, woid(*func) (void *), void *data);

這會動態地初始化一個由work指向的工作。

4. 工作佇列中待執行的函式

工作佇列待執行的函式原型是:

void work_handler(void *data)

這個函式會由一個工作者執行緒執行,因此,函式會執行在程序上下文中。預設情況下,允許響應中斷,並且不持有任何鎖。如果需要,函式可以睡眠。需要注意的是,儘管該函式執行在程序上下文中,但它不能訪問使用者空間,因為核心執行緒在使用者空間沒有相關的記憶體對映。通常在系統呼叫發生時,核心會代表使用者空間的程序執行,此時它才能訪問使用者空間,也只有在此時它才會對映使用者空間的記憶體。

5. 對工作進行排程

現在工作已經被建立,我們可以排程它了。想要把給定工作的待處理函式提交給預設的events工作執行緒,只需呼叫

schedule_work(&work);

work馬上就會被排程,一旦其所在的處理器上的工作者執行緒被喚醒,它就會被執行。

有時候並不希望工作馬上就被執行,而是希望它經過一段延遲以後再執行。在這種情況下,可以排程它在指定的時間執行:

schedule_delayed_work(&work, delay);

這時,&work指向的work_struct直到delay指定的時鐘節拍用完以後才會執行。

6. 工作佇列的簡單應用

  1. <SPAN style="FONT-WEIGHT: normal">#include<linux/module.h>  
  2. #include<linux/init.h> 
  3. #include<linux/workqueue.h> 
  4. staticstruct workqueue_struct *queue =NULL;  
  5. staticstruct work_struct work;  
  6. staticvoid work_handler(struct work_struct*data)  
  7. {  
  8. printk(KERN_ALERT"work handler function./n");  
  9. }  
  10. staticint __init test_init(void)  
  11. {  
  12. queue= create_singlethread_workqueue("helloworld"); /*建立一個單執行緒的工作佇列*/
  13. if(!queue)  
  14. goto err;  
  15. INIT_WORK(&work, work_handler);  
  16. schedule_work(&work);/*schedule_work是新增到系統的events workqueue, 要新增到自己的workqueue, 應該使用queue_work, 故此處有誤*/
  17. return 0;  
  18. err:  
  19. return-1;  
  20. }  
  21. staticvoi