在阅读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。该处理核心的全局监视器的状态也由具体实现来定义。
本文详细介绍了Linux内核中的自旋锁实现,特别是针对ARM架构的同步原语。通过分析spinlock_t结构和相关操作,如上锁、解锁过程,揭示了自旋锁如何确保多核处理器环境中的线程同步。此外,还简要提到了ARM架构下ldrex和strex指令在内存访问中的作用。
1646

被折叠的 条评论
为什么被折叠?



