帶你入門:
1.INIT_WORK(struct work_struct *work, void (*function)(void *), void *data) 上面一句只是定義了work和work對應的操作。 要是在實際使用的時候還是需要你去在適當的條件下激活這個work。只有激活了這個work, 這個work才有運行的機會。這個激活操作接口是shudule_work或是queue_work。 這兩個接口之后只是說這個work有了運行的機會,但是具體到什么時候運行,那要看你用哪個接口激活
的。 如果是shudule_work的話,系統中有個events內核線程,這個線程會處理你用shudule_work接口激活 的所有work。如果是queue_work的話,一般這種情況都是自己創建了一個單獨的處理線程,這樣將 你激活的work和這個線程聯系起來。至於什么時候運行,那就是events或是你定義的特定線程運行的時
候。
2.至於你提到的為什么要用到work。這個的話,我個人的理解是:一般用在對耗時處理上。比如, 當中斷發生的時候,你可以在中斷上下文中完成激活操作,讓那些耗時的操作在work中完成。
系統化講解:
1. 什么是workqueue Linux中的Workqueue機制就是為了簡化內核線程的創建。通過調用workqueue的接口就能創建內核線程。並且可以根據當前系統CPU的個數創建線程的數量,使得線程處理的事務能夠並行化。workqueue是內核中實現簡單而有效的機制,他顯然簡化了內核daemon的創建,方便了用戶的編程.
工作隊列(workqueue)是另外一種將工作推后執行的形式.工作隊列可以把工作推后,交由一個內核線程去執行,也就是說,這個下半部分可以在進程上下文中執行。最重要的就是工作隊列允許被重新調度甚至是睡眠。
2. 數據結構 我們把推后執行的任務叫做工作(work),描述它的數據結構為work_struct, struct work_struct { atomic_long_t data; /*工作處理函數func的參數*/ #define WORK_STRUCT_PENDING 0 /* T if work item pending execution */ #define WORK_STRUCT_STATIC 1 /* static initializer (debugobjects) */ #define WORK_STRUCT_FLAG_MASK (3UL) #define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK) struct list_head entry; /*連接工作的指針*/ work_func_t func; /*工作處理函數*/ #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif };
這些工作以隊列結構組織成工作隊列(workqueue),其數據結構為workqueue_struct, struct workqueue_struct { struct cpu_workqueue_struct *cpu_wq; struct list_head list; const char *name; /*workqueue name*/ int singlethread; /*是不是單線程 - 單線程我們首選第一個CPU -0表示采用默認的工作者線程event*/ int freezeable; /* Freeze threads during suspend */ int rt; };
如果是多線程,Linux根據當前系統CPU的個數創建cpu_workqueue_struct 其結構體就是, truct cpu_workqueue_struct { spinlock_t lock;/*因為工作者線程需要頻繁的處理連接到其上的工作,所以需要枷鎖保護*/ struct list_head worklist; wait_queue_head_t more_work; struct work_struct *current_work; /*當前的work*/ struct workqueue_struct *wq; /*所屬的workqueue*/ struct task_struct *thread; /*任務的上下文*/ } ____cacheline_aligned; 在在該結構主要維護了一個任務隊列,以及內核線程需要睡眠的等待隊列,另外還維護了一個任務上下文,即task_struct。 三者之間的關系如下:

3. 創建工作 3.1 創建工作queue a. create_singlethread_workqueue(name) 該函數的實現機制如下圖所示,函數返回一個類型為struct workqueue_struct的指針變量,該指針變量所指向的內存地址在函數內部調用kzalloc動態生成。所以driver在不再使用該work queue的情況下調用void destroy_workqueue(struct workqueue_struct *wq)來釋放此處的內存地址。

圖中的cwq是一per-CPU類型的地址空間。對於create_singlethread_workqueue而言,即使是對於多CPU系統,內核也只負責創建一個worker_thread內核進程。該內核進程被創建之后,會先定義一個圖中的wait節點,然后在一循環體中檢查cwq中的worklist,如果該隊列為空,那么就會把wait節點加入到cwq中的more_work中,然后休眠在該等待隊列中。
Driver調用queue_work(struct workqueue_struct *wq, struct work_struct *work)向wq中加入工作節點。work會依次加在cwq->worklist所指向的鏈表中。queue_work向cwq->worklist中加入一個work節點,同時會調用wake_up來喚醒休眠在cwq->more_work上的worker_thread進程。wake_up會先調用wait節點上的autoremove_wake_function函數,然后將wait節點從cwq->more_work中移走。
worker_thread再次被調度,開始處理cwq->worklist中的所有work節點...當所有work節點處理完畢,worker_thread重新將wait節點加入到cwq->more_work,然后再次休眠在該等待隊列中直到Driver調用queue_work...
b. create_workqueue

相對於create_singlethread_workqueue, create_workqueue同樣會分配一個wq的工作隊列,但是不同之處在於,對於多CPU系統而言,對每一個CPU,都會為之創建一個per-CPU的cwq結構,對應每一個cwq,都會生成一個新的worker_thread進程。但是當用queue_work向cwq上提交work節點時,是哪個CPU調用該函數,那么便向該CPU對應的cwq上的worklist上增加work節點。
c.小結 當用戶調用workqueue的初始化接口create_workqueue或者create_singlethread_workqueue對workqueue隊列進行初始化時,內核就開始為用戶分配一個workqueue對象,並且將其鏈到一個全局的workqueue隊列中。然后Linux根據當前CPU的情況,為workqueue對象分配與CPU個數相同的cpu_workqueue_struct對象,每個cpu_workqueue_struct對象都會存在一條任務隊列。緊接着,Linux為每個cpu_workqueue_struct對象分配一個內核thread,即內核daemon去處理每個隊列中的任務。至此,用戶調用初始化接口將workqueue初始化完畢,返回workqueue的指針。Workqueue初始化完畢之后,將任務運行的上下文環境構建起來了,但是具體還沒有可執行的任務,所以,需要定義具體的work_struct對象。然后將work_struct加入到任務隊列中,Linux會喚醒daemon去處理任務。
上述描述的workqueue內核實現原理可以描述如下:

3.2 創建工作 要使用工作隊列,首先要做的是創建一些需要推后完成的工作。可以通過DECLARE_WORK在編譯時靜態地建該結構: DECLARE_WORK(name,void (*func) (void *), void *data); 這樣就會靜態地創建一個名為name,待執行函數為func,參數為data的work_struct結構。 同樣,也可以在運行時通過指針創建一個工作: INIT_WORK(structwork_struct *work, woid(*func) (void *), void *data);
4. 調度 a. schedule_work
在大多數情況下, 並不需要自己建立工作隊列,而是只定義工作, 將工作結構掛接到內核預定義的事件工作隊列中調度, 在kernel/workqueue.c中定義了一個靜態全局量的工作隊列static struct workqueue_struct *keventd_wq;默認的工作者線程叫做events/n,這里n是處理器的編號,每個處理器對應一個線程。比如,單處理器的系統只有events/0這樣一個線程。而雙處理器的系統就會多一個events/1線程。 調度工作結構, 將工作結構添加到全局的事件工作隊列keventd_wq,調用了queue_work通用模塊。對外屏蔽了keventd_wq的接口,用戶無需知道此參數,相當於使用了默認參數。keventd_wq由內核自己維護,創建,銷毀。這樣work馬上就會被調度,一旦其所在的處理器上的工作者線程被喚醒,它就會被執行。
b. schedule_delayed_work(&work,delay); 有時候並不希望工作馬上就被執行,而是希望它經過一段延遲以后再執行。在這種情況下,同時也可以利用timer來進行延時調度,到期后才由默認的定時器回調函數進行工作注冊。延遲delay后,被定時器喚醒,將work添加到工作隊列wq中。
工作隊列是沒有優先級的,基本按照FIFO的方式進行處理。
5. 示例: #include <linux/module.h> #include <linux/init.h> #include <linux/workqueue.h>
static struct workqueue_struct *queue=NULL; static struct work_struct work;
staticvoid work_handler(struct work_struct *data) { printk(KERN_ALERT"work handler function.\n"); }
static int __init test_init(void) { queue=create_singlethread_workqueue("hello world");/*創建一個單線程的工作隊列*/ if (!queue) goto err;
INIT_WORK(&work,work_handler); schedule_work(&work);
return0; err: return-1; }
static void __exit test_exit(void) { destroy_workqueue(queue); } MODULE_LICENSE("GPL"); module_init(test_init); module_exit(test_exit);
=======================================================================
分成兩大部分,第一部分是用來執行work queue中每個節點上掛載的函數的內核線程,第二部分是從驅動程序的角度看work queue的使用。
第一部分 worker_thread內核線程
Linux系統啟動期間會創建一名為worker_thread線程,該線程創建之后就處於sleep狀態。這里所謂的內核線程,從調度器的角度就是一可 以調度的進程,從代碼的表現形式看,就是一函數。系統創建的這個worker_thread線程基於一workqueue_struct結構變量上(該結 構體變量的成員name為"events").
第二部分 work queue的使用
1.只考慮使用系統的keventd管理的工作隊列
驅動程序調用schedule_work向工作隊列遞交新的工作節點,schedule_work內部會喚醒worker_thread內核線程(使之進 程狀態為可調度)。在下一次進程調度時刻,worker_thread被調度執行,其主要任務便是調用它所管理工作隊列中每個工作節點上掛載的函數,調用 完畢該工作節點會從任務隊列中被刪除。當所有節點上的函數調用完畢,worker_thread繼續sleep,直到schedule_work再次被某 個驅動程序調用。
與使用驅動程序自己創建的工作對列的區別是:schedule_work內部是調用queue_work(keventd_wq, work),而使用驅動程序自己創建的工作隊列在調用queue_work時的第一個參數是驅動程序自己創建的工作隊列。
2.驅動程序使用自己創建的工作隊列
這種情況驅動程序調用create_workqueue。該函數的原理跟1中基本是一樣的,只不過再會創建一個內核進程,該內核進程的內核函數名字依然為worker_thread,只不過這個worker_thread工作在新的屬於驅動程序自己的工作隊列。
使用方法是:
a. 調用create_workqueue生成屬於驅動程序自己的work queue. struct workqueue_struct *my_workqueue = create_workqueue("my_workqueue");
b.調用queue_work象a中生成的my_workqueue中注冊工作節點, queue_work(my_workqueue, work)
這兩種情況下的內核線程其實都是利用了一個名為kthreadd的內核線程來創建工作隊列的worker_thread,其本質是向全局列表 kthread_create_list中加入相應的內核線程函數節點,由kthreadd來負責創建進程框架,然后運行 kthread_create_list中加入的節點上的內核線程函數。
向kthread_create_list中每加入一個內核線程函數節點,都會喚醒kthreadd線程。
當等待隊列是空時,worker_thread會睡眠在該等待隊列中。當driver調用queue_work時,再向等待隊列中加入工作節點的同時,會喚醒睡眠在該等待隊列上的worker_thread線程。
關於worker_thread內核線程的創立過程:
最開始由void __init init_workqueues(void)發起(Workqueue.c),依次的關鍵調用節點分別是(為了便於敘述,在內核代碼基礎上略有改動,但不影響核心調用過程)
create_workqueue("events");
__create_workqueue((("events"), 0, 0);
__create_workqueue_key("events", 0, 0, NULL, NULL);
在__create_workqueue_key("events", 0, 0, NULL, NULL)中:
1).首先創建一個struct workqueue_struct指針型變量*wq, wq->name = "events".這個新建的隊列指針將被記錄在全局變量keventd_wq中.
workqueue_struct定義如下:
struct workqueue_struct {
struct cpu_workqueue_struct *cpu_wq;
struct list_head list;
const char *name;
int singlethread;
int freezeable; /* Freeze threads during suspend */
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
2).把wq所在的隊列節點加入到一名為workqueues的全局變量中(list_add(&wq->list, &workqueues)).
3).調用create_workqueue_thread(),最終調用kthread_create(worker_thread, cwq, fmt, wq->name, cpu)來生成內核線程worker_thread.
Linux內核中最終是通過do_fork來生成內核線程(正如前面所說,這其實是個能被調度的進程,擁有自己的task_struct結構),這個過程 在內核中是個比較復雜的過程,比較重要的節點總結如下:在當前進程下調用do_fork來生成一個新進程時,會大量copy當前進程的 task_struct結構到新進程的task_struct變量中,新進程如果被調度運行,入口點(pc值)是 kernel_thread_helper函數,在該函數中會再次將pc值設置為kernel_thread()中的function指針,也就是在調用 kernel_thread函數時第一參數所表示的函數。
所以,在Linux系統初始化期間,會生成一個新進程,該進程的執行線程/函數為worker_thread,該進程被創建出來之后的狀態是STOP,這 意味着該線程無法進入調度隊列直到針對該進程調用wake_up_process(),該進程才會真正進入調度隊列,如果被調度,則開始運行 worker_thread函數。新進程被賦予的調度優先級為KTHREAD_NICE_LEVEL(-5),這個標志的實際含義將在Linux進程調度 的帖子里去寫。然后對於worker_thread線程函數本身,會進一步調整調度優先級(set_user_nice(current, -5)),這樣worker_thread所在進程的優先值將為-10,在可搶占式的Linux內核中,如此高的調度優先級極易導致一個調度時點,即使當 前進程也許正運行在內核態,也可能被切換出CPU,代之以worker_thread.
在kernel_thread_helper函數中,在調用內核線程函數之前設定返回地址為do_exit()函數,所以當內核線程函數退出的話將導致該進程的消失。
比如ARM中的kernel_thread_helper函數代碼:
extern void kernel_thread_helper(void);
asm( ".section .text\n"
" .align\n"
" .type kernel_thread_helper, #function\n"
"kernel_thread_helper:\n"
" mov r0, r1\n"
" mov lr, r3\n"
" mov pc, r2\n"
" .size kernel_thread_helper, . - kernel_thread_helper\n"
" .previous");
mov lr, r3設定內核線程函數返回后的返回地址,Linux內核代碼設定為do_exit().
可以想象,象worker_thread這種內核線程函數,一般不會輕易退出,除非對內核線程函數所在的進程上調用kthread_stop函數。
上述的kernel_thread_helper函數中的mov pc, r2將會導致worker_thread()函數被調用,該函數定義如下:
static int worker_thread(void *__cwq)
{
struct cpu_workqueue_struct *cwq = __cwq;
DEFINE_WAIT(wait);
if (cwq->wq->freezeable)
set_freezable();
set_user_nice(current, -5);
for (;;) {
prepare_to_wait(&cwq->more_work, &wait, TASK_INTERRUPTIBLE);
if (!freezing(current) &&
!kthread_should_stop() &&
list_empty(&cwq->worklist))
schedule();
finish_wait(&cwq->more_work, &wait);
try_to_freeze();
if (kthread_should_stop())
break;
run_workqueue(cwq);
}
}
正如前面猜測的那樣,該函數不會輕易退出,其核心是一for循環,如果任務隊列中有新的節點,則執行該節點上的函數(在run_workqueue()內部),否則worker_thread所在的進程將會繼續休眠。
對work queue的深入理解需要了解Linux的調度機制,最基本的是Linux內核的調度時機,因為這關系到驅動程序開發者能對自己注冊到任務隊列函數的執行 時機有大體的了解。在2.4內核中,除了進程主動調用schedule()這種主動的調度方式外,調度發生在由內核態向用戶態轉變(從中斷和系統調用返 回)的時刻,因為內核不可搶占性,所以內核態到內核態的轉變時調度不會發生。而在2.6內核中,因為內核可搶占已經被支持,這意味着調度的時機除了發生在 內核態向用戶態轉變時,運行在內核態的進程也完全有可能被調度出處理器,比如當前進程重新允許搶占(調用preempt_enable())。在內核態的 進程允許被搶占,意味着對高優先級進程的調度粒度更細:如果當前進程允許被搶占,那么一旦當前調度隊列中有比當前進程優先級更高的進程,當前進程將被切換 出處理器(最常見的情況是在系統調用的代碼中接收到中斷,當中斷返回時,2.4代碼會繼續運行被中斷的系統調用,而2.6代碼的可搶占性會導致一個調度時 點,原先被中斷的系統調用所在的進程可能會被調度隊列中更高優先級的進程所取代)。關於進程的調度,會在另外的帖子中詳細介紹。
create_singlethread_workqueue(name)與create_workqueue(name)
Driver調用這兩個宏來創建自己的工作隊列以及相應的內核進程(其內核線程函數為worker_thread,下來為了方便敘述,就簡稱該進程為worker_thread進程)
1. create_singlethread_workqueue(name)
該函數的實現機制如下圖所示,函數返回一個類型為struct workqueue_struct的指針變量,該指針變量所指向的內存地址在函數內部調用kzalloc動態生成。所以driver在不再使用該work queue的情況下調用void destroy_workqueue(struct workqueue_struct *wq)來釋放此處的內存地址。
圖中的cwq是一per-CPU類型的地址空間。對於create_singlethread_workqueue而言,即使是對於多CPU系統,內核也 只負責創建一個worker_thread內核進程。該內核進程被創建之后,會先定義一個圖中的wait節點,然后在一循環體中檢查cwq中的 worklist,如果該隊列為空,那么就會把wait節點加入到cwq中的more_work中,然后休眠在該等待隊列中。
Driver調用queue_work(struct workqueue_struct *wq, struct work_struct *work)向wq中加入工作節點。work會依次加在cwq->worklist所指向的鏈表中。queue_work向 cwq->worklist中加入一個work節點,同時會調用wake_up來喚醒休眠在cwq->more_work上的 worker_thread進程。wake_up會先調用wait節點上的autoremove_wake_function函數,然后將wait節點從 cwq->more_work中移走。
worker_thread再次被調度,開始處理cwq->worklist中的所有work節點...當所有work節點處理完 畢,worker_thread重新將wait節點加入到cwq->more_work,然后再次休眠在該等待隊列中直到Driver調用 queue_work...