阅读该文章前,需要对原子指令有所了解,推荐阅读 聊一聊原子操作和弱内存序
1、概念
内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:
- 一个是原地等待
- 一个是挂起当前进程,调度其他进程执行(睡眠)
Spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是“原地等待”的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗 CPU 资源)。
2、源码实现
2.1 内联汇编
asm(code : output operand list : input operand list : clobber list);
这种嵌入汇编的形式一共分为四个部分:
- code:汇编的操作代码,一条或者多条指令,如果是多条指令,需要在指令间使用 \n\t 隔开。与通用的汇编代码有一些不同:因为支持 C 变量的操作,所以在操作由第二、三部分提供的操作数时,使用 %n 来替代操作数;
- output operand list:表示输出的操作数,通常是一个或者多个 C 函数中的变量;
- input operand list:表示输入的操作数,通常是一个或者多个 C 函数中的变量;
- clobber list:告诉编译器这段汇编代码会修改哪些寄存器或状态
2.2 ARM32 上 spinlock 实现
arch\arm\include\asm\spinlock.h
#define TICKET_SHIFT 16
typedef struct {
union {
u32 slock;
struct __raw_tickets {
#ifdef __ARMEB__
u16 next;
u16 owner;
#else
u16 owner;
u16 next;
#endif
} tickets;
};
} arch_spinlock_t;
/*
* 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"
" add %1, %0, %4\n"
" strex %2, %1, [%3]\n"
" teq %2, #0\n"
" bne 1b"
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
: "cc");
while (lockval.tickets.next != lockval.tickets.owner) {
wfe();
lockval.tickets.owner = READ_ONCE(lock->tickets.owner);
}
smp_mb();
}
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
smp_mb();
lock->tickets.owner++;
dsb_sev();
}
在内联汇编中,%0、%1、%2… 是自动编号的操作数,它们在 “输出” 和 “输入” 约束中按顺序出现。例如:%0 代指 lockval; %1 代指 newval; %4 代指 (1 << TICKET_SHIFT)…
汇编 | C语言 | 解释 |
---|---|---|
1: ldrex %0, [%3] | lockval = lock | 读取锁的值赋值给 lockval |
add %1, %0, %4 | newval = lockval + (1 << 16) | 将 next++ 之后的值存在 newval 中 |
strex %2, %1, [%3] | lock = newval | 更新 lock 中的值,将是否成功结果存入在 tmp 中 |
teq %2, #0 | if(tmp == 0) | 判断上条指令是否成功,如果不成功执行 ”bne 1b” 跳到标号1执行 |
注:
arch_spinlock_t
是一个联合体,其内部仅包含一个slock
成员。因此,对lock->slock
的访问实质上就是对整个arch_spinlock_t
结构的访问- 在 ARM32 架构下,该自旋锁的实现本质上是一个 基于队列的排队锁(ticket lock)。每次调用
arch_spin_lock()
获取锁时,都会对锁的next
字段执行原子加一操作。next
的值相当于调用者在等待队列中排到的“号码” - 解锁操作(通常由 arch_spin_unlock() 实现)会对 owner 字段执行原子加一,使得下一个排队等待的线程可以获取锁;
- 上面所说的 “原子加一” 实际上是通过原子指令 ldrex、strex实现的
简单理解就是:
- 首先,从传入的 arch_spinlock_t 结构中读取当前的锁值,拷贝一份,并对其 next 字段执行原子加一操作。这一步为当前调用者分配一个排队编号;
- 随后,将更新后的锁值写回原始的 arch_spinlock_t 对象中,确保其他等待者可以看到该次编号递增结果;
- 进入等待阶段:当前线程通过轮询方式不断读取锁的 owner 字段,直到其值等于当前线程获得的 next 编号,表明轮到该线程持有锁,从而退出等待循环。