1. 程式人生 > >linux自旋鎖

linux自旋鎖

一、前言

在linux kernel的實現中,經常會遇到這樣的場景:共享資料被中斷上下文和程序上下文訪問,該如何保護呢?如果只有程序上下文的訪問,那麼可以考慮使用semaphore或者mutex的鎖機制,但是現在中斷上下文也參和進來,那些可以導致睡眠的lock就不能使用了,這時候,可以考慮使用spin lock。本文主要介紹了linux kernel中的spin lock的原理以及程式碼實現。由於spin lock是architecture dependent程式碼,因此,我們在第四章討論了ARM32和ARM64上的實現細節。

注:本文需要程序和中斷處理的基本知識作為支撐。

二、工作原理

1、spin lock的特點

我們可以總結spin lock的特點如下:

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

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

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

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

2、 場景分析

對於spin lock,其保護的資源可能來自多個CPU CORE上的程序上下文和中斷上下文的中的訪問,其中,程序上下文包括:使用者程序通過系統呼叫訪問,核心執行緒直接訪問,來自workqueue中work function的訪問(本質上也是核心執行緒)。中斷上下文包括:HW interrupt context(中斷handler)、軟中斷上下文(soft irq,當然由於各種原因,該softirq被推遲到softirqd的核心執行緒中執行的時候就不屬於這個場景了,屬於程序上下文那個分類了)、timer的callback函式(本質上也是softirq)、tasklet(本質上也是softirq)。

先看最簡單的單CPU上的程序上下文的訪問。如果一個全域性的資源被多個程序上下文訪問,這時候,核心如何交錯執行呢?對於那些沒有開啟preemptive選項的核心,所有的系統呼叫都是序列化執行的,因此不存在資源爭搶的問題。如果核心執行緒也訪問這個全域性資源呢?本質上核心執行緒也是程序,類似普通程序,只不過普通程序時而在使用者態執行、時而通過系統呼叫陷入核心執行,而核心執行緒永遠都是在核心態執行,但是,結果是一樣的,對於non-preemptive的linux kernel,只要在核心態,就不會發生程序排程,因此,這種場景下,共享資料根本不需要保護(沒有併發,談何保護呢)。如果時間停留在這裡該多麼好,單純而美好,在繼續前進之前,讓我們先享受這一刻。

當開啟premptive選項後,事情變得複雜了,我們考慮下面的場景:

(1)程序A在某個系統呼叫過程中訪問了共享資源R

(2)程序B在某個系統呼叫過程中也訪問了共享資源R

會不會造成衝突呢?假設在A訪問共享資源R的過程中發生了中斷,中斷喚醒了沉睡中的,優先順序更高的B,在中斷返回現場的時候,發生程序切換,B啟動執行,並通過系統呼叫訪問了R,如果沒有鎖保護,則會出現兩個thread進入臨界區,導致程式執行不正確。OK,我們加上spin lock看看如何:A在進入臨界區之前獲取了spin lock,同樣的,在A訪問共享資源R的過程中發生了中斷,中斷喚醒了沉睡中的,優先順序更高的B,B在訪問臨界區之前仍然會試圖獲取spin lock,這時候由於A程序持有spin lock而導致B程序進入了永久的spin……怎麼破?linux的kernel很簡單,在A程序獲取spin lock的時候,禁止本CPU上的搶佔(上面的永久spin的場合僅僅在本CPU的程序搶佔本CPU的當前程序這樣的場景中發生)。如果A和B執行在不同的CPU上,那麼情況會簡單一些:A程序雖然持有spin lock而導致B程序進入spin狀態,不過由於執行在不同的CPU上,A程序會持續執行並會很快釋放spin lock,解除B程序的spin狀態。

多CPU core的場景和單核CPU開啟preemptive選項的效果是一樣的,這裡不再贅述。

我們繼續向前分析,現在要加入中斷上下文這個因素。訪問共享資源的thread包括:

(1)執行在CPU0上的程序A在某個系統呼叫過程中訪問了共享資源R

(2)執行在CPU1上的程序B在某個系統呼叫過程中也訪問了共享資源R

(3)外設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上的中斷聯合使用。

linux kernel中提供了豐富的bottom half的機制,雖然同屬中斷上下文,不過還是稍有不同。我們可以把上面的場景簡單修改一下:外設P不是中斷handler中訪問共享資源R,而是在的bottom half中訪問。使用spin lock+禁止本地中斷當然是可以達到保護共享資源的效果,但是使用牛刀來殺雞似乎有點小題大做,這時候disable bottom half就OK了。

最後,我們討論一下中斷上下文之間的競爭。同一種中斷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上併發,具體我就不分析了,大家自行思考吧。

三、通用程式碼實現

1、檔案整理

和體系結構無關的程式碼如下:

(1)include/linux/spinlock_types.h。這個標頭檔案定義了通用spin lock的基本的資料結構(例如spinlock_t)和如何初始化的介面(DEFINE_SPINLOCK)。這裡的“通用”是指不論SMP還是UP都通用的那些定義。

(2)include/linux/spinlock_types_up.h。這個標頭檔案不應該直接include,在include/linux/spinlock_types.h檔案會根據系統的配置(是否SMP)include相關的標頭檔案,如果UP則會include該標頭檔案。這個頭文定義UP系統中和spin lock的基本的資料結構和如何初始化的介面。當然,對於non-debug版本而言,大部分struct都是empty的。

(3)include/linux/spinlock.h。這個標頭檔案定義了通用spin lock的介面函式宣告,例如spin_lock、spin_unlock等,使用spin lock模組介面API的驅動模組或者其他核心模組都需要include這個標頭檔案。

(4)include/linux/spinlock_up.h。這個標頭檔案不應該直接include,在include/linux/spinlock.h檔案會根據系統的配置(是否SMP)include相關的標頭檔案。這個標頭檔案是debug版本的spin lock需要的。

(5)include/linux/spinlock_api_up.h。同上,只不過這個標頭檔案是non-debug版本的spin lock需要的

(6)linux/spinlock_api_smp.h。SMP上的spin lock模組的介面宣告

(7)kernel/locking/spinlock.c。SMP上的spin lock實現。

標頭檔案有些凌亂,我們對UP和SMP上spin lock標頭檔案進行整理:

UP需要的標頭檔案 SMP需要的標頭檔案

linux/spinlock_type_up.h:
linux/spinlock_types.h:
linux/spinlock_up.h:
linux/spinlock_api_up.h:
linux/spinlock.h

asm/spinlock_types.h
linux/spinlock_types.h:
asm/spinlock.h
linux/spinlock_api_smp.h:
linux/spinlock.h

2、資料結構

根據第二章的分析,我們可以基本可以推斷出spin lock的實現。首先定義一個spinlock_t的資料型別,其本質上是一個整數值(對該數值的操作需要保證原子性),該數值表示spin lock是否可用。初始化的時候被設定為1。當thread想要持有鎖的時候呼叫spin_lock函式,該函式將spin lock那個整數值減去1,然後進行判斷,如果等於0,表示可以獲取spin lock,如果是負數,則說明其他thread的持有該鎖,本thread需要spin。

核心中的spinlock_t的資料型別定義如下:

typedef struct spinlock {
        struct raw_spinlock rlock; 
} spinlock_t;

typedef struct raw_spinlock {
    arch_spinlock_t raw_lock;
} raw_spinlock_t;

由於各種原因(各種鎖的debug、鎖的validate機制,多平臺支援什麼的),spinlock_t的定義沒有那麼直觀,為了讓事情簡單一些,我們去掉那些繁瑣的成員。struct spinlock中定義了一個struct raw_spinlock的成員,為何會如此呢?好吧,我們又需要回到kernel歷史課本中去了。在舊的核心中(比如我熟悉的linux 2.6.23核心),spin lock的命令規則是這樣:

通用(適用於各種arch)的spin lock使用spinlock_t這樣的type name,各種arch定義自己的struct raw_spinlock。聽起來不錯的主意和命名方式,直到linux realtime tree(PREEMPT_RT)提出對spinlock的挑戰。real time linux是一個試圖將linux kernel增加硬實時效能的一個分支(你知道的,linux kernel mainline只是支援soft realtime),多年來,很多來自realtime branch的特性被merge到了mainline上,例如:高精度timer、中斷執行緒化等等。realtime tree希望可以對現存的spinlock進行分類:一種是在realtime kernel中可以睡眠的spinlock,另外一種就是在任何情況下都不可以睡眠的spinlock。分類很清楚但是如何起名字?起名字絕對是個技術活,起得好了事半功倍,可維護性好,什麼文件啊、註釋啊都素那浮雲,閱讀程式碼就是享受,如沐春風。起得不好,註定被後人唾棄,或者拖出來吊打(這讓我想起給我兒子起名字的那段不堪回首的歲月……)。最終,spin lock的命名規範定義如下:

(1)spinlock,在rt linux(配置了PREEMPT_RT)的時候可能會被搶佔(實際底層可能是使用支援PI(優先順序翻轉)的mutext)。

(2)raw_spinlock,即便是配置了PREEMPT_RT也要頑強的spin

(3)arch_spinlock,spin lock是和architecture相關的,arch_spinlock是architecture相關的實現

對於UP平臺,所有的arch_spinlock_t都是一樣的,定義如下:

typedef struct { } arch_spinlock_t;

什麼都沒有,一切都是空啊。當然,這也符合前面的分析,對於UP,即便是開啟的preempt選項,所謂的spin lock也不過就是disable preempt而已,不需定義什麼spin lock的變數。

對於SMP平臺,這和arch相關,我們在下一節描述。

3、spin lock介面API

我們整理spin lock相關的介面API如下:

