深度解析Linux讀寫鎖邏輯
一、Linux為何會引入讀寫鎖?
除了mutex,在linux內(nèi)核中,還有一個(gè)經(jīng)常用到的睡眠鎖就是rw semaphore(后文簡稱為rwsem),它到底和mutex有什么不同呢?為何會有rw semaphore?無他,僅僅是為了增加內(nèi)核的并發(fā),從而增加性能而已。Mutex嚴(yán)格的限制只有一個(gè)thread可以進(jìn)入臨界區(qū),但是實(shí)際應(yīng)用中,有些場景對共享資源的訪問可以嚴(yán)格區(qū)分讀和寫的,并且是讀多寫少,這時(shí)候,其實(shí)多個(gè)讀的thread同時(shí)進(jìn)入臨界區(qū)是OK的,使用mutex則限制一個(gè)線程進(jìn)入臨界區(qū),從而導(dǎo)致性能的下降。
本文會描述linux5.15.81中讀寫鎖的數(shù)據(jù)結(jié)構(gòu)和邏輯過程。
二、如何抽象讀寫鎖的數(shù)據(jù)結(jié)構(gòu)?
下圖可以抽象rwsem相關(guān)的數(shù)據(jù)結(jié)構(gòu):

一個(gè)rwsem對象需要記錄兩種數(shù)據(jù):
讀寫鎖的狀態(tài)信息
和該讀寫鎖相關(guān)的任務(wù)信息
我們先看看讀寫鎖的狀態(tài)。讀寫鎖狀態(tài)字需要分別記錄讀鎖和寫鎖的狀態(tài):由于多個(gè)reader可以同時(shí)處于臨界區(qū),所以對于reader-owned的場景,讀鎖狀態(tài)變成了一個(gè)counter,來記錄臨界區(qū)內(nèi)reader的數(shù)量,counter等于0表示讀鎖為空鎖狀態(tài)。對于writer,其行為和互斥鎖一致,因此其寫鎖狀態(tài)和mutex一樣,仍然使用一個(gè)bit表示。
和讀寫相關(guān)的任務(wù)有兩類,一類是已經(jīng)持鎖的線程(即在臨界區(qū)的線程),另外一類是無法持鎖而需要等待的任務(wù)。對于writer持鎖情況,由于排他性,我們很清楚的知道是哪個(gè)task持鎖,那么一個(gè)task struct指針就足夠了記錄owner了。然而對于讀側(cè)可以多個(gè)reader進(jìn)入臨界區(qū),那么owner們需要組成一個(gè)隊(duì)列才可以記錄每一個(gè)臨界區(qū)的reader。不過在實(shí)際的rwsem實(shí)現(xiàn)中,由于跟蹤owner們開銷比較大,因此也是用一個(gè)task struct指針指向其一。具體linux代碼是這樣處理的:reader進(jìn)入的時(shí)候會設(shè)置owner task,但是離開讀臨界區(qū)并不會清除task指針。這樣,實(shí)際上對于讀,owner task應(yīng)該表示該任務(wù)曾經(jīng)擁有該鎖,并不表示是目前持鎖的owner task,也有可能已經(jīng)離開臨界區(qū),甚至該任務(wù)已經(jīng)銷毀。
如果持鎖失敗,無法進(jìn)入臨界區(qū),我們有兩種選擇:
樂觀自旋
掛入等待隊(duì)列
兩種選擇各有優(yōu)點(diǎn)和缺點(diǎn),總結(jié)如下:

在5.15的內(nèi)核中,只有在write持鎖路徑上有樂觀自旋的操作,reader路徑?jīng)]有,只有偷鎖的操作。當(dāng)樂觀自旋失敗后就會掛入等待隊(duì)列,阻塞當(dāng)前線程。(樂觀自旋功能有一個(gè)很有意思的發(fā)展過程,從開始支持writer的樂觀自旋,到支持全場景的樂觀自旋,然后又回到最初,有興趣可以查閱內(nèi)核的patch了解詳情)
在了解了rwsem的基本概念之后,我們一起來看看struct rw_semaphore數(shù)據(jù)結(jié)構(gòu),其成員描述如下:


由于是sleep lock,我們需要把等待的任務(wù)掛入隊(duì)列。在內(nèi)核中,struct rwsem_waiter用來抽象等待rwsem的任務(wù),其成員描述如下:

三、Rwsem外部接口API為何?
Rwsem模塊的外部接口API如下:


【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??


四、嘗試獲取讀鎖
和down_read不一樣,down_read_trylock只是嘗試獲取讀鎖,如果成功,那么自然是好的,直接返回1,如果失敗,也不會阻塞,只是返回0就可以了。代碼主邏輯在__down_read_trylock函數(shù)中,如下:

tmp的初始值設(shè)定為RWSEM_UNLOCKED_VALUE(0值),因此第一次循環(huán)是為當(dāng)前是空鎖而做的優(yōu)化:如果當(dāng)前的sem->count等于0,那么給sem->count賦值RWSEM_READER_BIAS,標(biāo)記持鎖成功,然后設(shè)定owner返回1即可。
如果快速獲取空鎖不成功,這時(shí)候tmp已經(jīng)賦值(等于sem->count),不再是0值了。通過對當(dāng)前sem->count的值可以判斷是否是可以進(jìn)入臨界區(qū)。持讀鎖失敗的情況包括:

如果判斷可以進(jìn)入讀臨界區(qū)(臨界區(qū)僅有reader并且沒有writer等待的場景),那么重新進(jìn)入循環(huán),如果sem->count保持不變,那么可以持鎖成功,給進(jìn)入臨界區(qū)的reader數(shù)目加一,并設(shè)置owner task和reader持鎖標(biāo)記(non-spinnable比特保持不變)。如果這期間有其他線程插入修改了count值,那么需要再次判斷是否能持讀鎖,重復(fù)上面的循環(huán)。如果判斷不可以進(jìn)入臨界區(qū),退出循環(huán),持鎖失敗。
五、獲取讀鎖
Reader獲取讀鎖的代碼主要在__down_read_common函數(shù)中,如下:

快速路徑
rwsem_read_trylock是快速路徑,代碼如下:

reader直接會給sem->count加RWSEM_READER_BIAS來增加讀臨界區(qū)的線程個(gè)數(shù),當(dāng)然這有可能失敗,那么就進(jìn)入慢速路徑(需要回退錯(cuò)誤增加讀臨界區(qū)線程數(shù)量)。如果恰好能夠進(jìn)入臨界區(qū),那么就直接設(shè)定owner返回即可。注意:這里*cntp保存了atomic add之后的新值。rwsem_down_read_slowpath會使用這個(gè)新值作為參數(shù)。
當(dāng)reader的數(shù)量過多(以至于都溢出了)的時(shí)候,需要禁止樂觀自旋。
這里是持鎖成功的路徑。RWSEM_READ_FAILED_MASK上一節(jié)已經(jīng)解釋,這里不再贅述。這里需要注意的是rwsem_set_reader_owned函數(shù)中flag的設(shè)定,由于reader進(jìn)入臨界區(qū),因此RWSEM_READER_OWNED也需要設(shè)定。RWSEM_RD_NONSPINNABLE標(biāo)記保持不變。
在快速路徑中,有兩種常見的情況會持鎖成功:一種是空鎖,另外一種是沒有任何waiter等待的純r(jià)eader并發(fā)。
2.慢速路徑
如果快速路徑持鎖失敗,那么進(jìn)入慢速路徑。慢速路徑代碼比較長,我們分段解析。首先是防止等待隊(duì)列中waiter任務(wù)餓死的代碼:

如果當(dāng)前的鎖被reader持有(至少有一個(gè)reader在臨界區(qū)),那么不再樂觀偷鎖而是直接進(jìn)行掛等待隊(duì)列的操作。為何怎么做呢?因?yàn)樾枰陴I死waiter和reader吞吐量上進(jìn)行平衡。一方面,連續(xù)的reader持續(xù)偷鎖的話會餓死等待隊(duì)列上的任務(wù)。另外,在喚醒路徑上,被喚醒的top reader會順便將隊(duì)列中的若干(不大于256個(gè))reader也同時(shí)喚醒,以便增加rwsem的吞吐量。所以這里的reader直接掛入隊(duì)列,累計(jì)多個(gè)reader以便可以批量喚醒。
Reader偷鎖的場景主要發(fā)生在喚醒top waiter的過程中,這時(shí)候臨界區(qū)沒有線程,被喚醒的reader或者writer也沒有持鎖(writer需要被調(diào)度到CPU上執(zhí)行之后才會試圖持鎖,高負(fù)載的場景下,鎖被偷的概率比較大,reader是喚醒后立刻持鎖,被偷的幾率小一點(diǎn))。具體樂觀偷鎖(optimistic lock stealing)的代碼如下:

