這是最簡單的spinlock,CPU會在硬件上提供一些指令來幫助OS實現spinlock,
比如x86就有xchg, LOCK指令前綴等指令。。。
test_and_set()可以利用這些指令對某個memory地址,來原子地完成:
寫入true到這個地址,同時返回這個地址儲存的舊的值。
void spin_lock(lock)
{
while (test_and_set(lock, true));
}
void spin_unlock(lock)
{
atomic_set(lock, false);
}
在SMP(shared bus)的環境裏,TAS lock的性能非常差。
首先test_and_set一般都需要serialization,即在執行這條指令前,
CPU必須要完成前面所有對memory的訪問指令(read and write)。
這是非常heavy的操作,使得CPU無法對指令進行reorder,從而優化執行。
其次,因爲每一次test_and_set都必須讀-寫lock這個變量。這就要涉及到
多個CPU之間cache coherence的問題。
當CPU1讀lock的時候,如果這個lock沒有在CPU1的cache中,就需要從memory中讀入,
因爲CPU又需要寫這個變量,所以在把這個變量讀入cache的時候,如果其他CPU已經
cache了這個變量,就需要invalidate它們。這樣在CPU1把lock讀入自己的cache中時,
這塊cacheline所cache的lock就是CPU1所獨佔的,CPU1就可以更改它的值了。
如果多個CPU在競爭這個spinlock的話,每一次test_and_set都需要完成以上的操作,
在系統總線上會產生大量的traffic,開銷是非常大的,而且unlock的CPU還必須同其它正在
競爭spinlock的CPU去競爭cacheline ownership. 隨着CPU數目的增多,性能會成衰減的非常快。
2. TTAS (test-and-test-and-set)
void spin_lock(lock)
{
while (test_and_set(lock, true))
while (lock != false);
}
TTAS lock的改進是,當有CPU(CPU0)已經抓住了這把鎖的話,CPU1就只會以只讀的方式
cache這個lock。這樣做的好處好處就是,CPU1在等待這把鎖的時候,不會在總線上
產生traffic,和其它CPU一起競爭cacheline的ownership。
第一次的test_and_set還是和TAS lock一樣,需要invalidate CPU0的cacheline,
這樣的話CPU1獨佔的擁有了cache lock變量的cacheline。當它發現鎖已經被別人鎖住了,
CPU1就開始進入第二層循環。
如果CPU2這時也想搶這把鎖,它執行test_and_set時,會invalidate CPU1的cacheline。
它也發現鎖已經被鎖住了,進入第二層循環。
這時CPU1想讀lock這個變量的時候,會cache miss,會把read的請求發到系統總線上,
從memory中讀入lock的值,同時CPU2的cache controller會發現總線上的這次交易,它
會把自己cache了lock的cacheline的狀態轉爲shared。
這樣CPU1和CPU2的cache裏都cache了lock,第二層循環就都只在CPU內部執行,不會產生
總線交易。
當CPU0釋放鎖的時候,會invalidate CPU1和CPU2的cacheline,CPU1/CPU2會cache miss,
重新讀入lock,發現鎖已經被釋放了,就會進行一個test_and_set(),誰先完成就搶到了
鎖,另一個就必須重新循環等待鎖的釋放。
TTAS lock在自己的local cache copy上spinning被稱爲local spinning。是設計高效
的spinlock非常重要的一個原理。
3. TTAS with random backoff
TTAS lock有一個問題是在釋放鎖的時候,會產生大量的總線交易,因爲所有在競爭的
CPU都會去作一個test_and_set().
在local spinning的時候,如果引入一定的延時(就像以太網的collision avoidance機制),
這樣就會有效的降低在鎖釋放時系統總線的競爭。
在2.6.25之前,Linux kernel x86的spinlock的實現就是這一類型的。
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");
}
首先是做一個test_and_set (LOCK; decb lock),如果發現已經被鎖住了,就
random backoff (rep; nop),然後作local test (cmpb)。
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
asm volatile("movb $1,%0" : "+m" (lock->slock) :: "memory");
}
4. FIFO ticket spinlock (solved the fairness problem)
TTAS with random backoff還有一個公平性的問題,當鎖釋放時,誰能搶到鎖是隨機的。並不是等待
最久的那個競爭者會得到鎖。這樣就可能造成一個thread會busy looping很長的時間而得不到鎖。
Linux kernel x86的ticket spinlock是有Nick Piggin實現的,在2.6.25中被接受。
(git commit id is: 314cdbefd1fd0a7acf3780e9628465b77ea6a836)
LWN上有一篇文章介紹了ticket spinlock的原理(http://lwn.net/Articles/267968/)。
一個spinlock被分成了兩個部分,owner是現在擁有鎖的lock owner的ticket id,Next是下一個能拿到鎖的ticket id,初始化的時候Owner = Next = 0。當current lock owner釋放鎖的時候,會把Owner域加1,這樣當拿着Next的鎖競爭者發現Owner已經變成自己的ticket id的時候,就認爲自己拿到了鎖了。
static __always_inline void __ticket_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");
}
1. 初始化 -> slock: owner = next = 0
2. CPU0第一個拿到了鎖 -> slock: owner = 0, next = 1
3. 當CPU1也想競爭這把鎖的時候,xaddw -> slock: owner = 0, next = 2 同時
inc: owner = 0, next = 1
它發現inc: owner != next (注意它是在測試inc這個變量),就等待(rep; nop),然後把s
lock的owner讀入inc。如果CPU0釋放了鎖,它會把slock:owner加1。這樣CPU1就會發現
inc:next = 1,owner = 1,它就認爲自己拿到了鎖。
4. 如果在CPU0釋放鎖之前,CPU2也來競爭這把鎖的話,CPU2: slock: owner = 0, next = 3
inc: owner = 0, next = 2。當CPU0釋放鎖的時候,inc:owner = 1, next = 2,它仍然會
繼續循環,等待CPU1釋放鎖。
references:
1. For SMP cache coherence, please see chapter 4 of Computer Architecture-A
Quantitative Approach.
2. Linux kernel source code.
3. For TAS, TTAS concept refer to chapter 7 of The Art of Multiprocessor
Programming.