大話Linux內核中鎖機制之原子操作、自旋鎖

很多人會問這樣的問題,Linux內核中提供了各式各樣的同步鎖機制到底有何作用?追根到底其實是由於操作系統中存在多進程對共享資源的併發訪問,從而引起了進程間的競態。這其中包括了我們所熟知的SMP系統,多核間的相互競爭資源,單CPU之間的相互競爭,中斷和進程間的相互搶佔等諸多問題。

通常情況下,如圖1所示,對於一段程序,我們的理想是總是美好的,希望它能夠這樣執行:進程1先對臨界區完成操作,然後進程2再去操作臨界區。但是往往現實總是殘酷的,進程1在執行過程中,進程2很可能在此插入一腳,導致兩個進程同時對臨界區進行讀寫訪問,讀是沒有問題,但寫的話問題就大了。這樣的話,得到的結果往往不是我們想要的。

大話Linux內核中鎖機制之原子操作、自旋鎖

 一個簡單的例子

因此,我們需要一些解決方法,在Linux內核中它提供瞭如下幾種鎖機制,供用戶在針對不同情況分別或配合使用,包括:原子操作、自旋鎖、內存屏障、讀寫自旋鎖、順序鎖、信號量、讀寫信號量、完成量、RCU機制、BKL(大內核鎖)等等,下面筆者將分五篇博文一一討論這些鎖機制。另外,本文所涉及的關於Linux內核源碼採用版本爲:Linux 3.3.1OK,讓我們首先討論有關原子操作和自旋鎖的相關內容吧。

一、原子操作

所謂的原子操作即是保證指令以原子的方式執行,它在執行過程中不被打斷。它包括了原子整數操作和原子位操作,在內核中分別定義於include\linux\types.harch\x86\include\asm\bitops.h。通常瞭解一個東西,我們是先了解它怎麼用的,因此,我們先來看看內核提供給用戶的一些接口函數。對於整數原子操作函數,如下圖1.1所示,下述有關加法的操作在內核中均有相應的減法操作。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖1.1      內核中的整數原子操作函數

如圖1.2展示的是內核中提供的一些主要位原子操作函數。同時內核還提供了一組與上述操作對應的非原子位操作函數,名字前多兩下劃線。由於不保證原子性,因此速度可能執行更快。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖1.2      內核中的位原子操作函數

下面筆者展示一個關於原子操作的具體例子,請注意加粗部分的內容,它的作用是實現了設備只能被一個進程打開。配合註釋的內容,應該不難理解,如圖1.3所示。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖1.3      原子操作示例程序

下面給出筆者在討論關於原子操作的認爲的一些比較重要的內容:1、原子操作在不同體系架構實現的方法不同,基本採用彙編實現;2、上述的整數原子函數集僅針對32位,內核中關於64位有另一套函數。3、對於SMP系統,內核還提供了local_t數據類型,實現對單個CPU的整數原子操作,接口函數僅將atomic_替換成local_即可,具體的定義可參見arch/x86/include/asm/local.h中定義。

接下來看它的實現核心,如圖1.4所示。鑑於篇幅的限制,中間關於SMP的彙編操作在這裏省去,感興趣的讀者可參見具體的內核源碼。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖1.4      原子操作的實現核心

可以看到對於SMP系統,它的實現核心是lock指令,而對於單CPU系統來說,則退化爲空操作,因爲對於單CPU來說,在某程序執行期間,不可能有其它CPU來中斷它的執行,因此,實際上,非SMP系統中的原子操作是沒有必要存在的。下面討論SMP系統。討論前,先了解x86中的lock指令。lock指令是一種前綴,它可與其他指令聯合,用來維持總線的鎖存信號直到與其聯合的指令執行完爲止。當CPU與其他處理機協同工作時,該指令可避免破壞有用信息。它對中斷沒有任何影響,因爲中斷只能在指令之間產生。lock前綴的真正作用是保持對系統總線的控制,直到整條指令執行完畢。

瞭解完lock指令的作用後,對於原子操作採用lock指令的原因就已經很明顯。但需注意:lock指令只是針對自身CPU進行處理。lock指令在執行中佔用CPU資源,從硬件上考慮,多核之間要負責相互通信,要讓某個核的修改被其他核發現,因此lock指令的過多使用必然降低系統的性能。

