併發可管理工作隊列的出現
在內核代碼中,經常希望延緩部分工作到將來某個時間執行,這樣做的原因很多,比如:在持有鎖時做大量(或者說費時的)工作不合適;或希望將工作聚集以獲取批處理的性能;或調用了一個可能導致睡眠的函數使得在此時執行新調度非常不合適等。
內核中提供了許多機制來提供延遲執行,如中斷的下半部處理可延遲中斷上下文中的部分工作;定時器可指定延遲一定時間後執行某工作;工作隊列則允許在進程上下文環境下延遲執行等。除此之外,內核中還曾短暫出現過慢工作機制 (slow work mechanism),還有異步函數調用 (asynchronous function calls) 以及各種私有實現的線程池等。在上面列出的如此多的內核基礎組件中,使用最多則是工作隊列。
*******************************************************************
慢工作機制
爲什麼說是“提供過內核中還曾短暫出現過慢工作機制 (slow work mechanism)”,
原因是在 mainline 內核中,曾經出現過慢工作機制 (slow work mechanism),
但隨着併發管理工作隊列 (cmwq) 的出現,它已經全部被 cmwq 所替換,淡出了 mainline。
********************************************************************
在討論之前,先定義幾個內核中使用工作隊列時用到的術語方便後面描述。
- workqueues:所有工作項被 ( 需要被執行的工作 ) 排列於該隊列,因此稱作工作隊列 (workqueues) 。
- worker thread:工作者線程 (worker thread) 是一個用於執行工作隊列中各個工作項的內核線程,當工作隊列中沒有工作項時,該線程將變爲 idle 狀態。
- single threaded(ST)::工作者線程的表現形式之一,在系統範圍內,只有一個工作者線程爲工作隊列服務
- multi threaded(MT):工作者線程的表現形式之一,在多 CPU 系統上每個 CPU 上都有一個工作者線程爲工作隊列服務
工作隊列之所以成爲使用最多的延遲執行機制,得益於它的實現中的一些有意思的地方:
- 使用的接口簡單明瞭
對於使用者,基本上只需要做 3 件事情,依次爲:
- 創建工作隊列 ( 如果使用內核默認的工作隊列,連這一步都可以省略掉 )
- 創建工作項
- 向工作隊列中提交工作項
- 執行在進程上下文中,這樣使得它可以睡眠,被調度及被搶佔
執行在進程上下文中是一個非常大的優勢,其他的下半部工作機制,基本上都運行於中斷上下文中,我們知道在中斷上下文裏,不能睡眠,不能阻塞;原因是中斷上下文並不與任何進程關聯,如在中斷上下文睡眠,調度器將不能將其喚醒,所以在中斷上下文中不能有導致內核進入睡眠的行爲,如持有信號量,執行非原子的內存分配等。工作隊列運行於進程上下文中 ( 他們通過內核線程執行 ),因此它完全可以睡眠,可以被調度,也可以被其他進程所搶佔。
- 在多核環境下的使用也非常友好
與 tasklet 機制相較而言,工作隊列可以在不同 CPU 上同時運行是個優勢。這使得該接口在多核情況下也非常適合,內核郵件列表中就曾經有過用軟中斷和工作隊列來替換不支持多 CPU 執行的 tasklet 的討論。
總體說來,工作隊列和定時器函數的處理有點類似,都是延遲執行相關的回調函數,但和定時器處理函數不同的是定時器回調函數只執行一次 ( 當然可以在執行時再次註冊以反覆調用,但這需要顯示的再次註冊 ), 且執行定時器回調函數時在時鐘中斷環境 , 限制較多,因此回調函數不能太複雜;而工作隊列是通過內核線程實現,一直有效,可重複執行,執行時可以休眠,因此工作隊列非常適合處理那些不是很緊急的任務,如垃圾回收處理等。
之前簡單討論了工作隊列使用上的便利性,依據工作隊列的使用步驟,在下面列出了在 2.6.36 之前提供的接口,並描述了使用時的一些選擇。由於工作隊列的實現中,已有默認的共享工作隊列,因此在選擇接口時,就出現了 2 種選擇:要麼使用內核已經提供的共享工作隊列,要麼自己創建工作隊列。
如選擇使用共享的工作隊列,基本的步驟爲:
1. 創建工作項
創建工作項的接口分爲靜態和動態方式,接口分別是:
typedef void (*work_func_t)(struct work_struct *work);
DECLARE_WORK(name, func);
DECLARE_DELAYED_WORK(name, func);
該系列宏靜態創建一個以 name 命名的工作項,並設置了回調函數 func
清單 2. 動態創建工作項
INIT_WORK(struct work_struct work, work_func_t func);
PREPARE_WORK(struct work_struct work, work_func_t func);
INIT_DELAYED_WORK(struct delayed_work work, work_func_t func);
PREPARE_DELAYED_WORK(struct delayed_work work, work_func_t func);
該系列宏在運行時初始化工作項 work,並設置了回調函數 func
2. 調度工作項
清單 3. 調度工作項
int schedule_work(struct work_struct *work);
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
上面兩個函數將工作項添加到共享的工作隊列,工作項隨後在某個合適時機將被執行。
如果因爲某些原因,如需要執行的是個阻塞性質的任務而不願或不能使用內核提供的共享工作隊列,這時需要自己創建工作隊列,則上述步驟和使用的接口則略有改變:
3. 創建工作隊列
在 2.6.36 之前,內核中的每個工作隊列都有一個專用的內核線程來爲它服務,創建工作隊列時,有 2 個選擇,可選擇系統範圍內的 ST,也可選擇每 CPU 一個內核線程的 MT,其接口如下:
清單 4. 創建工作隊列
create_singlethread_workqueue(name)
create_workqueue(name)
相對於 create_singlethread_workqueue,create_workqueue 同樣會分配一個 wq 的工作隊列。不同之處在於,對於多 CPU 系統而言,對每一個 active 的 CPU,都會爲之創建一個 per-CPU 的 cwq 結構,對應每一個 cwq,都會生成一個新的 worker_thread。
4. 創建工作項
創建工作項的接口和使用內核默認的共享工作隊列時是一樣的。
- 向工作隊列提交工作項
清單 5. 向工作隊列中提交工作項
int queue_work(workqueue_t *queue, work_t *work);
int queue_delayed_work(workqueue_t *queue, work_t *work, unsigned long delay);
它們都會將工作項 work 提交到工作隊列 queue,但第二個函數確保最少延遲 delay jiffies 之後該工作纔會被執行。對於 MT 的情況,當用 queue_work 向 cwq 上提交工作項節點時, 是哪個 active CPU 正在調用該函數,那麼便向該 CPU 對應的 cwq 上的 worklist 上增加工作項節點。
假如你需要取消一個掛起的工作隊列中的工作項 , 你可以調用:
清單 6. 取消工作隊列中掛起的工作項
int cancel_delayed_work(struct work_struct *work);
如果這個工作項在它開始執行前被取消,返回值是非零。內核保證給定工作項的執行不會在調用 cancel_delay_work 成功後被執行。 如果 cancel_delay_work 返回 0,則這個工作項可能已經運行在一個不同的處理器,並且仍然可能在調用 cancel_delayed_work 之後被執行。要絕對確保工作函數沒有在 cancel_delayed_work 返回 0 後在任何地方運行,你必須跟隨這個調用之後接着調用 flush_workqueue。在 flush_workqueue 返回後。任何在改調用之前提交的工作函數都不會在系統任何地方運行。
當你結束對一個工作隊列的使用後,你可以使用下面的函數釋放相關資源:
清單 7. 釋放工作隊列
void destroy_workqueue(struct workqueue_struct *queue);
前面比較了工作隊列與其他基於中斷上下文的延遲機制之間的優勢,但工作隊列並非沒有缺點。首先是公共的共享工作隊列不能提供更多的好處,因爲如果其中的任一工作項阻塞,則其他工作項將不能被執行,因此在實際的使用中,使用者多會自己創建工作隊列,而這又導致下面的一些問題:
- MT 的工作隊列導致了內核的線程數增加得非常的快,這樣帶來一些問題:一個是佔用了 pid 數目,這對於服務器可不是一個好消息,因爲 pid 實際上是一種全局資源;而大量的工作線程對於資源的競爭也導致了無效的調度,而這些調度其實是不需要的,對調度器也帶來了壓力。
- 現有的工作隊列機制某些情況下有導致死鎖的傾向,特別是在兩個工作項之間存在依賴時。如果你曾經調試過這種偶爾出現的死鎖,會知道這種問題讓人非常的沮喪。
併發可管理工作隊列 (Concurrency-managed workqueues)
在 2.6.36 之前的工作隊列,其核心是每個工作隊列都有專有的內核線程爲其服務——系統範圍內的 ST 或每個 CPU 都有一個內核線程的 MT。新的 cmwq 在實現上摒棄了這一點,不再有專有的線程與每個工作隊列關聯,事實上,現在變成了 Online CPU number + 1 個線程池來爲工作隊列服務,這樣將線程的管理權實際上從工作隊列的使用者交還給了內核。當一個工作項被創建以及排隊,將在合適的時機被傳遞給其中一個線程,而 cmwq 最有意思的改變是:被提交到相同工作隊列,相同 CPU 的工作項可能併發執行,這也是命名爲併發可管理工作隊列的原因。
cmwq 的實現遵循了以下幾個原則:
- 與原有的工作隊列接口保持兼容,cmwq 只是更改了創建工作隊列的接口,很容易移植到新的接口。
- 工作隊列共享 per-CPU 的線程池,提供靈活的併發級別而不再浪費大量的資源。
- 自動平衡工作者線程池和併發級別,這樣工作隊列的用戶不再需要關注如此多的細節。
在工作隊列的用戶眼中,cmwq 與之前的工作隊列相比,創建工作隊列的接口實現的後端有所改變,現在的新接口爲:
清單 8. cmwq 中創建工作隊列的後端接口
struct workqueue_struct
*alloc_workqueue(char *name, unsigned int flags, int max_active);
其中:
name:爲工作隊列的名字,而不像 2.6.36 之前實際是爲工作隊列服務的內核線程的名字。
flag 指明工作隊列的屬性,可以設定的標記如下:
- WQ_NON_REENTRANT:默認情況下,工作隊列只是確保在同一 CPU 上不可重入,即工作項不能在同一 CPU 上被多個工作者線程併發執行,但容許在多個 CPU 上併發執行。但該標誌標明在多個 CPU 上也是不可重入的,工作項將在一個不可重入工作隊列中排隊,並確保至多在一個系統範圍內的工作者線程被執行。
- WQ_UNBOUND:工作項被放入一個由特定 gcwq 服務的未限定工作隊列,該客戶工作者線程沒有被限定到特定的 CPU,這樣,未限定工作者隊列就像簡單的執行上下文一般,沒有併發管理。未限定的 gcwq 試圖儘可能快的執行工作項。
- WQ_FREEZEABLE:可凍結 wq 參與系統的暫停操作。該工作隊列的工作項將被暫停,除非被喚醒,否者沒有新的工作項被執行。
- WQ_MEM_RECLAIM:所有的工作隊列可能在內存回收路徑上被使用。使用該標誌則保證至少有一個執行上下文而不管在任何內存壓力之下。
- WQ_HIGHPRI:高優先級的工作項將被排練在隊列頭上,並且執行時不考慮併發級別;換句話說,只要資源可用,高優先級的工作項將儘可能快的執行。高優先工作項之間依據提交的順序被執行。
- WQ_CPU_INTENSIVE:CPU 密集的工作項對併發級別並無貢獻,換句話說,可運行的 CPU 密集型工作項將不阻止其它工作項。這對於限定得工作項非常有用,因爲它期望更多的 CPU 時鐘週期,所以將它們的執行調度交給系統調度器。
max_active:決定了一個 wq 在 per-CPU 上能執行的最大工作項。比如 max_active 設置爲 16 表示一個工作隊列上最多 16 個工作項能同時在 per-CPU 上同時執行。當前實行中,對所有限定工作隊列,max_active 的最大值是 512,而設定爲 0 時表示是 256;而對於未限定工作隊列,該最大值爲:MAX[512,4 * num_possible_cpus() ],除非有特別的理由需要限流或者其它原因,一般設定爲 0 就可以了。
cmwq 本質上是提供了一個公共的內核線程池的實現,其接口基本上和以前保持了兼容,只是更改了創建工作隊列的函數的後端,它實際上是將工作隊列和內核線程的一一綁定關係改爲由內核來管理內核線程的創建,因此在 cmwq 中創建工作隊列並不意味着一定會創建內核線程。
而之前的接口的則改爲基於 alloc_workqueue 來實現。
#define create_workqueue(name) \
alloc_workqueue((name), WQ_MEM_RECLAIM, 1)
#define create_freezeable_workqueue(name) \
alloc_workqueue((name), WQ_FREEZEABLE | WQ_UNBOUND | WQ_MEM_RECLAIM, 1)
#define create_singlethread_workqueue(name) \
alloc_workqueue((name), WQ_UNBOUND | WQ_MEM_RECLAIM, 1)
併發可管理工作隊列的實現
爲了知道工作者線程何時將睡眠或被喚醒,在內核中增加了一個 PF_WQ_WORKER 類型的標記,表明是工作者線程,並且添加了 2 個 hook 函數到當前的調度器中。
清單 10. 調度器中的 hook 函數
void wq_worker_waking_up(struct task_struct *task, unsigned int cpu);
struct task_struct *wq_worker_sleeping(struct task_struct *task, unsigned int cpu);
其中 wq_worker_waking_up 在一個工作者線程被喚醒時在 try_to_wake_up/try_to_wake_up_local 中被調用。而 wq_worker_sleeping 則在 schedule () 中被調用,表明該工作者線程將會睡眠,返回值是一個 task,它可在相同的 CPU 上被 try_to_wake_up_local 用來喚醒。現在 2 個 hook 函數都是硬編碼在內核的調度器中,後續可能會以其它形式改變其實現方式。
在 cmwq 的實現中,最重要的是其後端 gcwq:
清單 11. gcwq
/*
* Global per-cpu workqueue. There's one and only one for each cpu
* and all works are queued and processed here regardless of their
* target workqueues.
*/
struct global_cwq {
spinlock_t lock; /* the gcwq lock */
struct list_head worklist; /* L: list of pending works */
unsigned int cpu; /* I: the associated cpu */
unsigned int flags; /* L: GCWQ_* flags */
int nr_workers; /* L: total number of workers */
int nr_idle; /* L: currently idle ones */
/* workers are chained either in the idle_list or busy_hash */
struct list_head idle_list; /* X: list of idle workers */
struct hlist_head busy_hash[BUSY_WORKER_HASH_SIZE];
/* L: hash of busy workers */
struct timer_list idle_timer; /* L: worker idle timeout */
struct timer_list mayday_timer; /* L: SOS timer for dworkers */
struct ida worker_ida; /* L: for worker IDs */
// 爲了實現 CPU 熱插拔時候的委託機制
struct task_struct *trustee; /* L: for gcwq shutdown */
unsigned int trustee_state; /* L: trustee state */
wait_queue_head_t trustee_wait; /* trustee wait */
struct worker *first_idle; /* L: first idle worker */
} ____cacheline_aligned_in_smp;
它用來管理線程池,其數量爲每個 CPU 一個 gcwq,還有一個特定的 gcwq 爲未限定 (unbound) 工作隊列的工作項服務。需要注意的是在 cmwq 中只有 Number of online CPU + 1 (unbound) 個線程池。由於計數從 0 開始,所以可能的線程池的數目最大爲 NR_CPUS。由於涉及到 CPU 的熱插拔問題,因此只有 online 的 CPU 上纔有線程池與之綁定。
該結構體中的一些重要字段如下:
worklist:所有未決的工作項被鏈接在該鏈表中
cpu:表明該線程池和哪個 CPU 綁定,實現中有一個未綁定到任何 CPU 的 gcwq,其標記爲 WORK_CPU_UNBOUND,在代碼中,將這個未綁定到特定 CPU 的 gcwq 和綁定到 CPU 的 gcwq 一起處理,應此定義 WORK_CPU_UNBOUND = NR_CPUS,這也是代碼中的一個小小的技巧。
nr_workers:總的工作者線程數
nr_idle:當前的空閒工作者線程數
idle_list:空閒的工作者線程鏈接成該鏈表
busy_hash[BUSY_WORKER_HASH_SIZE] :正執行工作項任務的工作者線程放入該哈希表中
有了前面基礎,我們可以開始看看 cmwq 的實現,根據以往的經驗,從初始化部分開始:
清單 12. cm
static int __init init_workqueues(void)
{
unsigned int cpu;
int i;
// 註冊 CPU 事件的通知鏈,主要用於處理 CPU 熱插拔時候,將該 CPU 上的工作隊列遷移到 online 的 CPU
// 在 cmwq 中,將這種機制叫做 trustee
cpu_notifier(workqueue_cpu_callback, CPU_PRI_WORKQUEUE);
// 初始化 CPU 數目 +1 個 gcwq
for_each_gcwq_cpu(cpu) {
struct global_cwq *gcwq = get_gcwq(cpu);
…… .
}
// 初始化 online CPU 數目 +1 個工作者線程池
// 創建的線程命名方式如下 :
// 對於與 CPU 綁定的線程,以 ps 命令看到的爲:[kworker/cup_id:thread_id],cup_id 爲 CPU 的編號
// thread_id 爲創建的工作者線程 id,對於未綁定的 CPU 的線程池中的線程,則顯示爲
//[kworker/u:thread_id]
for_each_online_gcwq_cpu(cpu) {
…… .
worker = create_worker(gcwq, true);
…… .
start_worker(worker);
…… .
}
// 創建 4 個全局的工作隊列
system_wq = alloc_workqueue("events", 0, 0);
system_long_wq = alloc_workqueue("events_long", 0, 0);
system_nrt_wq = alloc_workqueue("events_nrt", WQ_NON_REENTRANT, 0);
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND,
WQ_UNBOUND_MAX_ACTIVE);
……
return 0;
}
爲了實現工作者線程池,針對每個工作者線程,封裝了一個結構體 worker 用於工作者線程的管理,如下:
struct worker {
// 與工作者線程的狀態有關係,如果工作者線程處於 idle 狀態,則使用 entry;如果處於 busy 狀態,
// 則使用哈希節點 hentry,參考 gcwq 中的 idle_list 和 busy_hash 字段
union {
struct list_head entry; /* L: while idle */
struct hlist_node hentry; /* L: while busy */
};
……
// 被調度的工作項 list,注意只有進入到該列表,工作項才真正被工作隊列處理
struct list_head scheduled; /* L: scheduled works */
// 被內核調度的實體,工作者線程在內核調度器看來只是一個 task 而已
struct task_struct *task; /* I: worker task */
struct global_cwq *gcwq; /* I: the associated gcwq */
/* 64 bytes boundary on 64bit, 32 on 32bit */
// 記錄上次 active 的時間,用於判定該工作者線程是否可以被 destory 時使用
unsigned long last_active; /* L: last active timestamp */
unsigned int flags; /* X: flags */
// 工作者線程的 id,用 ps 命令在用戶空間可以看到具體的值
int id; /* I: worker id */
struct work_struct rebind_work; /* L: rebind worker to cpu */
};
工作者線程池的主體執行是 worker_thread,其執行流程如下:
清單 14. 工作者線程的管理
static int worker_thread(void *__worker)
{
struct worker *worker = __worker;
struct global_cwq *gcwq = worker->gcwq;
// 告訴調度器這是一個工作者線程
worker->task->flags |= PF_WQ_WORKER;
woke_up:
spin_lock_irq(&gcwq->lock);
……
// 讓工作者從 idle 狀態離開,因爲新創建的工作者線程處於 idle 狀態,在讓該工作者線程工作時,需要從
// idle 狀態離開以執行相關的動作
worker_leave_idle(worker);
recheck:
// 檢查是否需要更多的工作者線程
// 檢查的依據是如果有高優先級的工作,如果工作隊列中有工作要做然而該 cpu 的全局隊列中卻已
// 經沒有空閒處理內核線程,那就有必要處理了
if (!need_more_worker(gcwq))
goto sleep;
// may_start_working 檢查 gcwq 中是否有 idle 的工作者線程
// manage_workers 在後面詳述
if (unlikely(!may_start_working(gcwq)) && manage_workers(worker))
goto recheck;
// 確保工作者線程的被調度 list 爲空
BUG_ON(!list_empty(&worker->scheduled));
// 設置標記表明工作者線程即將處理相關的工作項,類似於一個 busy 標記
worker_clr_flags(worker, WORKER_PREP);
// 基本流程爲,先將工作項合併到工作者線程的被調度 list,然後依次處理被調度 list 的工作
do {
struct work_struct *work =
list_first_entry(&gcwq->worklist,
struct work_struct, entry);
if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
/* optimization path, not strictly necessary */
// 注意這裏只是代碼路徑上的顯示的優化,本質上並不需要該路徑,else 的部分纔是代碼
// 邏輯的的所在,應此可以忽略這部分
process_one_work(worker, work);
if (unlikely(!list_empty(&worker->scheduled)))
process_scheduled_works(worker);
} else {
// 將 gcwq 的工作項移到工作線程的被調度列表,隨後工作者線程將依序處理被調度 list
// 處理單項工作項時使用的是 process_one_work
move_linked_works(work, &worker->scheduled, NULL);
process_scheduled_works(worker);
}
} while (keep_working(gcwq));
worker_set_flags(worker, WORKER_PREP, false);
// 如果沒有工作項需要處理,讓工作者線程進入睡眠狀態
sleep:
……
}
manage_workers 中處理需要被 destroy 的工作者線程,也決定是否需要創建新的工作者線程:在 maybe_destroy_workers 中去判定當工作線程數目是否被認定太多 ( 認定工作者線程過多的本質是個策略問題,實現者認爲如果 idle 的工作者多餘 1/4 個 busy 工作者就表示工作者線程過多 ),且該工作線程已經進入 idle 狀態 5 分鐘,則認定該工作者線程可以被 destroy;而 maybe_create_worker 決定是否需要創建新的工作者線程來爲工作隊列服務,判定的條件爲 如果有高優先級的工作,或工作隊列中有工作要做但該 CPU 的全局隊列中卻已經沒有空閒處理內核線程,那就有必要去創建新的工作者線程了。
併發可管理工作隊列進入 mainline 的時間並不長,但已經快速替換了老的工作隊列接口以及慢工作機制 (slow work mechanism),但這並不是它的唯一目標,它的長期目標則是希望在內核中提供一個通用的線程池機制,這樣,工作隊列的適用範圍將更爲普遍。
學習
- 查看文章“sched: prepare for cmwq, take#2”,裏面描述了在內核調度器中的 hook。
- 查看文章“Concurrency-managed workqueues”,Jonathan Corbet 詳細的描述了 cmwq 出現的原有,並初步綜述了 Tejun Heo 提出的解決方案的原理以及面臨的挑戰。
- 查看文章“Working on workqueues”,裏面對新接口進行了解釋。
- 參考
Concurrency Managed Workqueue (cmwq),這是 cmwq 的主要貢獻者 Tejun Heo 對 cmwq 各方面的一個描述。
- 在
developerWorks Linux 專區 尋找爲 Linux 開發人員(包括Linux 新手入門)準備的更多參考資料,查閱我們最受歡迎的文章和教程。
- 在 developerWorks 上查閱所有
Linux 技巧 和
Linux 教程。
- 隨時關注 developerWorks 技術活動和網絡廣播。
-------------
轉至 http://www.ibm.com/developerworks/cn/linux/l-cn-cncrrc-mngd-wkq/