Linux 锁机制(3)之自旋锁
1. 自旋锁
1.1 两种锁
内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:
- 一个是原地等待,如自旋锁
特点:不会引起调用者睡眠,而是一直循环查看该锁是否释放 - 一个是挂起当前进程,调度其他进程执行,如互斥锁,信号量等
特点:如果资源已经被占用,调用线程则进入睡眠等待
1.2 自旋锁
1.自旋锁简单说就是:
当一个线程获取了锁之后,其他试图获取这个锁的线程一直在循环等待获取这个锁,直至锁可用为止。
1.3 自旋名字来源:自旋锁一直循环等待,直到获取锁为止。
自旋锁一直循环等待,线程通过 busy-wait-loop 的方式来获取锁
即:当前的执行thread会不断的重新尝试直到获取锁进入临界区。
1.4 自旋锁优点:
- 自旋锁不会使线程状态发生切换,线程一直都是active的;
- 不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
2 自旋锁特点/使用:
2.1 临界区要小(否则出现死等,浪费cpu)
由于spin lock死等这种特性,因此它使用在那些代码不是非常复杂的临界区
如果临界区执行时间太长,那么不断在临界区门口“死等”的那些thread是多么的浪费CPU啊
(临界区代码短小,最好100行以内)
线程等待锁的时间很短,短到比线程两次上下文切换时间还少,说白了就是锁里操作的事情很简单。
2.2 线程获取自旋锁之前,要禁止当前处理器上的中断
防止获取锁的线程和中断形成竞争条件
例子说明:
比如:当前线程获取自旋锁后,在临界区中被中断处理程序打断,中断处理程序正好也要获取这个锁,
于是中断处理程序会等待当前线程释放锁,而当前线程也在等待中断执行完后再执行临界区和释放锁的代码。
2.3 自旋锁是不可递归的
递归的请求同一个自旋锁会自己锁死自己。
例子:
自旋锁可以在中断上下文中使用,但是试想一个场景:一个线程获取了一个锁,但是被中断处理程序打断,中断处理程序也获取了这个锁(但是之前已经被锁住了,无法获取到,只能自旋),中断无法退出,导致线程中后面释放锁的代码无法被执行,导致死锁。
2.4 由于不睡眠(忙等),自旋锁可以在中断上下文使用。
在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?
如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了,这时候,可以考虑使用spin lock。
在中断上下文,是不允许睡眠的。所以,这里需要的是一个不会导致睡眠的锁——spinlock。
参见:操作系统二:上下文,进程上下文和中断上下文
https://blog.csdn.net/lqy971966/article/details/119103989
3. spinlock_t 源码学习
3.1 spinlock 源码文件解析
根据CPU体系结构,spinlock分为SMP版本和UP版本
[root@localhost home]# uname -a
Linux localhost.localdomain 3.10.0-862.el7.x86_64 #1 SMP Fri Apr 20 16:44:24 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
[root@localhost home]#
root@localhost home]# find / -name *spinlock*
/* 针对 ARM 平台的 arch_spin_lock */
/usr/src/kernels/3.10.0-862.el7.x86_64/arch/x86/include/asm/qspinlock.h
/usr/src/kernels/3.10.0-862.el7.x86_64/arch/x86/include/asm/qspinlock_paravirt.h
/usr/src/kernels/3.10.0-862.el7.x86_64/arch/x86/include/asm/spinlock.h
/usr/src/kernels/3.10.0-862.el7.x86_64/arch/x86/include/asm/spinlock_types.h
/usr/src/kernels/3.10.0-862.el7.x86_64/drivers/hwspinlock
/usr/src/kernels/3.10.0-862.el7.x86_64/include/asm-generic/qspinlock.h
/usr/src/kernels/3.10.0-862.el7.x86_64/include/asm-generic/qspinlock_types.h
/usr/src/kernels/3.10.0-862.el7.x86_64/include/asm-generic/spinlock.h
/usr/src/kernels/3.10.0-862.el7.x86_64/include/config/arch/use/queued/spinlocks.h
/usr/src/kernels/3.10.0-862.el7.x86_64/include/config/paravirt/spinlocks.h
/usr/src/kernels/3.10.0-862.el7.x86_64/include/config/queued/spinlocks.h
/* 以下是和体系结构无关的代码*/
/usr/src/kernels/3.10.0-862.el7.x86_64/include/linux/bit_spinlock.h
/usr/src/kernels/3.10.0-862.el7.x86_64/include/linux/hwspinlock.h
/usr/src/kernels/3.10.0-862.el7.x86_64/include/linux/rwsem-spinlock.h
/usr/src/kernels/3.10.0-862.el7.x86_64/include/linux/spinlock.h
//spinlock.h 通用spin lock的接口函数声明,例如spin_lock、spin_unlock等
/usr/src/kernels/3.10.0-862.el7.x86_64/include/linux/spinlock_api_smp.h
//SMP上的spin lock模块的接口声明
/usr/src/kernels/3.10.0-862.el7.x86_64/include/linux/spinlock_api_up.h
//spinlock_api_up.h 这个头文件是non-debug版本的spin lock需要的
/usr/src/kernels/3.10.0-862.el7.x86_64/include/linux/spinlock_types.h
//spinlock_types.h 定义了通用spin lock的基本的数据结构(例如spinlock_t)
/usr/src/kernels/3.10.0-862.el7.x86_64/include/linux/spinlock_types_up.h
//spinlock_types.h文件会根据系统的配置,如果是SMPUP则会include该头文件
/usr/src/kernels/3.10.0-862.el7.x86_64/include/linux/spinlock_up.h
//spinlock_up.h 这个头文件是debug版本的SMP spin lock需要的
[root@localhost home]#
3.2 自旋锁API
方法 描述
spin_lock() 获取指定的自旋锁
spin_lock_irq() 禁止本地中断并获取指定的锁
spin_lock_irqsave() 保存本地中断的当前状态,禁止本地中断,并获取指定的锁
spin_unlock() 释放指定的锁
spin_unlock_irq() 释放指定的锁,并激活本地中断
spin_unlock_irqstore() 释放指定的锁,并让本地中断恢复到以前状态
spin_lock_init() 动态初始化指定的spinlock_t
spin_trylock() 试图获取指定的锁,如果未获取,则返回0
spin_is_locked() 如果指定的锁当前正在被获取,则返回非0,否则返回0
3.3 自旋锁变体 API
接口API的类型 spinlock中的定义 raw_spinlock的定义
定义spin lock并初始化 DEFINE_SPINLOCK DEFINE_RAW_SPINLOCK
动态初始化spin lock spin_lock_init raw_spin_lock_init
获取指定的spin lock spin_lock raw_spin_lock
获取指定的spin lock spin_lock_irq raw_spin_lock_irq
同时disable本CPU中断
保存本CPU当前的irq状态 spin_lock_irqsave raw_spin_lock_irqsave
disable本CPU中断并获取指定的spin lock
获取指定的spin lock spin_lock_bh raw_spin_lock_bh
同时disable本CPU的bottom half
释放指定的spin lock spin_unlock raw_spin_unlock
释放指定的spin lock spin_unlock_irq raw_spin_unock_irq
同时enable本CPU中断
释放指定的spin lock spin_unlock_irqstore raw_spin_unlock_irqstore
同时恢复本CPU的中断状态
获取指定的spin lock spin_unlock_bh raw_spin_unlock_bh
同时enable本CPU的bottom half
尝试去获取spin lock, spin_trylock raw_spin_trylock
如果失败,不会spin,而是返回非零值
判断spin lock是否是locked, spin_is_locked raw_spin_is_locked
如果其他的thread已经获取了该lock,那么返回非零值,否则返回0
3.4 自旋锁伪代码实现
spinlock_t lock;
spin_lock_init(lock); //初始化
……
spin_lock(&lock); //加锁
/* 临界区 */
spin_unlock(&lock); //释放锁
4. 扩展:自旋锁在处理中断处理下半部时注意事项:
4.1 进程上下文在对共享数据加锁前要禁止下半部的执行,解锁时再允许下半部的执行
下半部处理和进程上下文共享数据时,由于下半部的处理可以抢占进程上下文的代码,
所以进程上下文在对共享数据加锁前要禁止下半部的执行,解锁时再允许下半部的执行。
4.2 下半部在对共享数据加锁前要禁止中断处理(上半部),解锁时再允许中断的执行
中断处理程序(上半部)和下半部处理共享数据时,由于中断处理(上半部)可以抢占下半部的执行,
所以下半部在对共享数据加锁前要禁止中断处理(上半部),解锁时再允许中断的执行。
4.3 同类tasklet中的共享数据不需要保护
同一种tasklet不能同时运行,所以同类tasklet中的共享数据不需要保护。
4.4 同一个处理器上不会有tasklet相互抢占的情况
不同类tasklet中共享数据时,其中一个tasklet获得锁后,不用禁止其他tasklet的执行,因为同一个处理器上不会有tasklet相互抢占的情况
4.5 同一个处理器上不会有软中断互相抢占的情况
同类型或者非同类型的软中断在共享数据时,也不用禁止下半部,因为同一个处理器上不会有软中断互相抢占的情况
参考:
https://blog.csdn.net/zhoutaopower/article/details/86598839
https://blog.csdn.net/zhoutaopower/article/details/86598839
https://www.cnblogs.com/sky-heaven/p/12802063.html