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