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 的设计与实现
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章