多核操作系統中的自旋鎖-『以XV6 & Linux 為例』

厚臉皮引流

厚臉皮引流

自旋鎖是一個很神奇的東西,一個介于高效和低效之間的一個 『薛定諤』??的互斥機制。

自旋鎖的效率和它的應用場景有很大關系,在實際生產過程中,我們能在很多地方看見它的身影。

比如Linux kernal有挺多地方用到spinlock、 Nginx也有用到spinlock, 但很多時候自旋鎖在很多場景下并不能很好的發揮出它的高效優勢。

究竟什么時候我們應該使用SpinLock?

首先,要注意的是自旋鎖只適用于多核心狀態下(這個多核心指的是當前進程可用的核心數 > 1), 比如說你是一個8核Mac,但是你在一個限制1核的Docker環境中用SpinLock,卵用也沒有!!!

本質上,SpinLock之所以有效就是假定,當前存在另外一個CPU核心正在使用你所需要的資源。CPU只有一個你等也是白等。 (就好像一個癡漢暗戀一個人一樣,劃掉??)

其次,SpinLock之所以在一些場景下很高效是因為旋等消耗的時鐘周期遠小于上下文交換產生的時間。

我們來回顧一下Mutex 睡眠等待的過程。首先是嘗試上一次鎖,如果不行的話就通過調度算法找到一個優先級更高的Thread,然后才是保存寄存器,寫回被修改的數據,然后才是交換上下文。

可以看到這個代價是十分大的,而且交換上下文的代價是要??2的。一般來說,這個代價,在幾千~幾十萬時鐘周期。回顧一下一個4GHz的8代處理器,一個時間周期=0.25ns。交換一次的代價還是挺可觀的。

所以我們使用SpinLock的時候就需要保證我們的臨界區代碼,能夠在這個時鐘周期之類完成所有任務。

所以一般spinLock等待的代碼不會太長,一般幾行(具體需要看芯片和編譯環境),更不可能是I/O等待型的任務。(在XV6中,關于文件系統的操作都單獨使用基于SpinLock的SleepLock)

然后,其實SpinLock更適合系統態,不太適合用戶態。因為你用戶態沒法知道有沒有另外一個CPU在處理你所需要的資源。而且SpinLock并不適合多線程搶占一個資源的場景,比如說開了60個線程搶占一個一個內存資源,這個競爭、等待的代價就是超級大的一個數。

所以,其實自旋鎖的優勢、劣勢都很明顯,怎么來更好的用好它就是程序員??????的事情啦。

SpinLock in XV6

XV6其實是一個很Unix的教學操作系統,通過對XV6代碼的閱讀,我們其實能夠以更少代價來了解Unix是怎么做的。

SpinLock在XV6中定義在<spinlock.h><spinlock.c>兩個文件中,實際上代碼量不過100行,是很好的分析案例。

image

首先, SpinLock類中用了一個unsigned int 來表示是否被上鎖,然后還有lock的名字,被哪個CPU占用,還記錄了系統調用棧(這個實際上就是完全用來調試用的,當然一個健壯的OS需要方方面面考慮到)。

image

當我們需要去獲得這個鎖的時候,它會先去關中斷,再去檢查這個鎖有沒有被當前的CPU占用,然后是一個嘗試上鎖的循環,最后是標記被占用的CPU,記錄系統調用棧。

總的來說思路比較清晰,我們來看一下具體實現細節。

image

pushcli函數是用來實現關中斷過程的一個函數,先會去調用readeflags這個函數來讀取堆棧EFLAGS, 然后調用cli來實現關中斷。
如果是第一個進行關中斷的(嵌套關中斷數是0),則還會去再check一下elfags是不是不等于關中斷常量FL_IF

而readeflags()和cli()這兩個函數都是通過內聯匯編來實現的。

image

readeflags, 就是先去把efalgs寄存器當前內容保存到EFLAGS堆棧中,然后把EFLAGS堆棧中的值給到eflags變量。

image

而cli()就很簡單粗暴的調用cli匯編指令。

image

當我們關中斷之后,會去檢查當前CPU有沒有持有這個SpinLock。

image

如果已經持有這個SpinLock則會退出。

當完成這一系列常規操作之后,才是最關鍵的獲取鎖的步驟。

image

他會去調用xchg()這個內聯匯編函數。會去執行lock; xchgl %0, %1這個匯編代碼。

最關鍵的實際上是xchgl這個指令, 從效果上看xchgl 做的是一個交換兩個變量,并返回第一個變量這個事情。

