linux 鎖機制 - spinlock

  • 瞭解linux spinlock

1.spinlock

  在linux kernel的實現中,經常會遇到這樣的場景:共享數據被中斷上下文和進程上下文訪問,該如何保護呢?如果只有進程上下文的訪問,那麼可以考慮使用semaphore或者mutex的鎖機制,但是現在中斷上下文也參和進來,那些可以導致睡眠的lock就不能使用了,這時候,可以考慮使用spin lock。

  spinlock又稱自旋鎖,是實現保護共享資源而提出一種鎖機制。自旋鎖與互斥鎖比較類似,都是爲了解決對某項資源的互斥使用。

  無論是互斥鎖,還是自旋鎖,在任何時刻,最多隻能有一個保持者,只能有一個執行單元獲得鎖。但是兩者在調度機制上略有不同。對於互斥鎖,如果資源已經被佔用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。

2.spinlock原理

  跟互斥鎖一樣,一個執行單元要想訪問被自旋鎖保護的共享資源,必須先得到鎖,在訪問完共享資源後,必須釋放鎖。如果在獲取自旋鎖時,沒有任何執行單元保持該鎖,那麼將立即得到鎖;如果在獲取自旋鎖時鎖已經有保持者,那麼獲取鎖操作將自旋在那裏,直到該自旋鎖的保持者釋放了鎖。自旋鎖是一種比較低級的保護數據結構或代碼片段的原始方式,這種鎖可能存在兩個問題:

  • 死鎖
  • 過多佔用cpu資源

spin lock特點如下:

  • spin lock是一種死等的鎖機制。當發生訪問資源衝突的時候,可以有兩個選擇:一個是死等,一個是掛起當前進程,調度其他進程執行。spin lock是一種死等的機制,當前的執行thread會不斷的重新嘗試直到獲取鎖進入臨界區。

  • 只允許一個thread進入。semaphore可以允許多個thread進入,spin lock不行,一次只能有一個thread獲取鎖並進入臨界區,其他的thread都是在門口不斷的嘗試。

  • 執行時間短。由於spin lock死等這種特性,因此它使用在那些代碼不是非常複雜的臨界區(當然也不能太簡單,否則使用原子操作或者其他適用簡單場景的同步機制就OK了),如果臨界區執行時間太長,那麼不斷在臨界區門口“死等”的那些thread是多麼的浪費CPU啊(當然,現代CPU的設計都會考慮同步原語的實現,例如ARM提供了WFE和SEV這樣的類似指令,避免CPU進入busy loop的悲慘境地)

  • 可以在中斷上下文執行。由於不睡眠,因此spin lock可以在中斷上下文中適用。

3.spinlock適用情況

  自旋鎖比較適用於鎖使用者保持鎖時間比較短的情況。正是由於自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高於互斥鎖。

  信號量和讀寫信號量適合於保持時間較長的情況,它們會導致調用者睡眠,因此只能在進程上下文使用,而自旋鎖適合於保持時間非常短的情況,它可以在任何上下文使用。如果被保護的共享資源只在進程上下文訪問,使用信號量保護該共享資源非常合適,如果對共享資源的訪問時間非常短,自旋鎖也可以。但是如果被保護的共享資源需要在中斷上下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖。自旋鎖保持期間是搶佔失效的,而信號量和讀寫信號量保持期間是可以被搶佔的。自旋鎖只有在內核可搶佔或SMP(多處理器)的情況下才真正需要,在單CPU且不可搶佔的內核下,自旋鎖的所有操作都是空操作。另外格外注意一點:自旋鎖不能遞歸使用。

4.場景分析

