前言
随着SMP处理器核越来越多
自旋锁是一种简单的轻量级的锁机制,理论上来说自旋锁可以小到一位,该位表示锁的状态,获取锁的线程尝试使用原子指令比较并交换该位,当锁不可用时反复自旋。自旋锁忙等待的机制会浪费处理器资源,但与让线程睡眠的锁相比节省了两次上下文切换的开销,自旋锁特别适用临界区短且不能睡眠的情况。网上关于自旋锁的资料非常多,本文是一篇对自旋锁进行考古的文章。
正文:自旋锁考古
早期的自旋锁
数据结构spinlock_t
早期的内核中自旋锁的设计很简单,下面是Kernel 2.6.0中的自旋锁结构spinlock_t,可以看到仅仅是一个unsigned int类型的lock。
1
2
3
4
5
6typedef struct {
volatile unsigned int lock;
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned magic;
#endif
} spinlock_t;
spin_lock_init
自旋锁的初始化是将lock置1。
1
2
3#define SPIN_LOCK_UNLOCKED (spinlock_t) { 1 SPINLOCK_MAGIC_INIT }
#define spin_lock_init(x)do { *(x) = SPIN_LOCK_UNLOCKED; } while(0)
spin_lock
在CONFIG_SMP并且CONFIG_PREEMPT的时候,spin_lock如下。
单CPU或者没有打开内核抢占的情况比较简单暂不分析
1
2
3
4
5
6
7#if defined(CONFIG_SMP) && defined(CONFIG_PREEMPT)
#define spin_lock(lock) \
do { \
preempt_disable(); \
if (unlikely(!_raw_spin_trylock(lock))) \
__preempt_spin_lock(lock); \
} while (0)
上面的代码首先调用preempt_disable()关抢占,然后调用_raw_spin_trylock(lock)尝试加锁,函数中内嵌汇编的代码把lock->lock的值交换给oldval,把值0交换给lock->lock。如果oldval>0说明lock是未加锁状态,函数返回真,否则加锁失败返回假。
内嵌汇编语法:http://www.ethernut.de/en/documents/arm-inline-asm.html
1
2
3
4
5
6
7
8
9static inline int _raw_spin_trylock(spinlock_t *lock)
{
char oldval;
__asm__ __volatile__(
"xchgb %b0,%1"
:"=q" (oldval), "=m" (lock->lock)
:"0" (0) : "memory");
return oldval > 0;
}
我们回到spin_lock函数来看,如果_raw_spin_trylock成功那么if语句为假,反之trylock失败if为真才会调用__preempt_spin_lock(lock),其中的do-while循环就是自旋锁的自旋操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14void __preempt_spin_lock(spinlock_t *lock)
{
if (preempt_count() > 1) {
_raw_spin_lock(lock);
return;
}
do {
preempt_enable(); //开抢占,自旋过程允许其他线程抢占
while (spin_is_locked(lock)) //循环判断自旋锁状态
cpu_relax();
//自旋锁被释放处于未加锁状态,退出while循环,关抢占再次_raw_spin_trylock(lock)抢锁
preempt_disable();
} while (!_raw_spin_trylock(lock));
}
spin_unlock
spin_unlock逻辑很简单其实就是将lock值设置为1,然后开启抢占。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20#define spin_unlock(lock) \
do { \
_raw_spin_unlock(lock); \
preempt_enable(); \
} while (0)
static inline void _raw_spin_unlock(spinlock_t *lock)
{
char oldval = 1;
#ifdef CONFIG_DEBUG_SPINLOCK
if (lock->magic != SPINLOCK_MAGIC)
BUG();
if (!spin_is_locked(lock))
BUG();
#endif
__asm__ __volatile__(
spin_unlo
ck_string
);
}
不足
随着SMP处理器核越来越多,早期的自旋锁争用中不公平现象是很明显的。CPU0上的任务a持锁,其他CPU上的任务c等自旋等待,此时锁l所在各CPU的L1 Cache的Cache line状态是共享Shared (S),当任务a释放锁,将锁l的值写为1,这时候CPU0的Cache状态是已修改Modified (M),而其他CPU Cache状态是无效Invalid (I),如果此时CPU0上的任务b决定快速获取锁l,由于b与刚刚释放锁的a在同一处理器,b可以凭借拥有该高速缓存行而具有优势,对自旋等待更久的锁是不公平的。
Ticket spinlockOn an 8 core (2 socket) Opteron, spinlock unfairness is extremely noticable, with a userspace test having a difference of up to 2x runtime per thread, and some threads are starved or “unfairly” granted the lock up to 1 000 000 (!) times.
根据Nick Piggin在 8 core环境中的测试,某些线程被饥饿旋转1 000 000 次,如果core数更多这种不公平现象恐怕会更严重,从公平的角度来讲,拿锁的顺序也应该讲一个先来后到的原则,Nick在2.6.25 内核中引入了Ticket Spinlock。
[^1]: 详见 https://lwn.net/Articles/267968/
Ticket spinlock的设计思想是排队,把早期自旋锁中的lock拆分成owner和next两部分,owner表示当前持锁的排队号,next表示下一个来拿锁的发的号码,类似于我们去银行窗口办事,我们每个人办事之前都要取号(next),窗口也会显示当前正在服务的号码(owner)。
数据结构spinlock_t
下面基于3.10版本的内核分析Ticket spinlock,在不考虑config debug等等的情况下,spinlock_t其实是对raw_spinlock又套了一层(为了兼容rt分支),raw_spinlock中是arch_spinlock_t,arch_spinlock_t与体系结构相关,以x86为例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16typedef struct spinlock {
struct raw_spinlock rlock;
} spinlock_t;
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
} raw_spinlock_t;
typedef struct arch_spinlock {
union {
__ticketpair_t head_tail;
struct __raw_tickets {
__ticket_t head, tail;
} tickets;
};
} arch_spinlock_t;
spin_lock_init
初始化将raw_lock置0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14#define spin_lock_init(_lock)\
do {\
spinlock_check(_lock);\
raw_spin_lock_init(&(_lock)->rlock);\
} while (0)
# define raw_spin_lock_init(lock)\
do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0)
#define __RAW_SPIN_LOCK_INITIALIZER(lockname)\
{\
.raw_lock = __ARCH_SPIN_LOCK_UNLOCKED,\
SPIN_DEBUG_INIT(lockname)\
SPIN_DEP_MAP_INIT(lockname) }
#define __ARCH_SPIN_LOCK_UNLOCKED{ { 0 } }
spin_lock
可以看到spin_lock会调用__raw_spin_lock。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18static inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock)_raw_spin_lock(lock)
void __lockfunc _raw_spin_lock(raw_spinlock_t *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);
}
而在没有define自旋锁debug的时候__raw_spin_lock其实简化成下面的代码,首先关抢占,最终会调用arch_spin_lock。
1
2
3
4
5
6
7
8
9
10
11
12static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
do { } while (0) //spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
do_raw_spin_lock(lock) //LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
__acquire(lock);
arch_spin_lock(&lock->raw_lock);
}
x86的arch_spin_lock如下,首先inc.tail = 2(递增步长为2),然后xadd将tickets的值赋给inc,并将tickets.tail增加2,头尾相等说明unlock状态,可以直接拿锁,否则开始自旋。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25#define __TICKET_LOCK_INC2
static __always_inline void arch_spin_lock(arch_spinlock_t *lock)
{
// inc.tail = 2
register struct __raw_tickets inc = { .tail = TICKET_LOCK_INC };
//tickets的值赋给inc,然后将tickets.tail增加2
inc = xadd(&lock->tickets, inc);
//头尾相等说明unlock状态,可以直接拿锁
if (likely(inc.head == inc.tail))
goto out;
inc.tail &= ~TICKET_SLOWPATH_FLAG;
for (;;) {
unsigned count = SPIN_THRESHOLD;
//自旋,循环判断head、tail是否相等
do {
if (ACCESS_ONCE(lock->tickets.head) == inc.tail)
goto out;
cpu_relax();
} while (--count);
__ticket_lock_spinning(lock, inc.tail);
}
out:barrier();/* make sure nothing creeps before the lock is taken */
}
spin_unlock
与spin_lock类似,spin_unlock最后调用__raw_spin_unlock,do_raw_spin_unlock解锁之后开抢占。arch_spin_unlock函数if中与虚拟化有关,不考虑这种情况arch_spin_unlock只是将tickets.head增加TICKET_LOCK_INC。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
spin_release(&lock->dep_map, 1, _RET_IP_);
do_raw_spin_unlock(lock);
preempt_enable();
}
static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
{
arch_spin_unlock(&lock->raw_lock);
__release(lock);
}
static __always_inline void arch_spin_unlock(arch_spinlock_t *lock)
{
if (TICKET_SLOWPATH_FLAG &&
static_key_false(¶virt_ticketlocks_enabled)) {
arch_spinlock_t prev;
prev = *lock;
add_smp(&lock->tickets.head, TICKET_LOCK_INC);
/* add_smp() is a full mb() */
if (unlikely(lock->tickets.tail & TICKET_SLOWPATH_FLAG))
__ticket_unlock_slowpath(lock, prev);
} else
__add(&lock->tickets.head, TICKET_LOCK_INC, UNLOCK_LOCK_PREFIX);
}
不足
与早期简单的spinlock相比Ticket spinlock更公平,但是还是存在性能问题。Cache问题需要考虑,锁争用会导致大量的Cache颠簸,如果多个CPU之间重复获取自旋锁,则会有多个其他CPU不断查询其值,锁所在Cache line状态为Shared而不是Exclusive,对该Cache line的数据进行修改将其他CPU导致Cache未命中,如果锁不是独占一个Cache line还会影响同一Cache line中的其他数据,因此锁争用会大大降低系统性能。
MCS spinlock
内核开发者Tim Chen在内核中引入了MCS spinlock,通过让每个CPU在自己本地的自旋锁结构体变量上自旋,能够避免绝大部分的cache颠簸。
mcs_spinlock包含一个next指针和一个整形变量用于表示当前锁的状态。
1
2
3
4struct mcs_spinlock {
struct mcs_spinlock *next;
int locked; //0表示unlock
};
MCS锁会初始化一个全局的mcs_spinlock结构体,全局结构体next指针初始为NULL。当CPU 0取锁时,申请一个自己的msc_spinlock结构体,用原子指令将本地结构体地址与全局mcs_spinlock的next指针交换,交换后CPU 0得到的指针为空,说明当前可以持锁。在CPU 0持锁的过程中,CPU 1来取锁,CPU 1申请一个自己的msc_spinlock结构体,使用原子指令将本地msc_spinlock结构体地址与全局mcs_spinlock的next指针交换,此时CPU 1得到的是CPU 0的mcs_spinlock结构体地址,表示锁当前不可用,然后CPU 1会在CPU 0的结构体的next域中写入自己的mcs_spinlock结构体地址,然后在本地的mcs_spinlock上自旋。这样每一个每取锁CPU都需要申请自己的结构体,全局的mcs_spinlock结构体的next指针永远指向锁等待队列的队尾。
解锁的过程是这样的,当一个CPU要解锁时执行一条“有条件交换”的原子指令,如果next中的值依旧是自己的结构体地址,表示当前无其他CPU等锁,那么就将全局锁的next域置空,锁被释放。否则,修改next指向的结构体中locked域的值。,下一个CPU locked的值变化后,便会停止自旋。
如果不理解可以看 https://lwn.net/Articles/590243/
MCS并没有完全替代Ticket spinlock,其中一个原因是MCS的数据结构大于32bit,内核中很多重要的结构体中都使用了spinlock,其中一些(典型的如struct page)的体积是不允许变大的。
Queue spinlock
Queue spinlock是对大体积MSC锁的一种优化,qspinlock只占有32bit,qspinlock比较复杂,在下一篇文章中单独分析。
联系我
您可以直接在下方直接留言并留下您的邮箱,或者E-Mail联系我。