[内核同步]自旋锁spin_lock、spin_lock_irq 和 spin_lock_irqsave 分析

本文详细探讨了Linux进程中的自旋锁、中断上下文以及它们在内核空间的应用。自旋锁用于短时间保护临界区,避免内核抢占带来的竞争。spin_lock与spin_lock_irqsave的区别在于前者仅禁用内核抢占,后者还关闭本地中断以防止死锁。理解何时选择spin_lock和spin_lock_irqsave对于编写高效且健壮的内核代码至关重要。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于进程上下文,中断上下文,请看这篇文章

Linux进程上下文和中断上下文内核空间和用户空间

自旋锁的初衷:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。

单处理器的自旋锁

  • 首先,自旋锁的目的如果在系统不支持内核抢占时,自旋锁的实现也是空的,因为单核只有一个线程在执行,不会有内核抢占,从而资源也不会被其他线程访问到。

  • 其次,支持内核抢占,由于自旋锁是禁止抢占内核的,所以不会有其他的进程因为等待锁而自旋.

  • 最后,只有在多cpu下,其他的cpu因为等待该cpu释放锁,而处于自旋状态,不停轮询锁的状态。所以这样的话,如果一旦自旋锁内代码执行时间较长,等待该锁的cpu会耗费大量资源,也是不同于信号量和互斥锁的地方。

简单来说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。

自旋锁内睡眠禁止睡眠问题:如果自旋锁锁住以后进入睡眠,而此时又不能进行处理器抢占(锁住会disable prempt),其他进程无法获得cpu,这样也不能唤醒睡眠的自旋锁,因此不相应任何操作。

自旋锁为什么广泛用于内核:自旋锁是一种轻量级的互斥锁,可以更高效的对互斥资源进行保护。自旋锁本来就只是一个很简单的同步机制,在SMP之前根本就没这个东西,一切都是Event之类的同步机制,这类同步机制都有一个共性就是:一旦资源被占用都会产生任务切换,任务切换涉及很多东西的(保存原来的上下文,按调度算法选择新的任务,恢复新任务的上下文,还有就是要修改cr3寄存器会导致cache失效)这些都是需要大量时间的,因此用Event之类来同步一旦涉及到阻塞代价是十分昂贵的,而自旋锁的效率就远高于互斥锁。

在Linux内核中何时使用spin_lock,何时使用spin_lock_irqsave很容易混淆。首先看一下代码是如何实现的。

  • spin_lock

spin_lock -----> raw_spin_lock

1 static inline void __raw_spin_lock(raw_spinlock_t *lock)  
2 {  
3         preempt_disable();  
4         spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);  
5         LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);  
6 }   
  1. 只禁止内核抢占,不会关闭本地中断

  2. 为何需要关闭内核抢占:假如进程A获得spin_lock->进程B抢占进程A->进程B尝试获取spin_lock->由于进程B优先级比进程A高,先于A运行,而进程B又需要A unlock才得以运行,这样死锁。所以这里需要关闭抢占。这个原理RTOS的mutex/semaphore是否相同?

a. 因为ThreadX的semaphore,假如进程B获取sema失败,会一直等待,直到A进程释放,不会死锁。

b. Mutex: mutex获取一旦失败,进程会进入sleep,直到其他进程释放;而spin_lock则不同,会一直轮训访问,且直到时间片耗完。

  • spin_lock_irq

    spin_lock_irq------> raw_spin_lock_irq

1 static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)  
2 {  
3         local_irq_disable();  
4         preempt_disable();  
5         spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);  
6         LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);  
7 }  
  1. 禁止内核抢占,且关闭本地中断

  2. 那么在spin_lock中关闭了内核抢占,不关闭中断会出现什么情况呢?假如中断中也想获得这个锁,会出现和spin_lock中举得例子相同。所以这个时候,在进程A获取lock之后,使用spin_lock_irq将中断禁止,就不会出现死锁的情况

  3. 在任何情况下使用spin_lock_irq都是安全的。因为它既禁止本地中断,又禁止内核抢占。

  4. spin_lock比spin_lock_irq速度快,但是它并不是任何情况下都是安全的。

  • spin_lock_irqsave

spin_lock_irqsave------>__raw_spin_lock_irqsave

 1 static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
 2 {
 3     unsigned long flags;
 4 
 5     local_irq_save(flags);
 6     preempt_disable();
 7     spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
 8     /*
 9      * On lockdep we dont want the hand-coded irq-enable of
10      * do_raw_spin_lock_flags() code, because lockdep assumes
11      * that interrupts are not re-enabled during lock-acquire:
12      */
13 #ifdef CONFIG_LOCKDEP
14     LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
15 #else
16     do_raw_spin_lock_flags(lock, &flags);
17 #endif
18     return flags;
19 }
  1. 禁止内核抢占,关闭中断,保存中断状态寄存器的标志位

  2. spin_lock_irqsave在锁返回时,之前开的中断,之后也是开的;之前关,之后也是关。但是spin_lock_irq则不管之前的开还是关,返回时都是开的(?)。

  3. spin_lock_irq在自旋的时候,不会保存当前的中断标志寄存器,只会在自旋结束后,将之前的中断打开。

1. spin_lock/spin_unlock:

进程A中调用了spin_lock(&lock)然后进入临界区,此时来了一个中断(interrupt),该中断也运行在和进程A相同的CPU上,并且在该中断处理程序中恰巧也会spin_lock(&lock), 试图获取同一个锁。由于是在同一个CPU上被中断,进程A会被设置为TASK_INTERRUPT状态,中断处理程序无法获得锁,会不停的忙等,由于进程A被设置为中断状态,schedule()进程调度就无法再调度进程A运行,这样就导致了死锁!

但是如果该中断处理程序运行在不同的CPU上就不会触发死锁。因为在不同的CPU上出现中断不会导致进程A的状态被设为TASK_INTERRUPT,只是换出。当中断处理程序忙等被换出后,进程A还是有机会获得CPU,执行并退出临界区。所以在使用spin_lock时要明确知道该锁不会在中断处理程序中使用。

2. spin_lock_irq/spin_unlock_irq

spin_lock_irq----->raw_spin_lock_irq

spin_lock_irq 和 spin_unlock_irq, 如果你确定在获取锁之前本地中断是开启的,那么就不需要保存中断状态,解锁的时候直接将本地中断启用就可以啦

3. spin_lock_irqsave/spin_unlock_irqrestore

使用spin_lock_irqsave在于你不期望在离开临界区后,改变中断的开启/关闭状态!进入临界区是关闭的,离开后它同样应该是关闭的!

如果自旋锁在中断处理函数中被用到,那么在获取该锁之前需要关闭本地中断,spin_lock_irqsave 只是下列动作的一个便利接口:1 保存本地中断状态(这里的本地即当前的cpu的所有中断) 2 关闭本地中断 3 获取自旋锁 解锁时通过 spin_unlock_irqrestore完成释放锁、恢复本地中断到之前的状态等工作

参考:

  1. 什么情况下使用什么样的自旋锁:http://blog.youkuaiyun.com/wesleyluo/article/details/8807919

  2. 本地中断概念的理解:http://blog.youkuaiyun.com/adaptiver/article/details/6177646

  3. 深入理解自旋锁:http://blog.youkuaiyun.com/vividonly/article/details/6594195

  4. 自旋锁和互斥量区别:http://blog.youkuaiyun.com/kyokowl/article/details/6294341

  5. 自旋锁的临界区本地cpu不会发生任何进程调度:http://blog.chinaunix.net/uid-23769728-id-3367773.html

总结

这样是否可以这么说,spin_lock为了防止内核的抢占死锁,spin_lock_irq为了防止内核和中断的抢占死锁,spin_lock_irqsave 为了防止进入自旋状态丢掉之前的中断状态。

所以如果不懂的话,直接用spin_lock_irqsave是最靠谱的,如果懂的人,就可以选择用spin_lock

转自:

https://www.cnblogs.com/aaronLinux/p/5890924.html


推荐阅读:

专辑|Linux文章汇总

专辑|程序人生

专辑|C语言

我的知识小密圈

关注公众号,后台回复「1024」获取学习资料网盘链接。

欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

<think>好的,我现在需要回答用户关于spin_lock_irqsave在多核环境下的实现细节的问题。用户希望了解其实现原理,属于Linux内核同步机制的一部分。首先,我需要回忆spin_lock_irqsave的基本作用,然后结合多核环境下的具体实现来分析。 根据已有的引用资料,比如引用[2]提到自旋锁在单核多核的实现区别,而引用[3]说明在单核系统中,spin_lock实际上是关闭抢占。但用户的问题是多核下的情况,所以需要关注多核相关的处理。 首先,spin_lock_irqsave不仅会获取自旋锁,还会保存当前的中断状态并禁用本地中断。这在多核中尤为重要,因为除了防止其他核的竞争,还需要防止本地中断处理程序的并发访问。 接下来,我需要查阅相关内核代码的结构。引用[2]提到arch_spinlock_t是架构相关的,以ARM为例,spinlock_types.h定义了结构体。多核环境下,自旋锁的实现可能依赖于原子操作或硬件指令,比如ARM中的LDREXSTREX指令来实现原子操作。 另外,spin_lock_irqsave在多核中的实现需要确保在获取锁时,本地CPU的中断被禁用,并且保存中断状态,以便解锁时恢复。这涉及到调用preempt_disable()local_irq_save(),同时处理自旋锁的获取。对于多核,自旋锁可能使用ticket机制或其他公平锁机制来避免饥饿。 还需要注意,在多核系统中,自旋锁的获取可能涉及忙等待,即循环检查锁的状态,直到其他核心释放锁。同时,结合中断禁用,确保在持有锁期间不会因为中断处理而引发死锁。 引用[4]提到spin_unlock可能触发调度,但在spin_lock_irqsave的情况下,因为中断被禁用,解锁时会恢复中断状态,可能重新启用中断,并释放锁,允许其他核心获取。 需要确认这些步骤在内核代码中的具体实现,比如spin_lock_irqsave宏如何展开,涉及到哪些底层函数,以及如何与架构相关的代码交互。例如,在ARM架构中,spinlock的实现可能使用tickets结构,包含nextowner字段,通过原子操作来管理锁的获取。 总结起来,spin_lock_irqsave在多核环境中的实现步骤大致包括: 1. 保存当前中断状态并禁用本地中断。 2. 禁用抢占。 3. 通过原子操作获取自旋锁,若锁被占用则循环等待。 4. 其他核心必须等待当前核心释放锁后才能获取。 需要确保这些步骤在回答中清晰呈现,并引用提供的参考资料来支持说明,比如引用[2][3]关于架构相关实现单核多核区别的部分。同时,注意用户要求的中文回答LaTeX格式的正确应用,比如变量用$...$,独立公式用$$...$$,但当前问题可能涉及代码较多,数学表达式较少,但如果有的话需要正确应用。 最后,生成相关问题时要围绕spin_lock_irqsave的实现、使用场景、与其他锁的区别、性能影响等方面展开,确保问题相关且有深度。</think>### spin_lock_irqsave 多核实现原理 `spin_lock_irqsave` 是 Linux 内核中用于同步的核心机制,**在多核环境下需同时处理中断竞争跨核资源争用**。其实现结合了**自旋锁****中断状态管理**,具体步骤如下: 1. **保存并禁用本地中断** 通过 `local_irq_save(flags)` 保存当前中断状态到 `flags` 变量,并禁用本地 CPU 的中断。这一步防止当前 CPU 的中断处理程序与临界区代码发生竞争[^3]。 2. **禁用内核抢占** 调用 `preempt_disable()` 禁止内核抢占,确保当前线程在执行临界区时不会被其他线程抢占。 3. **获取自旋锁(多核实现)** 自旋锁在多核环境中的实现依赖于**原子操作****硬件指令**(如 ARM 的 `LDREX/STREX`)。以 ARM 架构为例,锁结构 `arch_spinlock_t` 包含两个字段: - `next`:表示下一个请求锁的 CPU 编号 - `owner`:表示当前持有锁的 CPU 编号 通过原子操作实现“排队”机制(ticket-based spinlock),确保公平性[^2]。 ```c typedef struct { union { u32 slock; struct __raw_tickets { u16 owner; u16 next; } tickets; }; } arch_spinlock_t; ``` 4. **忙等待与缓存一致性** 若锁已被其他核心占用,当前 CPU 进入忙等待(自旋),并通过缓存一致性协议(如 MESI)监听锁状态变化。释放锁时,`spin_unlock` 会更新 `owner` 字段并触发缓存同步,通知其他核心[^2][^4]。 --- ### 多核场景的关键设计 - **中断隔离**:禁用本地中断防止当前 CPU 的中断处理程序访问共享资源,但**其他 CPU 的中断仍可能触发**,需依赖自旋锁保护跨核访问[^3]。 - **内存屏障**:锁操作中隐含内存屏障(如 `smp_mb()`),确保临界区内的内存访问顺序性。 - **优化与扩展**:高版本内核引入 `qspinlock` 优化,通过哈希队列减少缓存行争用。 --- ### 示例代码(简化逻辑) ```c void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) { local_irq_save(flags); // 保存并禁用中断 preempt_disable(); // 禁用抢占 arch_spin_lock(&lock->raw_lock); // 架构相关的锁获取 } void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) { arch_spin_unlock(&lock->raw_lock); preempt_enable(); // 启用抢占 local_irq_restore(flags); // 恢复中断状态 } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值