至此,關於原子操作的內容基本上討論到這裏。總結一下,對於原子操作,它的優點就是簡單,但它的缺點也很明瞭,即是隻能作計數操作,保護的東西太少,從它所提供的接口函數即可看出。

二、自旋鎖

接下來筆者將討論關於自旋鎖的內容。它的定義可表述如下:某個進程在試圖加鎖的時候,若當前鎖已經處於鎖定狀態,試圖加鎖進程就進行不斷的旋轉,用一個死循環測試鎖的狀態,直到成功的獲得鎖。它在內核include\linux\spinlock_types.h中定義,核心的結構體及成員如圖2.1所示。大話Linux內核中鎖機制之原子操作、自旋鎖

圖2.1      自旋鎖核心結構體及成員

下面首先看下自旋鎖提供了哪些函數,依次定義include\linux\spinlock.h文件中,部分函數如圖2.2所示。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖2.2      自旋鎖提供的部分接口函數

同樣配合的看個例子,這個例子其實就是對device_count變量的保護,例子如圖2.3所示,同樣需注意加粗部分。仔細研究這個例子對於後續瞭解順序鎖有很大幫助,到時讀者便會發現它其實是順序鎖的核心實現理念。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖2.3      自旋鎖示例程序

上面關於自旋鎖的例子應該不難理解,下面讓我們深入自旋鎖加解鎖的核心源碼,進一步來看下它到底是怎麼實現的。首先,對於單CPU來說,它的機制實際上就是禁止和使能搶佔,下圖展示的是自旋鎖加鎖和解鎖在內核中層層迭代的源碼,特別注意加粗部分內容。深入琢磨下去,實際上這裏牽扯到一個引用計數器的概念。它是內存管理的一個技巧,可以看做C/C++中的一種垃圾回收機制,具體的內容讀者可以去了解,這裏不再細說。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖2.4      自旋鎖單CPU的加鎖函數實現核心

以上展示的是一個內核加鎖函數的源碼實現的過程,實際上對於解鎖也是這麼一個過程。如圖2.5所示。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖2.5      自旋鎖針對單CPU的實現源碼

總結來說,其實對於單CPU來說,其實就是很簡單的內容,對於CPU存在內核搶佔機制的,將禁止內核搶佔,否則,退化爲空操作。

對於SMP系統來說,它除了簡單的禁止或使能本CPU的搶佔機制外,還做了一些另外的操作。通過源碼的搜索,我們可以發現它的實現核心其實是圖2.6中所展示的兩個函數,採用AT&T彙編實現。看的挺複雜,但實際上分析起來還是很簡單的。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖2.6      自旋鎖針對SMP系統的實現源碼

在這裏,我們可以看到它真正實現了對於多進程的之間某個進程自旋的情況,看源碼前先記住幾條指令:”xaddw”, ”cmpb”, “movb”, “incb”,其中xaddw表示先交換源操作數和目的操作數的數值,然後兩個操作數再按字求和,最終結果保存在目的寄存器中;”cmpb”, “movb”, “incb”較爲簡單,後續的bbyte)後綴表示按字節執行這條指令。同時注意在Linux內核中,採用的AT&T彙編格式,指令操作數的順序是先源後目的,而不是x86彙編中的先目的後源,如圖2.6中的“xaddw”彙編指令則是%1所代表的寄存器爲目的寄存器,即lock->slock變量。下面我們看下具體的執行過程,其中P1P2表示系統中的兩個不同的進程,如圖2.7所描述。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖2.7      自旋鎖內核的執行過程

到這裏,讀者應該明白了到底自旋鎖是怎麼實現自旋的。注意:對於“xaddw”它實際上完成了三條指令的事,爲了防止被這個過程被打斷,所以加了LOCK_PREFIX宏,在前面的原子操作我們也看到了LOCK_PREFIX宏實際上是針對lock指令的包裝,當然是針對SMP系統。

