Linux 下自旋锁 spin_lock、spin_lock_irq 和 spin_lock_irqsave 分析

发布于:2025-06-15 ⋅ 阅读:(14) ⋅ 点赞:(0)

1、spin_lock

static __always_inline void spin_lock(spinlock_t *lock)
{
	raw_spin_lock(&lock->rlock);
}

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);
}

  从代码可以看到,spin_lock 会去关当前核调度,但是不会关当前核中断
  想象这样一个场景,一个外设处理线程正在尝试获取自旋锁 A,而这时外设中断产生,中断处理里面也尝试获取这把锁 A,就会出现死锁的现象。

这里有一个前提,被中断打断的线程,不会再参与操作系统调度。只能等待中断执行结束、恢复中断上下文去执行被打断的线程

  还有一点要注意的是,因为会关闭当前核调度,所以 spin_lock 所保护的临界区禁止调用可能引起睡眠接口。如果自旋锁锁住以后进入睡眠,而此时又不能进行当前核的调度,从而导致该 CPU 被挂起。

2、spin_lock_irq

static __always_inline void spin_lock_irq(spinlock_t *lock)
{
	raw_spin_lock_irq(&lock->rlock);
}

static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
	local_irq_disable();
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

  从代码可以看到,spin_lock_irq 会去不仅关当前核调度还会关当前核中断。这就解决了上面提到的死锁问题。

  但这会带来另一个问题,就是如果自旋锁所保护的临界区执行时间太长,当前核一直处于关中断状态,会极大影响操作系统的实时性。这一点就要求开发人员要注意,自旋锁所保护的临界区执行时间一定不能过长!

  对应的,spin_unlock_irq 会去开当前核中断。这会带来一个问题,如果调用 spin_lock_irq 之前当前核处于关中断状态,而因为你调用了 spin_unlock_irq,把当前核的中断强行打开了,这会带来不可预知的问题!

static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
	local_irq_disable();
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

2、spin_lock_irqsave

#define spin_lock_irqsave(lock, flags)				\
do {								\
	raw_spin_lock_irqsave(spinlock_check(lock), flags);	\
} while (0)

/*
 * If lockdep is enabled then we use the non-preemption spin-ops
 * even on CONFIG_PREEMPTION, because lockdep assumes that interrupts are
 * not re-enabled during lock-acquire (which the preempt-spin-ops do):
 */
#if !defined(CONFIG_GENERIC_LOCKBREAK) || defined(CONFIG_DEBUG_LOCK_ALLOC)

static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
	unsigned long flags;

	local_irq_save(flags);
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
	return flags;
}

  spin_lock_irqsave完美的解决了上面提出的各种问题,是最安全的自旋锁。该函数增加了一个参数,即在关闭当前核中断之前,将当前核的中断状态保存在 flags 变量中。等到需要spin_unlock_irqrestore解锁时,只恢复 flag,并不去强制打开中断。

  下面我们来看下它具体是怎么实现的。

#define raw_local_irq_save(flags)			\
	do {						\
		typecheck(unsigned long, flags);	\
		flags = arch_local_irq_save();		\
	} while (0)

  以 ARM32 架构为例,这是一段内联汇编。

#define arch_local_irq_save arch_local_irq_save
static inline unsigned long arch_local_irq_save(void)
{
	unsigned long flags;

	asm volatile(
		"	mrs	%0, " IRQMASK_REG_NAME_R "	@ arch_local_irq_save\n"
		"	cpsid	i"
		: "=r" (flags) : : "memory", "cc");
	return flags;
}
mrs %0, IRQMASK_REG_NAME_R
  • IRQMASK_REG_NAME_R 是一个宏,在 ARM32 架构下是 cpsr 寄存器。该句汇编的含义是,读取 cpsr 寄存器中当前核的中断状态,保存在 flag 参数中
cpsid	i
  • 该句汇编的含义是,关闭当前 CPU 的本地 IRQ 中断

#define arch_local_irq_restore arch_local_irq_restore
static inline void arch_local_irq_restore(unsigned long flags)
{
	asm volatile(
		"	msr	" IRQMASK_REG_NAME_W ", %0	@ local_irq_restore"
		:
		: "r" (flags)
		: "memory", "cc");
}

static inline void __raw_spin_unlock_irqrestore(raw_spinlock_t *lock,
					    unsigned long flags)
{
	spin_release(&lock->dep_map, _RET_IP_);
	do_raw_spin_unlock(lock);
	local_irq_restore(flags);
	preempt_enable();
}

  从这里看到,spin_unlock_irqrestore 解锁时,不会直接打开当前核中断,只会恢复上一次 cpsr 寄存器的值(也就是入参 flags)。