1. 程式人生 > >spinlock 及原子操作實現詳解

spinlock 及原子操作實現詳解

文章轉自: http://m.blog.csdn.net/arm7star/article/details/77092650

1、自旋鎖結構

typedef struct {
union {
u32 slock;
struct __raw_tickets {
#ifdef __ARMEB__
u16 next; ------ 下一個可以獲取自旋鎖的處理器,處理器請求自旋鎖的時候會儲存該值並對該值加1,然後與owner比較,檢查是否可以獲取到自旋鎖,每請求一次next都加1
u16 owner; ------ 當前獲取到/可以獲取自旋鎖的處理器,沒釋放一次都加1,這樣next與owner就儲存一致


#else
u16 owner;
u16 next;
#endif
} tickets;
};
} arch_spinlock_t;

2、獲取自旋鎖

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" ------ lockval = lock->slock (如果lock->slock沒有被其他處理器獨佔,則標記當前執行處理器對lock->slock地址的獨佔訪問;否則不影響)


" add %1, %0, %4\n"------ newval = lockval + (1 << TICKET_SHIFT)
" strex  %2, %1, [%3]\n" ------ strex tmp, newval, [&lock->slock] (如果當前執行處理器沒有獨佔lock->slock地址的訪問,不進行儲存,返回1;如果當前處理器已經獨佔lock->slock記憶體訪問,則對記憶體進行寫,返回0,清除獨佔標記) lock->tickets.next = lock->tickets.next + 1
" teq %2, #0\n"------ 檢查是否寫入成功lockval.tickets.next

" bne 1b"
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
: "cc");


while (lockval.tickets.next != lockval.tickets.owner) {------ 初始化時lock->tickets.owner、lock->tickets.next都為0,假設第一次執行arch_spin_lock,lockval = *lock,lock->tickets.next++,lockval.tickets.next等於lockval.tickets.owner,獲取到自旋鎖;自旋鎖未釋放,第二次執行的時候,lock->tickets.owner = 0, lock->tickets.next = 1,拷貝到lockval後,lockval.tickets.next != lockval.tickets.owner,會執行wfe等待被自旋鎖釋放被喚醒,自旋鎖釋放時會執行lock->tickets.owner++,lockval.tickets.owner重新賦值
wfe(); ------ 暫時中斷掛起執行,使處理器進入a low-power state等待狀態
lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);------ 重新讀取lock->tickets.owner
}


smp_mb();
}

3、釋放自旋鎖

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
smp_mb();
lock->tickets.owner++; ------ lock->tickets.owner增加1,下一個被喚醒的處理器會檢查該值是否與自己的lockval.tickets.next相等,lock->tickets.owner代表可以獲取的自旋鎖的處理器,lock->tickets.next你一個可以獲取的自旋鎖的owner;處理器獲取自旋鎖時,會先讀取lock->tickets.next用於與lock->tickets.owner比較並且對lock->tickets.next加1,下一個處理器獲取到的lock->tickets.next就與當前處理器不一致了,兩個處理器都與lock->tickets.owner比較,肯定只有一個處理器會相等,自旋鎖釋放時時對lock->tickets.owner加1計算,因此,先申請自旋鎖多處理器lock->tickets.next值更新,自然先獲取到自旋鎖
dsb_sev(); ------ 執行sev指令,喚醒wfe等待的處理器
}

========================

WFE:

     Wait For Event is a hint instruction that permits the processor to enter a low-power state until one of a number of
events occurs,

     Encoding A1 ARMv6K, ARMv7 (executes as NOP in ARMv6T2)
     WFE <c>

========================

LDREX

     Load Register Exclusive calculates an address from a base register value and an immediate offset, loads a word from

memory, writes it to a register and:

•     if the address has the Shared Memory attribute, marks the physical address as exclusive access for the

      executing processor in a global monitor

•     causes the executing processor to indicate an active exclusive access in the local monitor.

==========================

STREX

     Store Register Exclusive calculates an address from a base register value and an immediate offset, and stores a word

from a register to memory if the executing processor has exclusive access to the memory addressed.

ldrex/strex原子操作