當然,上述給出的源碼是最大隻支持256個處理器的情況,對於操作256個處理器的時候,內核中還有一套函數去處理,感興趣的可以去研究下。可能分析完源碼後有人會提出這個的疑問:如果P2P3都在等待自旋鎖,Linux系統如何保證能夠正確的順序執行呢?其實,這個在源碼中已經體現出來了,實際上,考慮slcok的值,我們可以觀察到它實際上已經保證了後續在等待自旋鎖的進程的順序執行性,比如上述分析過程中我們得到P2slcok=0x0201,假如P1還未釋放,P3又來申請自旋鎖,這時候,內核經過計算得到P3slcok=0x0301。進而繼續分析源碼,我們可知P3要想執行,必須得到P2執行完畢後(此時slcok=0x0302),方可有條件(slcok經過低8位加1後等於0x0303)申請到自旋鎖,從而無形中保證了申請自旋鎖進程的順序執行性。由於slock只有高8位用於保證順序性,所以這段源碼最大隻支持256個處理器同時申請自旋鎖。

另外說到自旋鎖,不得不提自旋鎖和中斷之間的關係,首先看一個雙重請求的例子,假如某一進程在臨界區正在執行,然而這時候,突然有一箇中斷來打斷了它,於是,在臨界區觸發了中斷處理程序,若中斷處理程序裏面也有包含申請自旋鎖的操作,這將造成一個大問題,即所謂的雙重請求的例子。如下圖2.8所示。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖2.8      雙重請求圖例

當然內核當然考慮了此種情況,於是在自旋鎖中就有了關於關閉中斷的函數:spin_lock_irq()spin_unlock_irq()。如圖2.9代碼所示的函數,但正如圖中所提的那樣,這兩個函數的使用是有條件的,它要求中斷在加鎖前必須是激活的。假如現在有一個進程,它的中斷本來就是關閉的,但是你通過這個鎖的過程後中斷變成開了,這不就又造成問題了嗎。考慮此種情況,內核中提出瞭如圖2.10所展示的自旋鎖函數:spin_lock_irqsave()spin_unlock_irqrestore()。可能讀者在此會有些許疑惑,既然flags是作爲spin_lock_irqsave()的輸出參數,理論上應當要有”&”符號纔對,這裏卻沒有。事實上,spin_lock_irqsave()flags不用“&”是因爲這個函數是以宏形式定義的,一直嵌套,最終到arch\x86\include\asm\irqflags.h下,感興趣的搜索到本源。一般來說,若是輸出參數不帶”&”符號的函數,幾乎都是採用宏形式的定義的,這點需注意。

大話Linux內核中鎖機制之原子操作、自旋鎖

       圖2.9  spin_lock_irq()的使用實例                            2.10  spin_lock_irqsave()的使用實例

關於中斷下半部的問題,還有幾點需要說明。首先如果存在中斷下半部和某個進程之間存在數據共享的問題,那就需要注意一下,因爲中斷下半部可搶佔進程上下文,因此下半部和進程之間存在臨界區時除了加鎖,還需要禁止下半部執行。內核中提供的包括函數spin_lock_bh() spin_unlock_bh()即是實現禁止或使能下半部。同時關於自旋鎖中的中斷還有兩點要說明: 若中斷處理程序與中斷下半部共享數據,則對數據區加鎖的同時也要禁止中斷,因爲中斷也是可搶佔中斷下半部。 若數據被軟中斷共享,也需要加鎖,因爲在不同處理器上存在軟中斷同時執行問題。

OK!上述討論了自旋鎖多方面的內容,下面是對自旋鎖一番總結。首先從前面的實現機制上,讀者可以看到自旋鎖主要針對SMP系統,而且它們存在搶佔情況。對於單CPU系統,自旋鎖的實現則退化爲空操作。其次自旋鎖是忙等待,要求臨界區執行時間短。再次自旋鎖可能引發死鎖,引發的情況有:自旋鎖的遞歸調用(雙重請求)或獲得自旋鎖後不釋放,最終將導致系統不可用。最後一點則是若自旋鎖在鎖定期間調用可能引發睡眠的函數,如kmalloc()等,從此一睡不醒”(因爲這時候“無人”負責喚醒,主要原因是連中斷都被關閉了,從此在無人喚醒,除非重啓系統)。這一點需特別注意。

至此,關於自旋鎖部分的內容到此討論結束,讓我們跳出來觀全局,如圖2.11所示。其實抽象的看自旋鎖的實現機制還是挺簡單的,而它提供的關於中斷的一系列函數則爲自旋鎖提供了安全帶的作用。

大話Linux內核中鎖機制之原子操作、自旋鎖

圖2.11    跳出來觀全局自旋鎖實現機制

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

轉載請註明出處:http://blog.sina.com.cn/huangjiadong19880706


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