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版本以下的實現。
該檔案的主要結構如下:
- #if __LINUX_ARM_ARCH__ >= 6
- ......(通過ldrex/strex指令的彙編實現)
- #else /* ARM_ARCH_6 */
- #ifdef CONFIG_SMP
- #error SMP not supported on pre-ARMv6 CPUs
- #endif
- ......(通過關閉CPU中斷的C語言實現)
- #endif /* __LINUX_ARM_ARCH__ */
- ......
- #ifndef CONFIG_GENERIC_ATOMIC64
- ......(通過ldrexd/strexd指令的彙編實現的64bit原子變數的訪問)
- #else /* !CONFIG_GENERIC_ATOMIC64 */
- #include <asm-generic/atomic64.h>
- #endif
- #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原始碼分析
- /*
- * ARMv6 UP 和 SMP 安全原子操作。 我們是用獨佔載入和
- * 獨佔儲存來保證這些操作的原子性。我們可能會通過迴圈
- * 來保證成功更新變數。
- */
- static inline void atomic_add(int i, atomic_t *v)
- {
- unsigned long tmp;
- int result;
- __asm__ __volatile__("@ atomic_add\n"
- "1: ldrex %0, [%3]\n"
- " add %0, %0, %4\n"
- " strex %1, %0, [%3]\n"
- " teq %1, #0\n"
- " bne 1b"
- : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
- : "r" (&v->counter), "Ir" (i)
- : "cc");
- }
原始碼分析:
注意:根據內聯彙編的語法,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構架中的原子操作是可以信任的。