Linux自旋鎖

加鎖(locking)是一種廣泛應用的同步技術。當內核控制路徑必須訪問共享數據結構或進入臨界區時,就需要爲自己獲取一把“鎖”。由鎖機制保護的資源非常類似於限制於房間內的資源,當某人進入房間時,就把門鎖上。如果內核控制路徑希望訪問資源,就試圖獲取鑰匙“打開門”。當且僅當資源空閒時,它才能成功。然後,只要它還想使用這個資源,門就依然鎖着。當內核控制路徑釋放了鎖時,門就打開,另一個內核控制路徑就可以進入房間。

Linux鎖的應用之一在多處理器環境中,取名叫自旋鎖(spin lock)。如果內核控制路徑發現自旋鎖“開着”,就獲取鎖並繼續自己的執行。相反,如果內核控制路徑發現鎖由運行在另一個CPU上的內核控制路徑“鎖着”,就在周圍“旋轉”,反覆執行一條緊湊的循環指令,直到鎖被釋放。

自旋鎖的循環指令表示“忙等”。即使等待的內核控制路徑無事可做(除了浪費時間),它也在CPU上保持運行。不過,自旋鎖通常非常方便,因爲很多內核資源只鎖1毫秒的時間片段;所以說,等待自旋鎖的釋放不會消耗太多CPU的時間。

一般來說,由自旋鎖所保護的每個臨界區都是禁止內核搶佔的。在單處理器系統上,這種鎖本身並不起鎖的作用,自旋鎖技術僅僅是用來禁止或啓用內核搶佔。請注意,在自旋鎖忙等期間,因爲並沒有進入臨界區,所以內核搶佔還是有效的,因此,等待自旋鎖釋放的進程有可能被更高優先級的所取代。這種設計是合理的,因爲不能因爲佔用CPU太久而使系統死鎖。

在Linux中,每個自旋鎖都用spinlock_t結構表示:
typedef struct {
   raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
   unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
   unsigned int magic, owner_cpu;
   void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
   struct lockdep_map dep_map;
#endif
} spinlock_t;

typedef struct {
   volatile unsigned int slock;
} raw_spinlock_t;

其中包含兩個重要的字段意義如下:

slock:該字段表示自旋鎖的狀態:值爲1表示“未加鎖”狀態,而任何負數和0都表示“加鎖”狀態。
break_lock:表示進程正在忙等自旋鎖(只在內核支持SMP和內核搶佔的情況下使用該標誌)。

內核提供六個宏用於初始化、測試及設置自旋鎖。所有這些宏都是基於原子操作的,這樣可以保證即使有多個運行在不同CPU上的進程試圖同時修改自旋鎖,自旋鎖也能夠被正確地更新。

1、spin_lock_init —— 初始化自旋鎖,並把自旋鎖的lock->raw_lock置爲1(未鎖)

# define spin_lock_init(lock)                    \
do {                                \
   static struct lock_class_key __key;            \
                               \
   __spin_lock_init((lock), #lock, &__key);        \
} while (0)

void __spin_lock_init(spinlock_t *lock, const char *name,
             struct lock_class_key *key)
{
#ifdef CONFIG_DEBUG_LOCK_ALLOC
   /*
    * Make sure we are not reinitializing a held lock:
    */
   debug_check_no_locks_freed((void *)lock, sizeof(*lock));
   lockdep_init_map(&lock->dep_map, name, key, 0);
#endif
   lock->raw_lock = (raw_spinlock_t)__RAW_SPIN_LOCK_UNLOCKED;
   lock->magic = SPINLOCK_MAGIC;
   lock->owner = SPINLOCK_OWNER_INIT;
   lock->owner_cpu = -1;
}

#define __RAW_SPIN_LOCK_UNLOCKED    { 1 }
#define SPINLOCK_MAGIC        0xdead4ead
#define SPINLOCK_OWNER_INIT    ((void *)-1L)

2、spin_unlock —— 把自旋鎖置爲1(未鎖)

#if defined(CONFIG_DEBUG_SPINLOCK) || defined(CONFIG_PREEMPT) || \
   !defined(CONFIG_SMP)
# define spin_unlock(lock)        _spin_unlock(lock)
#else //我們還是重點關注後面的吧
# define spin_unlock(lock)        __raw_spin_unlock(&(lock)->raw_lock)
#endif

void __lockfunc _spin_unlock(spinlock_t *lock)
{
   spin_release(&lock->dep_map, 1, _RET_IP_);
   _raw_spin_unlock(lock);
   preempt_enable();
}

# define _raw_spin_unlock(lock)        __raw_spin_unlock(&(lock)->raw_lock)

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
   __asm__ __volatile__(
       __raw_spin_unlock_string
   );
}

#define __raw_spin_unlock_string \
   "movb $1,%0" \
       :"+m" (lock->slock) : : "memory"

spin_unlock宏釋放以前獲得的自旋鎖,上面的代碼本質上執行下列彙編語言指令:
movb $1, slp->slock
並在隨後調用preempt_enable()(如果不支持內核搶佔,preempt_enable()什麼都做)。注意,因爲現在的80x86微處理器總是原子地執行內存中的只寫訪問,所以不用lock字節。

