arm架構下spinlock原理 (代碼解讀)【轉】


轉自:https://blog.csdn.net/adaptiver/article/details/72389453

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系統。

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

 

其中preempt_disable()是用來關閉掉搶占的。如果系統中打開了CONFIG_PREEMPT該選項的話,就是用來關閉系統的搶占,如果沒有開啟相當於什么都沒干,只是為了統一代碼。至於這里為什么需要關閉搶占,在后面會說。

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

這段代碼使用來調試使用的,沒有系統沒有開啟CONFIG_DEBUG_LOCK_ALLOC配置的話,這樣代碼也相當於什么都沒干。繼續往下。

  1.  
    define _raw_spin_lock (lock) __raw_spin_lock(&(lock)->raw_lock)
  2. static inline void __raw_spin_lock(raw_spinlock_t *lock)
  3. {
  4. unsigned long tmp;
  5.  
  6. __asm__ __volatile__(
  7.  
    "1: ldrex %0, [%1]\n"
  8.  
    " teq %0, #0\n"
  9.  
    " strexeq %0, %2, [%1]\n"
  10.  
    " teqeq %0, #0\n"
  11.  
    " bne 1b"
  12.  
    : "=&r" (tmp)
  13.  
    : "r" (&lock->lock), "r" (1)
  14.  
    : "cc");
  15.  
     
  16.  
    smp_mb();
  17.  
    }

 

回頭看看spinlock_t變量的定義:

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

 

通過層層的調用,最后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的結構體定義:

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

 

