內存屏障、讀寫自旋鎖和順序鎖

轉自: https://www.cnblogs.com/wuchanming/p/3816087.html

在上一篇博文中筆者討論了關於原子操作和自旋鎖的相關內容,本篇博文將繼續鎖機制的討論,包括內存屏障、讀寫自旋鎖以及順序鎖的相關內容。下面首先討論內存屏障的相關內容。

三、內存屏障

不知讀者是是否記得在筆者討論自旋鎖的禁止或使能的時候,提到過一個內存屏障函數。OK,接下來,筆者將討論內存屏障的具體細節內容。我們首先來看下它的概念,Memory Barrier是指編譯器和處理器對代碼進行優化(對讀寫指令進行重新排序)後,導致對內存的寫入操作不能及時的反應到讀操作中(鎖機制無法保證時序正確)。可能讀起來有點繞口,沒關係,我們繼續討論,內容完後,讀者再回來閱讀的時候便會有深層次的體會。在arch\x86\include\asm\system.h中定義。首先針對barrier函數(即在自旋鎖部分先前看到的那個),它主要針對編譯器的優化屏障,但它卻阻止不了CPU重排指令時序。由於CPU存在重排時序等問題,因此僅僅有一個barrier是不夠的,因此,內核還提供瞭如下幾種機制,如下圖3.1所展示的那部分函數。

           大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖

                               圖3.1      內核中的內存屏障函數接口

針對上述內容,值得一提的是,使用這些內存屏障,相當於停用了處理器或編譯器提供的優化機制,顯然這樣的結果必然影響程序的性能。

那麼這些函數如何能做到上述註釋中提到的內容呢?下面我們來看一下它們到底是如何實現的。實現的核心其實是依靠如下指令:① lock指令(已在第一部分中提及);② lfence指令:停止相關流水線,直到lfence之前對內存進行讀取操作的指令全部完成;③ sfence指令:停止相關流水線,直到sfence之前對內存進行寫入操作的指令全部完成;④ mfence指令:停止相關流水線,直到mfence之前對內存進行讀取和寫入操作的指令全部完成。瞭解完它們的核心指令後,再看其在內核中的源碼實現,如圖3.2所示。

大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖

圖3.2      部分內核屏障函數的核心源碼

圖3.2只展示部分函數,但關於smp_rmb()等函數的實現比較複雜,添加一些了針對smp系統特有的保護代碼,但其根本還是調用這些如rmb()等函數。至此,關於內存屏障部分的內容即討論到這裏,雖然內容較少,但其重要性不可忽略,同時,讀者若再回頭去看,去理解內存屏障的概念,是否會有新的收穫呢。

四、讀寫自旋鎖

接下來討論讀寫自旋鎖的實現原理,它其實是自旋鎖的升級版。同樣,我們先看看它能實現的功能:針對寫:最多隻有一個寫進程;針對讀:可以同時有多個讀寫單元。但讀和寫不能同時進行。它於include\linux\rwlock.h下定義,而它提供的函數形式和自旋鎖類似,僅將“spin_”替換成“read_”或“write_”。包括read_lock(lock)和read_unlock(lock)、write_lock(lock)和write_unlock(lock)等。接下來我們來看下它該是如何使用的。而關於它的使用其實很簡單,如圖4.1所示,很好理解,即是在臨界區前後分別加上加解鎖函數即可,這裏便不在細說。

大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖

 圖4.1      讀寫自旋鎖的使用示例

然後我們來討論它的實現核心。事實上,它的實現討論起來也挺簡單,就是有點繞。實現流程與spin_lock幾乎完全一致,唯一不同的是最後調用體系結構相關的函數是arch_read_lock而不是arch_spin_lock(自旋鎖的arch_spin_lock最終調用__ticket_spin_lock())等。

下面分析它的源碼實現。假如現在有進程P1向操作系統申請了讀寫自旋鎖,設讀寫鎖變量A已經被定義,它的初值爲RW_LOCK_UNLOCKED(0x01000000)。配合圖4.2,圖4.3所示的源碼內容,若此時進程P1對它申請讀鎖操作,則read_lock()對讀寫鎖變量A減1,如果相減後A的值結果爲負,則說明此時系統中已經有某個進程(設爲P2)對這個讀寫鎖變量使用寫鎖函數write_lock()上鎖,這時候系統便會持續等待P2對寫鎖的釋放,持續等待過程的源碼實現如圖4.5所示。假如進程P1讀鎖申請成功,若P1在使用讀鎖過程中,存在另一個進程P3申請寫鎖操作。此時write_lock()對讀寫鎖變量A減0x01000000並判斷,如果結果非零,則說明此時鎖變量A已被write_lock()函數上鎖或用read_lock()函數上鎖,進而便跳轉到圖4.6所示的寫鎖失敗的彙編函數,持續等待鎖變量A被進程P1釋放。

大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖圖4.2  讀鎖函數的內核源碼                                          圖4.3  寫鎖函數的內核源碼

下面給出對應的解鎖函數。如圖4.4所示。

大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖

圖4.4      解鎖函數的內核源碼

