![d534834dd70ebb6ca39e7d3a6aadac04.png](https://i-blog.csdnimg.cn/blog_migrate/ca44fb2d7d69e43a4c722d2a5c9f906a.png)
前面文章介绍的spinlock加锁的实现都是基于的arch_spin_lock()这个函数,但内核编程实际使用的通常是spin_lock(),它们中间还隔了好几层调用关系。先来看最外面的一层(代码位于/include/linux/spinlock.h):
static __always_inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock) _raw_spin_lock(lock)
接下来,_raw_spin_lock()的实现将出现分野。
【关闭调度】
- SMP实现
spinlock通常是用于多核系统(SMP)的,但它同样也可以用于单核(UP)的场景。针对SMP,_raw_spin_lock()的实现是这样的(定义在/include/linux/spinlock_api_smp.h):
#ifdef CONFIG_INLINE_SPIN_LOCK
#define _raw_spin_lock(lock) __raw_spin_lock(lock)
#endif
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
...
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
采用inline函数,可以减少函数调用的开销,提高执行速度,但不利于跟踪调试,所以内核提供了"CONFIG_INLINE_SPIN_LOCK"这个配置选项供用户选择。
越往内层,函数名前面的下划线"_"越多。可以看到,在最内侧的__raw_spin_lock()中,调用了preempt_disable()来关闭调度。也就是说,运行在一个CPU上的代码使用spin_lock()试图加锁之后,基于该CPU的线程调度和抢占就被禁止了,这也体现了spinlock作为"busy loop"形式的锁的语义。
到了do_raw_spin_lock()这一步,就进入了和架构相关的arch_spin_lock()。
static inline void do_raw_spin_lock(raw_spinlock_t *lock)
{
__acquire(lock);
arch_spin_lock(&lock->raw_lock);
...
}
- UP实现
再来看下_raw_spin_lock()针对UP系统的实现(代码位于/include/linux/spinlock_api_up.h):
#define _raw_spin_lock(lock) __LOCK(lock)
#define __LOCK(lock)
do { preempt_disable(); ___LOCK(lock); } while (0)
#define ___LOCK(lock)
do { __acquire(lock); (void)(lock); } while (0)
在UP的环境中,不再需要防止多个CPU对共享变量的同时访问,所以spin_lock()的作用仅仅是关闭调度,等同于(或者说退化成了)preempt_disable()。
之所以UP系统也支持使用spinlock相关的函数,是因为这样同一套代码可以同时支持UP和SMP的应用,只需要在配置中选择是否使用"CONFIG_SMP"就可以了。
【关闭中断】
spin_lock()可以防止线程调度,但不能防止硬件中断的到来,以及随后的中断处理函数(hardirq)的执行,这会带来什么影响呢?
试想一下,假设一个CPU上的线程T持有了一个spinlock,发生中断后,该CPU转而执行对应的hardirq。如果该hardirq也试图去持有这个spinlock,那么将无法获取成功,导致hardirq无法退出。在hardirq主动退出之前,线程T是无法继续执行以释放spinlock的,最终将导致该CPU上的代码不能继续向前运行,形成死锁(dead lock)。
![69cacb2814a11255f42cab1569066267.png](https://i-blog.csdnimg.cn/blog_migrate/36d8f2a8f0a928394792f58f8b035d56.png)
为了防止这种情况的发生,我们需要使用spin_lock_irq()函数,一个spin_lock()和local_irq_disable()的结合体,它可以在spinlock加锁的同时关闭中断。
![8645e95eb5d8408a96e1795c5759178b.png](https://i-blog.csdnimg.cn/blog_migrate/84e75c35cb1a1573c3856e6910967b56.png)
因为中断关闭的操作是可以嵌套的,更多的时候我们是使用local_irq_save()来记录关中断的状态,对应地一个更常用的函数就是spin_lock_irqsave()。
static inline unsigned long __raw_spin_lock_irq_save(raw_spinlock_t *lock)
{
unsigned long flags;
local_irq_save(flags);
__raw_spin_lock(lock);
return flags;
}
然而,local_irq_save()只能对本地CPU执行关中断操作,所以即便使用了spin_lock_irqsave(),如果其他CPU上发生了中断,那么这些CPU上的hardirq,也有可能试图去获取一个被本地CPU上运行的线程T占有的spinlock。
不过没有关系,因为此时hardirq和线程T运行在不同的CPU上,等到线程T继续运行释放了这个spinlock,hardirq就有机会获取到,不至于造成死锁。
对于UP系统,spin_lock_irqsave()的作用只剩下关闭中断了(中断关闭时不会产生时钟中断,调度自然也是关闭的),也就退化成了local_irq_save()。
【屏蔽softirq】
如果hardirq不会和线程共享变量,是不是就可以直接使用spin_lock()呢?非也,因为在切回被打断的线程之前,还可能会执行对应的softirq函数。如果该softirq可能访问和线程共享的变量,那么线程就应该使用spin_lock_bh(),一个spin_lock()加local_bh_disable()的二合一函数,否则也可能会导致dead lock。
![fc6983b5037c8b905aba41fee22c67e7.png](https://i-blog.csdnimg.cn/blog_migrate/9f70b59ea014be4e7d7096b94c552adc.png)
"bh"代表bottom half,而Linux中的bottom half包括softirq, tasklet和workqueue三种,由于workqueue是运行在进程上下文,所以这里的"bh"只针对softirq和tasklet。
【API的选择】
如果关闭了中断,hardirq不会执行,对应的softirq就更不会执行,可见,使用spin_lock_irqsave()无疑是最安全的,但同时也是开销最大的。
从程序性能的角度出发,在进程上下文中,对于不会和hardirq/softirq共享的变量,应该尽量使用更轻量级的spin_lock()。只会和softirq共享而不会和hardirq共享的,则应该使用spin_lock_bh()。
对于hardirq上下文,因为Linux是不支持hardirq嵌套的(参考这篇文章评论区的讨论),在hardirq执行期间,CPU对中断的响应默认是关闭的,所以可直接使用spin_lock()。
至于softirq上下文,因为有可能被hardirq打断,针对会和hardirq共享的变量,需使用spin_lock_irqsave()。
总之,在用一个锁之前,你得清楚有可能和你竞争这个锁的对手是谁。
至此,就可以解答上文留下的那个问题,即为什么一个CPU在一种context下,至多试图获取(或者说竞争)一个spinlock。线程使用spin_lock()试图获取spinlock A,此时发生了中断,如果hardirq获取spinlock B,那么该CPU就同时在试图获取2个spinlock。
![46e5e043ceab6ba462cefb2e5e5532e3.png](https://i-blog.csdnimg.cn/blog_migrate/3b66fc8de3352d57f95c92d42e37c4d7.png)
如果hardirq没有试图获取spinlock,执行完后进入了softirq,softirq试图获取spinlock B,然后又被另一个中断打断,新的hardirq在执行过程中又试图获取spinlock C,那么该CPU就同时在试图获取3个spinlock。
![2dd516364614879ca439080b356bf623.png](https://i-blog.csdnimg.cn/blog_migrate/ffbbbf231961f384080089f25ef51e50.png)
如果再加入nmi,以此类推,一个CPU至多同时试图获取4个spinlock。
spin_lock_irqsave()/spin_lock_bh()可以防止hardirq/softirq和线程共享变量造成的死锁,但这只是死锁可能出现的一种情况,也可以说是仅依靠选择合适的API就可以避免的死锁,更多死锁的场景和应对办法,将交由下文讨论。
参考:
https://www.kernel.org/doc/htmldocs/kernel-locking/index.html
字节岛 - 自旋锁spin_lock、spin_lock_irq以及spin_lock_irqsave的区别
原创文章,转载请注明出处。