前段時間重新研究了一下Linux的併發控制機制,對於核心的自旋鎖、互斥鎖、訊號量等機制及其變體做了底層程式碼上的研究。因為只有從原理上理解了這些機制,在編寫驅動的時候才會記得應該注意什麼。這些機制基本都從程式碼上理解了,但是唯有一個不是非常理解的是核心對於ARM構架中原子變數的底層支援,這個機制其實在自旋鎖、互斥鎖以及讀寫鎖等核心機制中都有類似的使用。這裡將學習的結果寫出,請大家指正。

    假設原子變數的底層實現是由一個彙編指令實現的,這個原子性必然有保障。但是如果原子變數的實現是由多條指令組合而成的,那麼對於SMP和中斷的介入會不會有什麼影響呢?我在看ARM的原子變數操作實現的時候,發現其是由多條彙編指令(ldrex/strex)實現的。在參考了別的書籍和資料後,發現大部分書中對這兩條指令的描訴都是說他們是支援在SMP系統中實現多核共享記憶體的互斥訪問。但在UP系統中使用,如果ldrex/strex和之間發生了中斷,並在中斷中也用ldrex/strex操作了同一個原子變數會不會有問題呢?就這個問題,我認真看了一下核心的ARM原子變數原始碼和ARM官方對於ldrex/strex的功能解釋,總結如下:

一、ARM構架的原子變數實現結構

    對於ARM構架的原子變數實現原始碼位於:arch/arm/include/asm/atomic.h

    其主要的實現程式碼分為ARMv6以上(含v6)構架的實現和ARMv6版本以下的實現。

該檔案的主要結構如下:

  1. #if __LINUX_ARM_ARCH__ >= 6
  2. ......(通過ldrex/strex指令的彙編實現)
  3. #else /* ARM_ARCH_6 */
  4. #ifdef CONFIG_SMP
  5. #error SMP not supported on pre-ARMv6 CPUs
  6. #endif
  7. ......(通過關閉CPU中斷的C語言實現)
  8. #endif /* __LINUX_ARM_ARCH__ */
  9. ...... 
  10.  #ifndef CONFIG_GENERIC_ATOMIC64
  11. ......(通過ldrexd/strexd指令的彙編實現的64bit原子變數的訪問)
  12. #else /* !CONFIG_GENERIC_ATOMIC64 */
  13. #include <asm-generic/atomic64.h>
  14. #endif
  15. #include <asm-generic/atomic-long.h>

      這樣的安排是依據ARM核心指令集版本的實現來做的:

(1)在ARMv6以上(含v6)構架有了多核的CPU,為了在多核之間同步資料和控制併發,ARM在記憶體訪問上增加了獨佔監測(Exclusive monitors)機制(一種簡單的狀態機),並增加了相關的ldrex/strex指令。請先閱讀以下參考資料(關鍵在於理解local monitor和Global monitor):

(2)對於ARMv6以前的構架不可能有多核CPU,所以對於變數的原子訪問只需要關閉本CPU中斷即可保證原子性。 

對於(2),非常好理解。

但是(1)情況,我還是要通過原始碼的分析才認同這種程式碼,以下我僅僅分析最具有代表性的atomic_add原始碼,其他的API原理都一樣。如果讀者還不熟悉C內嵌彙編的格式,請參考ARM GCC 內嵌彙編手冊》

二、核心對於ARM構架的atomic_add原始碼分析

  1. /*
  2. * ARMv6 UP 和 SMP 安全原子操作。 我們是用獨佔載入和
  3. * 獨佔儲存來保證這些操作的原子性。我們可能會通過迴圈
  4. * 來保證成功更新變數。
  5. */
  6. static inline void atomic_add(int i, atomic_t *v)
  7. {
  8. unsigned long tmp;
  9. int result;
  10. __asm__ __volatile__("@ atomic_add\n"
  11. "1: ldrex %0, [%3]\n"
  12. " add %0, %0, %4\n"
  13. " strex %1, %0, [%3]\n"
  14. " teq %1, #0\n"
  15. " bne 1b"
  16. : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
  17. : "r" (&v->counter), "Ir" (i)
  18. : "cc");
  19. }

原始碼分析: 

注意:根據內聯彙編的語法,result、tmp、&v->counter對應的資料都放在了暫存器中操作。如果出現上下文切換,切換機制會做暫存器上下文保護。

(1)ldrex %0, [%3]

意思是將&v->counter指向的資料放入result中,並且(分別在Local monitor和Global monitor中)設定獨佔標誌。

