Linux中的spinlock机制 - API的使用

本文详细介绍了spinlock在不同场景下的实现,如SMP多核系统和UP单核系统,以及如何选择spin_lock、spin_lock_irqsave、spin_lock_bh等API以防止死锁,特别关注了中断、hardirq和softirq的影响。
摘要由CSDN通过智能技术生成

一:spin_lock函数调用

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()的实现将出现分支。

二:针对相关场景需求基于spin_lock调整适配

关闭调度的自旋锁

SMP多核系统下spinlock实现

针对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单核系统下spinlock实现

再来看下_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()。

关闭软中断的自旋锁

如果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。

不同场景下以上spinlock上锁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://zhuanlan.zhihu.com/p/90634198

Linuxspinlock是一种自旋锁机制,用于保护对共享资源的访问,以防止同时访问导致的数据竞争问题。spinlock使用了一种称为自旋的技术,即当一个线程需要获取锁时,它会一直等待,直到锁被释放。这种等待是循环的,即线程会不断地检查锁的状态,直到锁被释放为止。 spinlock相比于传统的互斥量(mutex)和信号量(semaphore)等锁机制,具有更高的性能和灵活性。spinlock不需要使用内核调度器,因此不会产生额外的上下文切换开销。此外,spinlock可以用于任何需要保护的临界区代码,而不仅仅是用于进程之间的同步。 使用spinlock时,需要将其初始化为0,以便其他线程可以安全地访问共享资源。当一个线程需要获取锁时,它可以使用spin_lock函数来锁定spinlock。如果锁已经被其他线程占用,该线程将进入自旋状态,不断检查锁的状态。当该线程获取到锁时,它可以将共享资源置于临界区并执行相关操作。在操作完成后,该线程可以使用spin_unlock函数释放锁。 spinlock机制适用于一些简单的同步场景,例如在并发访问共享资源时保护临界区代码。然而,对于一些复杂的同步需求,可能需要使用更高级的同步机制,如读写锁(rwlock)或条件变量(condition variable)。 总之,spinlock是一种轻量级的自旋锁机制,适用于简单的同步场景,具有较高的性能和灵活性。它适用于任何需要保护的临界区代码,而不仅仅是用于进程之间的同步。在使用spinlock时,需要注意避免死锁和过度自旋等问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值