排隊自旋鎖(Ticket spinlocks)
(譯自http://lwn.net/Articles/267968/,作者Jonathan Corbet)
自旋鎖是Linux核心中最底層的互斥機制。因此,它們對核心的安全和效能有很大的影響,人們花很大力氣去優化各種自旋鎖的實現(不同的硬體體系結構會有不同的實現)也就不足為奇了。但是我們的優化之路還沒有走到終點,一個合併到2.6.25版本核心的patch告訴我們能做的還有很多。
在x86架構上的2.6.24版本核心中,自旋鎖用一個整數來表示。當為1時表示這個鎖是可被獲得的。spin_lock()函式被呼叫時,首先原子地對這個整數減1,然後看結果是否為0;如果為0,則鎖被呼叫者獲得;如果為負數,這說明這個鎖已經被別人得到了,然後它就會進入忙等/自旋狀態,一直迴圈到鎖的值變為正,然後再從頭開始,再次嘗試獲得鎖。
當臨界區的程式碼執行完畢後,鎖的所有者會將鎖的值設為1來釋放這個鎖。
這種實現效率很高,尤其在沒有競爭的情況下(實際在大多數情況下都沒有競爭)。想要看鎖的爭用有多激烈也很簡單——鎖的值越小,對鎖進行爭用的處理器越多。但是這種方法有一個弊端:它並不公平。當鎖被釋放後,第一個對這個鎖執行減1操作的處理器將會獲得這個鎖。我們並不能保證等待時間最長的處理器獲得這個鎖;實際上,剛剛釋放了鎖的處理器更容易再次獲得這個鎖,因為這個鎖正好在它的cache中。
有些人希望鎖的不公平不會造成問題;在許多情況下,當鎖的爭用很激烈時,即使不考慮鎖的公平性,爭用也會產生效能問題。Nick Piggin最近重新思考了這個問題,他注意到:
對於一個8核Opteron處理器,自旋鎖的不公平性是無法忽視的,在使用者空間對鎖的測試中,不同的執行緒的執行時間可能會相差兩倍,有些執行緒在嘗試1000000次後才能獲得鎖。
這樣的執行時間的差異自然是無法接受的。鎖的不公平性也會造成延遲問題,因為獲得鎖的時間可能任意長,也就無法對延遲長短做一個保證了。
Nick的迴應是一種新的自旋鎖實現,他將其稱為“排隊自旋鎖”。在它的最初版本中,自旋鎖佔用16位,分為兩個位元組,分別是“Next”欄位和“Owner”欄位。
每個位元組都可以認為是一個入場號,就像商店裡的叫號機,Next欄位表示叫號機即將列印分發的號,Owner欄位表示正在視窗前服務的號,商店的做法能保證顧客按照到達的順序被提供服務。
因此,在新方案中,鎖的初始值(兩部分)被初始化為0。spin_lock()函式首先記錄下鎖的值,然後將Next欄位加1——一次原子操作就能完成這些事情。如果Next欄位在加1之前等於Owner欄位,鎖就會被當前處理器獲得,剩餘工作就能繼續進行。否則處理器就會自旋,直到Owner欄位增長到正確的值(譯註:即函式一開始記錄下的Next欄位的值)。在這種方案中,釋放鎖時只需要將Owner欄位加1即可。
上述的實現也有一些小小的弊端,那就是處理器的數目不能超過256,處理器數量超過這個值的話,當發生激烈地鎖爭用時,不同的處理器可能會獲得相同的排隊號。由此產生的潛在問題是不可容忍的,在很多大型系統中,處理器數量早已超過了256。因此有一個附加的
在舊版本的自旋鎖實現中,發生鎖爭用時所有處理器都要去搶這個鎖。現在他們可以按照到達的順序排隊等候來獲得這個鎖了。多個執行緒的執行時間變得平均了,最大延遲也減小了(而且延遲也變得確定而不是任意長了)。Nick認為,新實現可能會帶來輕微的開銷,但是這個開銷在當代的處理器上非常小,而且相對於發生鎖爭用時出現的cache不命中帶來的開銷可以忽略。核心x86維護者們顯然認為消除激烈爭用帶來的收益高於這小小的開銷,別人大概也會認可吧。