1. 程式人生 > >arm架構下spinlock原理 (程式碼解讀)

arm架構下spinlock原理 (程式碼解讀)

http://blog.csdn.net/longwang155069/article/details/52055876

自旋鎖的引入

原子變數適用在多核之間多單一共享變數進行互斥訪問,如果要保護多個變數,並且這些變數之間有邏輯關係時,原子變數就不適用了。例如:常見的雙向連結串列。假設有三個連結串列節點A、B、C。需要將節點B插入節點A、C之間。如果CPU A剛好將A節點的後向指標指向B,但是還沒有將B的後向指標指向C。此時CPU B要遍歷連結串列,這將會一個災難性的後果。

如果共享資料段在中斷上下文或者程序上下文被訪問呢? 如果在程序上下文被訪問,完全可以使用訊號量semaphore機制來實現互斥。如果在中斷上下文被訪問呢? 就不能使用semaphore來實現互斥,因為semaphore會引起睡眠的。這時候就引入了spin_lock

spin_lock的實現思想

先說生活中一個示例,如果機智的你乘坐過火車的話,就一定知道早上6點-7點在火車上廁所的感受了。如果機智你的起來上廁所,發現一大堆人都等著上廁所,男女老少。接設你前面排了三個人,分別為A, B, C。 
當A進入廁所之後,關閉了廁所的門,然後就會看見一個紅燈亮著“有人“,這時候B,C和機智的你都在等待。當A出來後,B進去不到20s就出來了。然後進去了C,然後你就苦苦的在等待,一直在觀察這什麼時候紅燈熄滅,這讓機智的你等待了10min, 然後機智的你進去就10s搞定。好了關於生活的例子說完了,再回到spin_lock中。

可以將廁所當作臨界區。A, B, C, 機智的你是四個cpu, 紅燈是臨界區時候有cpu進入狀態。 
當A進入臨界區(廁所),然後就會將進入狀態修改為忙(紅燈亮),然後B,C以及機智的你都會判斷當前狀態,如果是忙,就等待,不忙就讓B先進去,B進入之後同樣的操作。

spin_lock早期程式碼分析

因為spin_lock在ARM平臺上的實現策略發生過變化,所以先分析以前版本2.6.18的spin_lock。

主要是以SMP系統分析,後面會稍帶分析UP系統。

<include/linux/spinlock.h>
----------------------------------------------------------
#define spin_lock(lock)         _spin_lock(lock)

<kernel/spinlock.c>
--------------------------------------------------------
void __lockfunc _spin_lock(spinlock_t *lock) { preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); _raw_spin_lock(lock); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

其中preempt_disable()是用來關閉掉搶佔的。如果系統中打開了CONFIG_PREEMPT該選項的話,就是用來關閉系統的搶佔,如果沒有開啟相當於什麼都沒幹,只是為了統一程式碼。至於這裡為什麼需要關閉搶佔,在後面會說。

spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
  • 1
  • 1

這段程式碼使用來除錯使用的,沒有系統沒有開啟CONFIG_DEBUG_LOCK_ALLOC配置的話,這樣程式碼也相當於什麼都沒幹。繼續往下。

define _raw_spin_lock(lock)     __raw_spin_lock(&(lock)->raw_lock)

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    unsigned long tmp;

    __asm__ __volatile__(
"1: ldrex   %0, [%1]\n"
"   teq %0, #0\n"
"   strexeq %0, %2, [%1]\n"
"   teqeq   %0, #0\n"
"   bne 1b"
    : "=&r" (tmp)
    : "r" (&lock->lock), "r" (1)
    : "cc");

    smp_mb();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

回頭看看spinlock_t變數的定義:

typedef struct {
    raw_spinlock_t raw_lock;
} spinlock_t;

typedef struct {
    volatile unsigned int lock;
} raw_spinlock_t;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通過層層的呼叫,最後spinlock_t就是一個volatile unsigned int型變數。

彙編程式碼 C語言 解釋
1: ldrex %0, [%1] tmp=lock->lock 讀取lock的狀態賦值給tmp
teq %0, #0 if(tmp == 0) 判斷lock的狀態是否為0。如果是0說明可以獲得鎖;如果不為0,說明自旋鎖處於上鎖狀態,不能訪問,執行bne 1b指令,跳到標號1處不停執行。
strexeq %0, %2, [%1] lock->lock=1 使用常量1來更新鎖的狀態,並將執行結果放入到tmp中
teqeq %0, #0 if(tmp == 0) 用來判斷tmp是否為0,如果為0,表明更新鎖的狀態成功;如果不為0表明鎖的狀態沒喲更新成功,執行”bne 1b”,跳轉到標號1繼續執行。

早期spin_lock存在的不公平性

還是回到火車上上廁所的故事中,某天早上去上廁所,發現有一大堆的人都在排隊。但是進去廁所的人已經進去了半個小時,後面的人已經開始等待不急了,有的謾罵起來,有人大喊憋不住了,機智你的剛好肚子疼,快憋不住了。剛好排在第一位是你的媳婦,然後你就插隊立馬上了廁所。你出來後,接著是你兒子,然後你全家。後面的人就一直等待了1個小時終於進入了廁所。

將這個現象轉移到程式中就是,在現代多核的cpu中,因為每個cpu都有chach的存在,導致不需要去訪問主存獲取lock,所以噹噹前獲取lock的cpu,釋放鎖後,使其他cpu的cache都失效,然後釋放的鎖在下一次就比較容易進入臨界去,導致出現了不公平。

ticket機制原理

先看最新的spin_lock的結構體定義:

typedef struct spinlock {
        struct raw_spinlock rlock;
} spinlock_t;

typedef struct raw_spinlock {
    arch_spinlock_t raw_lock;
} raw_spinlock_t;

typedef struct {
    union {
        u32 slock;
        struct __raw_tickets {
#ifdef __ARMEB__
            u16 next;
            u16 owner;
#else
            u16 owner;
            u16 next;
#endif
        } tickets;
    };
} arch_spinlock_t;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在分析程式碼之前,還需要解釋一下tickets中的owner和next的含義。詳細可見提交: 
546c2896a42202dbc7d02f7c6ec9948ac1bf511b

因為有cache的作用,導致本次釋放lock的cpu在下一次就可以更快的獲取鎖。所以在ARMv6上引入了”票”演算法來保證每個cpu都是像“FIFO“訪問臨界區。

還是說回到火車上廁所的事件,還是早上排隊上廁所。這時候好多人都插隊,導致沒有熟人的人一直上不了廁所,於是火車管理員(虛擬的,只是為了講解原理而已)出現了。火車管理員說“從現在開始不準插隊,我來監督,所有人排位一隊“。管理員站在廁所門口,讓大家都按次序排隊上廁所,這時候就沒有人插隊了。

將這個事件轉移到程式中的ticket中。剛開始的時候臨界區沒有cpu進入,狀態是空閒的。next和owner的值都是0,當cpu1進入臨界區後。將next++, 當cpu1從臨界區域執行完後,將owner++。這時候next和owner都為1,說明臨界區沒有cpu進入。這時候cpu2進入臨界區,將next++, 然後cpu2好像乾的活比較多,當cpu3進來後,next++,這時候next已經是3了,當cpu2執行完畢後,owner++,owner的值變為2, 表示讓cpu2進入臨界區,這就保障了各個cpu之間都是先來後到的執行。

ARM32 上spin_lock程式碼實現

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
    unsigned long tmp;
    u32 newval;
    arch_spinlock_t lockval;

    prefetchw(&lock->slock);
    __asm__ __volatile__(
"1: ldrex   %0, [%3]\n"
"   add %1, %0, %4\n"
"   strex   %2, %1, [%3]\n"
"   teq %2, #0\n"
"   bne 1b"
    : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
    : "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
    : "cc");

    while (lockval.tickets.next != lockval.tickets.owner) {
        wfe();
        lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
    }
    smp_mb();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
彙編 C語言 解釋
1: ldrex %0, [%3] lockval = lock 讀取鎖的值賦值給lockval
add %1, %0, %4 newval = lockval + (1 << 16) 將next++之後的值存在newval中
strex %2, %1, [%3] lock = newval 將新的值存在lock中,將是否成功結果存入在tmp中
teq %2, #0 if(tmp == 0) 判斷上條指令是否成功,如果不成功執行”bne 1b”跳到標號1執行
while (lockval.tickets.next != lockval.tickets.owner) {
    wfe();
    lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

當tickets中的next和owner不相等的時候,說明臨界區在忙, 需要等待。然後cpu會執行wfe指令。當其他cpu忙完之後,會更新owner的值,如果owner的值如果與next值相同,那到next號的cpu執行。

ARM64 上spin_lock程式碼實現

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
    unsigned int tmp;
    arch_spinlock_t lockval, newval;

    asm volatile(
    /* Atomically increment the next ticket. */
"   prfm    pstl1strm, %3\n"
"1: ldaxr   %w0, %3\n"
"   add %w1, %w0, %w5\n"
"   stxr    %w2, %w1, %3\n"
"   cbnz    %w2, 1b\n"
    /* Did we get the lock? */
"   eor %w1, %w0, %w0, ror #16\n"
"   cbz %w1, 3f\n"
    /*
     * No: spin on the owner. Send a local event to avoid missing an
     * unlock before the exclusive load.
     */
"   sevl\n"
"2: wfe\n"
"   ldaxrh  %w2, %4\n"
"   eor %w1, %w2, %w0, lsr #16\n"
"   cbnz    %w1, 2b\n"
    /* We got the lock. Critical section starts here. */
"3:"
    : "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
    : "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
    : "memory");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
彙編 C語言 解釋
prfm pstl1strm, %3 將lock變數讀到cache,增加訪問速度
1: ldaxr %w0, %3 lockval = lock 將lock的值賦值給lockval
add %w1, %w0, %w5 newval=lockval + (1 << 16) 將lock中的next++, 然後將結果賦值給newval
stxr %w2, %w1, %3 lock = newval 將newval賦值給lock,同時將是否設定成功結果存放到tmp
cbnz %w2, 1b if(tmp != 0)goto 1 如果tmp不為0,跳到標號1執行
eor %w1, %w0, %w0, ror #16 if(next == owner) 判斷next是否等於owner
cbz %w1, 3f if(newval == 0) 進入臨界區
2: wfe 自旋等待
ldaxrh %w2, %4 tmp = lock->owner 獲取當前的Owner值存放在tmp中
eor %w1, %w2, %w0, lsr #16 if(next == owner) 判斷next是否等於owner
cbnz %w1, 2b 如果不等跳到標號2自旋,負責進入臨界區域

ARM64 上spin_unlock程式碼實現

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
    asm volatile(
"   stlrh   %w1, %0\n"
    : "=Q" (lock->owner)
    : "r" (lock->owner + 1)
    : "memory");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

解鎖的操作相對簡單,就是給owner執行加1的操作。


http://blog.csdn.net/longwang155069/article/details/52211024

讀寫鎖引入

在前面小節分析了spin_lock的實現,可以知道spin_lock只允許一個thread進入臨界區,而且對進入臨界區中的操作不做細分。但是在實際中,對臨界區的操作分為讀和寫。如果按照spin_lock的實現,當