Linux kernel workqueue機制分析【轉】
- 2019 年 10 月 10 日
- 筆記
轉自:http://www.linuxsir.org/linuxjcjs/15346.html
在內核編程中,workqueue機制是最常用的異步處理方式。本文主要基於linux kernel 3.10.108的workqueue文檔分析其基本原理和使用API。
概覽
Workqueue(WQ)機制是Linux內核中最常用的異步處理機制。Workqueue機制的主要概念包括:work用於描述放到隊列里即將被執行的函數;worker表示一個獨立的線程,用於執行異步上下文處理;workqueue用於存放work的隊列。 當workqueue上有work條目時,worker線程被觸發來執行work對應的函數。如果有多個work條目在隊列,worker會按順序處理所有work。
CMWQ概述
在最初的WQ實現中,多線程WQ(MTWQ)在每個CPU上都有一個worker線程,單線程WQ(STWQ)則總共只有一個worker線程。一個MTWQ的worker個數和CPU核數相同,多年來,MTWQ大量使用使得線程數量大量增加,甚至超過了某些系統對PID空間默認32K的限制。 儘管MTWQ浪費大量資源,但其提供的並發水平還是不能讓人滿意。並發的限制在STWQ和MTWQ上都存在,雖然MT相對來說不那麼嚴重。MTWQ在每個CPU上提供了一個上下文執行環境,STWQ則在整個系統提供一個上下文執行環境。work任務需要競爭這些有限的執行環境資源,從而導致死鎖等問題。 並發和資源之間的緊張關係使得一些使用者不得不做出一些不必要的折中,比如libata的polling PIOs選擇STWQ,這樣就無法有兩個polling PIOs同時進行處理。因為MTWQ並不能提供高並發能力,因此async和fscache不得不實現自己的線程池來提供高並發能力。
Concurrency Managed Workqueue (CMWQ)重新設計了WQ機制,並實現如下目標:
- 保持原workqueue API的兼容;
- 使用per-CPU統一的worker池,為所有WQ共享使用並提供靈活的並發級別,同時不浪費不必要的資源;
- 自動調整worker池和並發級別,讓使用者不用關心這些細節。
CMWQ設計思想
一個work是一個簡單的結構體,保存一個函數指針用於異步執行。任何驅動或者子系統想要一個函數被異步執行,都需要設置一個work指向該函數並將其放入workqueue隊列。然後worker線程從隊列上獲取work並執行對應的函數,如果隊列里沒有work,則worker線程處於空閑狀態。這些worker線程用線程池機制來管理。
CMWQ設計時將面向用戶的workqueue機制和後台worker線程池管理機制進行了區分。後台的workqueue被稱為GCWQ(推測可能是Global Concurrency Workqueuq),在每個CPU上存在一個GCWQ,用於處理該CPU上所有workqueue的work。每個GCWQ有兩個線程池:一個用於普通work處理,另一個用於高優先級work處理。
內核子系統和驅動程序通過workqueue API創建和調度work,並可以通過設置flags來指定CPU核心、可重複性、並發限制,優先級等。當work放入workqueue時,通過隊列參數和屬性決定目標GCWQ和線程池,work最終放入對應線程池的共享worklist上。通過如果沒有特別設定,work會被默認放入當前運行的CPU核上的GCWQ線程池的worklist上。
GCWQ的線程池在實現時同時考慮了並發能力和資源佔用,僅可能佔用最小的資源並提供足夠的並發能力。每個CPU上綁定的線程池通過hook到CPU調度機制來實現並發管理。當worker被喚醒或者進入睡眠都會通知到線程池,線程池保持對當前可以運行的worker個數的跟蹤。通常我們不期望一個work獨佔CPU和運行很多個CPU周期,因此維護剛好足夠的並發以防止work處理的速度降低是最優的。當CPU上有一個或多個runnalbe的worker,線程池不會啟動新的work任務。當上一個running的work轉入睡眠,則立即調度一個新的worker。這樣當有work在pending的時候,CPU一直保持幹活的狀態。這樣來保證用最小的worker個數同時足夠的執行帶寬。
維持idle狀態的worker只是消耗部分kthreads的內存,因此CMWQ在殺掉idle的worker之前一段時間讓其活着。
unbound的WQ並不使用上述機制,而是用pseudo unbound CPU的線程池去儘快處理所有work。CMWQ的使用者來控制並發級別,並可以設置一個flag來忽略並發管理機制。
CMWQ通過創建更多的worker以及rescue-worker來保證任務按時處理。所有可能在內存回收的代碼路徑上執行的work必須放到特定的workqueue,該workqueue上有一個rescue-worker可以在內存壓力下執行,這樣避免在內存回收時出現死鎖。
API
alloc_workqueue() alloc_workqueue()用於分配一個WQ。原來的create_workqueue()系列接口已經棄用並計劃刪除。alloc_workqueue()有三個入參:@name, @flags, @max_active。name是workqueue的名字並也用於rescuer-thread(如果有的話)名稱。flags和max_active用於控制work分配執行環境、調度和執行。
flags WQ_NON_REENTRANT 默認一個WQ保證在同一個CPU上不會有重入性,即WQ上多個work不會再同一個CPU上並發執行,但會在多個CPU上並發執行。該flag標識在多個CPU上也不能重入,在整個系統級別都只有一個work在執行。
WQ_UNBOUND 該flag設定的WQ不綁定到CPU,其work將被一個特殊的CGWQ進行服務,該CGWQ上的worker不綁定任何CPU。unbound WQ犧牲了CPU親和性,主要用於下場景:
- 並發級別需求的波動非常大,如果使用bound WQ則會在不同CPU上創建大量的worker,並且這些worker大部分時間都是空閑的。
- 長時間運行的CPU密集型工作可以由系統調度程序更好的管理。
WQ_FREEZABLE 可凍結的WQ在系統suspend操作的freeze階段,暫停新的work執行直到解凍。
WQ_MEM_RECLAIM 可能用於內存回收路徑的WQ必須設置該flag。在內存緊張的時候也會保證至少有一個可執行的上下文用於該WQ。
WQ_HIGHPRI 高優先級的WQ的work會被放入GCWQ的高優先級線程池。高優先級的線程池的線程擁有高nice級別。普通的線程池和高優先級的線程池彼此獨立,互相不影響。
WQ_CPU_INTENSIVE 設置為CPU密集型的WQ的work不會影響並發級別,即CPU密集型的work執行時並不會阻止同一個線程池裡其他WQ的work的執行。這對希望獨佔CPU周期的work非常有用,由系統調度程序調度他們的執行。如果不設置該標記,則獨佔CPU周期的work會導致同一個線程池裡其他WQ的work得不到執行。 由於同一由CMWQ的並發管理進行調度,當非密集型的WQ的work運行過程中,也會導緻密集型的WQ的work被推遲。該flag僅適用於bound的WQ,對unbound的WQ無效。
max_active max_active用於指定WQ在每個CPU上最大的執行上下文個數,即並發處理的work個數。目前對於bound WQ,max_active最大可以設置為512,如果max_active入參為0,則使用默認值256。對於unbound WQ,最大值為512和4*cpu核數兩個裏面較大的值。 對於希望使用STWQ的使用者,可以設置max_active為1,並且設置WQ_UNBOUND標識。這樣整個系統里只有一個該WQ上的work正在執行。
struct workqueue_struct 函數alloc_workqueue()返回一個指向struct workqueue_struct的指針,代表一個workqueue。如下所示:
struct workqueue_struct *wq = alloc_workqueue("wq-name", WQ_NON_REENTRANT | WQ_MEM_RECLAIM, 0);
struct work_struct 數據結構struct work_struct定義了一個work,通過通過INIT_WORK系列宏定義初始化work,設置執行的函數。如下所示:
struct work_struct work; void worker_func(struct work_struct *work); INIT_WORK(&work, worker_func);
struct delayed_work 數據結構struct delayed_work定義了一個延遲work,延遲work通過設置定時器的方式延遲將work放入隊列。如下所示為其數據結構定義:
struct delayed_work { struct work_struct work; struct timer_list timer;
/* target workqueue and CPU ->timer uses to queue ->work */ struct workqueue_struct *wq; int cpu; };
可以看到delayed_work由一個work和一個定時器組成。delayed_work通過INIT_DELAYED_WORK系列宏定義進行初始化。如下所示:
struct delayed_work work; void worker_func(struct work_struct *work); INIT_DELAYED_WORK(&work, worker_func);
queue_work_on() 函數queue_work_on()將work放入workqueue隊列,其定義如下:
extern bool queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work);
函數queue_delayed_work將delayed_work在延遲delay個jiffies之後放入workqueue隊列,其定義如下:
staticinline bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsignedlong delay)
調試
因為work是由通用的工作線程執行的,因此需要一些技巧來定位workqueue使用者的一些錯誤行為。 worker線程通過ps可以看到:
root 5671 0.0 0.0 0 0 ? S 12:07 0:00 [kworker/0:1] root 5672 0.0 0.0 0 0 ? S 12:07 0:00 [kworker/1:2] root 5673 0.0 0.0 0 0 ? S 12:12 0:00 [kworker/0:0] root 5674 0.0 0.0 0 0 ? S 12:13 0:00 [kworker/1:0]
如果某個kworker瘋了,佔用CPU非常高,可能有如下兩種原因:
- 大量的work正在正在提交調度;
- 某個work佔用過多CPU;
第1種原因可以通過tracing機制來跟蹤:
$ echo workqueue:workqueue_queue_work > /sys/kernel/debug/tracing/set_event $ cat /sys/kernel/debug/tracing/trace_pipe > out.txt
如果某個worker忙於循環將大量work進行調度,通過輸出的work里的函數可以找到誰提交了大量的work。
第2種原因可以通過打印出worker的堆棧空間來分析是哪個work的函數正在處理:
$ cat /proc/THE_OFFENDING_KWORKER/stack
總結
在內核中直接使用kthread創建自己的線程進行異步處理帶來一定的複雜度以及資源浪費,而workqueue機製為內核模塊提供了簡單的接口來實現異步函數處理。CMWQ機制在使用盡量少的資源的同時保???了並發處理能力。
參考資料
Linux/Documentation/workqueue.txt