實際上這個指令,首先是一個原子性的操作,當然,這我們可以理解畢竟是多核狀態,如果有好幾個CPU核心來搶占同一個SpinLock,需要保證互斥性, 需要排他來訪問這塊內存空間。

其次xchgl是一個Intel CPU的鎖總線操作,對應到匯編上,就是自帶lock指令前綴,就算前面沒有加lock; 這個操作也是原子性的。

其次,既然是鎖總線操作,就有可能失敗,這個命令式非阻塞的,每次執行只是一次嘗試,所以這個while就說的通了。通過循環嘗試上鎖,來實現旋鎖機制。

最后,這條命令還用到了一個 read-modify-write的操作,這個操作,主要是因為在現代CPU中基本上都會使用Out of Order來對指令執行進行并行優化。

但是我們這個加鎖的過程是一個嚴格的時序依賴過程,我們必須保證,前面一個CPU加上了鎖,后面CPU來查詢的時候都顯示已經上鎖了。即read-modify-write順序執行。

在XV6 和 后面分析的Linux實際上都是用__sync_synchronize來實現這個過程的。

至此,XV6 SpinLock最關鍵的部分就解讀完了。

當已經拿到SpinLock的時候,就回去更新cpu,call stack來給DeBug使用。

image

實際上這段代碼是依次向前遍歷,來獲得棧底EBP,棧頂ESP,下一個指令地址EIP地址。

釋放也是相同的套路。

image

先去康康你這個SpinLock是不是已經被釋放了,然后取清空call stack, cpu,最后再來修改locked位。

這個地方就不用旋等,因為一個SpinLock只會被一個CPU占用。

最后是關中斷

image

還是一樣去檢查ELFAGS堆棧和中斷可用常量相不相等。

檢查當前CPU的嵌套中斷數是否大于0.

然后再來檢查cpu中斷標志是否不為0,最后再來開中斷

SleepLock in XV6

前面說到,實際上SpinLock不適用于臨界區是I/O等待的情況,所以在XV6中,關于文件系統的鎖機制是用SleepLock來實現的。

SleepLock定義在<proc.c>文件中

image

在這里在常規檢查之后,并沒有之前去請求SpinLock, 而是先去獲取ptable.lock(這也是一個SpinLock)。因為邏輯不相干,所以這個ptable.lock更容易獲得。

然后釋放SpinLock,以便造成堵塞。然后記錄下睡眠前狀態,把CPU交給調度程序來調度。直到被調度回CPU,先去釋放之前占用的ptable.lock, 然后再來獲取真正需要的SpinLock。

從而實現睡眠鎖,可以看到這個這個睡眠鎖實際上相關于用另外一個SpinLock來做通知的作用,相當于去搶占另外一個不是特別稀缺的資源。

image

而調度函數sched() 則是依次去檢查是否已經釋放了ptable.lock,檢查當前cpu的嵌套中斷數,檢查proc的狀態,檢查EFLAGS堆棧。最后才是switch上下文。

SpinLock in Linux

看完XV6的SpinLock實現,再來看Linux的SpinLock實現,就會發現驚人的相似。

本文用Linux kernal 版本號是4.19.30.

image

其實Linux下SpinLock的實現有好多種,上次分析了tryLock,這次來分析最基本的SpinLock.

Linux的SpinLock的Locked是一個叫做slock的變量,具體class定義就不放了,上鎖的函數在<include/>linux/spinlock.h>中

spin_lock會去調用raw_spin_lock。而raw_spin_lock這個函數式指向_raw_spin_lock.

而_raw_spin_lock則是一個隨著運行環境不同的函數。

當處于非SMP環境時,實際上就變成了一個簡單的禁用內核搶占。

而SMP環境中,則會去調用__raw_spin_lock函數,而這個函數才是真正的實現上鎖功能的函數。

大概思路和XV6基本一致,先去關中斷,然后鎖的有效性,最后再去真正的上鎖。

而上鎖這個函數LOCK_CONNECT()則是不同環境有不同的實現。

Linux kerenal 中 總共有15個實現,(不知道有沒有數錯)然后以其中幾個為例來具體分析。

以<arch/arc/include/asm/spinlock.h>為例

image

這個版本的arch_spin_trylock先去聲明一個__sync_synchronize(),這個操作和XV6中read-modify-write中一致。

然后相同是上鎖,檢查當前上鎖狀態,比較Locked slock, 如果不滿足條件則繼續循環。

當成功上鎖,則更改got_it,并返回。

實際上這個操作流程和XV6幾乎一樣,同樣的__sync_synchronize() 同樣的判斷加鎖情況,附帶循環比較。

其他版本的arch_spin_trylock大概思路也是相同的, 貼一下部分版本解析。

image
image
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容