1、为什么需要自旋锁
很多时候我们并不能采用其他的锁,比如读写锁、互斥锁、信号量等。一方面这些锁会发生上下文切换,他的时间是不可预期的,对于一些简单的、极短的临界区完全是一种性能损耗;
另一方面在中断上下文是不允许睡眠的,除了自旋锁以外的其他任何形式的锁都有可能导致睡眠或者进程切换,这是违背了中断的设计初衷,会发生不可预知的错误。
基于两点,我们需要自旋锁,他是不可替代的。
2、为什么自旋锁会禁止抢占
这一点其实很好理解,当一个 CPU 获取到一把自旋锁之后,开始执行临界区代码,此时假设他的时间片运转完毕,进程调度会主动触发调度将其调走,
执行另一个线程/进程,结果恰巧了这个线程/进程也需要用到该自旋锁,而上一个线程/进程还在停留在临界区内未释放锁,导致本进程无法获取到锁而形成死锁,
所以自旋锁为了规避此类情形的出现从而直接禁止对已经开始运行的临界区设置禁止抢占标志。
3、为什么临界区禁止睡眠
如果自旋锁锁住以后进入睡眠,而此时又不能进行处理器抢占,内核的调取器无法调取其他进程获得该 CPU,从而导致该 CPU 被挂起;
同时该进程也无法自唤醒且一直持有该自旋锁,进一步会导致其他使用该自旋锁的位置出现死锁。
4、自旋锁相关源码
spin_lock -----> raw_spin_lock
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); }
spin_lock_irq------> raw_spin_lock_irq
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock) { local_irq_disable(); // 关闭中断 preempt_disable(); // 禁止内核抢占 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
spin_lock_irqsave------>__raw_spin_lock_irqsave
static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock) { unsigned long flags; local_irq_save(flags); // 关闭中断,并保存中断的状态 preempt_disable(); // 禁止内核抢占 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); /* * On lockdep we dont want the hand-coded irq-enable of * do_raw_spin_lock_flags() code, because lockdep assumes * that interrupts are not re-enabled during lock-acquire: */ #ifdef CONFIG_LOCKDEP LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); #else do_raw_spin_lock_flags(lock, &flags); #endif return flags; }
local_irq_save------> arch_local_irq_save
static __always_inline unsigned long arch_local_irq_save(void) { unsigned long flags = arch_local_save_flags(); // 保存中断状态 arch_local_irq_disable(); // 关闭中断 return flags; }
总结:
spin_lock比spin_lock_irq速度快,但是它并不是任何情况下都是安全的。在使用spin_lock时要明确知道该锁不会在中断处理程序中使用。
spin_lock_irq在自旋的时候,不会保存当前的中断标志寄存器,只会在自旋结束后,将之前的中断打开。
spin_lock_irqsave在锁返回时,之前开的中断,之后也是开的;之前关,之后也是关。但是spin_lock_irq则不管之前的开还是关,返回时都是开的。
spin_lock_irq 和 spin_unlock_irq, 如果你确定在获取锁之前本地中断是开启的,那么就不需要保存中断状态,解锁的时候直接将本地中断启用就可以了。
spin_lock和spin_lock_irq在特殊情况下会导致死锁,spin_lock_irqsave是最安全的。
5、自旋锁的使用场景
spin_lock 使用场景
首先如果整个临界区都只位于进程上下文或者工作队列中,那么只需要采用最为方便的 spin_lock 即可,因为他不会发生中断抢占锁的情况,
哪怕中断抢占进程上下文也不会导致中断由于申请自旋锁而导致死锁。
还有一种情况就是在硬件中断中可以考虑使用 spin_lock 即可,因为硬件中断不存在嵌套(未必一定是这样,与平台有关),
所以只需要简单的上锁即可, 可以不需要关闭中断,保存堆栈等。
spin_lock_irq 使用场景
这个锁的变种适合在进程上下文/软中断 + 硬件中断这样的组合中使用,taskset 也是属于软中断的一种,所以也归在此类。
当然,这种类型的变种同样适合软中断/taskset + 进程上下文的组合,因为关闭了硬件中断,从源头就禁止执行软中断代码,
不过,对于这种类型的中断最好的方式是使用 spin_lock_bh 的方式,因为他只锁定软中断代码执行,而不关闭硬件中断,这样性能损耗更小。
spin_lock_irqsave 使用场景
这种类型的使用方式是最为安全以及便捷的,毕竟不需要考虑会不会发生死锁的问题(代码本身引入的死锁不在此类),
但是他也是性能损耗最大的代码,能不使用尽量不适用,在高速设备上,自旋锁已然成为了一种降低性能的瓶颈。
他最好只出现在在需要尝试 spin_lock 之前无法确定是否已经关闭中断的代码才使用,
如果代码能够确定在执行锁之前中断一定是打开的,那么使用 spin_lock_irq 是更佳的选择。
spin_lock_bh 使用场景
这种类型的变种是一种比 spin_lock_irq 更轻量的变种,只关闭中断底半部,其实就是关闭了软中断、Taskset 以及 Timer 等的一个抢占能力,
如果开发者确定编写的代码临界区只存在软中断/Taskset/Timer + 进程上下文这样的组合,则最好考虑使用 spin_lock_bh 这样的锁来禁止软中断进行抢占。
还有就是软中断与软中断自我抢占临界区访问时,也需要使用 spin_lock_bh 以上的中断锁,因为有可能软中断在执行的过程中,
自己被硬件中断打断,然后又执行到同样的代码,在别的 CPU 执行还好说,毕竟软中断可以在不同的 CPU 上执行同一个中断函数,
但是假设不幸运行在同一个 CPU 上,则会导致死锁。Taskset 由于在运行过程中钟只会运行一个实例,
所以不存在死锁问题,Taskset 与 Taskset 的锁竞争只需要使用 spin_lock 即可。
参考引用:
什么情况下使用什么样的自旋锁:http://blog.csdn.net/wesleyluo/article/details/8807919
本地中断概念的理解:http://blog.csdn.net/adaptiver/article/details/6177646
深入理解自旋锁:http://blog.csdn.net/vividonly/article/details/6594195
自旋锁和互斥量区别:http://blog.csdn.net/kyokowl/article/details/6294341
自旋锁的临界区本地cpu不会发生任何进程调度:http://blog.chinaunix.net/uid-23769728-id-3367773.html
https://www.cnblogs.com/aaronLinux/p/5890924.html
https://zhuanlan.zhihu.com/p/371721341
https://stackoverflow.com/questions/2559602/spin-lock-irqsave-vs-spin-lock-irq