分析Linux中Spinlock在ARM及X86平臺上的實現

本文主要以2.6.22.6內核分析Linux中spinlock在ARM及X86平臺上的實現(不同版本的內核實現形式會有一些差異,但原理大致相同)。此處默認大家已經熟悉了spinlock的使用,重點解釋容易引起迷惑的體系結構相關的實現部分。

一、spin_lock(lock)的實現

/***include/linux/spinlock.h中***/

#if defined(CONFIG_SMP) || defined(CONFIG_DEBUG_SPINLOCK)
//如果配置了SMP或配置自旋鎖調試功能
# include
#else //如果是單處理器且不配置自旋鎖調試功能
# include
#endif ……
#define spin_lock(lock) _spin_lock(lock)

1、如果是單處理器

/****include/linux/spinlock_api_up.h****/

#define _spin_lock(lock) __LOCK(lock)
#define __LOCK(lock) \
do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

(1)preempt_disable():禁止搶佔
(2)__acquire(lock):在include/linux/compiler.h中有定義
#ifdef __CHECKER__
……
# define __acquire(x) __context__(x,1)
# define __release(x) __context__(x,-1)
#else
……
# define __acquires(x)
# define __releases(x)

這是一對用於sparse對代碼檢測的相互關聯的函數定義,第一句表示要增加變量x的計數,增加量為1,第二句則正好相反,這個是用來函數編譯的過程中。如果在代碼中出現了不平衡的狀況,那麼在Sparse的檢測中就會報警。如果要使用Sparse檢測功能就需要安裝sparse工具(參考相關安裝方法),然後編譯內核

#make zImage C=1 (C=1,只檢測新編譯的文件,C=2是查所有文件)
Sparse會定義__CHECKER__,如果你沒有使用sparse工具,__acquire(lock)則定義為空

(3)(void)(lock):通過插入一個變量本身的求值表達式,使編譯器不再報警,如:“variable 'lock' is defined but never used”。這種求值不會影響運行時的速度。

2、如果配置了SMP

/****include/linux/spinlock_api_smp.h中****/

void __lockfunc _spin_lock(spinlock_t *lock) __acquires(lock);

/***kernel/spinlock.c***/

void __lockfunc _spin_lock(spinlock_t *lock)
{
preempt_disable();
//關閉搶佔
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
//自旋鎖調試用,在沒有定義自旋鎖調試的時候是空函數
_raw_spin_lock(lock);
}

/***include/linux/spinlock.h***/

#ifdef CONFIG_DEBUG_SPINLOCK
extern void _raw_spin_lock(spinlock_t *lock);//在lib/spinlock_debug.c中實現
#else //smp情況
# define _raw_spin_lock(lock) __raw_spin_lock(&(lock)->raw_lock)

3、__raw_spin_lock在ARM處理器上的實現

/******include/asm-arm/spinlock_types.h***/

typedef struct {
volatile unsigned int lock;
} raw_spinlock_t;

#define __RAW_SPIN_LOCK_UNLOCKED { 0 }

/******include/asm-arm/spinlock.h***/

#if __LINUX_ARM_ARCH__ < 6
#error SMP not supported on pre-ARMv6 CPUs //ARMv6後,才有多核ARM處理器
#endif
……
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
unsigned long tmp;
__asm__ __volatile__(
"1: ldrex %0, [%1]\n"
//取lock->lock放在 tmp裡,並且設置&lock->lock這個內存地址為獨佔訪問
" teq %0, #0\n"
//測試lock_lock是否為0,影響標誌位z
#ifdef CONFIG_CPU_32v6K
" wfene\n"
#endif
" strexeq %0, %2, [%1]\n"
//如果lock_lock是0,並且是獨佔訪問這個內存,就向lock->lock裡寫入1,並向tmp返回0,同時清除獨佔標記
" teqeq %0, #0\n"
//如果lock_lock是0,並且strexeq返回了0,表示加鎖成功,返回
" bne 1b"
//如果上面的條件(1:lock->lock裡不為0,2:strexeq失敗)有一個符合,就在原地打轉
: "=&r" (tmp) //%0:輸出放在tmp裡,可以是任意寄存器
: "r" (&lock->lock), "r" (1)
//%1:取&lock->lock放在任意寄存器,%2:任意寄存器放入1
: "cc"); //狀態寄存器可能會改變


smp_mb();
}

上述代碼關鍵在於LDREX和STREX指令的應用。DREX和STREX指令是在V6以後才出現的,代替了V6以前的swp指令。可以讓bus監控LDREX和STREX指令之間有無其它CPU和DMA來存取過這個地址,若有的話STREX指令的第一個寄存器裡設置為1(動作失敗),若沒有,指令的第一個寄存器裡設置為0(動作成功)。

不僅是自旋鎖用到LDREX和STREX指令,信號量的實現也是利用LDREX和STREX指令來實現的。