介面API的型別 spinlock中的定義 raw_spinlock的定義
定義spin lock並初始化 DEFINE_SPINLOCK DEFINE_RAW_SPINLOCK
動態初始化spin lock spin_lock_init raw_spin_lock_init
獲取指定的spin lock spin_lock raw_spin_lock
獲取指定的spin lock同時disable本CPU中斷 spin_lock_irq raw_spin_lock_irq
儲存本CPU當前的irq狀態,disable本CPU中斷並獲取指定的spin lock spin_lock_irqsave raw_spin_lock_irqsave
獲取指定的spin lock同時disable本CPU的bottom half spin_lock_bh raw_spin_lock_bh
釋放指定的spin lock spin_unlock raw_spin_unlock
釋放指定的spin lock同時enable本CPU中斷 spin_unlock_irq raw_spin_unock_irq
釋放指定的spin lock同時恢復本CPU的中斷狀態 spin_unlock_irqstore raw_spin_unlock_irqstore
獲取指定的spin lock同時enable本CPU的bottom half spin_unlock_bh raw_spin_unlock_bh
嘗試去獲取spin lock,如果失敗,不會spin,而是返回非零值 spin_trylock raw_spin_trylock
判斷spin lock是否是locked,如果其他的thread已經獲取了該lock,那麼返回非零值,否則返回0 spin_is_locked raw_spin_is_locked

在具體的實現面,我們不可能把每一個介面函式的程式碼都呈現出來,我們選擇最基礎的spin_lock為例子,其他的讀者可以自己閱讀程式碼來理解。

spin_lock的程式碼如下:

static inline void spin_lock(spinlock_t *lock)
{
    raw_spin_lock(&lock->rlock);
}

當然,在linux mainline程式碼中,spin_lock和raw_spin_lock是一樣的,在realtime linux patch中,spin_lock應該被換成可以sleep的版本,當然具體如何實現我沒有去看(也許直接使用了Mutex,畢竟它提供了優先順序繼承特性來解決了優先順序翻轉的問題),有興趣的讀者可以自行閱讀,我們這裡重點看看(本文也主要focus這個主題)真正的,不睡眠的spin lock,也就是是raw_spin_lock,程式碼如下:

#define raw_spin_lock(lock)    _raw_spin_lock(lock)

UP中的實現:

#define _raw_spin_lock(lock)            __LOCK(lock)

#define __LOCK(lock) \
  do { preempt_disable(); ___LOCK(lock); } while (0)

SMP的實現:

void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
    __raw_spin_lock(lock);
}

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    preempt_disable();
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

UP中很簡單,本質上就是一個preempt_disable而已,和我們在第二章中分析的一致。SMP中稍顯複雜,preempt_disable當然也是必須的,spin_acquire可以略過,這是和執行時檢查鎖的有效性有關的,如果沒有定義CONFIG_LOCKDEP其實就是空函式。如果沒有定義CONFIG_LOCK_STAT(和鎖的統計資訊相關),LOCK_CONTENDED就是呼叫do_raw_spin_lock而已,如果沒有定義CONFIG_DEBUG_SPINLOCK,它的程式碼如下:

static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
    __acquire(lock);
    arch_spin_lock(&lock->raw_lock);
}

__acquire和靜態程式碼檢查相關,忽略之,最終實際的獲取spin lock還是要靠arch相關的程式碼實現。

四、ARM平臺的細節

程式碼位於arch/arm/include/asm/spinlock.h和spinlock_type.h,和通用程式碼類似,spinlock_type.h定義ARM相關的spin lock定義以及初始化相關的巨集;spinlock.h中包括了各種具體的實現。

1、回憶過去

在分析新的spin lock程式碼之前,讓我們先回到2.6.23版本的核心中,看看ARM平臺如何實現spin lock的。和arm平臺相關spin lock資料結構的定義如下(那時候還是使用raw_spinlock_t而不是arch_spinlock_t):

typedef struct {
    volatile unsigned int lock;
} raw_spinlock_t;

一個整數就OK了,0表示unlocked,1表示locked。配套的API包括__raw_spin_lock和__raw_spin_unlock。__raw_spin_lock會持續判斷lock的值是否等於0,如果不等於0(locked)那麼其他thread已經持有該鎖,本thread就不斷的spin,判斷lock的數值,一直等到該值等於0為止,一旦探測到lock等於0,那麼就設定該值為1,表示本thread持有該鎖了,當然,這些操作要保證原子性,細節和exclusive版本的ldr和str(即ldrex和strexeq)相關,這裡略過。立刻臨界區後,持鎖thread會呼叫__raw_spin_unlock函式是否spin lock,其實就是把0這個數值賦給lock。

這個版本的spin lock的實現當然可以實現功能,而且在沒有衝突的時候表現出不錯的效能,不過存在一個問題:不公平。也就是所有的thread都是在無序的爭搶spin lock,誰先搶到誰先得,不管thread等了很久還是剛剛開始spin。在衝突比較少的情況下,不公平不會體現的特別明顯,然而,隨著硬體的發展,多核處理器的數目越來越多,多核之間的衝突越來越劇烈,無序競爭的spinlock帶來的performance issue終於浮現出來,根據Nick Piggin的描述:

