linux kernel的spin_lock的详细介绍(以arm64为例)

1、spin_lock的调用流程:

static __always_inline void spin_lock(spinlock_t *lock)
{
	raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock)	_raw_spin_lock(lock)
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)		__acquires(lock);
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
	__raw_spin_lock(lock);
}
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);
}
static inline int do_raw_spin_trylock(raw_spinlock_t *lock)
{
	return arch_spin_trylock(&(lock)->raw_lock);
}

static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
{
	arch_spin_unlock(&lock->raw_lock);  //调用到arch体系相关代码
	__release(lock);
}
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned int tmp;
	arch_spinlock_t lockval, newval;

	asm volatile(
	/* Atomically increment the next ticket. */
	ARM64_LSE_ATOMIC_INSN(
	/* LL/SC */
"	prfm	pstl1strm, %3\n"     //cache相关指令
"1:	ldaxr	%w0, %3\n"
"	add	%w1, %w0, %w5\n"
"	stxr	%w2, %w1, %3\n"
"	cbnz	%w2, 1b\n",
	/* LSE atomics */
"	mov	%w2, %w5\n"
"	ldadda	%w2, %w0, %3\n"
"	nop\n"
"	nop\n"
"	nop\n"
	)

	/* Did we get the lock? */
"	eor	%w1, %w0, %w0, ror #16\n"
"	cbz	%w1, 3f\n"
	/*
	 * No: spin on the owner. Send a local event to avoid missing an
	 * unlock before the exclusive load.
	 */
"	sevl\n"
"2:	wfe\n"          //&&&& 让core进入low-power state
"	ldaxrh	%w2, %4\n"
"	eor	%w1, %w2, %w0, lsr #16\n"
"	cbnz	%w1, 2b\n"            //&&&& 这一段是一个循环,也就是自旋等待
	/* We got the lock. Critical section starts here. */
"3:"
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
	: "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
	: "memory");
}

2、使用场景:
spin_lock:当线程A拿了锁,线程B再去拿锁,就会失败(拿不到),线程B就会自旋在哪里,等待锁释放.
mutex:当线程A拿了锁,线程B再去拿锁,就会失败(拿不到),会陷入sleep, 等到线程A释放了锁,线程B才会wakeup,获得该锁;
如果锁住的“事务”很简单,占用很少的时间,就应该使用spinlock,这个时候spinlock的代价比mutex会小很多。”事务”很快执行完毕,自旋的消耗远远小于陷入sleep和wake的消耗

3、问与答:
(1)、spin_lock中,会什么要禁止抢占(preempt_disable)?
(以单核为例)
P1 holds the lock and after a time is scheduled out.
Now P2 starts executing and let’s say requests the same spinlock. Since P1 has not released the spinlock, P2 must spin and do nothing. So the execution time of P2 is wasted. It makes more sense to let P1 release the lock and then allow it to be preempted.
Also since there is no other processor, simply by disallowing preemption we know that there will never be another process that runs in parallel on another processor and accesses the same critical section.
也就是说, process1正在持有该锁,此时发生了schedule后process2又去试图拿该锁,process2就会自旋在那里,时间就浪费了. 合理的做法应该是,让Process1(执行完临界区)释放该锁

(2)、在这样的场景下,使用spin lock可以保护访问共享资源R的临界区吗?
我们假设CPU0上的进程A持有spin lock进入临界区,这时候,外设P发生了中断事件,并且调度到了CPU1上执行,看起来没有什么问题,执行在CPU1上的handler会稍微等待一会CPU0上的进程A,等它立刻临界区就会释放spin lock的,但是,如果外设P的中断事件被调度到了CPU0上执行会怎么样?CPU0上的进程A在持有spin lock的状态下被中断上下文抢占,而抢占它的CPU0上的handler在进入临界区之前仍然会试图获取spin lock,悲剧发生了,CPU0上的P外设的中断handler永远的进入spin状态,这时候,CPU1上的进程B也不可避免在试图持有spin lock的时候失败而导致进入spin状态。为了解决这样的问题,linux kernel采用了这样的办法:如果涉及到中断上下文的访问,spin lock需要和禁止本CPU上的中断联合使用

spin lock和禁止本CPU上的中断联合使用:
linux kernel的spin_lock的详细介绍(以arm64为例)
(3)、假设只有一个cpu,如果把spin_lock中的preempt_disable注释掉, 即允许抢占。 那么使用spin_lock会产生死锁吗?
不会。threadA在执行时临界区时,被schedule出去了,thread B试图获取该锁,threadB会自旋那里(比较浪费cpu资源),等到再次被调度到threadA并且释放了该锁后,threadB才可以继续往下跑。

(4)、spinlock的临界区为什么不允许sleep(使用schedule类函数)?
Thread A调用spin_lock进去临界区,此时该cpu已经禁止抢占了(preempt_disable),如果此时调用sleep主动schedule出去后,该cpu就永远回不来了因为禁止抢占了。这样的话,如果threadB再试图获取该锁时,就会发生死锁。

4、 wfe/sev的使用

(1)、WFI,执行WFI指令后,ARM core会立即进入low-power standby state,直到有WFI Wakeup events发生。
(2)、WFE,执行WFE指令后,根据Event Register(一个单bit的寄存器,每个PE一个)的状态,有两种情况:
a. 如果Event Register为1,该指令会把它清零,然后执行完成(不会standby);
b. 如果Event Register为0,和WFI类似,进入low-power standby state,直到有WFE Wakeup events发生。
(3)、SEV指令,就是一个用来改变Event Register的指令,有两个:SEV会修改所有PE上的寄存器;SEVL,只修改本PE的寄存器值

WFE应用于arch_spin_lock场景:

a)资源空闲
b)Core1访问资源,acquire lock,获得资源
c)Core2访问资源,此时资源不空闲,执行WFE指令,让core进入low-power state
d)Core1释放资源,release lock,释放资源,同时会唤醒Core2(unlock中的staddlh会唤醒WFE)
e)Core2获得资源

我们也剖析下现有的代码:
arch_spin_lock进来后,先执行sevl (Event Register变成1),再执行wfe Event Register变成0),执行cbnz如果没有拿到锁,再跳转到2处,又执行wfe了,此时cpu进入low-power standby state
等到其它的cpu进程释放了该锁(发送sev信号后),当前cpu退出low-power standby state继续往下执行,执行cbnz获取到该锁,继续向下执行.

问题 : 那么“其它的cpu进程释放了该锁(发送sev信号后)”,其它cpu在哪里发送的sev信号呢???
回答 : 等待自旋锁的时候,使用指令ldaxrh(带有获取语义的独占加载,h表示halfword,即2字节)读取服务号,独占加载操作会设置处理器的独占监视器,记录锁的物理地址。
释放锁的时候,使用stlrh指令修改锁的值,stlrh指令会清除所有监视锁的物理地址的处理器的独占监视器,清除独占监视器的时候会生成一个唤醒事件
(可以查看armv8文档中的 “WFE wake-up events in AArch64 state” 章节,看看都是哪些事件或命令可以唤醒WFE,其中里面提到:An event caused by the clearing of the global monitor for the PE)

static inline void arch_spin_lock(arch_spinlock_t *lock)
{......
"	sevl\n"
"2:	wfe\n"
"	ldaxrh	%w2, %4\n"
"	eor	%w1, %w2, %w0, lsr #16\n"
"	cbnz	%w1, 2b\n"
	/* We got the lock. Critical section starts here. */
"3:"
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
	: "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
	: "memory");
}

执行staddlh会产生一个WFE的唤醒时间

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
	unsigned long tmp;

	asm volatile(ARM64_LSE_ATOMIC_INSN(
	/* LL/SC */
	"	ldrh	%w1, %0\n"
	"	add	%w1, %w1, #1\n"
	"	stlrh	%w1, %0",
	/* LSE atomics */
	"	mov	%w1, #1\n"
	"	nop\n"
	"	staddlh	%w1, %0")
	: "=Q" (lock->owner), "=&r" (tmp)
	:
	: "memory");
}
上一篇:linux kernel的spinlock在armv7和armv8中的不同


下一篇:OK6410A 开发板 (八) 74 linux-5.11 OK6410A linux 内核同步机制 信号量(count=1)的实现