(2)add %0, %0, %4

result = result + i

(3)strex %1, %0, [%3]

意思是將result儲存到&v->counter指向的記憶體中,此時 Exclusive monitors會發揮作用,將儲存是否成功的標誌放入tmp中。

(4) teq %1, #0

測試strex是否成功(tmp == 0 ??)

(5)bne 1b

如果發現strex失敗,從(1)再次執行。

通過上面的分析,可知關鍵在於strex的操作是否成功的判斷上。而這個就歸功於ARM的Exclusive monitors和ldrex/strex指令的機制。以下通過可能的情況分析ldrex/strex指令機制。(請閱讀時參考4.2.12. LDREX  STREX

1、UP系統或SMP系統中變數為非CPU間共享訪問的情況 

    此情況下,僅有一個CPU可能訪問變數,此時僅有Local monitor需要關注。

    假設CPU執行到(2)的時候,來了一箇中斷,並在中斷裡使用ldrex/strex操作了同一個原子變數。則情況如下圖所示:

  • A:處理器標記一個實體地址,但訪問尚未完畢
  • B:再次標記此實體地址訪問尚未完畢(與A重複)
  • C:進行儲存操作,清除以上標記,返回0(操作成功)
  • D:不會進行儲存操作,並返回1(操作失敗) 

也就是說,中斷例程裡的操作會成功,被中斷的操作會失敗重試。 

2、SMP系統中變數為CPU間共享訪問的情況

 

    此情況下,需要兩個CPU間的互斥訪問,此時ldrex/strex指令會同時關注Local monitor和Global monitor。

(i)兩個CPU同時訪問同個原子變數(ldrex/strex指令會關注Global monitor。)

  • A:將該實體地址標記為CPU0獨佔訪問,並清除CPU0對其他任何實體地址的任何獨佔訪問標記。
  • B:標記此實體地址為CPU1獨佔訪問,並清除CPU1對其他任何實體地址的任何獨佔訪問標記。
  • C:沒有標記為CPU0獨佔訪問,不會進行儲存,並返回1(操作失敗)。
  • D:已被標記為CPU1獨佔訪問,進行儲存並清除獨佔訪問標記,並返回0(操作成功)。

也就是說,後執行ldrex操作的CPU會成功。

(ii)同一個CPU因為中斷,“巢狀”訪問同個原子變數(ldrex/strex指令會關注Local monito)

  • A:將該實體地址標記為CPU0獨佔訪問,並清除CPU0對其他任何實體地址的任何獨佔訪問標記。
  • B:再次標記此實體地址為CPU0獨佔訪問,並清除CPU0對其他任何實體地址的任何獨佔訪問標記。
  • C:已被標記為CPU0獨佔訪問,進行儲存並清除獨佔訪問標記,並返回0(操作成功)。
  • D:沒有標記為CPU0獨佔訪問,不會進行儲存,並返回1(操作失敗)。

也就是說,中斷例程裡的操作會成功,被中斷的操作會失敗重試。

(iii)兩個CPU同時訪問同個原子變數,並同時有CPU因中斷“巢狀”訪問改原子變數(ldrex/strex指令會同時關注Local monitor和Global monitor)

雖然對於人來說,這種情況比較BT。但是在飛速執行的CPU來說,BT的事情隨時都可能發生。

  • A:將該實體地址標記為CPU0獨佔訪問,並清除CPU0對其他任何實體地址的任何獨佔訪問標記。
  • B:標記此實體地址為CPU1獨佔訪問,並清除CPU1對其他任何實體地址的任何獨佔訪問標記。
  • C:再次標記此實體地址為CPU0獨佔訪問,並清除CPU0對其他任何實體地址的任何獨佔訪問標記。
  • D:已被標記為CPU0獨佔訪問,進行儲存並清除獨佔訪問標記,並返回0(操作成功)。
  • E:沒有標記為CPU1獨佔訪問,不會進行儲存,並返回1(操作失敗)。
  • F:沒有標記為CPU0獨佔訪問,不會進行儲存,並返回1(操作失敗)。

    當然還有其他許多複雜的可能,也可以通過ldrex/strex指令的機制分析出來。從上面列舉的分析中,我們可以看出:ldrex/strex可以保證在任何情況下(包括被中斷)的訪問原子性。所以核心中ARM構架中的原子操作是可以信任的。