内核自旋锁与ARM同步原语

本文详细介绍了Linux内核中的自旋锁实现,特别是针对ARM架构的同步原语。通过分析spinlock_t结构和相关操作,如上锁、解锁过程,揭示了自旋锁如何确保多核处理器环境中的线程同步。此外,还简要提到了ARM架构下ldrex和strex指令在内存访问中的作用。

在阅读linux内核代码时,毫无疑问会遇到spin-lock,下面谈谈我对于spin-lock的arm源码分析。

首先看一下spinlock_t的结构

//arm/include/asm/spinlock_types.h
typedef struct {
	union {
		u32 slock;
		struct __raw_tickets {
#ifdef __ARMEB__
			u16 next;
			u16 owner;
#else
			u16 owner;
			u16 next;
#endif
		} tickets;
	};
} arch_spinlock_t;

//include/linux/spinlock_types.h
typedef struct raw_spinlock {
	arch_spinlock_t raw_lock;

} raw_spinlock_t;

//include/linux/spinlock_types.h
typedef struct spinlock {
	union {
		struct raw_spinlock rlock;

	};
} spinlock_t;

上面代码去除了一些debug模式时才有的成员。最终我们看到spinlock_t就是一个4字节的变量:一个u32的slock或者两个u16的next、owner,这里使用联合体以便在不同场景中以不同的模式进行访问。在普通锁模式时,使用的是slock,在读写锁模式时,使用的是next、owner。

下面来看锁的初始化。

//arm/include/asm/spinlock_types.h
#define __ARCH_SPIN_LOCK_UNLOCKED	{ { 0 } }

//include/linux/spinlock_types.h
#define __RAW_SPIN_LOCK_INITIALIZER(lockname)	\
	{					\
	.raw_lock = __ARCH_SPIN_LOCK_UNLOCKED,	\
	SPIN_DEBUG_INIT(lockname)		\
	SPIN_DEP_MAP_INIT(lockname) }

#define __RAW_SPIN_LOCK_UNLOCKED(lockname)	\
	(raw_spinlock_t) __RAW_SPIN_LOCK_INITIALIZER(lockname)

//include/linux/spinlock.h
# define raw_spin_lock_init(lock)				\
	do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0)
#endif

#define spin_lock_init(_lock)				\
do {							\
	spinlock_check(_lock);				\
	raw_spin_lock_init(&(_lock)->rlock);		\
} while (0)

可以看到,锁的初始化就是将其4个字节置0.

 

下面来看上锁的过程

//include/linux/spinlock.h
static inline int do_raw_spin_trylock(raw_spinlock_t *lock)
{
	return arch_spin_trylock(&(lock)->raw_lock);
}

static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
	__acquire(lock);//用于进行编译时静态检查,不会生成字节码
	arch_spin_lock(&lock->raw_lock);
}

#define LOCK_CONTENDED(_lock, try, lock)			\
do {								\
	if (!try(_lock)) {					\//尝试获取锁
		lock_contended(&(_lock)->dep_map, _RET_IP_);	\//展开后为空
		lock(_lock);//获取锁					\
	}							\
	lock_acquired(&(_lock)->dep_map, _RET_IP_);			\//展开后为空
} while (0)

//include/linux/spinlock_api_smp.h
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);
}

//kernel/locking/spinlock.c
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
	__raw_spin_lock(lock);
}
EXPORT_SYMBOL(_raw_spin_lock);

//include/linux/spinlock.h
#define raw_spin_lock(lock)	_raw_spin_lock(lock)

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

可以看到,上锁的第一步就是关闭内核抢占,以免在上锁过程中当前cpu核心执行别的代码,就是避免该CPU核心线程调度产生同步问题。 下一步是开始尝试获取锁,如果尝试获取锁不成功,将执行正式获取锁逻辑。arch_spin_lock()和arch_spin_trylock()是体系结构相关的函数。下面来看一下ARM架构上的实现。

//arm/include/asm/spinlock.h
static inline int arch_spin_trylock(arch_spinlock_t *lock)
{
	unsigned long contended, res;
	u32 slock;

	prefetchw(&lock->slock);
	do {
		__asm__ __volatile__(
		"	ldrex	%0, [%3]\n"	// 将R3地址的值加载到R0中,并将R3的地址标记为
						//独占状态
		"	mov	%2, #0\n"	// 将立即数0赋值给R2
		"	subs	%1, %0, %0, ror #16\n” // 将R0的值循环右移后再和R0相减,把结果
						// 放到R1中,并且Z Flag = if R1 == 0 then 1 else 0
		"	addeq	%0, %0, %4\n” // 如果Z flag为1则执行R0=R0+R4
		"	strexeq	%2, %0, [%3]” // 如果Z flag为1 并且R3中的地址为独占状
						//态,则把R0的值保存到R3的地址中,保存成功与否的
						// 结果放到R2中,并清楚独占状态。
		: "=&r" (slock), "=&r" (contended), "=&r" (res) // R0,R1,R2 寄存器 输出
		: "r" (&lock->slock), "I" (1 << TICKET_SHIFT) // R3,R4 寄存器 输入
		: "cc");
	} while (res);

	if (!contended) {
		smp_mb();
		return 1;
	} else {
		return 0;
	}
}

实现跟体系结构有关,需要执行特定体系的汇编代码。

代码逻辑为:将lock的值循环右移16位后减去原来的自己,也就是判断低16位是否等于高16位。如果相等,将lock的值加上0x10000(也就是高16位加1)写回lock,写回成功就返回1,写回不成功则重试一次.如果不相等则返回0。

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”	// 将R3的地址的值加载到R0,并将R3表示的地址标记为独占模式。
"	add	%1, %0, %4\n”	// 将R4和R0的值相加放到R1。
"	strex	%2, %1, [%3]\n"	// 将R1的值写到R3表示的地址中,如果该地址不处于独占
					// 模式,该操作将失败,将R2赋值为1.如果成功,将清除
					//该地址的独占模式,将R2置为0.
"	teq	%2, #0\n"	// 判断R2的值是否等于0.设置对应标志位。
"	bne	1b"	// 上面判断是否相等,不相等跳转到1
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp) // R0,R1,R2 寄存器 输出结果
	: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)  // R3,R4 寄存器 输入的结果
	: "cc");

	while (lockval.tickets.next != lockval.tickets.owner) {
		wfe(); // CPU进入低功耗状态, wfe指令并不会立即进入低功耗,而是会判断一个单bit的事件寄存
                        器,如果为1则将寄存器置为0后什么也不做,如过为0则会进入低功耗模式。
                        猜测该操作肯定是原子性的,所以这里不会有同步问题。
		lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
	}

	smp_mb();
}

 代码逻辑为:将lock的值加上0x10000(也就是高16位加1)写回lock,直到成功。然后循环判断旧的lock值(未加1前)的高16位是否和当前的lock的值的低16位相等,如果不相等就进入低功耗状态,循环直到相等。

可以看到,内核自旋锁是通过特定于体系结构的同步原语来实现的。获取锁时,如果高16位和低16位相等,表明可以获取锁,将高16位原子加1,表明占有锁;如果不相等,表明其他cpu核占有该锁,则将高16位加1后该处理核心进入低功耗模式;等待其他处理核心唤醒。

注意这里已经关闭内核抢占,不存在该处理核心的其他线程抢占该锁的情况,只可能是其他处理核心来获取该锁,使用16位数加1来判断是否获得锁,在65536个处理核心内的系统中都能正常工作。

下面来看一下解锁的过程

//include/linux/spinlock.h
static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
{
	arch_spin_unlock(&lock->raw_lock);
	__release(lock);//静态代码检查
}

//include/linux/spinlock_api_smp.h
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
	spin_release(&lock->dep_map, 1, _RET_IP_);//展开后为空
	do_raw_spin_unlock(lock);
	preempt_enable();//打开内核抢占
}

//kernel/locking/spinlock.c
void __lockfunc _raw_spin_unlock(raw_spinlock_t *lock)
{
	__raw_spin_unlock(lock);
}
EXPORT_SYMBOL(_raw_spin_unlock);

//include/linux/spinlock.h
#define raw_spin_unlock(lock)		_raw_spin_unlock(lock)

static inline void spin_unlock(spinlock_t *lock)
{
	raw_spin_unlock(&lock->rlock);
}

解锁的过程先调用体系结构相关的解锁代码,然后打开内核抢占,我们来看一下arm架构下的解锁实现:

//arm/include/asm/spinlock.h
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
	smp_mb(); // 全内存屏障
	lock->tickets.owner++;
	dsb_sev(); // 唤醒wfe进入低功耗的Core
}

可以看到比较简单,只是将owner成员也就是低16位加1,然后唤醒其他处理核心就可以了。

 

下面附加介绍一下arm架构同步原语,至于上面用到的内存屏障,后面再单独写一篇讨论。

arm架构使用两条指令来实现同步原语,分别是ldrex和strex。这些指令操作与cpu中的一个叫地址监视器协同工作,监视器提供了内存访问的机器状态和相关系统控制。根据内存是否具有可共享或不可共享内存属性,存在两种不同的监控模型。 

在处理核心的非共享内存中:

执行ldrex指令会导致:

1)执行核心标记对应的物理地址为独占访问状态。影响的物理地址大小由实现定义。

2)该核心的本地监视器转为独占访问状态。

执行strex指令会导致:

1)如果该核心的本地监视器处于独占访问状态。并且保存的地址和本地监视器监视的地址相同,则保存成功,Rd的值为0,否则由实现定义是否保存失败,如果失败Rd寄存器的值为1。本地监视器转为开放访问状态。

2)如果该核心的本地监视器处于开放访问状态,保存必然失败,Rd的值为1,本地监视器保持开放访问状态。

另外,如果使用非独占保存指令来将值保存到一个地址,该地址没有标记为独占访问状态但本地状态监视器为独占状态,其结果由具体实现定义。如果该地址标记为来独占访问状态,其结果也由具体实现定义。

 

在处理器共享内存中:

执行ldrex指令会导致:

1)被加载的地址标记为独占访问状态,并且其他的该处理核心加载并标记的地址的独占访问状态将被清楚。即同时只有一处地址被一个处理核心加载而标记。

2)该处理核心的本地监视器转为独占访问状态。

3)全局监视器转为独占访问状态。

执行strex指令时会导致:

1)如果满足这两个条件,保存操作会保证成功:1、保存的地址被标记为独占访问状态。2、该处理核心的全局监视器和本地监视器都为独占模式。这种情况下,Rd寄存器返回0。该核心的全局监视器的状态可能变为开放访问模式。如果该地址被其他核心的全局监视器标记为独占访问状态,则该核心的全局监视器保证转为开放访问模式。

2)如果该处理核心没有标记任何地址,保存操作将会失败。这时,Rd寄存器返回1,该核心的全局监视器保持开放访问状态。

3)如果该处理核心之前标记独占访问的地址和保存地址不一样,保存是否成功依赖具体实现。这是,如果成功,Rd为0,如果失败Rd为1。该处理核心的全局监视器的状态也由具体实现来定义。

 

 

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值