轉自:http://www.ibm.com/developerworks/cn/linux/l-cn-spinlock/index.html?utm_source=twitterfeed&utm_medium=twitter
引言
自旋鎖(Spinlock)是一種 Linux 核心中廣泛運用的底層同步機制。自旋鎖是一種工作於多處理器環境的特殊的鎖,在單處理環境中自旋鎖的操作被替換為空白操作。當某個處理器上的核心執行線程申請自旋鎖時,如果鎖可用,則獲得鎖,然後執行臨界區操作,最後釋放鎖;如果鎖已被佔用,線程並不會轉入睡眠狀態,而是忙等待該鎖,一旦鎖被釋放,則第一個感知此資訊的線程將獲得鎖。
長期以來,人們總是關注於自旋鎖的安全和高效,而忽視了自旋鎖的“公平”性。傳統的自旋鎖本質上用一個整數來表示,值為1代表鎖未被佔用。這種無序競爭的本質特點導致執行線程無法保證何時能取到鎖,某些線程可能需要等待很長時間。隨著電腦處理器個數的不斷增長,這種“不公平”問題將會日益嚴重。
排隊自旋鎖(FIFO Ticket Spinlock)是 Linux 核心 2.6.25 版本引入的一種新型自旋鎖,它通過儲存執行線程申請鎖的順序資訊解決了傳統自旋鎖的“不公平”問題。排隊自旋鎖的代碼由 Linux 核心開發人員 Nick Piggin 實現,目前只針對 x86 體繫結構(包括 IA32 和 x86_64),相信很快就會被移植到其它平台。
回頁首 傳統自旋鎖的實現與不足
Linux 核心自旋鎖的底層資料結構 raw_spinlock_t 定義如下: 清單 1. raw_spinlock_t 資料結構
typedef struct {unsigned int slock;} raw_spinlock_t;
slock 雖然被定義為不帶正負號的整數,但是實際上被當作有符號整數使用。slock 值為 1 代表鎖未被佔用,值為 0 或負數代表鎖被佔用。初始化時 slock 被置為 1。
線程通過宏 spin_lock 申請自旋鎖。如果不考慮核心搶佔,則 spin_lock 調用 __raw_spin_lock 函數,代碼如下所示: 清單 2. __raw_spin_lock 函數
static inline void __raw_spin_lock(raw_spinlock_t *lock){asm volatile("\n1:\t" LOCK_PREFIX " ; decb %0\n\t" "jns 3f\n" "2:\t" "rep;nop\n\t" "cmpb $0,%0\n\t" "jle 2b\n\t" "jmp 1b\n" "3:\n\t" : "+m" (lock->slock) : : "memory");}
LOCK_PREFIX 的定義如下: 清單 3. LOCK_PREFIX宏
#ifdef CONFIG_SMP#define LOCK_PREFIX \".section .smp_locks,\"a\"\n"\_ASM_ALIGN "\n"\_ASM_PTR "661f\n" /* address */\".previous\n"\"661:\n\tlock; "#else /* ! CONFIG_SMP */#define LOCK_PREFIX ""#endif
在多處理器環境中 LOCK_PREFIX 實際被定義為 “lock”首碼。
x86 處理器使用“lock”首碼的方式提供了在指令執行期間對匯流排加鎖的手段。晶片上有一條引線 LOCK,如果在一條彙編指令(ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG)前加上“lock” 首碼,經過彙編後的機器代碼就使得處理器執行該指令時把引線 LOCK 的電位拉低,從而把匯流排鎖住,這樣其它處理器或使用DMA的外設暫時無法通過同一匯流排訪問記憶體。
從 P6 處理器開始,如果指令訪問的記憶體地區已經存在於處理器的內部緩衝中,則“lock” 首碼並不將引線 LOCK 的電位拉低,而是鎖住本處理器的內部緩衝,然後依靠緩衝一致性協議保證操作的原子性。
decb 彙編指令將 slock 的值減 1。由於“減 1”是“讀-改-寫”操作,不是原子操作,可能會被同時申請鎖的其它處理器上的線程幹擾,所以必須加上“lock”首碼。
jns 彙編指令檢查 EFLAGS 寄存器的 SF(符號)位,如果為 0,說明 slock 原來的值為 1,則線程獲得鎖,然後跳到標籤 3 的位置結束本次函數調用。如果 SF 位為 1,說明 slock 原來的值為 0 或負數,鎖已被佔用。那麼線程轉到標籤 2 處不斷測試 slock 與 0 的大小關係,假如 slock 小於或等於 0,跳轉到標籤 2 的位置繼續忙等待;假如 slock 大於 0,說明鎖已被釋放,則跳轉到標籤 1 的位置重新申請鎖。
線程通過宏 spin_unlock 釋放自旋鎖,該宏調用 __raw_spin_unlock 函數: 清單 4. __raw_spin_unlock函數
static inline void __raw_spin_unlock(raw_spinlock_t *lock){asm volatile("movb $1,%0" : "+m" (lock->slock) :: "memory");}
可見 __raw_spin_unlock 函數僅僅執行一條彙編指令:將 slock 置為 1。
儘管擁有使用簡單方便、效能好的優點,自旋鎖也存在自身的不足:
由於傳統自旋鎖無序競爭的本質特點,核心執行線程無法保證何時可以取到鎖,某些執行線程可能需要等待很長時間,導致“不公平”問題的產生。這有兩方面的原因:
隨著處理器個數的不斷增加,自旋鎖的競爭也在加劇,自然導致更長的等待時間。
釋放自旋鎖時的重設操作將無效化所有其它正在忙等待的處理器的緩衝,那麼在處理器拓撲結構中臨近自旋鎖擁有者的處理器可能會更快地重新整理緩衝,因而增大獲得自旋鎖的機率。
由於每個申請自旋鎖的處理器均在全域變數 slock 上忙等待,系統匯流排將因為處理器間的緩衝同步而導致繁重的流量,從而降低了系統整體的效能。
回頁首 排隊自旋鎖的設計原理
傳統自旋鎖的“不公平”問題在鎖競爭激烈的伺服器系統中尤為嚴重,因此 Linux 核心開發人員 Nick Piggin 在 Linux 核心 2.6.25 版本中引入了排隊自旋鎖:通過儲存執行線程申請鎖的順序資訊來解決“不公平”問題。
排隊自旋鎖仍然使用原有的 raw_spinlock_t 資料結構,但是賦予 slock 域新的含義。為了儲存順序資訊,slock 域被分成兩部分,分別儲存鎖持有人和未來鎖申請者的票據序號(Ticket Number),如下圖所示: 圖 1. Next 和 Owner 域
如果處理器個數不超過 256,則 Owner 域為 slock 的 0-7 位,Next 域為 slock 的 8-15 位,slock 的高 16 位不使用;如果處理器個數超過 256,則 Owner 和 Next 域均為 16 位,其中 Owner 域為 slock 的低 16 位。可見排隊自旋鎖最多支援 216=65536 個處理器。
只有 Next 域與 Owner 域相等時,才表明鎖處於未使用狀態(此時也無人申請該鎖)。排隊自旋鎖初始化時 slock 被置為 0,即 Owner 和 Next 置為 0。核心執行線程申請自旋鎖時,原子地將 Next 域加 1,並將原值返回作為自己的票據序號。如果返回的票據序號等於申請時的 Owner 值,說明自旋鎖處於未使用狀態,則直接獲得鎖;否則,該線程忙等待檢查 Owner 域是否等於自己持有的票據序號,一旦相等,則表明鎖輪到自己擷取。線程釋放鎖時,原子地將 Owner 域加 1 即可,下一個線程將會發現這一變化,從忙等待狀態中退出。線程將嚴格地按照申請順序依次擷取排隊自旋鎖,從而完全解決了“不公平”問題。
回頁首 排隊自旋鎖的實現
排隊自旋鎖沒有改變原有自旋鎖的調用介面,該 API 是以 C 語言宏的形式提供給開發人員。下表列出 6 個主要的 API 和相對應的底層實現函數: 表 1. 排隊自旋鎖 API
| 宏 |
底層實現函數 |
描述 |
| spin_lock_init |
無 |
將鎖置為初始未使用狀態(值為 0) |
| spin_lock |
__raw_spin_lock |
忙等待直到 Owner 域等於本地票據序號 |
| spin_unlock |
__raw_spin_unlock |
Owner 域加 1,將鎖傳給後續等待線程 |
| spin_unlock_wait |
__raw_spin_unlock_wait |
不申請鎖,忙等待直到鎖處於未使用狀態 |
| spin_is_locked |
__raw_spin_is_locked |
測試鎖是否處於使用狀態 |
| spin_trylock |
__raw_spin_trylock |
如果鎖處於未使用狀態,獲得鎖;否則直接返回 |
下面介紹其中 3 個底層函數的實現細節,假定處理器個數不超過 256。
__raw_spin_is_locked 清單 5. __raw_spin_is_locked 函數
static inline int __raw_spin_is_locked(raw_spinlock_t *lock){int tmp = *(volatile signed int *)(&(lock)->slock);return (((tmp >> 8) & 0xff) != (tmp & 0xff));}
此函數判斷 Next 和 Owner 域是否相等,如果相等,說明自旋鎖處於未使用狀態,返回 0;否則返回1。
tmp 這種複雜的賦值操作是為了直接從記憶體中取值,避免處理器緩衝的影響。
__raw_spin_lock 清單 6. __raw_spin_lock 函數
static inline void __raw_spin_lock(raw_spinlock_t *lock){short inc = 0x0100;__asm__ __volatile__ (LOCK_PREFIX "xaddw %w0, %1\n""1:\t""cmpb %h0, %b0\n\t""je 2f\n\t""rep ; nop\n\t""movb %1, %b0\n\t"/* don't need lfence here, because loads are in-order */"jmp 1b\n""2:":"+Q" (inc), "+m" (lock->slock)::"memory", "cc");}
LOCK_PREFIX 宏在前文中已經介紹過,就是“lock”首碼。
xaddw 彙編指令將 slock 和 inc 的值交換,然後把這兩個值相加後的和存到 slock 中。也就是說,該指令執行完畢後,inc 存有原來的 slock 值作為票據序號,而 slock 的 Next 域被加 1。
comb 比較 inc 變數的高位和低位位元組是否相等,如果相等,表明鎖處於未使用狀態,直接跳轉到標籤 2 的位置退出函數。
如果鎖處於使用狀態,則不停地將當前的 slock 的 Owner 域複製到 inc 的低位元組處(movb 指令),然後重複 c 步驟。不過此時 inc 變數的高位和低位位元組相等表明輪到自己擷取了自旋鎖。
__raw_spin_unlock 清單 7. __raw_spin_unlock 函數
static inline void __raw_spin_unlock(raw_spinlock_t *lock){__asm__ __volatile__(UNLOCK_LOCK_PREFIX "incb %0":"+m" (lock->slock)::"memory", "cc");}
在 IA32 體繫結構下,如果使用 PPro SMP 系統或者啟用了 X86_OOSTORE,則 UNLOCK_LOCK_PREFIX 被定義為“lock”首碼;否則被定義為空白。
incb 指令將 slock 最低位位元組也就是 Owner 域加 1。