Linux内核spinlock实现
linux内核自旋锁spinlock实现详解(基于ARM处理器)
内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:
- 一个是原地等待
- 一个是挂起当前进程,调度其他进程执行(睡眠)
spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是“原地等待”的方式 解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制: 自旋锁不应该被长时间的持有(消耗 CPU 资源)
本文基于linux-4.9.262版本
1. 自旋锁结构
typedef struct spinlock {
union {
struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
typedef struct {
union {
u32 slock;
struct __raw_tickets {
#ifdef __ARMEB__
u16 next;
u16 owner;
#else
u16 owner;
u16 next;
#endif
} tickets;
};
} arch_spinlock_t;
说明:
ARMEB = ARM EABI Big-endian #大端字节序
ARMEL = ARM EABI Little-endian #小端字节序
EABI = Embedded Application Binary Interface
u16 next表示下一个可以获取自旋锁的处理器,处理器请求自旋锁的时候会保存该值并对该值加1,然后与owner比较,检查是否可以获取到自旋锁,每请求一次next都加1
u16 owner表示当前获取到/可以获取自旋锁的处理器,每释放一次都加1,这样next与owner就保存一致
2. 获取自旋锁
static __always_inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock) _raw_spin_lock(lock)
// include/linux/spinlock_api_up.h
#define _raw_spin_lock(lock) __LOCK(lock)
#define __LOCK(lock) \
do { preempt_disable(); ___LOCK(lock); } while (0)
// preempt_disable禁止内核抢占
#define ___LOCK(lock) \
do { __acquire(lock); (void)(lock); } while (0)
# define __acquire(x) (void)0
// include/linux/spinlock_api_smp.h
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock) __acquires(lock);
// kernek/locking/spinlock.c
#ifndef CONFIG_INLINE_SPIN_LOCK
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
__raw_spin_lock(lock);
}
// 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);
}
// include/linux/lockdep.h
#define LOCK_CONTENDED(_lock, try, lock) \
lock(_lock)
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);可以展开为
do_raw_spin_lock(lock);
// linux-4.9.262/kernel/locking/spinlock_debug.c
/*
* We are now relying on the NMI watchdog to detect lockup instead of doing
* the detection here with an unfair lock which can cause problem of its own.
*/
void do_raw_spin_lock(raw_spinlock_t *lock)
{
debug_spin_lock_before(lock);
arch_spin_lock(&lock->raw_lock);
debug_spin_lock_after(lock);
}
//linux-4.9.262/arch/arm/include/asm/spinlock.h
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" //[1]
" add %1, %0, %4\n" //[2]
" strex %2, %1, [%3]\n" //[3]
" teq %2, #0\n" //[4]
" bne 1b" //[5]
: "=&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 = ACCESS_ONCE(lock->tickets.owner);
}
smp_mb(); // 适用于MP的内存屏障
}
我们先来看看LDREX和STREX两条指令的语义。其实LDREX和STREX指令,是将单纯的更新内存的原子操作分成了两个独立的步骤。
(1) LDREX用来读取内存中的值,并标记对该段内存的独占访问:
LDREX Rx, [Ry]
上面的指令意味着,读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。
如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。
(2) 而STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:
STREX Rx, Ry, [Rz]
如果执行这条指令的时候发现Rz已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。
而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器Rx的值设置成1。
一旦某条STREX指令执行成功后,以后再对同一段内存尝试使用STREX指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。
[1]将%3指向的内存数值 保存给%0的内存位置。lockval = lock->slock (如果lock->slock没有被其他处理器独占,则标记当前执行处理器对lock->slock地址的独占访问;否则不影响)
[2] newval = lockval + (1 << TICKET_SHIFT)
[3] strex tmp, newval, [&lock->slock] 。如果当前执行处理器没有独占lock->slock地址的访问,不进行存储,将tmp设置为1;如果当前处理器已经独占lock->slock内存访问,则对内存进行写,返回0,清除独占标记。lock->tickets.next = lock->tickets.next + 1
[4] teq %2, #0\n" 检查是否写入成功lockval.tickets.next
[5]bne 1b 如果[4]中tmp不等于0,返回标签1处继续执行
while (lockval.tickets.next != lockval.tickets.owner) { // 初始化时lock->tickets.owner、lock->tickets.next都为0,假设第一次执行arch_spin_lock,lockval = *lock,lock->tickets.next++,lockval.tickets.next等于lockval.tickets.owner,获取到自旋锁;自旋锁未释放,第二次执行的时候,lock->tickets.owner = 0, lock->tickets.next = 1,拷贝到lockval后,lockval.tickets.next != lockval.tickets.owner,会执行wfe等待被自旋锁释放被唤醒,自旋锁释放时会执行lock->tickets.owner++,lockval.tickets.owner重新赋值
wfe(); // 暂时中断挂起执行
lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner); // 重新读取lock->tickets.owner
}
__asm__ __volatile__
(__asm__
用于指示编译器在此插入汇编语句, __volatile__
用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编
3. 释放自旋锁
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
smp_mb();
lock->tickets.owner++;
dsb_sev(); // 执行sev指令,唤醒wfe等待的处理器
}
lock->tickets.owner++作用:
lock->tickets.owner增加1,下一个被唤醒的处理器会检查该值是否与自己的lockval.tickets.next相等,lock->tickets.owner代表可以获取的自旋锁的处理器,lock->tickets.next你一个可以获取的自旋锁的owner;处理器获取自旋锁时,会先读取lock->tickets.next用于与lock->tickets.owner比较并且对lock->tickets.next加1,下一个处理器获取到的lock->tickets.next就与当前处理器不一致了,两个处理器都与lock->tickets.owner比较,肯定只有一个处理器会相等,自旋锁释放时时对lock->tickets.owner加1计算,因此,先申请自旋锁多处理器lock->tickets.next值更新,自然先获取到自旋锁