帶您進入內核開發的大門 | 自旋鎖的使用

2020-12-22 itworld123

自旋鎖應該是Linux內核中使用最多的鎖了,其它鎖很多都依賴自旋鎖實現。我們今天先介紹自旋鎖,後續在介紹Linux內核的其它互斥機制。從字面意義上我們可以看出,自旋鎖處於自旋的狀態,什麼是自旋狀態呢?就是原地打轉,不停的循環。下面幾點是自旋鎖的特點:

自旋鎖(spin lock)是一種死等的鎖機制。在作業系統中,為了防止資源被兩個線程同時訪問,導致數據不一致,通常需要一種鎖機制。通常有有兩種實現方式:一個是死等,另外一個是掛起當前進程,調度其他進程執行。而自旋鎖則是一種死等的機制,當前的執行thread會不斷的重新嘗試直到獲取鎖進入臨界區。只允許一個thread進入。信號量(semaphore)可以允許多個thread同時進入,而自旋鎖一次只能有一個thread獲取鎖並進入臨界區,其他的thread都是在門口不斷的嘗試。執行時間短。由於自旋鎖死等這種特性,因此它使用在那些代碼不是非常複雜的臨界區(也就是切換線程成本相對於臨界區的訪問成本很高的場景)。如果臨界區執行時間太長,那麼不斷在臨界區門口「死等」的那些thread將會耗費大量的計算資源,顯然是不合適的。可以在中斷上下文執行。由於不睡眠,因此自旋鎖可以在中斷上下文中適用。基本接口介紹

了解了自旋鎖的基本特徵,接下來我們介紹一下自旋鎖的常用接口。對於基本的使用,自旋鎖的使用很簡單,主要涉及3部分內容:

定義一個自旋鎖對臨界區加鎖使用完後解鎖定義一個自旋鎖

定義自旋鎖就好像定義一個變量。下面函數用於定義一個自旋鎖。

自旋鎖加鎖

自旋鎖加鎖就是組織其它線程對相同臨界區的訪問,使用方法也非常簡單。函數原型如下:

自旋鎖解鎖

不多廢話了,下面是函數原型:

試探加鎖

由於自旋鎖在臨界區已經加鎖的情況下會導致其它想進入臨界區的線程處於忙等狀態,這樣會消耗CPU資源。有的時候我們不想這樣,內核又提供了另外一個接口,該接口會首先判斷是否可以進入,如果可以進入則獲取鎖資源,否則返回失敗。

應用示例

我們這裡只是一個非常簡單的示例,在該示例中有2個線程,分別進行對同一個變量的運算。如果沒有保護機制,數據將被計算混亂。通過自旋鎖,使得計算可以依次有序的進行,從而保證數據的正確性。

自旋鎖的實現

自旋鎖的基本原理和使用是比較簡單的,下面我們分析一下自旋鎖的實現。任何機制的出現肯定是為了解決某些問題,我們先看一下自旋鎖是為了解決什麼問題。

大家都知道,在CPU和內存之間包含一級緩存、二級緩存和三級緩存等緩存。原因很簡單,因為CPU訪問緩存的速度更快,將將經常訪問的數據加載到緩存中,可以減少內存訪問的頻率,從而提高計算的性能。CPU、緩存和內存的關係如下圖所示(簡圖,省略了好多內容):

計算機緩存機制

那麼緩存跟我們的自旋鎖又有什麼關係呢?有了緩存之後,就可能出現緩存中數據和內存中數據不一致的情況。一方面是CPU無法直接更改內存中的值,更改操作需要至少讀和寫兩條指令,從而導致改寫操作的非原子性;另一方面是因為緩存是為了提升性能,因此CPU更新了緩存內容後原則上不會馬上更新到內存,否則就失去了緩存的意義。

如2所示,在沒有任何保護機制的情況下,假設在內存中有一個變量val本來值為2,兩個處理器同時對該值進行操作,左邊CPU從內存讀取該值,並進行+1運算後該值為3。而在右側CPU從內存讀取數據,並進行+2運算後結果為4。這樣,兩個CPU更新到內存的時機不同,則內存中的結果就會不同,也就是程序每次運行產生的結果可能是不一樣的。問題的關鍵是CPU同時對內存中的數據進行了讀和運算操作,而該操作本身不具備原子性,也就是分為讀和寫兩步,這樣導致結果非預期。這種情況顯然我們是不能接受的,這種情況下就需要自旋鎖出馬了。自旋鎖的目的就是將對同一個數據的並行運算串行化,也就是你改完後我再改,從而保證結果的一致性。

緩存數據不一致

老版本的實現解析

