並發可管理工作隊列的出現 慢工作機制
為什麼說是“提供過核心中還曾短暫出現過慢工作機制 (slow work mechanism)”,原因是在 mainline核心中,曾經出現過慢工作機制 (slow work mechanism),但隨著並發管理工作隊列 (cmwq) 的出現,它已經全部被 cmwq所替換,淡出了 mainline
在核心代碼中,經常希望延緩部分工作到將來某個時間執行,這樣做的原因很多,比如:在持有鎖時做大量(或者說費時的)工作不合適;或希望將工作聚集以擷取批處理的效能;或調用了一個可能導致睡眠的函數使得在此時執行新調度非常不合適等。
核心中提供了許多機制來提供順延強制,如中斷的下半部處理可延遲中斷上下文中的部分工作;定時器可指定延遲一定時間後執行某工作;工作隊列則允許在進程上下文環境下順延強制等。除此之外,核心中還曾短暫出現過慢工作機制 (slow work mechanism),還有非同步函數調用(asynchronous function calls)以及各種私人實現的線程池等。在上面列出的如此多的核心基礎組件中,使用最多則是工作隊列。
工作隊列 (workqueues)
在討論之前,先定義幾個核心中使用工作隊列時用到的術語方便後面描述。 workqueues:所有工作項目被 ( 需要被執行的工作 ) 排列於該隊列,因此稱作工作隊列 (workqueues) 。 worker thread:工作者線程 (worker thread) 是一個用於執行工作隊列中各個工作項目的核心線程,當工作隊列中沒有工作項目時,該線程將變為 idle 狀態。 single threaded(ST)::工作者線程的表現形式之一,在系統範圍內,只有一個工作者線程為工作佇列服務 multi threaded(MT):工作者線程的表現形式之一,在多 CPU 系統上每個 CPU 上都有一個工作者線程為工作佇列服務
工作隊列之所以成為使用最多的順延強制機制,得益於它的實現中的一些有意思的地方: 使用的介面簡單明了
對於使用者,基本上只需要做 3 件事情,依次為: 建立工作隊列 ( 如果使用核心預設的工作隊列,連這一步都可以省略掉 ) 建立工作項目 向工作隊列中提交工作項目 執行在進程上下文中,這樣使得它可以睡眠,被調度及被搶佔
執行在進程上下文中是一個非常大的優勢,其他的下半部工作機制,基本上都運行於中斷上下文中,我們知道在中斷上下文裡,不能睡眠,不能阻塞;原因是中斷上下文並不與任何進程關聯,如在中斷上下文睡眠,調度器將不能將其喚醒,所以在中斷上下文中不能有導致核心進入睡眠的行為,如持有訊號量,執行非原子的記憶體配置等。工作隊列運行於進程上下文中 ( 他們通過核心線程執行 ),因此它完全可以睡眠,可以被調度,也可以被其他進程所搶佔。 在多核環境下的使用也非常友好
與 tasklet 機制相較而言,工作隊列可以在不同 CPU 上同時運行是個優勢。這使得該介面在多核情況下也非常適合,核心郵件清單中就曾經有過用非強制中斷和工作隊列來替換不支援多 CPU 執行的 tasklet 的討論。
總體說來,工作隊列和定時器函數的處理有點類似,都是順延強制相關的回呼函數,但和定時器處理函數不同的是定時器回呼函數只執行一次 (當然可以在執行時再次註冊以反覆調用,但這需要顯示的再次註冊 ), 且執行定時器回呼函數時在時鐘中斷環境 ,限制較多,因此回呼函數不能太複雜;而工作隊列是通過核心線程實現,一直有效,可重複執行,執行時可以休眠,因此工作隊列非常適合處理那些不是很緊急的任務,如記憶體回收處理等。
工作隊列的使用和一些缺陷 注意
在2.6.20 之前,建立工作項目的介面並不是這個樣子,而是在 2.6.20之時對工作隊列的介面進行過一次“瘦身”,其原因則非常簡單,工作隊列的使用越來越多,能節省一個位元組對於核心也是一件好事情,這次瘦身也將工作項目在建立時就明確區分為一般的工作項目和需要延遲某段時間再執行的工作項目
之前簡單討論了工作隊列使用上的便利性,依據工作隊列的使用步驟,在下面列出了在 2.6.36之前提供的介面,並描述了使用時的一些選擇。由於工作隊列的實現中,已有預設的共用工作隊列,因此在選擇介面時,就出現了 2種選擇:要麼使用核心已經提供的共用工作隊列,要麼自己建立工作隊列。
如選擇使用共用的工作隊列,基本的步驟為:
1. 建立工作項目
建立工作項目的介面分為靜態和動態方式,介面分別是:
清單 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 刻度,所以將它們的執行調度交給系統調度器。 代碼的遷移
在之前的代碼中,一些使用者依賴於 ST 中的嚴格執行順序,這種行為在 cmwq 中可以將 max_active 設為 1,flag 設定為 WQ_UNBOUND 來獲得相同的行為
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 來實現。
清單 9. 基於新後端介面的實現
#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) |
調度器中的 hook 函數
為了知道工作者線程何時將睡眠或被喚醒,在核心中增加了一個 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函數都是寫入程式碼在核心的調度器中,後續可能會以其它形式改變其實現方式。
並發可管理工作隊列的後端 gcwq
在 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 ofonline 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. cmwq 的初始化
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 用於工作者線程的管理,如下:
清單 13. 工作者的管理結構體
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 */ }; |
未討論的主題
本文沒有討論處理 CPU 熱插拔和在記憶體回收路徑的處理,這兩種情況在 cmwq 中分別使用 trustee 和 rescurer 機制,有興趣的讀者可以自行參考代碼或文檔
工作者線程池的主體執行是 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 workmechanism),但這並不是它的唯一目標,它的長期目標則是希望在核心中提供一個通用的線程池機制,這樣,工作隊列的適用範圍將更為普遍。
參考資料
學習 查看文章“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 技術活動和網路廣播。