所謂偷鎖就是不樂觀自旋(要有排隊(duì)),不管先來后到,直接獲取鎖。允許偷鎖的場景是這樣的:臨界區(qū)沒有writer持鎖,也沒有設(shè)置handoff,正在喚醒top waiter的過程中,并且有任務(wù)在等待隊(duì)列的情況。這時(shí)候進(jìn)入慢速路徑的reader可以先于top waiter喚醒之前把鎖偷走。需要特別說明的是:這時(shí)候reader counter已經(jīng)加一,還是盡量讓reader偷鎖成功,否則還需要回退。
當(dāng)前線程獲得了讀鎖,需要設(shè)置owner,畢竟它是臨界區(qū)的新客
如果偷鎖成功并且它是臨界區(qū)第一個(gè)reader,那么它還會把等待隊(duì)列中的reader都喚醒(前提是top waiter不是writer),帶領(lǐng)大家一起往前沖(這里會打破FIFO的順序,懲罰了隊(duì)列中的writer)。具體是通過rwsem_mark_wake來標(biāo)記喚醒的reader,然后通過wake_up_q將reader喚醒并進(jìn)入讀臨界區(qū)。為了減低對等待中的writer線程的影響,這時(shí)候?qū)eader的并發(fā)是受限的,最多可以喚醒MAX_READERS_WAKEUP個(gè)reader。
如果偷鎖不成功,當(dāng)前的reader還是需要進(jìn)入阻塞狀態(tài):

準(zhǔn)備好掛入等待隊(duì)列的rwsem waiter數(shù)據(jù),需要特別說明的是這里的timeout時(shí)間:目前手機(jī)平臺的HZ設(shè)置的是250,也就是說在觸發(fā)handoff機(jī)制之前waiter需要至少在隊(duì)列中等待一個(gè)tick(4ms)的時(shí)間。這里的timeout是指handoff timeout,為了防止偷鎖或者自旋導(dǎo)致等待隊(duì)列中的top waiter有一個(gè)長時(shí)間的持鎖延遲。在timeout時(shí)間內(nèi),樂觀偷鎖或者自旋可以順利進(jìn)行,但是一旦超時(shí)就會設(shè)定handoff標(biāo)記,樂觀偷鎖或者自旋被禁止,鎖的所有權(quán)需要遞交給等待隊(duì)列中的top waiter。
如果目前等待隊(duì)列為空,那么要做一些額外的處理。例如入隊(duì)之前肯定給安排上RWSEM_FLAG_WAITERS這個(gè)標(biāo)記。
當(dāng)然,在入隊(duì)之前還要垂死掙扎一下(等待隊(duì)列為空的時(shí)候邏輯簡單一些,不需要喚醒隊(duì)列上的wait),看看是不是當(dāng)前有機(jī)可乘,如果是這樣,那么就順勢而為,直接持鎖成功,而且counter都已經(jīng)準(zhǔn)備好了,前面已經(jīng)加一了。
等待隊(duì)列非空的時(shí)候,邏輯稍微負(fù)載一點(diǎn)。調(diào)用rwsem_add_waiter函數(shù)即可以把當(dāng)前任務(wù)掛入等待隊(duì)列尾部。這時(shí)候也需要把之前武斷增加的counter給修正回來了(adjustment初始化為-RWSEM_READER_BIAS)。如果是第一個(gè)waiter,也順便設(shè)置了RWSEM_FLAG_WAITERS標(biāo)記。
在當(dāng)前線程進(jìn)入阻塞之前,我們需要進(jìn)行試圖持鎖的動作(上面是空隊(duì)列場景檢查,這里的邏輯稍微復(fù)雜一點(diǎn),由于已經(jīng)入隊(duì),這里需要調(diào)用rwsem_mark_wake函數(shù)來完成阻塞后喚醒的動作),畢竟這時(shí)候可能恰好owner離開臨界區(qū),變成空鎖。

