linux锁机制:ticket spinlock

spinlock 

      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机制

来到主题,为解决上述问题,内核引入ticket spinlock,解决了不公平问题,它是如何做到的?

再举个例子:去过银行都知道,办业务先取张票,票上面有一个编号,每新来一个客户编号会加1,银行显示屏上会显示当前正服务的客户编号。
当有多位客户等待时,银行按照编号顺序来对其进行服务。由此,实现了公平性,这类似于FIFO算法。

ticket spinlock就是采用了这种机制,spinlock的val变量被分割成2部分:
next是发票机最后发出的编号,而owner是正在被服务的编号。

ticket-spinlock.png

typedef struct {
        union {
                atomic_t  val;
                struct __raw_tickets {
                        u16 next;
                        u16 owner;
                } tickets;
        };
} arch_spinlock_t;

ticket spinlock伪代码实现

//加锁
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

linux arm32中spinlock实现:

/*
 * 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,这将带来更大的性能提升。
 

你可能感兴趣的:(linux内核,linux同步)