Linux內核中的幾種自旋鎖的實現

前言

自旋鎖是爲實現保護共享資源而提出一種鎖機制。自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。

本文主要介紹了linux kernel中的spin lock 各個版本的優化與實現,包括spinlock, tickect spinlock, mcs spinlock, qspinlock.

1. 傳統的spinlock

原理:
當某個處理器上的內核執行線程申請自旋鎖時,如果鎖可用,則獲得鎖,然後執行臨界區操作,最後釋放鎖;如果鎖已被佔用,線程並不會轉入睡眠狀態,而是忙等待該鎖,一旦鎖被釋放,則第一個感知此信息的線程將獲得鎖。

實現:
raw_spinlock_t 數據結構

typedef struct {
    unsigned int slock;
} raw_spinlock_t;

傳統的自旋鎖本質上用一個整數來表示,值爲1代表鎖未被佔用, 爲0或者爲負數表示被佔用。

在單處理機環境中可以使用特定的原子級彙編指令swap和test_and_set實現進程互斥,(Swap指令:交換兩個內存單元的內容;test_and_set指令取出內存某一單元(位)的值,然後再給該單元(位)賦一個新值) 這些指令涉及對同一存儲單元的兩次或兩次以上操作,這些操作將在幾個指令週期內完成,但由於中斷只能發生在兩條機器指令之間,而同一指令內的多個指令週期不可中斷,從而保證swap指令或test_and_set指令的執行不會交叉進行.
在多處理機環境中情況有所不同,例如test_and_set指令包括“取”、“送”兩個指令週期,兩個CPU執行test_and_set(lock)可能發生指令週期上的交叉,假如lock初始爲0, CPU1和CPU2可能分別執行完前一個指令週期並通過檢測(均爲0),然後分別執行後一個指令週期將lock設置爲1,結果都取回0作爲判斷臨界區空閒的依據,從而不能實現互斥.
爲在多CPU環境中利用test_and_set指令實現進程互斥,硬件需要提供進一步的支持,以保證test_and_set指令執行的原子性. 這種支持目前多以“鎖總線”(bus locking)的形式提供的,由於test_and_set指令對內存的兩次操作都需要經過總線,在執行test_and_set指令之前鎖住總線,在執行test_and_set指令後開放總線,即可保證test_and_set指令執行的原子性。
用法如下:
多處理機互斥算法(自旋鎖算法)

do{
	b=1;
	while(b) {
		lock(bus);
		b = test_and_set(&lock);
		unlock(bus);
	}
	臨界區
	lock = 0;
	其餘部分
}while(1)

linux內核中鎖的實現:
加鎖:

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
	__asm__ __volatile__(
		__raw_spin_lock_string
		:"=m" (lock->slock) : : "memory");
}

解鎖:

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

不足:
由於傳統自旋鎖無序競爭的本質特點,內核執行線程無法保證何時可以取到鎖,某些執行線程可能需要等待很長時間,導致“不公平”問題的產生。
這有兩方面的原因:

  1. 隨着處理器個數的不斷增加,自旋鎖的競爭也在加劇,自然導致更長的等待時間。
    釋放自旋鎖時的重置操作將無效化所有其它正在忙等待的處理器的緩存,那麼在處理器拓撲結構中臨近自旋鎖擁有者的處理器可能會更快地刷新緩存,因而增大獲得自旋鎖的機率。
  2. 由於每個申請自旋鎖的處理器均在全局變量 slock 上忙等待,系統總線將因爲處理器間的緩存同步而導致繁重的流量,從而降低了系統整體的性能。

2. ticket spinlock

原理:
Linux 內核 2.6.25 版本中引入了排隊自旋鎖:通過保存執行線程申請鎖的順序信息來解決“不公平”問題。

排隊自旋鎖仍然使用原有的 raw_spinlock_t 數據結構,但是賦予 slock 域新的含義。爲了保存順序信息,slock 域被分成兩部分,分別保存鎖持有者和未來鎖申請者的票據序號(Ticket Number),如下圖所示:
在這裏插入圖片描述

只有 Next 域與 Owner 域相等時,才表明鎖處於未使用狀態(此時也無人申請該鎖)。排隊自旋鎖初始化時 slock 被置爲 0,即 Owner 和 Next 置爲 0。內核執行線程申請自旋鎖時,原子地將 Next 域加 1,並將原值返回作爲自己的票據序號。如果返回的票據序號等於申請時的 Owner 值,說明自旋鎖處於未使用狀態,則直接獲得鎖;否則,該線程忙等待檢查 Owner 域是否等於自己持有的票據序號,一旦相等,則表明鎖輪到自己獲取。線程釋放鎖時,原子地將 Owner 域加 1 即可,下一個線程將會發現這一變化,從忙等待狀態中退出。線程將嚴格地按照申請順序依次獲取排隊自旋鎖,從而完全解決了“不公平”問題。

實現:
排隊自旋鎖沒有改變原有自旋鎖的調用接口。
ticket spinlock數據結構