如果這時(shí)候發(fā)現(xiàn)鎖的owner恰好都離開了臨界區(qū),那么我們是需要執(zhí)行喚醒top waiter操作的,喚醒之前需要清除禁止樂觀自旋的標(biāo)記,畢竟目前臨界區(qū)沒有任何線程。
除了上面說的場景需要喚醒,在reader持鎖并且我們是隊(duì)列中的第一個(gè)waiter的時(shí)候,也需要喚醒的動作(喚醒自己)。
阻塞部分的代碼邏輯如下:

在rwsem_mark_wake函數(shù)中我們會喚醒reader并將其等待對象的task成員(waiter.task)設(shè)置為NULL。因此,這里如果發(fā)現(xiàn)waiter.task等于NULL,那么說明是該線程被正常喚醒,那么從阻塞狀態(tài)返回,持鎖成功。
如果在該線程阻塞的時(shí)候,有其他任務(wù)發(fā)送信號給該線程,那么就持鎖失敗退出。如果已經(jīng)被喚醒,同時(shí)又收到信號,這時(shí)候需要首先完成喚醒,持鎖成功,然后在其他的合適點(diǎn)再處理該信號。當(dāng)然,大部分的rwsem都是D狀態(tài),也就不需要處理信號了。
進(jìn)入阻塞狀態(tài),讓調(diào)度器選擇next task
六、釋放讀鎖
釋放讀鎖的代碼邏輯主要在__up_read函數(shù)中,如下:

需要強(qiáng)調(diào)的是:這里僅僅是減去了讀臨界區(qū)的counter計(jì)數(shù),并沒有清除owner中的task pointer。此外,當(dāng)?shù)却?duì)列有waiter并且沒有writer或者reader在臨界區(qū)的時(shí)候,我們會調(diào)用rwsem_wake來喚醒等待隊(duì)列的線程。因?yàn)榕R界區(qū)已經(jīng)沒有線程,所以需要清除nonspinable標(biāo)記。喚醒的動作主要是通過rwsem_mark_wake和wake_up_q來完成的,wake_up_q比較簡單,我們就不贅述了,主要看看rwsem_mark_wake的邏輯。
我們首先給出wake type的解釋:

在RWSEM_WAKE_READERS場景中,多個(gè)reader被喚醒,并且當(dāng)前很可能是空鎖狀態(tài),為了防止writer搶鎖,因此會先讓top waiter持有讀鎖,然后慢慢處理后續(xù)。RWSEM_WAKE_READ_OWNED則沒有這個(gè)顧慮,因?yàn)閱拘颜咭呀?jīng)持有讀鎖。
在釋放讀鎖的場景中,rwsem_mark_wake使用的是RWSEM_WAKE_ANY參數(shù),具體的代碼如下:

這段代碼是處理top waiter是writer的邏輯。這時(shí)候,如果wake type是RWSEM_WAKE_ANY,即不關(guān)心喚醒的是reader還是writer,只要喚醒等待隊(duì)列頭部的waiter就好。如果top waiter是writer,我們只需要將這個(gè)writer喚醒即可,不需要修改鎖的狀態(tài),出隊(duì)等操作,這些都是在喚醒之后完成。如果wake type是其他兩種類型(都是喚醒reader的),那么就直接返回。也就是說在rwsem_mark_wake想要喚醒reader的場景中,如果top waiter是writer,那么將不會喚醒任何reader線程。如果top waiter是reader的話,那么基本上是需要喚醒一組reader了。