進程上下文

  考慮下面的場景:

  • 進程A在某個系統調用過程中訪問了共享資源R;

  • 進程B在某個系統調用過程中也訪問了共享資源R;

  假設在A訪問共享資源R的過程中發生了中斷,中斷喚醒了沉睡中的,優先級更高的B,在中斷返回現場的時候,發生進程切換,B啓動執行,並通過系統調用訪問了R,如果沒有鎖保護,則會出現兩個thread進入臨界區,導致程序執行不正確。

  加上spin lock:A在進入臨界區之前獲取了spin lock,同樣的,在A訪問共享資源R的過程中發生了中斷,中斷喚醒了沉睡中的,優先級更高的B,B在訪問臨界區之前仍然會試圖獲取spin lock,這時候由於A進程持有spin lock而導致B進程進入了永久的spin……怎麼解決? 在A進程獲取spin lock的時候,禁止本CPU上的搶佔。

Note:
  上面的永久spin的場合僅僅在本CPU的進程搶佔本CPU的當前進程這樣的場景中發生。如果A和B運行在不同的CPU上,那麼會簡單一些:A進程雖然持有spin lock,而導致B進程進入spin狀態,不過由於運行在不同的CPU上,A進程會持續執行並會很快釋放spin lock,解除B進程的spin狀態。

中斷上下文

  • 運行在CPU0上的進程A在某個系統調用過程中訪問了共享資源R

  • 運行在CPU1上的進程B在某個系統調用過程中也訪問了共享資源R

  • 外設P的中斷handler中也會訪問共享資源R

  在這樣的場景下,使用spin lock可以保護訪問共享資源R的臨界區嗎?

  假設CPU0上的進程A持有spin lock進入臨界區,這時候,外設P發生了中斷事件,並且調度到了CPU1上執行,看起來沒有什麼問題,執行在CPU1上的handler會稍微等待一會CPU0上的進程A,等它離開臨界區就會釋放spin lock的;但是,如果外設P的中斷事件被調度到了CPU0上執行會怎麼樣?CPU0上的進程A在持有spin lock的狀態下被中斷上下文搶佔,而搶佔它的CPU0上的handler在進入臨界區之前仍然會試圖獲取spin lock,悲劇發生了,CPU0上的P外設的中斷handler永遠的進入spin狀態,這時候,CPU1上的進程B也不可避免在試圖持有spin lock的時候失敗而導致進入spin狀態。

  爲了解決這樣的問題,linux kernel採用:如果涉及到中斷上下文的訪問,spin lock需要和禁止本CPU上的中斷聯合使用

  kernel中提供bottom half機制,雖然同屬中斷上下文,不過還是稍有不同。可以把上面的場景簡單修改一下:外設P不是中斷handler中訪問共享資源R,而是在的bottom half中訪問。使用spin lock+禁止本地中斷當然是可以達到保護共享資源的效果,但是隻需要disable bottom half就可以了。

  最後,討論中斷上下文之間的競爭。同一種中斷handler之間在uni core和multi core上都不會並行執行,這是linux kernel的特性。如果不同中斷handler需要使用spin lock保護共享資源,對於新的內核(不區分fast handler和slow handler),所有handler都是關閉中斷的,因此使用spin lock不需要關閉中斷的配合。bottom half又分成softirq和tasklet,同一種softirq會在不同的CPU上併發執行,因此如果某個驅動中的sofirq的handler中會訪問某個全局變量,對該全局變量是需要使用spin lock保護的,不用配合disable CPU中斷或者bottom half。tasklet更簡單,因爲同一種tasklet不會多個CPU上併發,。

5.spinlock的定義以及相應的API

5.1.struct spinlock

include/linux/spinlock_types.h:
   61 typedef struct spinlock {
   62     union {
   63         struct raw_spinlock rlock;                                                                     
   72     };
   73 } spinlock_t;

   20 typedef struct raw_spinlock {
   21     arch_spinlock_t raw_lock;                                                                          
   29 } raw_spinlock_t;