On an 8 core (2 socket) Opteron, spinlock unfairness is extremely noticable, with a userspace test having a difference of up to 2x runtime per thread, and some threads are starved or “unfairly” granted the lock up to 1 000 000 (!) times.

多麼的不公平,有些可憐的thread需要飢餓的等待1000000次。本質上無序競爭從概率論的角度看應該是均勻分佈的,不過由於硬體特性導致這麼嚴重的不公平,我們來看一看硬體block:

lock

lock本質上是儲存在main memory中的,由於cache的存在,當然不需要每次都有訪問main memory。在多核架構下,每個CPU都有自己的L1 cache,儲存了lock的資料。假設CPU0獲取了spin lock,那麼執行完臨界區,在釋放鎖的時候會呼叫smp_mb invalide其他忙等待的CPU的L1 cache,這樣後果就是釋放spin lock的那個cpu可以更快的訪問L1cache,操作lock資料,從而大大增加的下一次獲取該spin lock的機會。

2、回到現在:arch_spinlock_t

ARM平臺中的arch_spinlock_t定義如下(little endian):

typedef struct {
    union {
        u32 slock;
        struct __raw_tickets {
            u16 owner;
            u16 next;
        } tickets;
    };
} arch_spinlock_t;

本來以為一個簡單的整數型別的變數就搞定的spin lock看起來沒有那麼簡單,要理解這個資料結構,需要了解一些ticket-based spin lock的概念。如果你有機會去九毛九去排隊吃飯(宣告:不是九毛九的飯託,僅僅是喜歡麵食而常去吃而已)就會理解ticket-based spin lock。大概是因為便宜,每次去九毛九總是無法長驅直入,門口的笑容可掬的靚女會給一個ticket,上面寫著15號,同時會告訴你,當前狀態是10號已經入席,11號在等待。

回到arch_spinlock_t,這裡的owner就是當前已經入席的那個號碼,next記錄的是下一個要分發的號碼。下面的描述使用普通的計算機語言和在九毛九就餐(假設九毛九隻有一張餐桌)的例子來進行描述,估計可以讓吃貨更有興趣閱讀下去。最開始的時候,slock被賦值為0,也就是說owner和next都是0,owner和next相等,表示unlocked。當第一個個thread呼叫spin_lock來申請lock(第一個人就餐)的時候,owner和next相等,表示unlocked,這時候該thread持有該spin lock(可以擁有九毛九的唯一的那個餐桌),並且執行next++,也就是將next設定為1(再來人就分配1這個號碼讓他等待就餐)。也許該thread執行很快(吃飯吃的快),沒有其他thread來競爭就呼叫spin_unlock了(無人等待就餐,生意慘淡啊),這時候執行owner++,也就是將owner設定為1(表示當前持有1這個號碼牌的人可以就餐)。姍姍來遲的1號獲得了直接就餐的機會,next++之後等於2。1號這個傢伙吃飯巨慢,這是不文明現象(thread不能持有spin lock太久),但是存在。又來一個人就餐,分配當前next值的號碼2,當然也會執行next++,以便下一個人或者3的號碼牌。持續來人就會分配3、4、5、6這些號碼牌,next值不斷的增加,但是owner巋然不動,直到欠扁的1號吃飯完畢(呼叫spin_unlock),釋放飯桌這個唯一資源,owner++之後等於2,表示持有2那個號碼牌的人可以進入就餐了。 

3、介面實現

同樣的,這裡也只是選擇一個典型的API來分析,其他的大家可以自行學習。我們選擇的是arch_spin_lock,其ARM32的程式碼如下:

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插入。更具體的細節參考Linux核心同步機制之(一):原子操作中的描述

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

(6)如果當前spin lock的狀態是locked,那麼呼叫wfe進入等待狀態。更具體的細節請參考ARM WFI和WFE指令中的描述。

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

(8)memory barrier的操作,具體可以參考memory barrier中的描述。

  arch_spin_lock函式ARM64的程式碼(來自4.1.10核心)如下:

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”-----(A)-----------lockval = lock
“    add    %w1, %w0, %w5\n”-------------newval = lockval + (1 << 16),相當於next++
“    stxr    %w2, %w1, %3\n”--------------lock = newval
“    cbnz    %w2, 1b\n”--------------是否有其他PE的執行流插入?有的話,重來。
    /* Did we get the lock? */
