news 2026/4/16 21:24:13

linux内核 - spinlock

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
linux内核 - spinlock

自旋锁(spinlock)是一种基于硬件实现的锁机制,它依赖硬件提供的原子操作能力(例如 test_and_set。在非原子实现中,这个操作通常会被分解为“读-修改-写”三个步骤)。它是最简单、最基础的一种锁机制,其工作方式如下。

当 CPU B 正在运行,而任务 B 想要获取自旋锁(即调用自旋锁的加锁函数),但此时该锁已经被另一个 CPU(例如 CPU A 上运行的任务 A 已经调用了加锁函数并持有该锁)占用,那么 CPU B 就会在一个 while 循环中不断“自旋等待”(从而阻塞任务 B),直到另一个 CPU 释放该锁(即任务 A 调用解锁函数)。

这种“自旋等待”只会发生在多核系统上,因为只有多个 CPU 同时运行时才可能出现一个 CPU 等待另一个 CPU 释放锁的情况。在单核系统中不会发生这种情况,因为同一时刻只能运行一个任务:要么任务持有锁并继续执行,要么任务不运行直到锁被释放。

自旋锁本质上是“由 CPU 持有的锁”,这一点与互斥锁(mutex)不同,后者是“由任务持有的锁”。

自旋锁的工作方式是:在本地 CPU 上禁用调度器(即执行获取自旋锁的任务所在的 CPU)。这意味着该 CPU 上正在运行的任务不会被抢占,除非发生中断请求(IRQ),前提是本地 CPU 上未禁用中断(后面会详细说明)。

换句话说,自旋锁用于保护在任意时刻只能被一个 CPU 访问的资源,这使其适用于对称多处理(SMP)系统的并发安全,以及执行原子操作。

自旋锁不仅仅依赖硬件提供的原子操作函数。例如在 Linux 内核中,抢占状态依赖于一个每 CPU(per-CPU)变量:如果该变量等于 0,表示允许抢占;如果大于 0,则表示抢占被禁用(此时 schedule() 调度函数将无法执行)。

因此,禁用抢占(preempt_disable())的实现方式,就是将当前 per-CPU 变量(实际上是 preempt_count)加 1;而 preempt_enable() 则会将该变量减 1,并检查新的值是否为 0,如果为 0,则调用 schedule()。

这些加法/减法操作必须是原子的,因此依赖 CPU 提供原子加法/减法指令能力。

自旋锁可以通过两种方式创建:一种是使用 DEFINE_SPINLOCK 宏进行静态定义,如下所示;另一种是在运行时对一个未初始化的自旋锁调用 spin_lock_init() 进行初始化。

static DEFINE_SPINLOCK(my_spinlock);

为了理解其工作原理,只需要查看该宏在 include/linux/spinlock_types.h 中的定义,如下:

#define DEFINE_SPINLOCK(x) spinlock_t x = \ __SPIN_LOCK_UNLOCKED(x)

它可以这样使用:

static DEFINE_SPINLOCK(foo_lock);

在这之后,这个自旋锁可以通过名字 foo_lock 访问,它的地址是 &foo_lock。

然而,对于动态(运行时)分配的情况,更好的做法是将自旋锁嵌入到一个更大的结构体中,为该结构体分配内存,然后对其中的自旋锁成员调用 spin_lock_init(),如下代码所示:

struct bigger_struct { spinlock_t lock; unsigned int foo; [...] }; static struct bigger_struct *fake_init_function() { struct bigger_struct *bs; bs = kmalloc(sizeof(struct bigger_struct), GFP_KERNEL); if (!bs) return -ENOMEM; spin_lock_init(&bs->lock); return bs; }

通常情况下,尽可能使用 DEFINE_SPINLOCK 是更好的选择。它提供了编译期初始化,并且代码更简洁,几乎没有实际缺点。

在使用自旋锁时,可以通过内联函数 spin_lock() 和 spin_unlock() 来加锁和解锁,这两个函数定义在 include/linux/spinlock.h 中,如下所示:

static __always_inline void spin_unlock(spinlock_t *lock) static __always_inline void spin_lock(spinlock_t *lock)

不过,使用这种方式的自旋锁也存在一些已知限制。虽然自旋锁可以防止本地 CPU 上的抢占,但它无法防止该 CPU 被中断“占用”(即进入中断处理程序执行)。

假设一种情况:CPU 为了保护某个资源,以任务 A 的名义持有一个自旋锁。此时发生了一个中断,CPU 会暂停当前任务并跳转到该中断处理函数。到目前为止一切正常。

但如果这个中断处理函数也需要获取同一个自旋锁(也就是说该资源同时被中断处理程序共享),那么它会进入无限自旋状态,因为它试图获取一个已经被当前任务持有的锁,而当前任务又被它自己所在的中断抢占了。这种情况会导致死锁(deadlock)。

为了解决这个问题,Linux 内核提供了 _irq 版本的自旋锁函数。这类函数在禁用/启用抢占的同时,也会禁用/启用本地 CPU 的中断。这些函数是 spin_lock_irq() 和 spin_unlock_irq(),定义如下:

static void spin_unlock_irq(spinlock_t *lock) static void spin_lock_irq(spinlock_t *lock)

但是我们可能会认为这种方法已经足够安全了,实际上并不是。_irq 版本只能部分解决问题。

假设在执行你的代码之前,CPU 上已经有一些中断处于关闭状态。当你调用 spin_unlock_irq() 时,它不仅会释放锁,还会重新开启中断,但这种行为可能是不正确的,因为它并不知道在加锁之前哪些中断是开启的,哪些是关闭的。

这使得 spin_lock_irq() 在“已经处于关中断上下文(IRQs off-context)”时变得不安全,因为它对应的 spin_unlock_irq() 会简单粗暴地重新开启中断,从而可能错误地开启那些在调用 spin_lock_irq() 之前本来就是关闭的中断。

因此,只有在你明确知道当前本地 CPU 的中断是开启状态时,才适合使用 spin_lock_irq(),也就是说,你必须确保在调用它之前,没有其他代码已经关闭了本地 CPU 的中断。

现在设想一种更好的方式:在获取锁之前保存当前中断状态,并在释放锁时完全恢复它,那么就不会再出现上述问题。

为此,Linux 内核提供了 _irqsave 版本的函数,它的行为与 _irq 版本类似,但额外增加了“保存并恢复中断状态”的功能。这些函数是 spin_lock_irqsave() 和 spin_unlock_irqrestore(),定义如下:

spin_lock_irqsave(spinlock_t *lock, unsigned long flags) spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)

spin_lock() 以及它的所有变体都会自动调用 preempt_disable(),从而在本地 CPU 上禁用抢占;而 spin_unlock() 及其变体则会调用 preempt_enable(),尝试重新启用抢占。

(注意——这里说的是“尝试”启用!!!因为是否真正启用取决于当前是否还有其他自旋锁仍然被持有,这会影响抢占计数器(preemption counter)的值。)

如果条件满足,preempt_enable() 内部还可能会调用 schedule() 进行调度(取决于计数器当前的值,而该值应该为 0)。

因此,spin_unlock() 本身也是一个抢占点,并且可能会重新开启抢占。

虽然禁用中断可以防止内核抢占(例如调度器的时钟中断被关闭),但并不能阻止被保护的代码段主动调用调度器(schedule() 函数)。

许多内核函数都会间接调用调度器,例如那些涉及自旋锁的函数。因此,即使是一个简单的 printk() 调用,也可能会触发调度器,因为它会使用保护内核消息缓冲区的自旋锁。

内核通过增加或减少一个全局以及 per-CPU 变量(默认值为 0,表示允许调度)来控制调度器的启用或禁用,这个变量叫做 preempt_count。

当该变量大于 0 时(schedule() 函数会检查这一点),调度器会直接返回而不执行任何操作。这个变量会在每次调用 spin_lock* 系列函数时递增;而在释放自旋锁(spin_unlock* 系列函数)时递减。

当该计数从 1 减到 0 时,调度器可能会被重新触发,这意味着你的临界区实际上并不是完全原子的。

因此,仅仅通过禁用中断,只能在“被保护代码本身不会触发调度”的情况下防止内核抢占。

另外需要注意的是:持有自旋锁的代码不能进入睡眠状态,因为如果睡眠就无法被唤醒(要记住,此时本地 CPU 的定时器中断和调度器可能已经被关闭)。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 21:22:15

Laravel2.x:被遗忘的PHP框架遗珠

Laravel 2.x 是 Laravel 框架的早期版本(发布于2010年),已停止维护多年。其核心特性与现代版本差异较大,例如: 路由差异 2.x 版本采用闭包路由定义,不支持现代的路由控制器语法: // Laravel 2.x…

作者头像 李华
网站建设 2026/4/16 21:19:28

Formily终极指南:5步实现JSON驱动的现代化表单开发

Formily终极指南:5步实现JSON驱动的现代化表单开发 【免费下载链接】formily 📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3 项…

作者头像 李华
网站建设 2026/4/16 21:16:29

算法中的二分法(二分查找)详解及示例

一、什么是算法中的二分法?二分法(Binary Search),又称折半查找,是一种在有序数组中查找某个目标值的高效查找算法。其核心思想是:每次将查找范围分成两半,排除不可能包含目标值的一半&#xff…

作者头像 李华
网站建设 2026/4/16 21:15:23

golang如何实现WAF Web应用防火墙_golang WAF Web应用防火墙实现详解

Go实现WAF的核心是将过滤逻辑嵌入标准http.Handler链,通过中间件式Handler包装原始handler,在轻量检查(如SQL注入关键词、路径遍历)后放行;需注意双重编码解码、代理头可信解析、body流控制及白名单优先等关键细节。Go…

作者头像 李华
网站建设 2026/4/16 21:14:29

淘宝NPM镜像证书过期问题全面解析:从报错到多镜像源切换实战

1. 淘宝NPM镜像证书过期问题详解 那天早上我正急着给项目添加新功能,运行npm install后突然蹦出个红色报错:"request to https://registry.npm.taobao.org failed, reason: certificate has expired"。这就像你早上赶着上班发现地铁停运一样让…

作者头像 李华