4、__raw_spin_lock在X86處理器上的實現

/******include/asm-i386/spinlock_types.h***/

typedef struct {
unsigned int slock;
} raw_spinlock_t;
#define __RAW_SPIN_LOCK_UNLOCKED { 1 }

/******include/asm-i386/spinlock.h***/

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
asm volatile("\n1:\t"
LOCK_PREFIX " ; decb %0\n\t"
// lock->slock減1
"jns 3f\n"
//如果不為負.跳轉到3f.3f後面沒有任何指令,即為退出
"2:\t"
"rep;nop\n\t"
//重複執行nop.nop是x86的小延遲函數
"cmpb $0,%0\n\t"
"jle 2b\n\t"


//如果lock->slock不大於0,跳轉到標號2,即繼續重複執行nop
"jmp 1b\n"
//如果lock->slock大於0,跳轉到標號1,重新判斷鎖的slock成員
"3:\n\t"
: "+m" (lock->slock) : : "memory");
}

decb將lock減一,他前面的lock指令標識在執行decb的時候,要鎖住內存總線,另外的cpu不能訪問內存,注意decb並不是原子操作,他將變量從內存中讀出來放到寄存器,然後減一,在寫入內存,如果這個時候其他的cpu也進行同樣的操作,就會導致decb的結果不確定,也就是說操作的原子性會找到破壞,另外這裡decb不用擔心執行了多少次,因為在unlock的時候,會被直接寫為1。如果decb的結果小於0,標識無法獲取spinlock,如果結果大於等於0表示是已經獲取了spinlock,直接退出。memory強制gcc編譯器假設ram所有內存單元都已經被彙編指令修改,這樣cpu的reg和cache中的數據就會作廢,cpu將不得不在使用數據的時候重新從內存中讀取,這就阻止了cpu又將reg和cache中的數據拿去做優化,而避免訪問內存,這種技術叫做內存屏障,memory barrier。

分析Linux中Spinlock在ARM及X86平臺上的實現


在多處理器環境中 LOCK_PREFIX 實際被定義為 “lock”前綴。x86 處理器使用“lock”前綴的方式提供了在指令執行期間對總線加鎖的手段。芯片上有一條引線 LOCK,如果在一條彙編指令(ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG)前加上“lock” 前綴,經過彙編後的機器代碼就使得處理器執行該指令時把引線 LOCK 的電位拉低,從而把總線鎖住,這樣其它處理器或使用DMA的外設暫時無法通過同一總線訪問內存。

jns 彙編指令檢查 EFLAGS 寄存器的 SF(符號)位,如果為 0,說明 slock 原來的值為 1,則線程獲得鎖,然後跳到標籤 3 的位置結束本次函數調用。如果 SF 位為 1,說明 slock 原來的值為 0 或負數,鎖已被佔用。那麼線程轉到標籤 2 處不斷測試 slock 與 0 的大小關係,假如 slock 小於或等於 0,跳轉到標籤 2 的位置繼續忙等待;假如 slock 大於 0,說明鎖已被釋放,則跳轉到標籤 1 的位置重新申請鎖。

二、spin_unlock(lock)的實現

/***include/linux/spinlock.h***/

#if defined(CONFIG_DEBUG_SPINLOCK) || defined(CONFIG_PREEMPT) || \
!defined(CONFIG_SMP)
# define spin_unlock(lock) _spin_unlock(lock)
……
#else
# define spin_unlock(lock) \


do {__raw_spin_unlock(&(lock)->raw_lock); __release(lock); } while (0)

1、 如果是單處理器

/****include/linux/spinlock_api_up.h****/

#define _spin_unlock(lock) __UNLOCK(lock)
#define __UNLOCK(lock) \
do { preempt_enable(); __release(lock); (void)(lock); } while (0)

完成前文的獲取鎖的逆過程

2、如果配置了SMP

# define spin_unlock(lock) \
do {__raw_spin_unlock(&(lock)->raw_lock); __release(lock); } while (0)

3、__raw_spin_unlock在ARM處理器上的實現

/******include/asm-arm/spinlock.h***/

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
smp_mb();
__asm__ __volatile__(
" str %1, [%0]\n" // 向lock->lock裡寫0,解鎖
#ifdef CONFIG_CPU_32v6K
" mcr p15, 0, %1, c7, c10, 4\n" /* DSB */
" sev"
#endif
:
: "r" (&lock->lock), "r" (0) //%0取&lock->lock放在任意寄存器,%1:任意寄存器放入0
: "cc");
}

__raw_spin_unlock只是簡單的給lock->lock裡寫0。

4、__raw_spin_unlock在X86處理器上的實現

/***include/asm-i386/spinlock.h***/

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
asm volatile("movb $1,%0" : "+m" (lock->slock) :: "memory");
}

__raw_spin_unlock 函數僅僅執行一條彙編指令:將lock-> slock 置為 1。


分享到:


相關文章: