[toc]
kernel/locking/spinlock.c 自旋锁(Spinlock) 内核中最基础的高性能对称多处理器(SMP)锁
历史与背景
这项技术是为了解决什么特定问题而诞生的?
自旋锁(Spinlock)是为了解决**对称多处理器(SMP)**系统中最基础的并发控制问题而诞生的:如何保护一段极短的、在任何执行上下文中都可能被访问的临界区。
在单处理器(UP)系统中,要保护一段临界区,最简单的方法是禁用本地中断。这样可以确保在执行临界区代码时,不会被中断处理程序打断,从而避免了并发。
然而,在多处理器(SMP)系统中,禁用本地CPU的中断并不能阻止另一个CPU同时执行并访问同一块共享数据。因此,需要一种CPU之间的互斥机制。自旋锁就是为此设计的最底层、最高效的锁。它主要解决:
- SMP互斥:提供一种在多个CPU之间进行互斥的机制。
- 原子上下文(Atomic Context)保护:中断处理程序、软中断、tasklet等上下文是不能睡眠的。自旋锁通过“忙等待”而不是“睡眠”来等待锁,因此它是唯一可以在这些原子上下文中安全使用的锁类型。
它的发展经历了哪些重要的里程碑或版本迭代?
Linux的自旋锁实现为了追求更高的性能和公平性,经历了多次重要演进。
- 简单的Test-and-Set锁:最早的实现基于简单的原子测试和设置指令。这种锁存在严重的公平性问题,在多CPU高竞争下,一个CPU可能被其他CPU持续“插队”,导致饥饿。
- 票据自旋锁(Ticket Spinlocks):这是一个关键的里程碑,解决了公平性问题。它引入了一种类似银行排队叫号的机制,每个尝试获取锁的CPU都会领取一个“票号(ticket)”,然后等待“叫号器”叫到自己的号码。这保证了锁的获取是**先进先出(FIFO)**的,避免了饥饿。这是很长一段时间内Linux内核的标准实现。
- 队列自旋锁(Queued Spinlocks - qspinlocks):在拥有大量CPU的现代NUMA(非统一内存访问)系统上,票据锁会导致所有等待的CPU都在同一个内存地址(锁变量)上自旋,造成严重的缓存行争用(cache-line bouncing),影响扩展性。为了解决这个问题,内核引入了
qspinlock
。它让每个等待的CPU在一个per-CPU的变量上自旋,只有一个CPU负责监视主锁,形成了一个隐式的等待队列,极大地降低了总线流量和缓存争用。这是当前Linux内核的默认实现。 - 调试支持(
CONFIG_DEBUG_SPINLOCK
):增加了强大的调试功能,用于检测常见的自旋锁误用,如初始化前使用、重复加锁、解锁一个未被自己持有的锁等。
目前该技术的社区活跃度和主流应用情况如何?
自旋锁是Linux内核的基石,是实现所有其他更高级同步原语的基础。其代码位于内核最核心、对性能最敏感的部分。社区的活跃度主要体现在:
- 针对新的处理器架构,持续优化其原子操作的实现。
- 在极端的并发场景下,对其性能和扩展性进行微调。
自旋锁被内核中所有对性能要求极致、且不能睡眠的路径所使用,例如:
- 中断处理程序
- 调度器核心逻辑
- 内存管理子系统(如页分配器)
- 网络数据包处理路径
核心原理与设计
它的核心工作原理是什么?
自旋锁的核心原理是忙等待(Busy-Waiting)。当一个CPU尝试获取一个已经被其他CPU持有的自旋锁时,它不会像互斥锁那样进入睡眠状态,而是会执行一个紧凑的循环(“自旋”),不断地检查锁是否已经被释放。
以票据自旋锁(Ticket Spinlock)为例:
- 数据结构:锁变量包含两个部分:
owner
: 当前持有锁的“票号”。next
: 即将被分发的下一个“票号”。
- 加锁 (
spin_lock
):
a. 一个CPU想要获取锁,它会原子地执行“获取并加一”操作在next
字段上,得到自己的票号。
b. 然后,它进入一个循环,不断地读取owner
字段,直到owner
的值等于它自己手中的票号。 - 解锁 (
spin_unlock
):
a. 持有锁的CPU完成临界区的工作后,它只需简单地将owner
字段加一。
b. 这个操作会使得下一个正在自旋等待的CPU(持有新owner
票号的那个)退出其自旋循环,从而成功获取锁。
它的主要优势体现在哪些方面?
- 极高的性能:在无竞争或低竞争的情况下,加锁和解锁操作通常只需要一两个原子指令,开销极小。
- 可在任何上下文中使用:因为它从不睡眠,所以它是唯一可以在中断上下文(硬中断、软中断、tasklet)安全使用的锁。这是它与
mutex
最根本的区别。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 浪费CPU资源:在等待锁时,自旋的CPU持续消耗100%的CPU时间,没有做任何有意义的工作。因此,自旋锁绝对不能用于保护可能耗时较长的临界区。
- 严禁睡眠:持有自旋锁的临界区内绝对不允许调用任何可能导致睡眠的函数。如果一个CPU持有锁并进入睡眠,其他需要该锁的CPU将会永远自旋下去,导致整个系统死锁。
- 与中断的死锁风险:如果在进程上下文中获取了一个自旋锁,而此时一个中断发生,并且该中断处理程序也尝试获取同一个自旋锁,那么系统会立即死锁。因为中断处理程序会抢占进程上下文,而它又在等待进程上下文释放锁。为了避免这种情况,当需要同时保护数据不被中断和其他CPU访问时,必须使用
spin_lock_irqsave()
/spin_unlock_irqrestore()
,它们会在加锁时禁用本地中断。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
自旋锁是为临界区极短、严禁睡眠的场景量身定做的。
- 中断处理程序:这是自旋锁最经典、最正当的使用场景。中断处理程序必须快速完成,且不能睡眠,因此只能使用自旋锁来保护其共享数据。
- 调度器和定时器:内核调度器和定时器子系统的核心数据结构(如运行队列)可能会在中断上下文和进程上下文中被同时访问,且操作必须极快,因此使用自旋锁。
- 工作队列和Tasklet:这些下半部机制在执行时也处于不能睡眠的原子上下文,因此它们内部的同步也依赖自旋锁。
是否有不推荐使用该技术的场景?为什么?
- 临界区较长:如果持有锁的时间可能超过几个微秒,就应该使用
mutex
,以避免其他CPU长时间空转。 - 临界区内可能睡眠:绝对禁止。例如,临界区内需要调用
kmalloc(GFP_KERNEL)
、copy_from_user()
、msleep()
或任何可能导致I/O阻塞的操作,必须使用mutex
。 - 高竞争且临界区不极短:如果一个锁被频繁地竞争,并且持有时间不是瞬时的,那么让等待者睡眠(使用
mutex
)可能比让多个CPU同时自旋对系统整体吞吐量的影响更小。
对比分析
请将其 与 其他相似技术 进行详细对比。
特性 | 自旋锁 (Spinlock) | 互斥锁 (Mutex) |
---|---|---|
基本行为 | 自旋 (忙等待,占用CPU)。 | 睡眠 (放弃CPU)。 |
使用上下文 | 任何上下文 (硬中断、软中断、进程)。 | 仅进程上下文 (可以睡眠)。 |
临界区约束 | 绝对不能睡眠,必须极短。 | 可以睡眠,可以较长。 |
性能(无竞争) | 非常高 (通常是单个原子指令)。 | 非常高 (通过快速路径原子操作)。 |
性能(有竞争) | 低 (浪费CPU,可能导致总线争用)。 | 中等 (有上下文切换的开销,但CPU可用于其他任务)。 |
中断安全 | 需要手动通过_irqsave 版本来禁用本地中断。 |
不能在中断上下文中使用。 |
死锁风险 | 较高 (与中断、抢占相关的死锁风险)。 | 较低 (调试机制更完善)。 |
典型场景 | 中断处理、调度器、高性能数据路径。 | 设备驱动中的ioctl 、read /write ,文件系统。 |
include/linux/spinlock_api_up.h
__LOCK
1 | /* |
__UNLOCK
1 |
arch/arm/include/asm/spinlock_types.h
1 | typedef struct { |
include/linux/spinlock.h
DEFINE_LOCK_GUARD_1 raw_spinlock_irqsave
1 | DEFINE_LOCK_GUARD_1(raw_spinlock_irqsave, raw_spinlock_t, |
bit_spinlock 位自旋锁
bit_spin_lock
是一个基于位的自旋锁实现,用于在多核或多线程环境中实现低开销的同步机制。它通过操作特定内存地址中的单个位来实现锁的获取和释放。以下是对代码的详细解释:
bit_spinlock
和 spinlock
的异同点
相同点
基本原理:
- 两者都属于自旋锁,通过忙等待的方式实现锁的获取和释放。
- 都用于保护共享资源,防止多个线程或 CPU 同时访问导致数据竞争。
无阻塞特性:
- 两者都不会引起线程的阻塞,而是通过循环检查锁的状态来等待锁的释放。
适用场景:
- 都适用于短时间的临界区保护,避免因上下文切换导致的性能开销。
- 都可以在中断上下文中使用。
多核支持:
- 两者都支持多核系统中的并发同步。
不同点
特性 | bit_spinlock |
spinlock |
---|---|---|
锁的粒度 | 基于单个位的锁,每个位表示一个独立的锁。 | 基于整个变量的锁,通常是一个整数或布尔值。 |
内存占用 | 占用更少的内存,可以在同一变量的不同位上实现多个锁。 | 每个锁需要一个独立的变量。 |
实现复杂度 | 需要操作特定位,逻辑稍微复杂。 | 实现相对简单,直接操作整个变量。 |
性能 | 在锁竞争较少的情况下性能更高,适合细粒度锁。 | 在锁竞争较多的情况下性能更稳定。 |
使用场景 | 适用于需要多个小锁的场景,例如位图操作。 | 适用于保护单一资源或较大临界区的场景。 |
调试支持 | 调试支持较少,难以跟踪锁的状态。 | 提供丰富的调试支持(如死锁检测)。 |
各自的使用场景
bit_spinlock
的使用场景
位图操作:
- 当需要对位图中的单个位进行加锁时,
bit_spinlock
是理想选择。例如,管理内存页的分配状态。
- 当需要对位图中的单个位进行加锁时,
细粒度锁:
- 需要多个小锁来保护不同的资源时,
bit_spinlock
可以通过操作同一变量的不同位来实现,节省内存。
- 需要多个小锁来保护不同的资源时,
高性能场景:
- 在锁竞争较少的情况下,
bit_spinlock
的性能更高,适合性能敏感的场景。
- 在锁竞争较少的情况下,
嵌入式系统:
- 在内存资源有限的嵌入式系统中,
bit_spinlock
的低内存占用是一个优势。
- 在内存资源有限的嵌入式系统中,
spinlock
的使用场景
单一资源保护:
- 当需要保护一个共享资源时,
spinlock
是更简单和直接的选择。
- 当需要保护一个共享资源时,
中断上下文:
- 在中断处理程序中,
spinlock
可以与spin_lock_irqsave
等接口结合使用,确保中断安全。
- 在中断处理程序中,
多核系统中的同步:
- 在多核系统中,
spinlock
可以用于同步多个线程或 CPU 对共享资源的访问。
- 在多核系统中,
调试和开发:
- 如果需要丰富的调试支持(如死锁检测),
spinlock
是更好的选择。
- 如果需要丰富的调试支持(如死锁检测),
总结
bit_spinlock
更适合细粒度锁和内存占用敏感的场景,例如位图操作和嵌入式系统。spinlock
更适合保护单一资源或需要调试支持的场景,例如中断上下文和多核同步。- 在实际开发中,应根据具体需求选择合适的锁机制,以在性能和复杂性之间取得平衡。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 wdfk-prog的个人博客!
评论