“    eor    %w1, %w0, %w0, ror #16\n”--lockval中的next域就是自己的號碼牌,判斷是否等於owner
“    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”--------------------否則進入spin
“    ldaxrh    %w2, %4\n”----(A)---------其他cpu喚醒本cpu,獲取當前owner值
“    eor    %w1, %w2, %w0, lsr #16\n”---------自己的號碼牌是否等於owner?
“    cbnz    %w1, 2b\n”----------如果等於,持鎖進入臨界區,否者回到2,即繼續spin
    /* 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)Load-Acquire/Store-Release指令的應用。Load-Acquire/Store-Release指令是ARMv8的特性,在執行load和store操作的時候順便執行了memory barrier相關的操作,在spinlock這個場景,使用Load-Acquire/Store-Release指令代替dmb指令可以節省一條指令。上面程式碼中的(A)就標識了使用Load-Acquire指令的位置。Store-Release指令在哪裡呢?在arch_spin_unlock中,這裡就不貼程式碼了。Load-Acquire/Store-Release指令的作用如下:

       -Load-Acquire可以確保系統中所有的observer看到的都是該指令先執行,然後是該指令之後的指令(program order)再執行

       -Store-Release指令可以確保系統中所有的observer看到的都是該指令之前的指令(program order)先執行,Store-Release指令隨後執行

(2)第二個知識點是關於在arch_spin_unlock程式碼中為何沒有SEV指令?關於這個問題可以參考ARM ARM文件中的Figure B2-5,這個圖是PE(n)的global monitor的狀態遷移圖。當PE(n)對x地址發起了exclusive操作的時候,PE(n)的global monitor從open access遷移到exclusive access狀態,來自其他PE上針對x(該地址已經被mark for PE(n))的store操作會導致PE(n)的global monitor從exclusive access遷移到open access狀態,這時候,PE(n)的Event register會被寫入event,就好象生成一個event,將該PE喚醒,從而可以省略一個SEV的指令。

注: 

(1)+表示在嵌入的彙編指令中,該運算元會被指令讀取(也就是說是輸入引數)也會被彙編指令寫入(也就是說是輸出引數)。
(2)=表示在嵌入的彙編指令中,該運算元會是write only的,也就是說只做輸出引數。
(3)I表示運算元是立即數

原創文章,轉發請註明出處。蝸窩科技

Change log:

1、2015/11/5,加入ARM64的程式碼實現部分的分析

2、2015/11/17,增加ARM64程式碼中的兩個知識點的描述

標籤: spin lock 自旋鎖

評論:

fy
2017-11-02 19:58 hi 說到spin lock
說一個老生常談的問題吧。
如果一個臨界區X是有程序上下文和中斷上下文都會訪問的。那按理 spin lock應該做到
1.禁止本地中斷 2.禁止搶佔 3.自旋

那又由於核心搶佔真正能執行的前提是 從一箇中斷返回時(A) 並且當前允許搶佔(B)並且當前中斷開啟(C)。。。
因為上面1.中已經禁止本地中斷了,即使2.禁止搶佔不做也沒關係了。
我個人覺得確實是不需要2.禁止搶佔的。只要1禁止本地中斷和3.自旋 一起就可以達到保護臨界區X
如果真的不加上 2.禁止搶佔,會不會有問題,我想了一些case,暫時沒想到。
當然,我是預設臨界區X內不會主動放棄CPU為前提的。(臨界區也確實不應該睡眠才是)

不知道linuxer對此什麼看法。 回覆 wowo
2017-11-06 17:07 @fy:我有點看不太明白這個問題,不過可以試著說一下我的理解:
spin lock有兩大類:
一類是spin_lock,做的事情就是禁止搶佔+自旋,這樣可以保護那些沒有中斷參與的臨界區,代價也小一些。
一類是spin_lock_irqsave,做的事情是禁止中斷+(禁止搶佔+自旋),這樣在第一類的基礎上也可以保護有中斷參與的臨界區。
你好像在質疑第二類中禁止搶佔的必要性,確實,沒什麼必要。可spin_lock_irqsave要呼叫spin_lock啊,再封出來一個介面?豈不是有點畫蛇添足了? 回覆 fy
2017-11-11 16:08 @wowo:是的。我只是就這裡的“禁止搶佔”非必要,問問wowo的個人理解。
確實是非必要的。
非常感謝。 回覆 LOONG
2017-11-29 13:10 @fy:前來膜拜郭大俠:)
那天看文嘉轉了郭大俠在另一個群裡討論這個問題(關搶佔有沒有必要),覺得受益匪淺,學習了。
當時就想:或者作者當初也並不是一氣呵成呢?這樣定然有commit log可以查詢。試著搜了一下,發現目前的git庫已經沒有相關記錄了。於是上網搜到了這個問題:
https://stackoverflow.com/questions/13263538/linux-kernel-spinlock-smp-why-there-is-a-preempt-disable-in-spin-lock-irq-sm
基本跟郭大俠分析的一樣:)
不知@fy有何理解?或許可以直接問Dave Miller,說不定他跟郭大俠一樣nice:) 回覆
2017-11-30 08:38 @LOONG:多謝支援!問題是一樣的問題,但是在stackoverflow上也沒有得到回答,不過在前幾天的微信群裡,這裡問題已經討論的比較清楚了。 回覆
2017-11-30 08:50 @fy:非常好的問題,非常精彩的思考點。

禁止了中斷的確等於了禁止搶佔,但是並不意味著它們兩個完全等同,因為在preempt disable---preempt enable這個的呼叫過程中,在開啟搶佔的時候有一個搶佔點,核心控制路徑會在這裡檢查搶佔,如果滿足搶佔條件,那麼會立刻排程schedule函式進行程序切換,但是local irq disable---local irq enable的呼叫中,並沒有顯示的搶佔檢查點,當然,中斷有點特殊,因為一旦開啟中斷,那麼pending的中斷會進來,並且在返回中斷點的時候會檢查搶佔,但是也許下面的這個場景就無能為力了。程序上下文中呼叫如下序列:
(1)local irq disable
(2)wake up high level priority task
(3)local irq enable
當喚醒的高優先順序程序被排程到本CPU執行的時候,按理說這個高優先順序程序應該立刻搶佔當前程序,但是這個場景無法做到。在呼叫try_to_wake_up的時候會設定need resched flag並檢查搶佔,但是由於中斷disable,因此不會立刻呼叫schedule,但是在step (3)的時候,由於沒有檢查搶佔,這時候本應立刻搶佔的高優先順序程序會發生嚴重的排程延遲…..直到下一個搶佔點到來。 回覆 wowo
2017-11-30 10:06 @linuxer:這個場景還是挺有意思的,不過話說回來了,spin_lock_xxx的目的是保護臨界區,關preempt的搶佔點什麼事呢?愛搶佔不搶佔啊~
本質上還是spin_lock需要通過開關搶佔進行臨界區保護,到spin_lock_irqxxx的時候,順便做一下,再順便幫preempt一個忙而已。
所以這個場景並不能構成“spin_lock_irqsave關搶佔是否有必要的理由”,最後的答案還是沒必要(因為“保護”的目的已經達到),至於是不是可以增加一個搶佔點,在local_irq_restore的時候,呼叫一下preempt_check_resched豈不是更直接? 回覆 zoro
2017-10-31 17:18 如果CPU0上有個執行緒A獲得了鎖後在執行臨界程式碼,這個時候CPU0上發生了中斷,中斷中也申請同一個鎖,這樣的話這個CPU0豈不是就進入低功耗等待模式了?不能再去做其他事情了。

如果CPU0進入了低功耗等待模式,那麼其他的CPU能不能去釋放這個鎖,同時把CPU0從低功耗等待模式退出呢? 回覆
2017-11-02 12:02 @zoro:你說的場景屬於錯誤的使用了鎖的機制,如果一個臨界區會被中斷和執行緒並非訪問,那麼你需要的核心同步機制是spin lock+disable local irq 回覆 benjoying
2017-07-18 18:08 (1)執行在CPU0上的程序A在某個系統呼叫過程中訪問了共享資源R

(2)執行在CPU1上的程序B在某個系統呼叫過程中也訪問了共享資源R

(3)外設P的bottom half中也會訪問共享資源R

這時候,使用spin lock+禁止本地中斷當然是可以達到保護共享資源的效果,但是disable中斷會影響系統的中斷延遲。在這種場景下,最適合的策略是:使用spin lock+禁止bottom half的方法。
@linuxer, 在這裡有點不太明白,bottom half一般是用來做一些比較費時的事情,那如果把這個disable掉,那這部分如何處理呢,感謝! 回覆
2017-07-19 14:34 @benjoying:一般而言,這種臨界區不會太長,一旦離開臨界區,打開了bottom half的話,delay的bottom half會立刻執行。 回覆 benjoying
2017-07-19 20:37 @linuxer:@linuxer 感謝感謝,之前把這個意思曲解了謝謝! 回覆 michael
2017-04-13 22:02 @linuxer

您的文章中這樣寫到:
最後,我們討論一下中斷上下文之間的競爭。同一種中斷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上併發,具體我就不分析了,大家自行思考吧

下面是我對這段話的分析,幫忙分析是否正確:
(1)中斷handler+spinlock
1.同一種中斷不能併發執行在多個CPU上,因此同一種中斷訪問時不需要spinlock保護.
2.如果兩個中斷handler運行同一個CPU上,由於新的核心disable local irq,因此這種情況不需要spinlock保護.
3.如果兩個中斷handler執行在不同的CPU上,這種情況需要spinlock保護,且呼叫函式spin_lock_irqsave進行保護?
(2)softirq+spinlock
4.同一種softirq可以執行在多個CPU上,因此需要spinlock保護,呼叫哪個API進行保護?
5.不同的softirq可以執行在多個CPU上,因此需要spinlock保護,呼叫哪個API進行保護?
(3)tasklet+spinlock
6.同一種tasklet只能執行在同一個CPU上序列執行,因此不需要spinlock保護.
7.不同的tasklet可以執行在不同的CPU上執行,因此需要進行spinlock保護,呼叫哪個API進行保護?
(4)workqueue+spinlock
workqueue屬於執行緒化,但優先順序仍高於普通程序。
8.同一個work在某一時間點上只能在同一CPU上執行,是不是就不需要spinlock保護?
9.不同的work訪問共享資源時,由於可以執行在不同的CPU上,就需要spinlock保護,呼叫哪個API進行保護? 回覆 michael
2017-04-13 22:07 @michael:追加兩個問題:
1.我的理解,中斷也是一種搶佔,它是一種非同步搶佔,是不是呼叫了preempt_disable禁止了本CPU的搶佔,是不是也disable中斷了?
2.所謂的搶佔,是不是指同一CPU上程序、中斷間爭奪CPU的控制權,而兩個CPU之間,不存在搶佔?也就是說,執行在不同CPU上的兩個執行緒間,不存在搶佔了 回覆 John
2017-08-15 19:28 @michael:同樣追加兩個回答:
1.我的理解,中斷也是一種搶佔,它是一種非同步搶佔,是不是呼叫了preempt_disable禁止了本CPU的搶佔,是不是也disable中斷了?
    A:preempt_disable只是一種軟體方法,達到同步的目的。而disable中斷是一個涉及硬體的行為。所以diable中斷是一個更底層的行為,它的結果可以影響軟體,比如使scheduler沒有辦法執行,更不能執行preempt。
2.所謂的搶佔,是不是指同一CPU上程序、中斷間爭奪CPU的控制權,而兩個CPU之間,不存在搶佔?也就是說,執行在不同CPU上的兩個執行緒間,不存在搶佔了
    A:我的理解是這裡的搶佔指的是進入臨界區。獲得CPU的控制權是手段,目的還是要進入臨界區完成一些操作。 回覆 John
2017-08-15 19:20 @michael:我的理解是linuxer表達的意思是所有的臨界區都是在相同的handler中共享的。比如同種softirq的handler之間或者不同softirq handler之間,他們會訪問全域性的變數。並不考慮softirq handler與硬中斷handler(top handler)之間的臨界區這種情況。
以下是我的一些理解,回答如下:
(1)中斷handler+spinlock
1.同一種中斷不能併發執行在多個CPU上,因此同一種中斷訪問時不需要spinlock保護.
    A:我的理解是你的觀點是正確的。spin lock主要是防止其他的CPU進入臨界區,如果這個臨界區只在這個中斷handler中訪問的話,那麼由於只有一個handler(同一種中斷不能併發執行在多個CPU上),所以不需要spin lock保護。
2.如果兩個中斷handler運行同一個CPU上,由於新的核心disable local irq,因此這種情況不需要spinlock保護.
    A:由於中斷handler執行時是關中斷的,所以不存在兩個中斷handler運行同一個CPU上的情況。
3.如果兩個中斷handler執行在不同的CPU上,這種情況需要spinlock保護,且呼叫函式spin_lock_irqsave進行保護?
    A:兩個中斷handler執行在不同的CPU上,這種情況需要spinlock保護,但是並不需要關閉中斷。因為中斷handler執行時local cpu是關中斷的。所以只需使用spin_lock就行了。
(2)softirq+spinlock
4.同一種softirq可以執行在多個CPU上,因此需要spinlock保護,呼叫哪個API進行保護?
    A:同1,基於linuxer原文的意思,不考慮softirq handler和硬中斷handler臨界區情況,所以只需要spin_lock就可以了。
5.不同的softirq可以執行在多個CPU上,因此需要spinlock保護,呼叫哪個API進行保護?
    A:同4。
(3)tasklet+spinlock
6.同一種tasklet只能執行在同一個CPU上序列執行,因此不需要spinlock保護.
    A:我的理解是你的觀點是正確的。
7.不同的tasklet可以執行在不同的CPU上執行,因此需要進行spinlock保護,呼叫哪個API進行保護?
    A:只需要spin_lock就可以了
(4)workqueue+spinlock
workqueue屬於執行緒化,但優先順序仍高於普通程序。
8.同一個work在某一時間點上只能在同一CPU上執行,是不是就不需要spinlock保護?
    A:我不確定同一個work是否可以同時執行在多個CPU上。如果是的話,必須使用spin lock防止其他CPU干擾。
9.不同的work訪問共享資源時,由於可以執行在不同的CPU上,就需要spinlock保護,呼叫哪個API進行保護?
    A:基於linuxer原文的意思,不考慮work與其他人(softirq handler,thread,top handler)共享臨界區的情況,spin_lock就可以了。 回覆 ron
2017-02-27 18:27 @linuxner: 在arm32中,next和owner初始化都為0, 當第一個thread獲取spin_lock的時候 彙編中 next++, 但是owner仍然為0, 做完彙編之後就會判斷 這兩個值是否相等。
    while (lockval.tickets.next != lockval.tickets.owner) {------------(5)
        wfe();-------------------------------(6)
        lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);------(7)
    }
由於owner從未改變所以此時豈不是一直在迴圈裡出不來嗎? 多謝! 回覆
2017-02-27 19:23 @ron:lock->tickets.owner的值怎麼會不變呢?其他thread終究會unlock從而修改owner的。 回覆 ron
2017-02-28 14:57 @linuxer:謝謝@linuxer,我的意思是 第一次獲取鎖的時候, next和owner初始化都為0, 當進入arch_spin_lock()的時候,next和owner都沒有改變, 但是在彙編裡 “add    %1, %0, %4\n” 此時next++, 後面接著就while 判斷,理論上這時候是unlock的,而且鎖應該是誰申請 誰釋放。
spin_lock ——–> 第一次進入的時候 next++, 而owner未改變,應該會while迴圈
do_citical()
spin_unlock
如果上述情況成立,那其他thread應該先spin_lock申請鎖,此時上面的鎖還在while迴圈。。。。 回覆
2017-03-01 00:21 @ron:也許彙編程式碼影響了你的邏輯判斷,我用c程式碼重新寫過spinlock的過程(當然,這裡無法保證原子操作了,不過我們主要是理解邏輯):
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
    unsigned long tmp;
    u32 newval;
    arch_spinlock_t lockval;

    lockval = lock->slock;—-獲得了next和owner值,
    lock->tickets.next++;—-lock中的next++,lockval不變
    
如果這時候當前的號碼牌(lockval.tickets.next)和owner相等,說明可以持有鎖長驅直入。
    while (lockval.tickets.next != lockval.tickets.owner) {
    wfe();
    lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);—用當前spinlock中的owner值更新lockval中的owner值
    }

    smp_mb();
}
你把lock和lockval這兩個變數搞混了。 回覆 ron
2017-03-01 09:34 @linuxer:@linuxer 明白了, 是我把兩個變數的值混淆了, 一語驚醒夢中人啊… 非常感謝。 回覆 Jeffle
2016-11-17 17:27 你好,我用的核心版本是3.6.10,其中看到 spin_unlock對owner進行修改時並沒有使用strex,而是直接令owner++,我覺得會有一個問題,那就是如果一個程序並不呼叫spin_lock,而是一上來直接呼叫spin_unlock,這裡就存在一個bug,
作者的意思是“only one CPU can be performing an unlock() operation for a given lock, this doesn’t need to be exclusive”,是不是說作者認為這種bug應該由程式設計者自己來解決,而不是自旋鎖的機制本身來解決呢?

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
-    unsigned long tmp;
-    u32 slock;
-
    smp_mb();
-
-    __asm__ __volatile__(
-“    mov    %1, #1\n”
-“1:    ldrex    %0, [%2]\n”
-“    uadd16    %0, %0, %1\n”
-“    strex    %1, %0, [%2]\n”
-“    teq    %1, #0\n”
-“    bne    1b”
-    : “=&r” (slock), “=&r” (tmp)
-    : “r” (&lock->slock)
-    : “cc”);
-
+    lock->tickets.owner++;
    dsb_sev();
} 回覆 hony
2017-04-30 11:00 @Jeffle:應該是由使用者保證的,就像使用互斥鎖時使用不當就會產生死鎖一樣道理。 回覆 randy
2016-09-08 16:24 請問下,spinlock的使用限制,除了不能阻塞性操作、耗時太久,critical section中是不是也不能開中斷?對於spinlock_irq而言,內部實現為關local irq,如果critical section中開了中斷是不是就會有問題?如果是的話,為啥很多資料裡面沒有提到這點呢? 回覆
2016-09-09 09:00 @randy:spinlock的介面有多個,例如spin_lock、spin_lock_bh、spin_lock_irq,不同的場景,使用不同的介面。對於spin_lock而言,其實是沒有關閉本地中斷的需求的,因此,在進入臨界區的時候,中斷是開啟的,如果在臨界區有對中斷的操作也是OK的。對於spin_lock_irq,其臨界區是關中斷的,因此,在臨界區內當然不能開啟中斷,這是常識,因此沒有必要提出。 回覆 randy
2016-09-09 09:30 @linuxer:瞭解, linuxer,thanks 回覆
2016-04-27 19:54 新的核心這裡改變比較大,確實改善了公平性,保證了先來先得。
但是next和owner都是16 bits,最大也就是65536,如果next overflow,但是owner不變的話,可能有兩個thread可以同時獲得lock。不過就目前的CPU硬體規模,(如最大cores數目100?)應該不會有問題了,就是說沒有65536個cpu thread去競爭這個spin lock了。
當然如果next和owner同時overflow的話也自然也不會有問題,只要不相差65536以上。 回覆 hony
2017-04-30 11:04 @simonzhang:#if (CONFIG_NR_CPUS < 256)
typedef u8  __ticket_t;
typedef u16 __ticketpair_t;
#else
typedef u16 __ticket_t;
typedef u32 __ticketpair_t;
#endif
從作者的定義看cpu數不會超過16位。 回覆 zhr2130
2016-03-02 17:59 @linuxer:問一個比較low但一直困擾我的問題。

關於spin lock(或其他同步機制如訊號量,互斥鎖),是如何和它要保護的共享資源(或臨界區)關聯起來的。是根據spin lock定義的位置麼。比如要用spin lock保護一個共享的struct,那麼在這個struct中新增一個spin lock的element應該是可行的。放在struct外面可以麼。另外對於其它型別的共享資源,如何和spin lock關聯呢?

看了很多參考資料,都只是介紹在進入臨界區時要獲得spin lock,卻沒有說這個spin lock應該放在哪。難道這個spin lock 可以隨便定義在任何位置麼??

非常感謝 回覆