iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 5
1
自我挑戰組

Linux Inside - Synchronization & Interrupt系列 第 5

Spinlock 原始碼觀摩(—)

參考

相關檔案

Spinlock 可以分成 UP(Uni Processing) 以及 SMP(Symmetric Multi-Processing) 兩個部份來看,這代表兩種不同的架構下,spinlock 的實作會有些不同

如果想要知道每個檔案詳細代表什麼,可以參考這份檔案 include/linux/spinlock.h 裡面的註解,有詳細解釋每個檔案在幹麻,但他的檔案路徑有些有打錯,比如說 asm 其實應該要是 asm-generic

如果是要了解 Debug 的模式,還需要其他檔案,但我們以下完全不探討如何 Debug

資料結構

typedef struct spinlock {
    struct raw_spinlock rlock;
} spinlock_t;

typedef struct raw_spinlock {
    arch_spinlock_t raw_lock;
} raw_spinlock_t;

typedef struct qspinlock {
    atomic_t val;
} arch_spinlock_t;

typedef struct {
    volatile int counter;
}  atomic_t; 

只是一個 volatile int 在 Kernel 中居然需要包這麼多層 ...... 非常的不直覺,也讓人無法理解這命名到底在衝三小

參考 自旋锁spin_lock和raw_spin_lock

上面那篇文章提到,有人提出了一個新的版本,希望在 Critical Section(CS) 中,要使得 spin_lock 允許被搶佔,但 Linux 中已經使用了大量的 spinlock_t,如果要全部的地方都做修改,太過不切實際,所以就將 spinlock_t 改名為 raw_spinlock_t,並將原本的 raw_spinlock_t 改名為 arch_spinlock_t,這樣就可以直接使用 spinlock_t

總之,在那次更新 spinlock 就已經不是教科書上定義的 spinlock 了,在 Linux 中是可以被搶佔的

作者幫我們下了三個結論

  1. 盡可能使用 spin_lock
  2. 在絕對不允許被搶佔的地方使用 raw_spin_lock
  3. 如果 CS 夠小,使用 raw_spin_lock
  • spinlock_t
    允許被搶佔

  • raw_spinlock_t
    不允許被搶佔

  • arch_spinlock_t
    和處理器架構相關的程式碼,會依照架構不同要重新撰寫,如:x86 與 ARM 的差別

API

以下將對應的 Function 放在一起,但只解釋前者的意義

  • spin_lock_init 初始化
  • spin_lock, spin_unlock 獲取 Lock
  • spin_lock_irq, spin_unlock_irq 在 Local Processor 上禁止硬體中斷產生,並獲取指定 Lock
  • spin_lock_irqsave, spin_lock_irqrestore 保存 Local Processor 當前的 irq 狀態,然後在 Local Processor 上禁止硬體中斷產生,並獲取指定 Lock
  • spin_lock_bh, spin_unlock_bh 禁止軟體中斷產生,並獲取指定 Lock
  • spin_trylock 嘗試獲得 Lock ,失敗的話會直接返回,而不是執行 Spin
  • etc.

可以發現有四種上鎖方法 spin_lock, spin_lock_irq, spin_lock_irqsave, spin_lock_bh,只是前置作業不太一樣,但他核心運作原理,都與 spin_lock 相同,所以只需要挑 spin_lock_init, spin_lock 來閱讀,即可知道運作原理

spin_lock_init

#define spin_lock_init(_lock)            \
do {                        \
    spinlock_check(_lock);                \
    raw_spin_lock_init(&(_lock)->rlock);    \
} while (0)

static __always_inline raw_spinlock_t *spinlock_check(spinlock_t *lock)
{
    return &lock->rlock;
}

#define raw_spin_lock_init(lock)        \
do {                        \
    *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock);    \
} while (0)

#define __RAW_SPIN_LOCK_UNLOCKED(lockname)      \
         (raw_spinlock_t) __RAW_SPIN_LOCK_INITIALIZER(lockname)

#define __RAW_SPIN_LOCK_INITIALIZER(lockname)            \
         {                                                      \
             .raw_lock = __ARCH_SPIN_LOCK_UNLOCKED,             \
         }

#define __ARCH_SPIN_LOCK_UNLOCKED       { { .val = ATOMIC_INIT(0) } }