可以看出解鎖函數中的實現其實簡單的加1和減1操作。下面重點關注加鎖函數中的兩個申請鎖失敗時的兩個的鏈接函數,如圖4.5,圖4.6所示。採用純彙編實現,由於圖4.5所示的源碼和圖4.6所示的源碼內容實現機理實際上均是相同,故這裏僅重點分析圖4.5源碼。圖4.5的源碼搞懂了,圖4.6的源碼自然也不是問題。

在分析源碼前,可能讀者對於源碼中的rep指令等有些許疑問。事實上,rep重複串操作直到cx寄存器的內容爲0爲止。結合的指令有限,只有MOVS、STOS、LODS、INS和OUTS,如rep stosb;等。而rep; nop是一個混合指令,被翻譯成pause指令。分號在此非註釋,AT&T中表示指令的有效定界符。pause指令會向處理器提供一種提示:告訴處理器所執行的代碼序列是一個自旋等待狀態,處理器會根據這個提示而避開內存序列衝突(Intel手冊說明)。

當read_lock()函數申請讀鎖失敗後,首先將rw變量加1(注:rdi實際上代表的是rw,它們之間是通過寄存器來傳遞的,未加1前rw的值爲-1)。緊接着,在執行cmpl指令前rdi的值爲0,於是同立即數1進行比較。如果不相等,則繼續跳轉到“1”標誌位處繼續循環比較,直到相等,因爲相等的時候,說明系統執行了讀鎖的釋放函數,將rw變量加1了,具體可參看read_unlock()函數實現(圖4.4所示)。此時在退出__read_lock_failed函數之前還需將rdi變量即rw變量減1,表示另一進程申請讀鎖成功,從而保證後續申請讀寫鎖的進程的正確性。至此,已將__read_lock_failed函數源碼分析完畢,至於__writed_lock_failed也是類似的思想,讀者可自己類比推理即可理解。

大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖

           圖4.5  讀鎖函數中的跳轉函數                                          圖4.6  寫鎖函數中的跳轉函數

至此,關於讀寫自旋鎖的內容基本討論完畢。此時,讀者若再回頭去看,去理解讀寫自旋鎖的功能,是否有更多新的收穫呢。

五、順序鎖

緊接着筆者將討論順序鎖的內容。它的定義如下:讀執行單元不會被寫單元阻塞,允許讀寫同時進行,但寫單元之間仍然互斥。但是簡單的從定義上看,我們很容易就能發現一個問題:讀操作期間,發生寫操作,怎麼辦?不用擔心,內核必然考慮到了這一點,解決方法是:讀執行單元重新讀取數據,確保得到的數據是完整的。但需注意的是:被保護的資源不能含指針,否則重讀的時候可能出問題(如指針被釋放)。順序鎖在include\linux\seqlock.h文件中定義和實現。首先看下它的結構體實現,如圖5.1所示。

大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖

圖5.1      順序鎖的結構體定義

特別注意結構體中的sequence變量,這個變量是順序鎖核心內容,後續將會解釋原因。然後我們討論一下它的使用示例,如圖5.2所示。示例中的do-while循環實現的就是本節一開始提到的那個問題,若讀操作期間發生了寫操作,則實現重讀,而實現重讀的關鍵就是對sequence變量的有效利用。

大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖

圖5.2      順序鎖的使用示例

下面我們來看一下順序鎖的源碼實現,如圖5.3,圖5.4,圖5.5所示。首先針對寫操作函數,圖5.3所示,可以看到它實際上就是利用自旋鎖是對sequence變量的保護,進程在申請寫鎖的時候,只是簡單的將sequence變量的值增1,這和我們一開始關於自旋鎖的例子特別的類似(讀者可回頭看看)。

大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖

 圖5.3      寫操作函數的內核源碼

對於讀操作函數,依據圖5.4,圖5.5所示。可以看到在申請讀鎖的時候,首先將當前的所的順序,也就是sequence變量的值保存下來(這時候已在do-while循環中),然後在read_seqretry函數中作判斷,檢測當前的讀鎖變量是否發生變化,若發生變化(調用寫鎖函數,sequence變量的值增1),則重新讀,正如圖5.2所給出的示例一般;若不發生變化,則表示申請讀鎖成功。至於源碼中的unlikely()函數,那是內核中提供了關於變量檢測的函數,它的檢測速度比一般的if-else更快,至於爲何快,將又是一套較複雜的內容,感興趣的可以看查相關方面的資料,這裏不再細說。

大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖

圖5.4      讀操作的內核源碼

大話Linux內核中鎖機制之內存屏障、讀寫自旋鎖及順序鎖

圖5.5      讀操作的內核源碼

通過閱讀源碼,可看到它實現了可允許同時讀寫,但寫單元之間仍然互斥。其實,可以回頭看一下先前提到的那個關於順序鎖的示例就會明白的更加透徹。

出於文章篇幅的限制,本篇博文到此結束,後續將會給出《大話Linux內核中鎖機制之信號量、讀寫信號量》,感興趣的讀者可繼續閱讀後一篇博文。由於筆者水平所限,博文中難免有出錯之處,歡迎讀者指出,大家相互討論,共同進步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章