typedef struct arch_spinlock {
	union {
		__ticketpair_t head_tail;
		struct __raw_tickets {
			__ticket_t head, tail;
		} tickets;
	};
} arch_spinlock_t;

申請自旋鎖時,原子地將tail加1,釋放時,head加1。只有head域和tail域的值相等時,才表明鎖處於未使用的狀態。

加鎖:

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    asm volatile("\n1:\t"
             LOCK_PREFIX " ; decb %0\n\t"
             "jns 3f\n"
             "2:\t"
             "rep;nop\n\t"
             "cmpb $0,%0\n\t"
             "jle 2b\n\t"
             "jmp 1b\n"
             "3:\n\t"
             : "+m" (lock->slock) : : "memory");
}

解鎖:

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

不足:
在大規模多處理器系統和 NUMA系統中,排隊自旋鎖(包括傳統自旋鎖)存在一個比較嚴重的性能問題:由於執行線程均在同一個共享變量 slock 上自旋,申請和釋放鎖的時候必須對 slock 進行修改,這將導致所有參與排隊自旋鎖操作的處理器的緩存變得無效。如果排隊自旋鎖競爭比較激烈的話,頻繁的緩存同步操作會導致繁重的系統總線和內存的流量,從而大大降低了系統整體的性能。

3. mcs spinlock

原理:
核心思想是:每個鎖的申請者(處理器)只在一個本地變量上自旋。MCS Spinlock是其中一種基於鏈表結構的自旋鎖。

MCS Spinlock的設計目標如下:
保證自旋鎖申請者以先進先出的順序獲取鎖(FIFO Ordering)。
只在本地可訪問的標誌變量上自旋。
在處理器個數較少的系統中或鎖競爭並不激烈的情況下,保持較高性能。
自旋鎖的空間複雜度(即鎖數據結構和鎖操作所需的空間開銷)爲常數。
在沒有處理器緩存一致性協議保證的系統中也能很好地工作。

MCS Spinlock採用鏈表結構將全體鎖申請者的信息串成一個單向鏈表,如圖 1 所示。每個鎖申請者必須提前分配一個本地結構 mcs_lock_node,其中至少包括 2 個域:本地自旋變量 waiting 和指向下一個申請者 mcs_lock_node 結構的指針變量 next。waiting 初始值爲 1,申請者自旋等待其直接前驅釋放鎖;爲 0 時結束自旋。而自旋鎖數據結構 mcs_lock 是一個永遠指向最後一個申請者 mcs_lock_node 結構的指針,當且僅當鎖處於未使用(無任何申請者)狀態時爲 NULL 值。MCS Spinlock 依賴原子的“交換”(swap)和“比較-交換”(compare_and_swap)操作,缺乏後者的話,MCS Spinlock 就不能保證以先進先出的順序獲取鎖,從而可能造成“飢餓”(Starvation)。

圖 1. MCS Spinlock 示意圖
在這裏插入圖片描述
實現:
MCS spinlock接口有2個版本,
版本1:每個鎖有NR_CPUS大的node數組, mcs_lock_node 結構可以在處理器所處節點的內存中分配,從而加快訪問速度.

typedef struct _mcs_lock_node {
    volatile int waiting;
    struct _mcs_lock_node *volatile next;
} ____cacheline_aligned_in_smp mcs_lock_node;
 
typedef mcs_lock_node *volatile mcs_lock;
 
typedef struct {
    mcs_lock slock;
    mcs_lock_node nodes[NR_CPUS];
} raw_spinlock_t;

spin_lock(&lock)
spin_unlock(&lock)
版本2:
spin_lock(&lock, &node);
spin_unlock(&lock, &node);

加鎖:

static __always_inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    int cpu;
    mcs_lock_node *me;
    mcs_lock_node *tmp;
    mcs_lock_node *pre;
     
    cpu = raw_smp_processor_id();                                 (a)
    me = &(lock->nodes[cpu]);
    tmp = me;
    me->next = NULL;
 
    pre = xchg(&lock->slock, tmp);                              (b)
    if (pre == NULL) {
        /* mcs_lock is free */
        return;                                                 (c)
    }
 
    me->waiting = 1;                                               (d)
    smp_wmb();                                                      (e)
    pre->next = me;                                                (f)
     
    while (me->waiting) {                                         (g)    
        asm volatile (“pause”);
    }   
}

try_lock

static __always_inline int __raw_spin_trylock(raw_spinlock_t *lock)
{
    int cpu;
    mcs_lock_node *me;
 
    cpu = raw_smp_processor_id();
    me = &(lock->nodes[cpu]);
    me->next = NULL;
     
    if (cmpxchg(&lock->slock, NULL, me) == NULL)             (a)
        return 1;
    else
        return 0;
}

解鎖:

static __always_inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
    int cpu;
    mcs_lock_node *me;
    mcs_lock_node *tmp;
     
    cpu = raw_smp_processor_id();
    me = &(lock->nodes[cpu]);
    tmp = me;
 
    if (me->next == NULL) {                                      (a)
        if (cmpxchg(&lock->slock, tmp, NULL) == me) {   (b)
            /* mcs_lock I am the last. */
            return;
        }
        while (me->next == NULL)                            (c)
            continue;
    }
 
    /* mcs_lock pass to next. */
    me->next->waiting = 0;                                       (d)
}