為了容易理解,我們先看一下老版本(2.6.0)的Linux內核的自旋鎖是如何實現的。我們從自旋鎖的數據結構開始,對於X86體系結構具體定義如下。

typedef struct {volatile unsigned int lock; #ifdef CONFIG_DEBUG_SPINLOCK unsigned magic; #endif} spinlock_t;

忽略掉調試功能的代碼,其實就變的很簡單了,也就是只有一個volatile unsigned int 的變量。volatile關鍵字表示該變量的改變馬上更新到內存,不進行緩存。

在介紹之前我們可以猜測一下,通過這個變量進行標識自旋鎖是否已經加鎖,如果為1則表示已經加鎖,為0則表示沒有加鎖。在沒有加鎖的情況下,線程可以獲得該鎖,然後將變量置為1。其它線程由於發現該值為1,所以只能等待。而當線程解鎖的時候,將該變量設置為0,此時其它變量就可以進行加鎖了。或者將加鎖和未加鎖的標示反過來,也就是0表示加鎖,1表示未加鎖。這個都無所謂,只是一種狀態標識。

實際上Linux內核的實現就是上面我們描述的樣子。我們通過自旋鎖的幾個函數分別看一下其具體實現。

自旋鎖初始化

下面代碼是鎖的初始化代碼(初始化為1),可以看出其實就是將結構體的變量初始化為1。

自旋鎖加鎖

接下來我們看一下自旋鎖加鎖的具體實現。外面的do-while是Linux內核宏定義的慣用手法,這裡知道就可以了。可以看到這裡一共調用了3個函數,具體每個函數的作用在代碼中介紹。

下面代碼是試圖加鎖函數(_raw_spin_trylock)的代碼。這裡面是通過內嵌彙編的方式實現的。這段彙編的大概含義是對lock->lock與0值進行交換,並將lock->lock存儲到oldval中,如果oldval為正值,就返回1,

否則返回0。這裡面lock->lock就是存儲鎖數據的變量,前面我們介紹過,其初始值(未加鎖狀態)為1。因此,如果該鎖處於為加鎖狀態時,交換後oldval中的值為1,此時會加鎖成功,函數的返回值為真。如果lock->lock中的值為0,交換後oldval中的值為0,那此時加鎖就失敗,函數的返回值為假。

加鎖失敗的情況下,spin_lock函數會進入__preempt_spin_lock函數的邏輯。粗看代碼,這裡又有一個do-while循環,這個循環其實就是自旋鎖自旋的地方。可以看到while裡面試圖對鎖進行加鎖操作,如果加鎖成功會退出循環,而如果失敗則繼續循環。

總結一下,自旋鎖的實現其實就是通過原子彙編語言實現了對自旋鎖變量的賦值操作,並且在無法加鎖的情況下自旋等待。

自旋鎖解鎖

自旋鎖解鎖的流程要稍微簡單一些。下面將相關的幾部分代碼放到一起了。具體調用過程是spin_unlock->_raw_spin_unlock->spin_unlock_string。解鎖完成後需要開啟搶佔。

新版本的實現解析

有可能存在多個線程同時爭搶自旋鎖的情況,但老版本的實現無法保證前搶的一定能得到自旋鎖。因此新版本(2.6.25以後)的實現排隊功能,也就是先到先得。我們照例先從自旋鎖的數據結構開始,對於X86體系結構具體定義如下(這裡是最終定義,前面的嵌套調用關係這裡沒有介紹,大家自行看一下)。這裡__ticketpair_t和__ticket_t就是無符號整型數,大家可以自行看一下代碼。

這裡實際上還可以理解為一個整數兩部分(或者兩個整數)。有些人可能會好奇,通過整數就可以實現排隊自旋鎖了?是的,就是這麼簡單,當然還需要一些算法,程序本身就是數據結構和算法的集合嘛!先看一下這個結構體是怎麼用的。

自旋鎖數據結構示意圖

其實排隊自旋鎖的原理很簡單,就是判斷head和tail兩個變量的值,如果相等則為未加鎖,否則說明已經處於加鎖狀態。自旋鎖初始化將head和tail都設置為0。當有線程加鎖的時候,首先判斷head和tail是否相等,相等就將tail加1,此時加鎖成功。如果兩者不相等則表示已經有其它線程加鎖,此時只能等待。

自旋鎖加鎖

這裡面使用了一個名為cmpxchg的函數,該函數完成的功能是:將old和ptr指向的內容比較,如果相等,則將new寫入到ptr中,返回old,如果不相等,則返回ptr指向的內容。這裡請自行閱讀代碼,本文就不貼代碼了。

嘗試加鎖實現

自旋鎖解鎖實現

自旋鎖解鎖實現要簡單的多,下面是X86的實現。僅僅是將head進行加1操作。需要注意的是必須保證該操作的原子性。這裡就不多廢話了。

能力增強

除了上面介紹的基本的加鎖和解鎖的接口外,Linux內核還實現增強的功能。比如可以在中斷環境中使用的自旋鎖和下半部使用的自旋鎖等等。下面是自旋鎖所涉及的接口列表。

我們以支持中斷的自旋鎖為例進行說明。其實支持中斷的自旋鎖內部僅僅增加了禁止本地中斷的函數調用。具體為什麼加這個,大家可以自行思考一下,原因是比較清楚的,本文不再贅述。

關於自旋鎖的內容還很多,比如讀寫自旋鎖,單核自旋鎖的特殊處理等等。由於篇幅問題,本文不再贅述。後續我們在另起文章進行闡述。

相關焦點

  • Linux內核同步機制的自旋鎖原理及綜合應用實例
    一、自旋鎖本文引用地址:http://www.eepw.com.cn/article/149309.htm自旋鎖鎖是專為防止多處理器並發而引入的一種鎖,它在內核中大量應用於中斷處理等部分(對於單處理器來說,防止中斷處理中的並發可簡單採用關閉中斷的方式,即在標誌寄存器中關閉/打開中斷標誌位,不需要自旋鎖)。
  • 樂觀鎖&悲觀鎖&自旋鎖
    而且CAS避免了請求作業系統來裁定鎖的問題,不需要進入內核,不需要切換線程,操作自旋機率較少,因此可以獲得更高的性能不用麻煩作業系統,直接在CPU內部就搞定了五、自旋鎖何謂自旋鎖?它是為實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。
  • Linux 自旋鎖spinlock,教你如何把ubuntu弄死鎖
    自旋鎖的優點自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是active的;不會使線程進入阻塞狀態,減少了不必要的上下文切換,執行速度快。非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候需要從內核態恢復,需要線程上下文切換。
  • 改善Linux內核實時性方法的研究與實現
    但是只有可調度的進程才可這麼做,如果中斷處理代碼仍然使用原來的自旋鎖,那麼互斥鎖取代自旋鎖改進內核實時性的努力將大打折扣。  線程化的中斷管理可以有效的解決這些問題。中斷線程化後,中斷將作為內核線程運行而且賦予不同的實時優先級,實時任務可以有比中斷線程更高的優先級,這樣,實時任務就可以作為最高優先級的執行單元來運行,即使在嚴重負載下仍有實時性保證。
  • Ai聘網JAVA面試之深入理解自旋鎖
    獲取鎖的線程一直處於活躍狀態,但是並沒有執行任何有效的任務,使用這種鎖會造成busy-waiting。它是為實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執行單元獲得鎖。但是兩者在調度機制上略有不同。
  • 源碼時代java小課堂之線程鎖之自旋鎖
    java中定義了非常多的鎖,很多同學面試對於鎖,感覺非常茫然,於是源碼的老師決定,將這些鎖拆分開來注意分析講解,這篇我們先聊聊自旋鎖1.自旋鎖是基於CAS實現2. synchronized重量級鎖是基於系統內核3.
  • Linux的共享內存與自旋鎖
    共享的數據屬於多個進程的共同資源,類似於多線程的數據一樣,也要使用鎖來保護。線程是使用pthread_mutex來保護,進程可以使用fcntl()對共享內存的文件描述符加鎖,也可以把共享內存的前4位元組當作自旋鎖spinlock來使用。
  • Java 中15種鎖的介紹:公平鎖,可重入鎖,獨享鎖,互斥鎖,樂觀鎖,分段鎖,自旋鎖等等
    無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執行單元獲得鎖。但是兩者在調度機制上略有不同。對於互斥鎖,如果資源已經被佔用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那裡看是否該自旋鎖的保持者已經釋放了鎖,」自旋」一詞就是因此而得名。
  • 使用Kdump檢查Linux內核崩潰
    Initramfs 是可選的;例如,當內核使用 CONFIG_INITRAMFS_SOURCE 編譯時,您不需要它。通常,從第一個 initramfs 中保存一個不一樣的捕獲 initramfs,因為在捕獲 initramfs 中自動執行 vmcore 的副本能獲得更好的效果。
  • 如何理解互斥鎖、條件變量、讀寫鎖以及自旋鎖?
    得到鎖的線程可以進入臨界區執行代碼。但和無數個好似『故意把簡單概念複雜化』的計算機術語一樣,自旋鎖的本質簡單的難以置信。要了解自旋鎖,首先了解自旋。什麼是自旋(spin)呢?更為通俗的一個詞是『忙等待』(busy waiting)。最最通俗的一個理解,其實就是死循環……。單看使用方法和使用互斥量的代碼是差不多的。只不過自旋鎖不會引起線程休眠。當共享資源的狀態不滿足的時候,自旋鎖會不停地循環檢測狀態。
  • Ubuntu中升級Linux內核
    隨著8月底Linux內核4.2發布,經過八個RC候選版後,Linux Kernel 4.2正式版成為最為重大的版本之一,單是新代碼就增加了100萬行,同時還移除了大約25萬行老舊代碼。  新內核4.2有哪些改進:  ●重寫英特爾的x86彙編代碼  ●支持新的ARM板和SoC  ●對F2FS的per-file加密  ●AMD GPU內核DRM驅動程序  ●對Radeon DRM驅動的VCE1視頻編碼支持  ●初步支持英特爾Broxton Atom SoC
  • Linux 內核學習:環境搭建和內核編譯
    選擇"advanced option"-->"expert install"進入專家安裝模式(現在專家那麼多,咱也來冒充一回吧!!)。彈出了一列選項,其實裡面有很多選項都沒必要使用的,大部分都可以在系統安裝完成後進行設置。
  • 關鍵的Windows內核數據結構一覽(上)
    在翻譯fuzzySecurity的Windows exploit開發系列教程第十部分時,覓得此文,甚佳,不敢獨酌。在我們的「Windows internals and debugging」課程中,學生經常會問我們這樣的一些問題:Windows內核使用哪種數據結構來實現互斥量?本文試圖通過描述Windows內核和設備驅動所使用的一些關鍵數據結構來回答這樣的問題。
  • 淺談嵌入式MCU軟體開發之利用Cortex-M內核的DWT模塊的內核周期計數器測量S32K14xMCU的應用代碼執行時間
    數據監測點(即數據斷點),讓CPU內核進入調試狀態(debug state)或者產生一個調試監測器異常(DebugMonitor exception);數據跟蹤(data tracing);與其他調試資源(比如ETM模塊)一起使用產生調試信號;內核程序計數器值跟蹤(PC value tracing)內核周期計數匹配(cycle
  • 英特爾終於醒了:招工程師開發全新內核
    鑑於AMD的最新產品的競爭力,英特爾顯然開始強化設計能力,有消息稱英特爾正在招聘工程師開發「新一代內核」。英特爾終於醒了(圖片來自baidu)在新發布的招聘信息中,英特爾為位於俄勒岡州希爾斯伯勒的團隊招聘一名工程師,該團隊的使命是開發「新一代內核」。
  • Linux內核工程師是怎麼步入內核殿堂的?
    大家看到的高手都是聰明人,他們不做內核開發做其他工作也會是很牛X,我們這些普通人之所以說要堅持和時間,是因為我們不管做什麼事情,能有口飽飯吃都得靠堅持和時間。內核開發也是寫軟體代碼,和其他的軟體代碼開發工作沒啥區別:創造有人用的代碼。這就是一份工作,把工作做好,領薪水買米麵油氣交房前。可能不同的是內核軟體沒有用戶界面,普通用戶沒法直接看到效果。
  • Java知識進階-程式設計師必懂的自旋鎖TicketLock原理-知識鋪
    解決公平性: 解決之前普通CAS自旋鎖(前面講解過,手寫一個自旋鎖)等待線程會不停自旋,隨機獲取鎖,導致先到的線程反而獲取不到鎖的公平性。關鍵點3: AtomicInteger 保準原子性關鍵之處,這裡使用的是它的getAndIncrement函數,這個函數特點返回的是添加前的排隊號,而又把後期的排隊號給加上了1。
  • Java知識進階-程式設計師鍛鍊自己,手寫自旋鎖-知識鋪
    優點: 減少內核態和用戶態切換的消耗缺點: 不停自旋消耗CPU二、 動手實現簡單自旋鎖2.1 利用 AtomicReference 實現AtomicReference 原子性,底層實現CAS操作,可以對比普通對象的引用
  • Linux內核概述
    但是我希望我的文字略帶微笑著去面對這些代碼,去面對 Linux 森林,然後你從這個森林走出來後,可以明白森林裡的有哪些路,你下一次想帶個妹子進去約會,可以找到屬於自己的旮旯。那我們先簡單說說作業系統,作業系統是面向用戶的,計算機用戶可以使用計算機作業系統來工作,聊天,玩遊戲,我們使用的這些東西都是應用軟體,對應用程式來說,內核就是它的作業系統,這個系統可以為應用程式工作,管理應用程式。