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