在分析代碼之前,還需要解釋一下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代碼實現

  1.  
    static inline void arch_spin_lock(arch_spinlock_t *lock)
  2.  
    {
  3.  
    unsigned long tmp;
  4.  
    u32 newval;
  5.  
    arch_spinlock_t lockval;
  6.  
     
  7.  
    prefetchw(&lock->slock);
  8.  
    __asm_ _ __volatile__(
  9.  
    "1: ldrex %0, [%3]\n"
  10.  
    " add %1, %0, %4\n"
  11.  
    " strex %2, %1, [%3]\n"
  12.  
    " teq %2, #0\n"
  13.  
    " bne 1b"
  14.  
    : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
  15.  
    : "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
  16.  
    : "cc");
  17.  
     
  18.  
    while (lockval.tickets.next != lockval.tickets.owner) {
  19.  
    wfe();
  20.  
    lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
  21.  
    }
  22.  
    smp_mb();
  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執行
  1.  
    while (lockval.tickets.next != lockval.tickets.owner) {
  2.  
    wfe() ;
  3.  
    lockval .tickets.owner = ACCESS_ONCE(lock->tickets.owner);
  4.  
    }

 

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

ARM64 上spin_lock代碼實現

  1.  
    static inline void arch_spin_lock(arch_spinlock_t *lock)
  2.  
    {
  3.  
    unsigned int tmp;
  4.  
    arch_spinlock_t lockval, newval;
  5.  
     
  6.  
    asm volatile(
  7.  
    /* Atomically increment the next ticket. */
  8.  
    " prfm pstl1strm, %3\n"
  9.  
    "1: ldaxr %w0, %3\n"
  10.  
    " add %w1, %w0, %w5\n"
  11.  
    " stxr %w2, %w1, %3\n"
  12.  
    " cbnz %w2, 1b\n"
  13.  
    /* Did we get the lock? */
  14.  
    " eor %w1, %w0, %w0, ror #16\n"
  15.  
    " cbz %w1, 3f\n"
  16.  
    /*
  17.  
    * No: spin on the owner. Send a local event to avoid missing an
  18.  
    * unlock before the exclusive load.
  19.  
    */
  20.  
    " sevl\n"
  21.  
    "2: wfe\n"
  22.  
    " ldaxrh %w2, %4\n"
  23.  
    " eor %w1, %w2, %w0, lsr #16\n"
  24.  
    " cbnz %w1, 2b\n"
  25.  
    /* We got the lock. Critical section starts here. */
  26.  
    "3:"
  27.  
    : "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
  28.  
    : "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
  29.  
    : "memory");
  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代碼實現

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

 

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

 

 

讀寫鎖rwlock

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

 

讀寫鎖引入

在前面小節分析了spin_lock的實現,可以知道spin_lock只允許一個thread進入臨界區,而且對進入臨界區中的操作不做細分。但是在實際中,對臨界區的操作分為讀和寫。如果按照spin_lock的實現,當多個read thread都想進入臨界區讀取的時候,這時候只有一個read thread進入到臨界區,這樣效率和性能明顯下降。所以就針對某些操作read thread占絕大多數的情況下,提出了讀寫鎖的概念。
 

讀寫鎖的基本原理

加鎖操作

  • 假設當前臨界區沒有任何進程,這時候read進程或者write進程都可以進來,但是只能是其一
  • 如果當前臨界區只有一個read進程,這時候任意的read進程都可以進入,但是write進程不能進入
  • 如果當前臨界區只有一個write進程,這時候任何read/write進程都無法進入。只能自旋等待
  • 如果當前當前臨界區有好多個read進程,同時read進程依然還會進入,這時候進入的write進程只能等待。直到臨界區一個read進程都沒有,才可進入

解鎖操作

  • 如果在read進程離開臨界區的時候,需要根據情況決定write進程是否需要進入。只有當臨界區沒有read進程了,write進程方可進入。
  • 如果在write進程離開臨界區的時候,無論write進程或者read進程都可進入臨界區,因為write進程是排它的。

讀寫鎖的定義

[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. typedef struct {  
  2.     arch_rwlock_t raw_lock;  
  3. } rwlock_t;  
  4.   
  5. typedef struct {  
  6.     volatile unsigned int lock;  
  7. } arch_rwlock_t;  
可以看到讀寫鎖與spin_lock的定義最終相同,只是名字不同罷了。

讀寫鎖API

加鎖API

  1. read_lock(lock)/write_lock(lock)                                                               #獲取指定的鎖
  2. read_trylock(lock)/write_trylock(lock)                                                       #嘗試獲取鎖,如果失敗不spin,直接返回
  3. read_lock_irq(lock)/write_lock_irq(lock)                                                   #獲取指定的鎖,同時關掉本地cpu中斷
  4. read_lock_irqsave(lock, flags)/write_lock_irqsave(lock, flags)                #保存本地cpu的irq標志,然后關掉cpu中斷,獲取指定鎖
  5. read_lock_bh(lock)/read_lock_bh(lock)                                                   #獲取指定的鎖,同時關掉中斷下半部(bottom half)

解鎖API

  1. read_unlock(lock)/write_unlock(lock)                                                      #釋放指定的鎖
  2. read_unlock_irq(lock)/write_unlock_irq(lock)                                          #釋放指定的鎖,同時使能cpu中斷
  3. read_unlock_irqrestore/write_unlock_irqrestore                                     #釋放鎖,同時使能cpu中斷,恢復cpu的標識
  4. read_unlock_bh/write_unlock_bh                                                            #釋放鎖,同時使能cpu中斷的下半部

讀寫鎖的實現

寫入者加鎖操作:

[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. /* 
  2.  * Write lock implementation. 
  3.  * 
  4.  * Write locks set bit 31. Unlocking, is done by writing 0 since the lock is 
  5.  * exclusively held. 
  6.  * 
  7.  * The memory barriers are implicit with the load-acquire and store-release 
  8.  * instructions. 
  9.  */  
  10.   
  11. static inline void arch_write_lock(arch_rwlock_t *rw)  
  12. {  
  13.     unsigned int tmp;  
  14.   
  15.     asm volatile(  
  16.     "   sevl\n"  
  17.     "1: wfe\n"  
  18.     "2: ldaxr   %w0, %1\n"  
  19.     "   cbnz    %w0, 1b\n"  
  20.     "   stxr    %w0, %w2, %1\n"  
  21.     "   cbnz    %w0, 2b\n"  
  22.     : "=&r" (tmp), "+Q" (rw->lock)  
  23.     : "r" (0x80000000)  
  24.     : "memory");  
  25. }  
通過注釋: write操作的上鎖操作是給bit31寫1, 解鎖操作就是給bit31寫0
 
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. "   sevl\n"  
  2. "1: wfe\n"  
使cpu進入低功耗模式
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. 2:  ldaxr   %w0, %1\n  
讀取鎖的值,賦值給tmp變量
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. cbnz    %w0, 1b  
如果tmp的值不為0, 跳轉到標號1重新執行。不等於0說明有read/write進程正在持有鎖,所以需要進入低功耗等待。
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. stxr    %w0, %w2, %1  
將鎖的bit31設置為1, 然后將設置結果放入tmp中。
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. cbnz    %w0, 2b  
如果tmp的值不為0,說明上條指令執行失敗,跳轉到標號2繼續執行。
 
可以看到,對於wirte操作,只要臨界區有read/write進程存在,就需要自旋等待,直到臨界區沒有任何進程存在。

寫入者解鎖操作:

[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. static inline void arch_write_unlock(arch_rwlock_t *rw)  
  2. {  
  3.     asm volatile(  
  4.     "   stlr    %w1, %0\n"  
  5.     : "=Q" (rw->lock) : "r" (0) : "memory");  
  6. }  
寫操作很簡單,就是將鎖的值全部清為0而已。
 

讀取者加鎖操作:

[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. /* 
  2.  * Read lock implementation. 
  3.  * 
  4.  * It exclusively loads the lock value, increments it and stores the new value 
  5.  * back if positive and the CPU still exclusively owns the location. If the 
  6.  * value is negative, the lock is already held. 
  7.  * 
  8.  * During unlocking there may be multiple active read locks but no write lock. 
  9.  * 
  10.  * The memory barriers are implicit with the load-acquire and store-release 
  11.  * instructions. 
  12.  */  
  13. static inline void arch_read_lock(arch_rwlock_t *rw)  
  14. {  
  15.     unsigned int tmp, tmp2;  
  16.   
  17.     asm volatile(  
  18.     "   sevl\n"  
  19.     "1: wfe\n"  
  20.     "2: ldaxr   %w0, %2\n"  
  21.     "   add %w0, %w0, #1\n"  
  22.     "   tbnz    %w0, #31, 1b\n"  
  23.     "   stxr    %w1, %w0, %2\n"  
  24.     "   cbnz    %w1, 2b\n"  
  25.     : "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)  
  26.     :  
  27.     : "memory");  
  28. }  
讀取者進入臨界區先要判斷是否有write進程在臨界區,如果有必須自旋。如果沒有,則可以進入臨界區。
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. 2:  ldaxr   %w0, %2  
讀取鎖的值,賦值給tmp變量。
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. add %w0, %w0, #1  
將tmp的值加1, 然后將結果放入tmp中。
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. tbnz    %w0, #31, 1b  
判斷tmp[31]是否等於0,不等於0也就是說write進程在臨界區,需要自旋等待,跳到標號1繼續。
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. stxr    %w1, %w0, %2  
將tmp的值復制給lock,然后將結果放入tmp2中。
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. cbnz    %w1, 2b  
判斷tmp2是否等於0,不等於0就跳到標號2繼續。
 
可以看到read操作需要先判斷臨界區是否有write進程存在,如果有就需要自旋。

讀取者解鎖操作:

[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. static inline void arch_read_unlock(arch_rwlock_t *rw)  
  2. {  
  3.     unsigned int tmp, tmp2;  
  4.   
  5.     asm volatile(  
  6.     "1: ldxr    %w0, %2\n"  
  7.     "   sub %w0, %w0, #1\n"  
  8.     "   stlxr   %w1, %w0, %2\n"  
  9.     "   cbnz    %w1, 1b\n"  
  10.     : "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)  
  11.     :  
  12.     : "memory");  
  13. }  
讀取者退出臨界區只需要將鎖的值減1即可。
 
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. 1:  ldxr    %w0, %2  
讀取鎖的值,復制給tmp
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. sub %w0, %w0, #1  
將tmp的值減去1,同時將結果放入到tmp中
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. stlxr   %w1, %w0, %2  
將tmp的值復制給lock,然后將結果存放到tmp2
[cpp]  view plain  copy
 
 
 
 
  在CODE上查看代碼片派生到我的代碼片
  1. cbnz    %w1, 1b  
如果tmp2的值不為0,就跳轉到標號1繼續執行。

小節

從上面的定義可知,lock的是一個unsigned int的32位數。 0-32bit用來表示read thread counter, 31bit用來表示write therad counter。 這樣設計是因為write進程每次進入臨界區只能有一個,所以一個bit就可以。剩余的31bit位全部給read therad使用。
 
從概率上將,當一個進程試圖去寫時,成功獲得鎖的幾率要遠小於讀進程概率。所以在一個讀寫相互依賴的系統中,這種設計會導致讀取者飢餓,也就是沒有數據可讀。所以讀寫鎖使用的系統就是讀操作占用絕大多數,這樣讀寫操作就比以前的spin lock大大提升效率和性能。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM