一、自旋锁基本原理
自旋锁是一种对临界资源进行互斥访问的手段,获得锁的的任务能对临界进行操作,操作完后解锁,没有获得锁的任务则循环等待锁可用,相当于原地打转自旋。下面用一个例子说明一下自旋锁的工作原理。
把自旋锁看做内存中的一个变量flag,当flag=1时表示已经上锁,flag=0表示为解锁状态。线程或进程要操作临界资源时先上锁(设置flag=1),操作完成后解锁(设置flag=0)。如果上锁的时候发现已经flag=1,则循环等待查询flag值,直到flag=0.
假设SMP系统中有两个cpu,cpu1和cpu2,cpu1是运行着线程1,cpu2上运行着线程2.
1、cpu1的线程1要操作临界资源,先查看flag值是否为0,此时flag值为0,顺利完成加锁并操作临界资源
2、线程1未临界操作完成时cpu2的线程2也要操作临界资源
3、cpu2的线程2先看flag值是1,则进入循环等待查询flag值,直到flag等0才能成功加锁。
4、cpu1的线程1处理完成临界资源后解锁,设置flag=0
5、cpu2的线程2成功等待到flag值为0,并设置flag=1加锁处理临界资源,处理完成后解锁。
以上是不同cpu直接竞争临界资源的情况,相同cpu不同线程情况也是这样。实际处理中设置flag=1和falg=0需要原子操作。原子操作可参考链接:https://www.bilibili.com/read/cv16561601/
这么操作会带来一个弊端,在多核多线程强占情况下,谁优先获得锁完全是无序的,就像大家一哄而上,谁抢到就是谁的,并没有先来后到。我们希望先等待的线程能先获得锁,就像餐馆门口取号排队一样,先来先就餐。
假设一个餐馆只有一张桌子,一次只能有一个人吃饭。门口有个自动取票机,取票机上显示两个值,ower和next,ower表示当前可以就餐的号码,next表示取到的号码都是next,每取出一个号,next就会加1.每个吃饭的顾客要吃饭先取票,然后把自己的号码取票机上的ower号码比较,如果和自己的一样,就可以进去就餐,否则等待。
初始状态下取票机上的ower=next=0,
1、来了第一个顾客,取到的号码是0,然后取票机会自动next++,这个顾客看到自己的号码和取票机上的ower号码一样都是0,就进去就餐。
2、第一个顾客很快吃完,自动取票机把ower++,表示下一个可以吃饭的号码为1.这时ower=next=1。
3、来了第二个顾客,这个顾客取到的号码是1,然后看一下取票机上的ower也是1,就进去就餐。
4、第二个顾客还没吃完的时候又来了第三个顾客,第三个顾客取到的号码是2,此时取票号机上的ower=1,这个顾客需要等待。
5、第二个顾客吃饭很慢,这时又来了第四个顾客,第四个顾客取到的号码是3,此时取票号机上的ower=1,这个顾客需要等待。
6、第二个顾客终于吃完了,自动取号机把ower++,此时ower=2。
7、号码为2的顾客看到ower的值和自己的一样,就进去就餐。
8、第三个顾客也很快吃完,自动取号机把ower++,此时ower=3。
9、第四个顾客看到ower值和自己去一样,就进去就餐
如果把餐桌比作临界资源,吃饭顾客比作cpu。通过这种排队取号机制,可以让cpu有序的竞争临界资源。linux自旋锁也是采用这种机制。
二、自旋锁使用
2.1自旋锁操作函数
linux中的自旋锁用结构体spinlock_t 表示,定义在include/linux/spinlock_type.h。自旋锁的接口函数全部定义在include/linux/spinlock.h头文件中,实际使用时只需include<linux/spinlock.h>即可
方式1:
include<linux/spinlock.h>
spinlock_t lock; //定义自旋锁
spin_lock_init(&lock); //初始化自旋锁
spin_lock(&lock); //获得锁,如果没获得成功则一直等待
....... //处理临界资源
spin_unlock(&lock); //释放自旋锁
方式2:
include<linux/spinlock.h>
spinlock_t lock; //定义自旋锁
spin_lock_init(&lock); //初始化自旋锁
if (spin_trylock(&lock)) //尝试获得自旋锁,获得成功返回true,不成功返回false,不会等待
{
....... //处理临界资源
spin_unlock(&lock); //释放自旋锁
}
spin_lock只能解决进程间互斥问题,如果中断中也要访问临界资源,还需要关中断才行。linux提供了相应的接口函数:
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()
2.2
三、自旋锁源码分析
3.1 自旋锁相关文件介绍
对于UP系统和SMP系统,自旋锁的实现不同,对于UP系统,自旋锁实现很简单,只是禁止cpu调度即可,SMP系统实现会稍微复杂。
include/linux/spinlock.h
这个头文件定义了自旋锁的接口函数,spin_lock_init、spin_lock、spin_trylock、spin_unlock等。
**-->*include/linux/spinlock_types.h ***
这个头文件定义了自旋锁基本类型spinlock_t、raw_spinlock_t。
-->asm/spinlock_types.h
这个是SMP系统相关的头文件,这头文件定义了体系结构相关的自旋锁结构体,arm是在arch/arm/include/asm/spinlock_types.h,arm64是在arch/arm64/include/asm/spinlock_types.h
-->linux/spinlock_types_up.h
这个是UP系统相关头文件,定义了UP系统自旋锁结构体,Debug版本用的
-->asm/spinlock.h
这个是SMP系统相关的头文件,这个头文件是自旋锁的实现,跟体系结构有关,arm是在arch/arm/include/asm/spinlock.h,arm64是在arch/arm64/include/asm/spinlock.h。
-->include/linux/spinlock_up.h
这个是UP系统相关的头文件,Debug版本用到
-->linux/spinlock_api_smp.h
SMP系统自旋锁的接口定义
-->linux/spinlock_api_up.h
3.2 自旋锁调用过程
SMP系统加锁函数调用流程
-->spin_lock //include/linux/spinlock.h
-->raw_spin_lock
-->_raw_spin_lock
-->__raw_spin_lock //对于SMP系统,linux/spinlock_api_smp.h
-->preempt_disable //关闭内核抢占
-->spin_acquire //检查锁的有效性
-->LOCK_CONTENDED
-->arch_spin_lock //这里是真正的加锁实现,见下面源码分析
UP系统加锁函数调用流程
-->spin_lock //include/linux/spinlock.h
-->raw_spin_lock
-->_raw_spin_lock
-->__LOCK //对于UP系统,include/linux/spinlock_api_up.h
-->preempt_disable //UP系统加锁很简单,仅仅禁止内核抢占即可
3.3 arm32自旋锁源码分析
arm32的自旋锁结构体定义在arch/arm/include/asm/spinlock_types.h中
#define TICKET_SHIFT 16
typedef struct {
union {
u32 slock;
struct __raw_tickets {
#ifdef __ARMEB__ //大端模式
u16 next;
u16 owner;
#else
u16 owner;
u16 next;
#endif
} tickets;
};
} arch_spinlock_t; //自旋锁
#define __ARCH_SPIN_LOCK_UNLOCKED { { 0 } }
typedef struct {
u32 lock;
} arch_rwlock_t; //读写锁
#define __ARCH_RW_LOCK_UNLOCKED { 0 }
#endif
arch_spinlock_t的内存分布如下所示:
arch_spinlock_t总共32位,next占用低16bit,ower占用高16bit
arm自旋锁和读写锁的函数实现都是定义在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); //预取指令,向将lock->slock取到cash缓存,以提高读写速度
__asm__ __volatile__(
"1: ldrex %0, [%3]\n" //原子访问,lockval=*lock,这里先保存一份lock,因为传进来的lock是多个线程公用的,有可能被别的线程改掉
" add %1, %0, %4\n" //newval = lockval + (1<<16),相当于newval=lockcal.tickets.next++
" strex %2, %1, [%3]\n" //*lock = *((arch_spinlock_t)&newval),相当于执行了lock->tickets.netx++
" teq %2, #0\n" //判断原子操作是否成功,如果不成功则返回到第一步继续原子操作
" bne 1b"
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
: "cc");
while (lockval.tickets.next != lockval.tickets.owner) { //如果不相等表示锁还未解开,
wfe(); //WFE指令,让cpu进入休眠,解锁的线程会用sev指令唤醒cpu
lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner); //其它线程已经用完临界资源解锁,调用sev指令唤醒该cpu,还要需要重新去加载lock的owner值
}
smp_mb(); //设置内存屏障
}
从加锁的源码中可以看到,没有获得锁的cpu会一直休眠等待被唤醒,无法进行线程调度。如果处理临界资源需要较多时间,用自旋锁会影响cpu效率。
解锁源码分析
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
smp_mb(); //设置内存屏障
lock->tickets.owner++; //owner自增
dsb_sev(); //执行sev指令唤醒休眠中cpu
}
可以看到解锁owner++不需要原子操作,因为获得锁的线程只有一个,能解锁的线程也只有一个,解锁没有竞争存在
3.4 arm64自旋锁源码分析
arm64自旋锁结构体定义在arch/arm64/include/asm/spinlock_types.h中,如下所示
typedef struct {
#ifdef __AARCH64EB__ //大端模式
u16 next;
u16 owner;
#else
u16 owner;
u16 next;
#endif
} __aligned(4) arch_spinlock_t; //arm64自旋锁接头体
#define __ARCH_SPIN_LOCK_UNLOCKED { 0 , 0 }
typedef struct {
volatile unsigned int lock;
} arch_rwlock_t; //arm64读写锁结构体
arm64自旋锁和读写锁的函数实现都是定义在arch/arm64/include/asm/spinlock.h中
加锁源码分析:
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"
"1: ldaxr %w0, %3\n" //ldaxr指令和ldrex指令功能一样,都是独占访问指令,lockval = *lock
" add %w1, %w0, %w5\n" // newval = lockval+1<<16,相当于newval=lockval.next++
" stxr %w2, %w1, %3\n" //stxr指令功能和strex指令一样,*lock = newval,相当于执行了lock->next++
" cbnz %w2, 1b\n", //如果独占访问不成功则跳转到第一步继续执行
/* LSE atomics */
" mov %w2, %w5\n"
" ldadda %w2, %w0, %3\n"
__nops(3)
)
/* Did we get the lock? */
" eor %w1, %w0, %w0, ror #16\n" //判断lockval.next和lockval.ower是否相等
" cbz %w1, 3f\n" //如果相等则表示加锁成功,跳转到步骤3,结束加锁过程
/*
* No: spin on the owner. Send a local event to avoid missing an
* unlock before the exclusive load.
*/
" sevl\n" //指令的功能是发送一个本地事件,避免错过其他处理器释放自旋锁时发送的事件。
"2: wfe\n" //加锁不成功则进入休眠等待
" ldaxrh %w2, %4\n" //其它cpu解锁,唤醒当前cpu,重新加载lock->owner值,tmp = lock->owner
" eor %w1, %w2, %w0, lsr #16\n" //再次判断next和ower是否相等
" cbnz %w1, 2b\n" //如果不相等表示未获得锁,跳到步骤2继续休眠,否则成功获得锁,结束加锁过程
/* 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");
}
解锁源码分析:
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" //tmp = lock->owner
" add %w1, %w1, #1\n" //tmp += 1
" stlrh %w1, %0", //lock->owner = tmp ,实现lock->owner++功能
/* LSE atomics */
" mov %w1, #1\n"
" staddlh %w1, %0\n"
__nops(1))
: "=Q" (lock->owner), "=&r" (tmp)
:
: "memory");
}
关于在arch_spin_unlock代码中为何没有SEV指令?关于这个问题可以参考ARM ARM文档中的Figure B2-5,这个图是PE(n)的global monitor的状态迁移图。当PE(n)对x地址发起了exclusive操作的时候,PE(n)的global monitor从open access迁移到exclusive access状态,来自其他PE上针对x(该地址已经被mark for PE(n))的store操作会导致PE(n)的global monitor从exclusive access迁移到open access状态,这时候,PE(n)的Event register会被写入event,就好象生成一个event,将该PE唤醒,从而可以省略一个SEV的指令。
内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料
学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈
原文作者:极致Linux内核
原文地址:Linux内核:进程管理——自旋锁 - 知乎(版权归原文作者所有,侵权留言联系删除)