3、spin_unlock_wait —— 等待,直到自旋鎖變爲1(未鎖)

#define spin_unlock_wait(lock)    __raw_spin_unlock_wait(&(lock)->raw_lock)
#define __raw_spin_unlock_wait(lock) \
   do { while (__raw_spin_is_locked(lock)) cpu_relax(); } while (0)
#define __raw_spin_is_locked(x) \
       (*(volatile signed char *)(&(x)->slock) <= 0) //如果大於0則爲真,表示未鎖,則跳出while循環
#define cpu_relax()    rep_nop() //在循環中執行一條空指令:
static inline void rep_nop(void)
{
   __asm__ __volatile__("rep;nop": : :"memory");
}

4、spin_is_locked( ) —— 如果自旋鎖被置爲1(未鎖),返回0;否則,返回1

#define spin_is_locked(lock)    __raw_spin_is_locked(&(lock)->raw_lock)
#define __raw_spin_is_locked(x) \
       (*(volatile signed char *)(&(x)->slock) <= 0)

5、spin_trylock( ) —— 把自旋鎖置爲0(鎖上),如果原來鎖的值是1,則返回1;否則,返回0
#define spin_trylock(lock)        __cond_lock(_spin_trylock(lock))
int __lockfunc _spin_trylock(spinlock_t *lock)
{
   preempt_disable();
   if (_raw_spin_trylock(lock)) {
       spin_acquire(&lock->dep_map, 0, 1, _RET_IP_);
       return 1;
   }

   preempt_enable();
   return 0;
}

6、spin_lock —— 加鎖:循環,直到自旋鎖變爲1(未鎖),然後,把自旋鎖置爲0(鎖上)

spin_lock是最重要的一個宏。首先,我們看到在include/spinlock.h頭文件裏,有:

#if defined(CONFIG_SMP) || defined(CONFIG_DEBUG_SPINLOCK)
# include <linux/spinlock_api_smp.h>  //多處理器情況
#else
# include <linux/spinlock_api_up.h>   //單處理器情況
#endif

#define spin_lock(lock)            _spin_lock(lock)
#ifdef __LINUX_SPINLOCK_API_UP_H  
#define _spin_lock(lock)            __LOCK(lock)   //單處理器情況
#else  
注意,該代碼上邊有一句#if !defined(CONFIG_PREEMPT) || !defined(CONFIG_SMP) || \
   defined(CONFIG_DEBUG_LOCK_ALLOC)
別去管它,因爲上邊的英文註釋寫得很清楚了,這句代碼的意思是即使沒有定義內核搶佔或SMP,或者是自旋鎖調試,只要lockdep激活了,也就是我們在剛纔spinlock_t定義的那裏看到的#ifdef CONFIG_DEBUG_LOCK_ALLOC,都會假設在整個鎖的調試期間保持關中斷。這句#if就是這個意思,千萬別理解成沒有定義內核搶佔、SMP或自旋鎖調試了,切記切記。
/*
* If lockdep is enabled then we use the non-preemption spin-ops
* even on CONFIG_PREEMPT, because lockdep assumes that interrupts are
* not re-enabled during lock-acquire (which the preempt-spin-ops do):
*/
//多處理器情況,並且允許內核搶佔:
void __lockfunc _spin_lock(spinlock_t *lock)
{
   //禁止搶佔
   preempt_disable();
   //這個函數在沒有定義自旋鎖調試的時候是空函數,我們不去管它
   spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
   //相當於_raw_spin_lock(lock)
   LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock);
}

在沒有定義自旋鎖調試的時候,LOCK_CONTENDED宏定義如下
#define LOCK_CONTENDED(_lock, try, lock) \
   lock(_lock)

我們看到其實就是調用_raw_spin_lock宏(位於include/linux/spinlock.h):
# define _raw_spin_lock(lock)        __raw_spin_lock(&(lock)->raw_lock)

於是,定位到include/asm-i386/Spinlock.h
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
   asm(__raw_spin_lock_string : "+m" (lock->slock) : : "memory");
}

展開:
#define __raw_spin_lock_string \
   "\n1:\t" \
   //原子減,如果不爲負,跳轉到3f,3f後面沒有任何指令,即爲退出
   LOCK_PREFIX " ; decb %0\n\t" \    
   "jns 3f\n" \
   "2:\t" \
   //重複執行nop,nop是x86的小延遲函數,執行空操作
   "rep;nop\n\t" \
   //比較0與lock->slock的值,如果lock->slock不大於0,跳轉到標號2,即繼續重複執行nop
   "cmpb $0,%0\n\t" \
   "jle 2b\n\t" \
   //如果lock->slock大於0,跳轉到標號1,重新判斷鎖的slock成員
   "jmp 1b\n" \
   "3:\n\t"

在上面的函數中,大家可能對"jmp 1b\n“比較難以理解。在我們一般的觀念裏,獲得一個鎖,將其值減1;釋放鎖時將其值加1;實際上在自旋鎖的實現中lock->slock只有兩個可能值,一個是0. 一個是1。釋放鎖的時候並不是將lock->slock加1,而是將其賦爲1。請看在前面的自旋鎖釋放代碼spin_unlock中的詳細分析。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章