arch/arm/include/asm/spinlock_types.h:
   11 typedef struct {
   12     union {
   13         u32 slock;
   24 } arch_spinlock_t;  //spin lock是和architecture相關的,arch_spinlock是architecture相關的實現

  首先定義一個spinlock_t的數據類型,其本質上是一個整數值(對該數值的操作需要保證原子性),該數值表示spin lock是否可用。初始化的時候被設定爲1。當thread想要持有鎖的時候調用spin_lock函數,該函數將spin lock那個整數值減去1,然後進行判斷,如果等於0,表示可以獲取spin lock,如果是負數,則說明其他thread的持有該鎖,本thread需要spin。

5.2.API
在這裏插入圖片描述
5.3.自旋鎖定義

動態的:
spinlock_t lock;
spin_lock_init (&lock);

靜態的:
DEFINE_SPINLOCK(lock);

5.4.spin_lock

該函數實現:

  • 1.只禁止內核搶佔,不會關閉本地中斷;

  • 2.爲何需要關閉內核搶佔:假如進程A獲得spin_lock->進程B搶佔進程A->進程B嘗試獲取spin_lock->由於進程B優先級比進程A高,先於A運行,而進程B又需要A unlock才得以運行,這樣死鎖。所以這裏需要關閉搶佔。

函數調用流程:

spin_lock()
	\->raw_spin_lock()
		\->__raw_spin_lock
			{
				preempt_disable(); //關閉內核搶佔
				spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); //獲取鎖
				LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);//上鎖
			}
			-----------------------------  arch
			-> arch_spin_lock()
			-> arch_spin_trylock()
  • 調用preempt_disable()來關閉調度。也就是說,運行在一個CPU上的代碼使用spin_lock()試圖加鎖之後,基於該CPU的線程調度和搶佔就被禁止了,這也體現了spinlock作爲"busy loop"形式的鎖的語義。
  • do_raw_spin_lock():和架構相關的arch_spin_lock()。
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock) 
{ 
    __acquire(lock); 
    arch_spin_lock(&lock->raw_lock); 
}

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

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

    while (lockval.tickets.next != lockval.tickets.owner) {------------(5) 
        wfe();-------------------------------(6) 
        lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);------(7) 
    }

    smp_mb();------------------------------(8) 
}
  • (1)和preloading cache相關的操作,主要是爲了性能考慮

  • (2)將slock的值保存在lockval這個臨時變量中

  • (3)將spin lock中的next加一

  • (4)判斷是否有其他的thread插入。

  • (5)判斷當前spin lock的狀態,如果是unlocked,那麼直接獲取到該鎖

  • (6)如果當前spin lock的狀態是locked,那麼調用wfe進入等待狀態。

  • (7)其他的CPU喚醒了本cpu的執行,說明owner發生了變化,該新的own賦給lockval,然後繼續判斷spin lock的狀態,也就是回到step 5。

  • (8)memory barrier的操作。

  假設一個CPU上的線程T持有了一個spinlock,發生中斷後,該CPU轉而執行對應的hardirq。如果該hardirq也試圖去持有這個spinlock,那麼將無法獲取成功,導致hardirq無法退出。在hardirq主動退出之前,線程T是無法繼續執行以釋放spinlock的,最終將導致該CPU上的代碼不能繼續向前運行,形成死鎖(dead lock)。
在這裏插入圖片描述
  爲了防止這種情況的發生,我們需要使用spin_lock_irq()函數,一個spin_lock()和local_irq_disable()的結合體,它可以在spinlock加鎖的同時關閉中斷。

4.5.spin_lock_irq

1 static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)  
2 {  
3         local_irq_disable();  
4         preempt_disable();  
5         spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);  
6         LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);  
7 }  
  • 1.禁止內核搶佔,且關閉本地中斷
  • 2.那麼在spin_lock中關閉了內核搶佔,不關閉中斷會出現什麼情況呢?假如中斷中也想獲得這個鎖,會出現和spin_lock中舉得例子相同。所以這個時候,在進程A獲取lock之後,使用spin_lock_irq將中斷禁止,就不會出現死鎖的情況。
  • 3.在任何情況下使用spin_lock_irq都是安全的。因爲它既禁止本地中斷,又禁止內核搶佔。
  • 4.spin_lock比spin_lock_irq速度快,但是它並不是任何情況下都是安全的。

4.6.spin_lock_irqsave

 1 static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
 2 {
 3     unsigned long flags;
 4 
 5     local_irq_save(flags);
 6     preempt_disable();
 7     spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
 8     /*
 9      * On lockdep we dont want the hand-coded irq-enable of
10      * do_raw_spin_lock_flags() code, because lockdep assumes
11      * that interrupts are not re-enabled during lock-acquire:
12      */
13 #ifdef CONFIG_LOCKDEP
14     LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
15 #else
16     do_raw_spin_lock_flags(lock, &flags);
17 #endif
18     return flags;
19 }
  • 1.禁止內核搶佔,關閉中斷,保存中斷狀態寄存器的標誌位;

  • 2.spin_lock_irqsave在鎖返回時,之前開的中斷,之後也是開的;之前關,之後也是關。但是spin_lock_irq則不管之前的開還是關,返回時都是開的。

  • 3.spin_lock_irq在自旋的時候,不會保存當前的中斷標誌寄存器,只會在自旋結束後,將之前的中斷打開。

  然而local_irq_save()只能對本地CPU執行關中斷操作,所以即便使用了spin_lock_irqsave(),如果其他CPU上發生了中斷,那麼這些CPU上hardirq,也有可能試圖去獲取一個被本地CPU上運行的線程T佔有的spinlock。不過沒有關係,因爲此時hardirq和線程T運行在不同的CPU上,等到線程T繼續運行釋放了這個spinlock,hardirq就有機會獲取到,不至於造成死鎖。

6.Wait for Event mechanism

  A PE can use the Wait for Event (WFE) mechanism to enter a low-power state, depending on the value of an Event Register for that PE. To enter the low-power state, the PE executes a Wait For Event instruction, WFE, and if the Event Register is clear, the PE can enter the low-power state.

  If the PE does enter the low-power state, it remains in that low-power state until it receives a WFE wake-up event.

  The architecture does not define the exact nature of the low-power state, except that the execution of a WFE instruction must not cause a loss of memory coherency.

Spinlock as an example of using Wait For Event and Send Event

  A multiprocessor operating system requires locking mechanisms to protect data structures from being accessed simultaneously by multiple PEs. These mechanisms prevent the data structures becoming inconsistent or corrupted if different PEs try to make conflicting changes. If a lock is busy, because a data structure is being used by one PE, it might not be practical for another PE to do anything except wait for the lock to be released. For example, if a PE is handling an interrupt from a device, it might need to add data received from the device to a queue. If another PE is removing data from the same queue, it will have locked the memory area that holds the queue. The first PE cannot add the new data until the queue is in a consistent state and the second PE has released the lock. The first PE cannot return from the interrupt handler until the data has been added to the queue, so it must wait.

  Typically, a spin-lock mechanism is used in these circumstances:

  • A PE requiring access to the protected data attempts to obtain the lock using single-copy atomic synchronization primitives such as the Load-Exclusive and Store-Exclusive operations.
  • If the PE obtains the lock it performs its memory operation and then releases the lock.
  • If the PE cannot obtain the lock, it reads the lock value repeatedly in a tight loop until the lock becomes available. When the lock becomes available, the PE again attempts to obtain it.

  A spin-lock mechanism is not ideal for all situations:

  • In a low-power system the tight read loop is undesirable because it uses energy to no effect.
  • In a multi-PE implementation the execution of spin-locks by multiple waiting PEs can degrade overall performance.

Using the Wait For Event and Send Event mechanism can improve the energy efficiency of a spinlock:

  • A PE that fails to obtain a lock executes a WFE instruction to request entry to a low-power state, at the time when the exclusive monitor is set holding the address of the location holding the lock.
  • When a PE releases a lock, the write to the lock location causes the exclusive monitor of any PE monitoring the lock location to be cleared. This clearing of the exclusive monitors generates a WFE wake-up event for each of those PEs. Then, these PEs can attempt to obtain the lock again.

refer to

  • https://www.cnblogs.com/aaronLinux/p/5890924.html
  • https://zhuanlan.zhihu.com/p/90634198
  • https://blog.csdn.net/changexhao/article/details/80666823
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章