一、前言
一種新的機制出現的原因往往是為了解決實際的問題,雖然linux kernel中已經提供了workqueue的機制,那么為何還要引入cmwq呢?也就是說:舊的workqueue機制存在什么樣的問題?在新的cmwq又是如何解決這些問題的呢?它接口是如何呈現的呢(驅動工程師最關心這個了)?如何兼容舊的驅動呢?本文希望可以解開這些謎題。
本文的代碼來自linux kernel 4.0。
二、為何需要CMWQ?
內核中很多場景需要異步執行環境(在驅動中尤其常見),這時候,我們需要定義一個work(執行哪一個函數)并掛入workqueue。處理該work的線程叫做worker,不斷的處理隊列中的work,當處理完畢后則休眠,隊列中有work的時候就醒來處理,如此周而復始。一切看起來比較完美,問題出在哪里呢?
(1)內核線程數量太多。如果沒有足夠的內核知識,程序員有可能會錯誤的使用workqueue機制,從而導致這個機制被玩壞。例如明明可以使用default workqueue,偏偏自己創建屬于自己的workqueue,這樣一來,對于那些比較大型的系統(CPU個數比較多),很可能內核啟動結束后就耗盡了PID space(default最大值是65535),這種情況下,你讓user space的程序情何以堪?雖然default最大值是可以修改的,從而擴大PID space來解決這個問題,不過系統太多的task會對整體performance造成負面影響。
(2)盡管消耗了很多資源,但是并發性如何呢?我們先看single threaded的workqueue,這種情況完全沒有并發的概念,任何的work都是排隊執行,如果正在執行的work很慢,例如4~5秒的時間,那么隊列中的其他work除了等待別無選擇。multi threaded(更準確的是per-CPU threaded)情況當然會好一些(畢竟多消耗了資源),但是對并發仍然處理的不是很好。對于multi threaded workqueue,雖然創建了thread pool,但是thread pool的數目是固定的:每個oneline的cpu上運行一個,而且是嚴格的綁定關系。也就是說本來線程池是一個很好的概念,但是傳統workqueue上的線程池(或者叫做worker pool)卻分割了每個線程,線程之間不能互通有無。例如cpu0上的worker thread由于處理work而進入阻塞狀態,那么該worker thread處理的work queue中的其他work都阻塞住,不能轉移到其他cpu上的worker thread去,更有甚者,cpu0上隨后掛入的work也接受同樣的命運(在某個cpu上schedule的work一定會運行在那個cpu上),不能去其他空閑的worker thread上執行。由于不能提供很好的并發性,有些內核模塊(fscache)甚至自己創建了thread pool(slow work也曾經短暫的出現在kernel中)。
(3)dead lock問題。我們舉一個簡單的例子:我們知道,系統有default workqueue,如果沒有特別需求,驅動工程師都喜歡用這個workqueue。我們的驅動模塊在處理release(userspace close該設備)函數的時候,由于使用了workqueue,那么一般會flush整個workqueue,以便確保本driver的所有事宜都已經處理完畢(在close的時候很有可能有pending的work,因此要flush),大概的代碼如下:
flush work是一個長期過程,因此很有可能被調度出去,這樣調用close的進程被阻塞,等到keventd_wq這個內核線程組完成flush操作后就會wakeup該進程。但是這個default workqueue使用很廣,其他的模塊也可能會schedule work到該workqueue中,并且如果這些模塊的work也需要獲取鎖A,那么就會deadlock(keventd_wq阻塞,再也無法喚醒等待flush的進程)。解決這個問題的方法是創建多個workqueue,但是這樣又回到了內核線程數量大多的問題上來。
我們再看一個例子:假設某個驅動模塊比較復雜,使用了兩個work struct,分別是A和B,如果work A依賴 work B的執行結果,那么,如果這兩個work都schedule到一個worker thread的時候就出現問題,由于worker thread不能并發的執行work A和work B,因此該驅動模塊會死鎖。Multi threaded workqueue能減輕這個問題,但是無法解決該問題,畢竟work A和work B還是有機會調度到一個cpu上執行。造成這些問題的根本原因是眾多的work競爭一個執行上下文導致的。
(4)二元化的線程池機制。基本上workqueue也是thread pool的一種,但是創建的線程數目是二元化的設定:要么是1,要么是number of CPU,但是,有些場景中,創建number of CPU太多,而創建一個線程又太少,這時候,勉強使用了single threaded workqueue,但是不得不接受串行處理work,使用multi threaded workqueue吧,占用資源太多。二元化的線程池機制讓用戶無所適從。
三、CMWQ如何解決問題的呢?
1、設計原則。在進行CMWQ的時候遵循下面兩個原則:
(1)和舊的workqueue接口兼容。
(2)明確的劃分了workqueue的前端接口和后端實現機制。CMWQ的整體架構如下:
對于workqueue的用戶而言,前端的操作包括二種,一個是創建workqueue。可以選擇創建自己的workqueue,當然也可以不創建而是使用系統缺省的workqueue。另外一個操作就是將指定的work添加到workqueue。在舊的workqueue機制中,workqueue和worker thread是密切聯系的概念,對于single workqueue,創建一個系統范圍的worker thread,對于multi workqueue,創建per-CPU的worker thread,一切都是固定死的。針對這樣的設計,我們可以進一步思考其合理性。workqueue用戶的需求就是一個異步執行的環境,把創建workqueue和創建worker thread綁定起來大大限定了資源的使用,其實具體后臺是如何處理work,是否否啟動了多個thread,如何管理多個線程之間的協調,workqueue的用戶并不關心。
基于這樣的思考,在CMWQ中,將這種固定的關系被打破,提出了worker pool這樣的概念(其實就是一種thread pool的概念),也就是說,系統中存在若干worker pool,不和特定的workqueue關聯,而是所有的workqueue共享。用戶可以創建workqueue(不創建worker pool)并通過flag來約束掛入該workqueue上work的處理方式。workqueue會根據其flag將work交付給系統中某個worker pool處理。例如如果該workqueue是bounded類型并且設定了high priority,那么掛入該workqueue的work將由per cpu的highpri worker-pool來處理。
讓所有的workqueue共享系統中的worker pool,即減少了資源的浪費(沒有創建那么多的kernel thread),又保證了靈活的并發性(worker pool會根據情況靈活的創建thread來處理work)。
3、如何解決線程數目過多的問題?
在CMWQ中,用戶可以根據自己的需求創建workqueue,但是已經和后端的線程池是否創建worker線程無關了,是否創建新的work線程是由worker線程池來管理。系統中的線程池包括兩種:
(1)和特定CPU綁定的線程池。這種線程池有兩種,一種叫做normal thread pool,另外一種叫做high priority thread pool,分別用來管理普通的worker thread和高優先級的worker thread,而這兩種thread分別用來處理普通的和高優先級的work。這種類型的線程池數目是固定的,和系統中cpu的數目相關,如果系統有n個cpu,如果都是online的,那么會創建2n個線程池。
(2)unbound 線程池,可以運行在任意的cpu上。這種thread pool是動態創建的,是和thread pool的屬性相關,包括該thread pool創建worker thread的優先級(nice value),可以運行的cpu鏈表等。如果系統中已經有了相同屬性的thread pool,那么不需要創建新的線程池,否則需要創建。
OK,上面講了線程池的創建,了解到創建workqueue和創建worker thread這兩個事件已經解除關聯,用戶創建workqueue僅僅是選擇一個或者多個線程池而已,對于bound thread pool,每個cpu有兩個thread pool,關系是固定的,對于unbound thread pool,有可能根據屬性動態創建thread pool。那么worker thread pool如何創建worker thread呢?是否會數目過多呢?
缺省情況下,創建thread pool的時候會創建一個worker thread來處理work,隨著work的提交以及work的執行情況,thread pool會動態創建worker thread。具體創建worker thread的策略為何?本質上這是一個需要在并發性和系統資源消耗上進行平衡的問題,CMWQ使用了一個非常簡單的策略:當thread pool中處于運行狀態的worker thread等于0,并且有需要處理的work的時候,thread pool就會創建新的worker線程。當worker線程處于idle的時候,不會立刻銷毀它,而是保持一段時間,如果這時候有創建新的worker的需求的時候,那么直接wakeup idle的worker即可。一段時間過去仍然沒有事情處理,那么該worker thread會被銷毀。
4、如何解決并發問題?
我們用某個cpu上的bound workqueue來描述該問題。假設有A B C D四個work在該cpu上運行,缺省的情況下,thread pool會創建一個worker來處理這四個work。在舊的workqueue中,A B C D四個work毫無疑問是串行在cpu上執行,假設B work阻塞了,那么C D都是無法執行下去,一直要等到B解除阻塞并執行完畢。
對于CMWQ,當B work阻塞了,thread pool可以感知到這一事件,這時候它會創建一個新的worker thread來處理C D這兩個work,從而解決了并發的問題。由于解決了并發問題,實際上也解決了由于競爭一個execution context而引入的各種問題(例如dead lock)。
四、接口API
1、初始化work的接口保持不變,可以靜態或者動態創建work。
2、調度work執行也保持和舊的workqueue一致。
3、創建workqueue。和舊的create_workqueue接口不同,CMWQ采用了alloc_workqueue這樣的接口符號,相關的接口定義如下:
在描述這些workqueue的接口之前,我們需要準備一些workqueue flag的知識。
標有WQ_UNBOUND這個flag的workqueue說明其work的處理不需要綁定在特定的CPU上執行,workqueue需要關聯一個系統中的unbound worker thread pool。如果系統中能找到匹配的線程池(根據workqueue的屬性(attribute)),那么就選擇一個,如果找不到適合的線程池,workqueue就會創建一個worker thread pool來處理work。
WQ_FREEZABLE是一個和電源管理相關的內容。在系統Hibernation或者suspend的時候,有一個步驟就是凍結用戶空間的進程以及部分(標注freezable的)內核線程(包括workqueue的worker thread)。標記WQ_FREEZABLE的workqueue需要參與到進程凍結的過程中,worker thread被凍結的時候,會處理完當前所有的work,一旦凍結完成,那么就不會啟動新的work的執行,直到進程被解凍。
和WQ_MEM_RECLAIM這個flag相關的概念是rescuer thread。前面我們描述解決并發問題的時候說到:對于A B C D四個work,當正在處理的B work被阻塞后,worker pool會創建一個新的worker thread來處理其他的work,但是,在memory資源比較緊張的時候,創建worker thread未必能夠成功,這時候,如果B work是依賴C或者D work的執行結果的時候,系統進入dead lock。這種狀態是由于不能創建新的worker thread導致的,如何解決呢?對于每一個標記WQ_MEM_RECLAIM flag的work queue,系統都會創建一個rescuer thread,當發生這種情況的時候,C或者D work會被rescuer thread接手處理,從而解除了dead lock。
WQ_HIGHPRI說明掛入該workqueue的work是屬于高優先級的work,需要高優先級(比較低的nice value)的worker thread來處理。
WQ_CPU_INTENSIVE這個flag說明掛入該workqueue的work是屬于特別消耗cpu的那一類。為何要提供這樣的flag呢?我們還是用老例子來說明。對于A B C D四個work,B是cpu intersive的,當thread正在處理B work的時候,該worker thread一直執行B work,因為它是cpu intensive的,特別吃cpu,這時候,thread pool是不會創建新的worker的,因為當前還有一個worker是running狀態,正在處理B work。這時候C Dwork實際上是得不到執行,影響了并發。
了解了上面的內容,那么基本上alloc_workqueue中flag參數就明白了,下面我們轉向max_active這個參數。系統不能允許創建太多的thread來處理掛入某個workqueue的work,最多能創建的線程數目是定義在max_active參數中。
除了alloc_workqueue接口API之外,還可以通過alloc_ordered_workqueue這個接口API來創建一個嚴格串行執行work的一個workqueue,并且該workqueue是unbound類型的。create_*的接口都是為了兼容過去接口而設立的,大家可以自行理解,這里就不多說了。
-
接口
+關注
關注
33文章
8598瀏覽量
151159 -
驅動
+關注
關注
12文章
1840瀏覽量
85293
原文標題:郭健: currency Managed Workqueue(CMWQ)概述
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論