Linux x86 spinlock實現之分析

1. TAS lock (test-and-set)
這是最簡單的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/)。

[ticket spinlock]
一個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.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章