不足:
版本1的mcs spinlock 鎖佔用空間大
版本二的mcs spinlock 使用時需要傳入mode, 和之前的spinlock api不兼容,無法替換ticket spinlock.

4. qspinlock

原理:
qspinlock 是內核4.x引入的,主要基於mcs spinlock的設計思想,解決了mcs spinlock接口不一致或空間太大的問題。
它的數據結構體比mcs lock大大減小, 同ticket spinlock一樣大小。qspinlock的等待變量是全局變量。

實現:
數據結構:

qspinlock的數據結構定義在kernel/qspinlock.c中
struct __qspinlock {
	union {
		atomic_t val;
#ifdef __LITTLE_ENDIAN
		struct {
			u8	locked;
			u8	pending;
		};
		struct {
			u16	locked_pending;
			u16	tail;
		};
#else
		struct {
			u16	tail;
			u16	locked_pending;
		};
		struct {
			u8	reserved[2];
			u8	pending;
			u8	locked;
		};
#endif

可以看到qspinlock 就是一個原子變量,但是在實際使用中卻將這個原子變量分成很多位域
具體位域如下:

/*
 * Bitfields in the atomic value:
 *
 * When NR_CPUS < 16K
 *  0- 7: locked byte
 *     8: pending
 *  9-15: not used
 * 16-17: tail index
 * 18-31: tail cpu (+1)
 *
 * When NR_CPUS >= 16K
 *  0- 7: locked byte
 *     8: pending
 *  9-10: tail index
 * 11-31: tail cpu (+1)
 */

queued_spin_lock:

static __always_inline void queued_spin_lock(struct qspinlock *lock)
{
        u32 val;
 
        val = atomic_cmpxchg_acquire(&lock->val, 0, _Q_LOCKED_VAL);
        if (likely(val == 0))
                return;
        queued_spin_lock_slowpath(lock, val);
}

在大規模多處理器系統和 NUMA系統中, 使用qspinlock 可以較好的提高鎖的性能。

5. 鎖性能的比較

寫一個spinlock的性能測試驅動,在等待相同時間後比較spinlock 臨界區域的值, 從而比較各個鎖的性能差異。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/kernel/h>
#include <linux/spinlock.h>
#include <linux/random.h>
#include <linux/slab.h>
#incude <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/atomic.h>

int spinlock_num;

struct worker {
	int burns;
	struct task_struct *task;
}
static struct worker *workers;
static int threads = 2;
module_param(threads, int, 0);

static spinlock_t lock;
static int runtime = 10;
module_param(runtime, int, 0);

static int bench_running;
static task_struct *monitor_task;

static int rerun, done;
module_param(rerun, int, S_IRUGO|S_ISUSR);
module_param(done, int, S_IRUGO|S_ISUSR);

static int work(void *data)
{
	struct worker *wk = (struct worker*)arg;
	while(!kthread_should_stop()) {
		cond_resched();
	
	if (!ACCESS_ONCE(bench_running))
		continue;
	spin_lock(&lock)
	spinlock_num++;
	spin_unlock(&lock);
	}
	return 0;
}

static int monitor(void *unused)
{
	int i, c;
	int total, min, max, avg;
	
repeat:
	total = 0, min = INT_MAX, max = 0, avg = 0;
	
	spinlock_num = 0;
	
	workers = (struct worker *)kzalloc(sizeof(struct worker) * threads, GFP_KERNEL);
	for (i = 0; i < threads; i++) {
		c = i %num_online_cpus();
		workers[i].task = kthread_create(work, &workers, "locktest/%d:%d", c, i);
		kthread_bind(workers[i].task, c);
		wake_up_process(workers[i].task);
	}
	bench_running = 0;
	for (i = 0; i < threads; i++) {
		if (workers[i].task)
			kthread_stop(workers[i].task);
	}
	kfree(workers);
	printk("lockresult:%6d %8d %12d\n", num_online_cpus(), threads, spinlock_num);
	done = 1;
	while(!kthread_should_stop()) {
		schedule_timeout(1);
		if (cmpxchg(&rerun, done, 0)) {
			done = 0;
			goto repeat;
		}
	}
	return 0;
}

static int locktest_init(void)
{
	monitor_task = kthread_run(monitor, NULL, "monitor");
	return 0;
}

static void locktest_exit(void)
{
	kthread_stop(monitor_task);
}

module_init(locktest_init);
module_exit(locktest_exit);
MODULE_LICENSE("GPL");

從測試結果來看, 在cpu較少的情況下, qspinlock的性能和ticket spinlock的性能差不多, 在CPU較多的情況下,qspinlock的性能遠好於ticket spinlock.

6. 參考資料

  1. 自旋鎖-百度百科
  2. Linux 內核的排隊自旋鎖(FIFO Ticket Spinlock)
  3. 高性能自旋鎖 MCS Spinlock 的設計與實現
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章