spinlock即自旋锁,在linux内核中广泛运用的底层同步机制,相对于可睡眠锁,spinlock采用spin的方式获取锁(busy-wait),
避免了context_switch的开销,在短暂临界区访问场景下速度明显提升,性能更高,对memory等critical section互斥访问,发挥着重要作用。
spinlock对内核的数据安全性和并发性能有很大的影响。这些年来,内核开发者在自旋锁的实现上做了大量优化工作。
早期内核下,如linux2.6.24,自旋锁spinlock用一个整数值来表示,表明了锁是否可用,初值设为1。
spin_lock()函数通过递减val(原子方式),然后查看是否为0,若为0则成功拿锁,若为负数则代表锁已属于他人,所以它进入spin状态,不断查询val值直到变为1。当锁的拥有者完成critical section的执行,将val置为1,即释放锁。
显而易见,这种方式拿锁非常快,尤其是当没有锁竞争的时候,性能非常不错。不过这种方法有一个缺点:它是不公平的。
当锁的onwer释放锁后,锁的等待者需要发起竞争,这种机制没有办法保证等待时间最长的CPU能优先获得锁,并且激烈的竞争增加了额外的总线开销。
事实上,刚刚释放锁的那个处理器,由于拥有高速缓存原因,很大概率会优先拿到锁,同样无法保证锁的公平性,所以某些场景下spinlock会带来性能损失、实时性降低。
所以在该机制下,我们很难确保一个CPU从申请拿锁到真实获取锁的延迟时间,极端情况下,拿锁的时间可能是任意长。某些高要求实时性的业务场景是不能容忍的。
举个生活中的例子:
spinlock好比火车上上厕所,很多人同时竞争一个厕所,而恰巧你吃了不干净的东西,很捉急,若没有公平性,后果是灾难性的。
来到主题,为解决上述问题,内核引入ticket spinlock,解决了不公平问题,它是如何做到的?
再举个例子:去过银行都知道,办业务先取张票,票上面有一个编号,每新来一个客户编号会加1,银行显示屏上会显示当前正服务的客户编号。
当有多位客户等待时,银行按照编号顺序来对其进行服务。由此,实现了公平性,这类似于FIFO算法。
ticket spinlock就是采用了这种机制,spinlock的val变量被分割成2部分:
next是发票机最后发出的编号,而owner是正在被服务的编号。
typedef struct {
union {
atomic_t val;
struct __raw_tickets {
u16 next;
u16 owner;
} tickets;
};
} arch_spinlock_t;
//加锁
spin_lock(lock *l)
{
int n = atomic_add(1, l.next); //先拿票
while(n != atomic_read(l.owner) + 1) //查看是否轮到自己
cpu_relax();
}
//放锁
spin_unlock(lock *l)
{
atomic_add(1, l.owner); //服务完成,叫下一号
}
假设有8个线程同时竞争锁,如下示例:now_serving代表正在持锁的线程,next_ticket代表最后到来的线程,各线程会按顺序进行拿锁。
now_serving next_ticket
| |
V V
... 0 1 2 3 4 5 6 7 8 9 10 ...
\_________/
8 threads
holding one ticket each
/*
* ARMv6 ticket-based spin-locking.
*
* A memory barrier is required after we get a lock, and before we
* release it, because V6 CPUs are assumed to have weakly ordered
* memory.
*/
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
u32 newval;
arch_spinlock_t lockval;
prefetchw(&lock->slock);
__asm__ __volatile__(
"1: ldrex %0, [%3]\n" /* 原子方式,读取锁的值赋值给lockval */
" add %1, %0, %4\n" /* 将next字段++之后的值存在newval中 */
" strex %2, %1, [%3]\n" /* 原子方式,将新的值存在lock中,写入否成功结果存入在tmp中 */
" teq %2, #0\n" /* 判断是否写入成功,不成功跳到标号1重新执行 */
" bne 1b"
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
: "cc");
/* 查询是否可以拿锁,若next != owner说明已有人持锁,自旋 */
while (lockval.tickets.next != lockval.tickets.owner) {
wfe();
lockval.tickets.owner = READ_ONCE(lock->tickets.owner);
}
smp_mb();
}
/* 释放锁比较简单,将owner++即可 */
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
smp_mb();
lock->tickets.owner++;
dsb_sev();
}
ticket spinlock自旋锁出现之前,所有处理器争夺一个锁先看谁能抢到它。现在他们排队等好,获取锁的顺序到达。
虽然ticket spinlock看起来很cool,但已然是过去时了,在较高版本内核版本中spinlock再次变身,升级为queued spinlock,这将带来更大的性能提升。