转自:https://zhuanlan.zhihu.com/p/90634198

Linux中的spinlock机制[四] - API的使用

兰新宇

talk is cheap

 

前面文章介绍的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)。

为了防止这种情况的发生,我们需要使用spin_lock_irq()函数,一个spin_lock()和local_irq_disable()的结合体,它可以在spinlock加锁的同时关闭中断。

因为中断关闭的操作是可以嵌套的,更多的时候我们是使用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。

"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。

如果hardirq没有试图获取spinlock,执行完后进入了softirq,softirq试图获取spinlock B,然后又被另一个中断打断,新的hardirq在执行过程中又试图获取spinlock C,那么该CPU就同时在试图获取3个spinlock。

如果再加入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的区别

 

原创文章,转载请注明出处。

查看原文