執(zhí)行到這里,我們需要喚醒等待隊(duì)列頭部的若干reader線程去持鎖。由于writer有可能會在這個(gè)階段偷鎖,因此,這里我們會先讓top waiter(reader)持鎖,然后再慢慢去計(jì)算到底需要喚醒多少個(gè)reader并將其喚醒。如果當(dāng)前線程已經(jīng)持有了讀鎖(wake type的類型是RWSEM_WAKE_READ_OWNED),則不需要提前持鎖,直接越過這部分的邏輯即可。
如果的確發(fā)生了writer通過樂觀自旋偷鎖,那么我們需要檢查設(shè)置handoff的條件。如果reader被writer阻塞太久,那么我們設(shè)定handoff標(biāo)記,要求rwsem的writer停止通過樂觀自旋偷鎖,將鎖的所有權(quán)轉(zhuǎn)交給top waiter(reader)
上面已經(jīng)向rwsem的count增加reader計(jì)數(shù),這里把owner也設(shè)定上(flag也同步安排,這里non-spinnable bit保持不變)。隨后top waiter的reader會喚醒若干隊(duì)列中的non top reader,但是它們都不配擁有名字。
讀鎖已經(jīng)安排的妥妥的了,下面就是慢慢喚醒等待隊(duì)列的reader了。我們通過兩步來完成喚醒:
將等待隊(duì)列中的reader摘下放入到一個(gè)單獨(dú)的列表中(wlist),同時(shí)對reader進(jìn)行計(jì)數(shù)。后續(xù)這個(gè)計(jì)數(shù)會寫入rwsem 的reader counte域。
對于wlist中的每一個(gè)waiter對象(reader任務(wù)),清除waiter->task并將它們放入wake_q以便稍后被喚醒。
我們先看第一輪計(jì)算喚醒reader個(gè)數(shù)的計(jì)數(shù):

對于rwsem,其公平性是區(qū)分讀寫的。對于讀,如果top waiter是reader,那么所有的reader都可以進(jìn)入臨界區(qū),不管reader在隊(duì)列中的順序。對于writer,我們要確保其公平性,我們要按照writer在隊(duì)列中的順序依次持鎖。根據(jù)上面的原則,我們會略過隊(duì)列中的writer,將盡量多的reader喚醒并進(jìn)入臨界區(qū)
喚醒數(shù)量不能大于256,否則會餓死writer
根據(jù)喚醒的reader數(shù)量計(jì)算count調(diào)整值
Rwsem的count成員還有一些bit用來標(biāo)記當(dāng)前讀寫鎖狀態(tài)(waiter bit和handoff bit),也需要根據(jù)情況進(jìn)行調(diào)整:

如果等待隊(duì)列為空了,肯定是要清除waiter flag,同時(shí)要清除handoff flag,畢竟沒有什么等待任務(wù)可以遞交鎖了。
雖然隊(duì)列非空,但已經(jīng)喚醒了reader,那么需要清除handoff標(biāo)記,畢竟top waiter已經(jīng)被喚醒去持鎖了,完成了鎖的遞交。
完成sem->count的調(diào)整
第二輪將喚醒的reader加入喚醒隊(duì)列,具體的邏輯如下:

主要是把等待任務(wù)對象的task成員設(shè)置為NULL,喚醒之后根據(jù)這個(gè)成員來判斷是正常喚醒還是異常喚醒路徑。
這里對喚醒等待隊(duì)列上的reader和writer處理是不一樣的。對于writer,喚醒之然后被調(diào)度到之后再去試圖持鎖。對于reader,在喚醒路徑上就已經(jīng)持鎖(增加rwsem的reader count,并且修改了相關(guān)的狀態(tài)標(biāo)記)。之所以這么做主要是降低調(diào)度的開銷,畢竟若干個(gè)reader線程被喚醒之后,獲得CPU資源再去持鎖,持鎖失敗然后繼續(xù)阻塞,這些都會增加調(diào)度的負(fù)載。
七、嘗試獲取寫鎖
和down_write不一樣,down_write_trylock只是嘗試獲取寫鎖,如果成功,那么自然是好的,直接返回1,如果失敗,也不會阻塞,只是返回0就可以了。代碼主邏輯在rwsem_write_trylock函數(shù)中,如下:

tmp的初始值設(shè)定為RWSEM_UNLOCKED_VALUE(0值),對于writer而言,只有rwsem是空鎖的時(shí)候才能進(jìn)入臨界區(qū)。如果當(dāng)前的sem->count等于0,那么給sem->count賦值RWSEM_WRITER_LOCKED,標(biāo)記持鎖成功,并且把owner設(shè)定為當(dāng)前task。
atomic_long_try_cmpxchg_acquire函數(shù)有三個(gè)參數(shù),從左到右分別是value,old和new。該函數(shù)會對比value和old,如果相等那么執(zhí)行賦值value=new同時(shí)返回true。如果不相等,不執(zhí)行賦值操作,直接返回false。
八、獲取寫鎖
Writer獲取寫鎖的代碼主要在__down_write_common函數(shù)中,如下:

rwsem_write_trylock(快速路徑)上一節(jié)已經(jīng)描述,我們主要看慢速路徑的邏輯(樂觀自旋我們下面會講,這里暫且略過):

首先準(zhǔn)備好一個(gè)等待任務(wù)對象(棧上)并初始化,將其掛入等待隊(duì)列。在真正睡眠之前,我們需要做一些喚醒動作(和reader持鎖過程類似,有可能在掛入等待隊(duì)列的時(shí)候,臨界區(qū)線程恰好離開,變成空鎖),具體邏輯如下:

如果我們是等待隊(duì)列的top waiter(等待隊(duì)列從空變?yōu)榉强眨?,那么需要設(shè)定RWSEM_FLAG_WAITERS標(biāo)記,直接進(jìn)入后續(xù)阻塞邏輯。如果不是,那么邏輯要復(fù)雜點(diǎn),需要掃描一下之前掛入隊(duì)列的任務(wù),看看是否需要喚醒。
如果是writer持鎖,那么不需要任何喚醒動作,畢竟writer是排他的
如果是空鎖狀態(tài),我們需要喚醒top waiter(RWSEM_WAKE_ANY,top writer或者reader們)。你可能會疑問:為何空鎖還要喚醒等待隊(duì)列的線程?當(dāng)前線程快馬加鞭去持鎖不就OK了嗎?這主要是和handoff邏輯相關(guān),這時(shí)候更應(yīng)該持鎖的是等待隊(duì)列中設(shè)置了handoff的那個(gè)waiter,而不是當(dāng)前writer。如果是reader在臨界區(qū)內(nèi),那么,我們將喚醒本等待隊(duì)列頭部的所有reader(RWSEM_WAKE_READERS)。
上面僅僅是標(biāo)記喚醒者,這里的代碼段完成具體的喚醒動作
下面進(jìn)入具體writer的阻塞過程:

調(diào)用rwsem_try_write_lock試圖持鎖,如果成功持鎖則退出循環(huán),不再阻塞。有兩個(gè)邏輯路徑會路過這里。一個(gè)是線程持鎖失敗進(jìn)入這里,另外一個(gè)是阻塞后被喚醒試圖持鎖。
有pending的信號,異常路徑退出
持鎖失敗但是設(shè)置了handoff,那么該線程對owner進(jìn)行自旋等待,以便加快鎖的傳遞。
進(jìn)入阻塞狀態(tài)
喚醒之后,重新試圖持鎖。Writer和reader不一樣,writer是喚醒之后自己再通過rwsem_try_write_lock試圖持鎖,而reader是在喚醒路徑上持鎖。
rwsem_try_write_lock代碼如下:

如果已經(jīng)設(shè)置了handoff,并且自己不是top waiter(top waiter才是鎖要遞交的對象),返回false,持鎖失敗。如果是top waiter,那么就設(shè)置handoff_set,標(biāo)記自己就是鎖遞交的目標(biāo)任務(wù)。
如果當(dāng)前rwsem已經(jīng)有了owner,那么說明該鎖被偷走了。在適當(dāng)?shù)臈l件下(等待超時(shí))設(shè)置handoff標(biāo)記,防止后續(xù)繼續(xù)被搶。如果已經(jīng)設(shè)置了handoff就不必重復(fù)設(shè)置了。
如果當(dāng)前rwsem沒有owner,則持鎖成功,清除handoff標(biāo)記并根據(jù)情況設(shè)置waiter標(biāo)記。
通過原子操作來持鎖,成功操作后退出循環(huán),否則是有其他線程插入,需要重復(fù)上面的邏輯。

至此我們要不獲取了鎖并清除了handoff bit(B邏輯塊),或者沒有獲取鎖,僅僅是設(shè)置了handoff bit(A邏輯塊)。
九、釋放寫鎖
除了清除了owner task成員,其他邏輯和釋放讀鎖類似,不再贅述。
十、樂觀自旋的條件
只有writer在進(jìn)入慢速路徑的時(shí)候才會進(jìn)行樂觀自旋,而rwsem_can_spin_on_owner函數(shù)用來判斷writer是否可以樂觀自旋:

本cpu上需要reschedule,還自旋個(gè)毛線,趕緊去睡眠也順便觸發(fā)一次調(diào)度
讀取sem->owner,標(biāo)記部分保存在flags臨時(shí)變量中,任務(wù)指針保存在owner中
如果該rwsem已經(jīng)禁止了對應(yīng)的nonspinnable標(biāo)志,那么肯定是不能樂觀自旋了。如果當(dāng)前rwsem沒有禁止,那么需要看看owner的狀態(tài)。這里需要特別說明的是:為了方便debug,我們在釋放讀鎖的時(shí)候并不會清除owner task。也就是說,對于reader而言,owner中的task信息是最后進(jìn)入臨界區(qū)的那個(gè)reader,僅此而已,實(shí)際這個(gè)task可能已經(jīng)離開臨界區(qū),甚至已經(jīng)銷毀都有可能。所以,如果rwsem是reader擁有,那么其實(shí)判斷owner是否在cpu上運(yùn)行是沒有意義的,因此owner是reader的話是允許進(jìn)行樂觀自旋的(ret的缺省值是true),通過超時(shí)來控制自旋的退出。如果rwsem是writer擁有,那么owner的的確確是正在持鎖的線程,如果該線程沒有在CPU上運(yùn)行(不能很快離開臨界區(qū)),那么也不能樂觀自旋。
十一、rwsem_spin_on_owner
函數(shù)rwsem_spin_on_owner的功能是對rwsem的owner task進(jìn)行樂觀自旋(即不斷輪詢其狀態(tài),僅writer有效),詳細(xì)的代碼邏輯如下:

在自旋之前,首先要獲得初始的狀態(tài)(owner task指針以及2-bit LSB flag),當(dāng)這些狀態(tài)發(fā)生變化才好退出自旋。
rwsem_owner_state函數(shù)會根據(jù)當(dāng)前的owner task和flag判斷當(dāng)前的owner state。owner state的狀態(tài)總結(jié)如下:

只有明確的知道當(dāng)前rwsem的owner是某個(gè)writer線程且沒有禁止自旋的時(shí)候才開啟下面的自旋過程。對于其他情況,例如reader owned的場景,我們不需要spin on owner,直接返回。
C.只要owner task或者flag其一發(fā)生變化,這里就會停止輪詢,同時(shí)也會返回當(dāng)前的狀態(tài),說明停止自旋的原因。例如當(dāng)owner task(一定是writer)離開臨界區(qū)的時(shí)候會清空rwsem的owner域(owner task和flag會清零),這時(shí)候自旋的writer會停止自旋,到外層函數(shù)會去試圖持鎖。當(dāng)然也有可能是其他自旋writer搶到了鎖,owner task從A切到B。無論那種情況,統(tǒng)一終止對owner的自旋。
D.如果當(dāng)前cpu需要reschedule或者owner task沒有正在運(yùn)行,那么也需要停止自旋
十二、Writer的樂觀自旋
和mutex的樂觀自旋的概念是類似的,想要進(jìn)行rwsem的樂觀自旋,首先要獲取osq鎖,只有獲得了osq lock才能進(jìn)入rwsem的樂觀自旋,否則自旋在per cpu的mcs lock上。Writer通過rwsem_optimistic_spin完成整個(gè)樂觀自旋的過程。對于writer owned場景,自旋發(fā)生在rwsem_spin_on_owner中,上一節(jié)已經(jīng)描述了,這里我們主要看reader owned的情況,這時(shí)候通過for loop不斷自旋去持鎖:


對于rwsem,只有writer-owned場景能清楚的知道owner task是哪一個(gè)。因此,如果是writer-owned場景,會在rwsem_spin_on_owner函數(shù)進(jìn)行自旋。對于非writer-owned場景(reader-owned場景或者禁止了樂觀自旋),在rwsem_spin_on_owner函數(shù)中會直接返回。從rwsem_spin_on_owner函數(shù)返回會給出owner state,如果需要退出樂觀自旋,那么這里break掉,自旋失敗,下面就準(zhǔn)備掛入等待隊(duì)列了。
每次退出rwsem_spin_on_owner并且沒有要退出自旋的時(shí)候,都試著去獲取rwsem,如果持鎖成功那么退出樂觀自旋。
C和D是對reader-owned場景的處理。每次rwsem的owner state發(fā)生變化(從non-reader變成reader-owned狀態(tài))時(shí)都會重新初始化 rspin_threshold。
Owner state沒有發(fā)生變化,那么當(dāng)前試圖持鎖的writer可以進(jìn)行樂觀自旋,但是需要有一個(gè)度,畢竟rwsem的臨界區(qū)內(nèi)可能有多個(gè)reader線程,這有可能使得writer樂觀自旋很長時(shí)間。設(shè)置自旋門限閾值的公式是Spinning threshold = (10 + nr_readers/2)us,最大25us(30 reader)。一旦自旋超期,那么將調(diào)用rwsem_set_nonspinnable禁止樂觀自旋。
對于writer-owned場景,need_resched在函數(shù)rwsem_spin_on_owner中完成,對于reader-owned場景,也是需要檢查owner task所在cpu的resched情況。畢竟當(dāng)前任務(wù)如果有調(diào)度需求,無論reader持鎖還是writer持鎖場景都要停止自旋。
在reader-owned場景中,由于無法判定臨界區(qū)reader們的執(zhí)行狀態(tài),因此rt線程的樂觀自旋需要更加的謹(jǐn)慎,畢竟有可能自旋的rt線程和臨界區(qū)的reader在一個(gè)CPU上從而導(dǎo)致活鎖現(xiàn)象。當(dāng)然也不能禁止rt線程的自旋,畢竟在臨界區(qū)為空的情況下,rt自旋會有一定的收益的。允許rt線程自旋的場景有兩個(gè):
lock owner正在釋放鎖,sem->owner被清除但是鎖還沒有釋放。
鎖是空閑的并且sem->owner已清除,但是在我們嘗試獲取鎖之前另一個(gè)任務(wù)剛剛進(jìn)入并獲取了鎖(例如一個(gè)自旋的writer先于我們進(jìn)入臨界區(qū))。
十三、關(guān)于handoff
設(shè)置handoff標(biāo)記
設(shè)置handoff往往是發(fā)生在喚醒持鎖階段。對于等待隊(duì)列的writer,喚醒之后要調(diào)度執(zhí)行后才去持鎖,這是一個(gè)長路徑,很可能被其他的write或者reader把鎖搶走。喚醒等待隊(duì)列中的reader們有點(diǎn)不一樣,在喚醒路徑上就會從這一組待喚醒的reader們選出一個(gè)代表(一般是top waiter)去持鎖,然后再一個(gè)個(gè)的喚醒。在這個(gè)reader代表線程持鎖的時(shí)候也有可能由于writer偷鎖而失?。╮eader雖然也會偷鎖,但是偷鎖的reader也會喚醒等待隊(duì)列的reader們,完成top waiter未完成的工作)。無論是reader還是writer,如果喚醒后持鎖失敗,并且等待時(shí)間已經(jīng)超過了RWSEM_WAIT_TIMEOUT,這時(shí)候就會設(shè)置handoff bit,防止等待隊(duì)列的waiter餓死。具體設(shè)置handoff bit的場景如下:

2.清除handoff標(biāo)記
標(biāo)記了hand off之后,快速路徑、樂觀偷鎖(reader)、樂觀自旋(writer)都無法完成持鎖,鎖最終會遞交給top waiter的線程,完成持鎖。一旦完成持鎖,handoff標(biāo)記就會被清除。具體清除handoff bit的場景包括:
3.確保鎖的所有權(quán)遞交給top waiter
十四、結(jié)論
標(biāo)準(zhǔn)linux內(nèi)核的讀寫鎖是在公平性、吞吐量和延遲選擇了比較均衡的策略,這樣的策略在手機(jī)平臺上(特別是重載場景下)不能算是“優(yōu)秀”,只能是合格吧。實(shí)際上,在手機(jī)用戶交互場景中,我們更期望是確保用戶體驗(yàn)相關(guān)線程的持鎖時(shí)延,同時(shí)兼顧吞吐量。在這樣的背景下,OPPO內(nèi)核團(tuán)隊(duì)對linux中的讀寫鎖進(jìn)行了優(yōu)化,下一次有機(jī)會可以分享我們在讀寫鎖的持鎖時(shí)延方面做的改進(jìn)。
原文作者:內(nèi)核工匠
