ARM 架构下 spin_lock 实现

发布于:2025-04-13 ⋅ 阅读:(39) ⋅ 点赞:(0)

阅读该文章前,需要对原子指令有所了解,推荐阅读 聊一聊原子操作和弱内存序

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执行

注:

  1. arch_spinlock_t 是一个联合体,其内部仅包含一个 slock 成员。因此,对 lock->slock 的访问实质上就是对整个 arch_spinlock_t 结构的访问
  2. 在 ARM32 架构下,该自旋锁的实现本质上是一个 基于队列的排队锁(ticket lock)。每次调用 arch_spin_lock() 获取锁时,都会对锁的 next 字段执行原子加一操作。next 的值相当于调用者在等待队列中排到的“号码”
  3. 解锁操作(通常由 arch_spin_unlock() 实现)会对 owner 字段执行原子加一,使得下一个排队等待的线程可以获取锁;
  4. 上面所说的 “原子加一” 实际上是通过原子指令 ldrex、strex实现的

简单理解就是:

  • 首先,从传入的 arch_spinlock_t 结构中读取当前的锁值,拷贝一份,并对其 next 字段执行原子加一操作。这一步为当前调用者分配一个排队编号;
  • 随后,将更新后的锁值写回原始的 arch_spinlock_t 对象中,确保其他等待者可以看到该次编号递增结果;
  • 进入等待阶段:当前线程通过轮询方式不断读取锁的 owner 字段,直到其值等于当前线程获得的 next 编号,表明轮到该线程持有锁,从而退出等待循环。

网站公告

今日签到

点亮在社区的每一天
去签到