#define ATOMIC_INIT(i)	{ (i) }

這部份很直觀的就能夠理解,最終就是給予一個 0 作為初始值,代表 Unlocked 的意思

spin_lock

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

#define raw_spin_lock(lock)    _raw_spin_lock(lock)

實作都要分成兩個部份來看 UP, SMP,從 _raw_spin_lock 開始產生差異

UP

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

只是單純的禁止搶佔

SMP

#define _raw_spin_lock(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);
}

禁止搶佔後,嘗試獲得 Lock,以及搶佔 Lock

中間涵式

  • preeempt_disable
#define preempt_disable() \
do { \
	preempt_count_inc(); \
	barrier(); \
} while (0)

static __always_inline int preempt_count(void)
{
	return READ_ONCE(current_thread_info()->preempt_count);
}

preempt_count 是一個搶佔用的計數器,但他又不只是搶佔計數器,他同時代表了搶佔計數器、software irq counter, hard irq counter,反正他就是一個 int 數值,透過他是否等於 0 ,來決定是否可以搶佔,而這個數值會放在 thread_info 的架構中,因為一個 Process 在執行的時候,基礎單位就是 Thread 嘛,所以等於你自己帶在身上的意思啦,如果想知道更多 preempt_count 的定義,可以查閱 include/linux/preempt.h 的註解

  • spin_acquire
#define spin_acquire(l, s, t, i)                lock_acquire_exclusive(l, s, t, NULL, i)
#define lock_acquire_exclusive(l, s, t, n, i)           lock_acquire(l, s, t, 0, 1, n, i)
void lock_acquire(struct lockdep_map *lock, unsigned int subclass,
                  int trylock, int read, int check,
                  struct lockdep_map *nest_lock, unsigned long ip)
{
         unsigned long flags;

         if (unlikely(current->lockdep_recursion))
                return;

         raw_local_irq_save(flags);
         check_flags(flags);

         current->lockdep_recursion = 1;
         trace_lock_acquire(lock, subclass, trylock, read, check, nest_lock, ip);
         __lock_acquire(lock, subclass, trylock, read, check,
                        irqs_disabled_flags(flags), nest_lock, ip, 0, 0);
         current->lockdep_recursion = 0;
         raw_local_irq_restore(flags);
}

raw_local_irq_save 就是指 irq_save,但這跟硬體相關,所以再幫他包一層界面,方便不同硬體實作各自的涵式

lockdep 如何偵測 Deadlock,這有一整套機制,等我們了解完 Spinlock 再做研究

__lock_acquire 原始碼在這裡 是關於如何獲得 Lock 的部份,但程式碼太多了,就不全部貼上來看,在檔案註解中有提到幾點

  1. 這個涵式是為了 Spinlock 以及 Mutex 存在
  2. 呼叫這個涵式之前,務必確認已經禁止中斷產生
  3. 程式開頭大量檢查是否有被 Lock
  4. 程式碼中存在大量的數值初始化,在確保參數不為 NULL
// 確認現在深度,但暫時不把深度++
unsigned int depth;
depth = curr->lockdep_depth;

......

// 對 depth 做完各種確認,就可以開始初始化 hlock
struct held_lock *hlock;
hlock = curr->held_locks + depth;

......

// 初始化 hlock 完成,沒啥問題就可以幫 depth++
curr->lockdep_depth++;

經過一連串檢查都沒有問題的話,就可以成功獲得 Lock

  • LOCK_CONTENDED
#define LOCK_CONTENDED(_lock, try, lock)			\
do {								\
	if (!try(_lock)) {					\
		lock_contended(&(_lock)->dep_map, _RET_IP_);	\
		lock(_lock);					\
	}							\
	lock_acquired(&(_lock)->dep_map, _RET_IP_);			\
} while (0)
         
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
        __acquire(lock);
         arch_spin_lock(&lock->raw_lock);
}

#define arch_spin_lock(l)		queued_spin_lock(l)

終於牽扯到 Queued Spinlock 了,見下個小節

PS:本人今明兩天出門不在家,這都是預先寫好的稿,有點水,請見諒


上一篇
Memory Barrier
下一篇
Spinlock 原始碼觀摩(二) & Livelock
系列文
Linux Inside - Synchronization & Interrupt18

尚未有邦友留言

立即登入留言