自旋锁
2026/7/16大约 7 分钟内核组件内核同步自旋锁SMP
自旋锁
📚 本节导读
学习时长: 约 20 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: 任务管理、互斥锁、中断管理、SMP 基础
自旋锁(Spinlock)是一种忙等待的同步机制。与互斥锁在无法获取时会阻塞任务不同,自旋锁在无法获取时会让 CPU 不停地循环检查("自旋"),直到锁可用。它主要用于 SMP 多核系统中保护极短的临界区,以及在中断上下文中使用。
🎯 学习目标
- 理解自旋锁的忙等待机制及其适用场景
- 掌握自旋锁的基本 lock/unlock 操作
- 理解 IRQ 安全版本的使用场景
- 了解自旋锁调试检查机制
- 能正确区分自旋锁与互斥锁的使用场景
一、自旋锁概念
1.1 什么是自旋锁
自旋锁是一种不会导致任务阻塞的锁。当锁被占用时,请求锁的 CPU 在循环中不断检查锁状态,直到获得锁。
┌─────────────────────────────────────────────────────────────────┐
│ 自旋锁模型(SMP 系统) │
│ │
│ CPU 0 CPU 1 │
│ ┌──────────┐ ┌──────────┐ │
│ │ 任务 A │ │ 任务 B │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ lock(spin) │ lock(spin) │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 获得锁! │ │ 锁被占用 │ │
│ │ 进入临界区│ │ 自旋等待..│←── 循环检查 │
│ └────┬─────┘ └────┬─────┘ │
│ │ 临界区执行 │ 仍在自旋.. │
│ │ │ │
│ │ unlock(spin) │ 获得锁! │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 释放锁 │ │ 进入临界区│ │
│ └──────────┘ └──────────┘ │
│ │
│ ⚠️ 关键约束:持有自旋锁期间不能睡眠、不能阻塞 │
└─────────────────────────────────────────────────────────────────┘1.2 自旋锁 vs 互斥锁
| 特性 | 自旋锁 | 互斥锁 |
|---|---|---|
| 等待方式 | 忙等待(CPU 空转) | 阻塞等待(CPU 让出) |
| 使用场景 | 极短临界区(微秒级) | 较长的临界区 |
| 中断上下文 | 支持 | 不支持 |
| SMP 保护 | 核心场景 | 通过内部自旋锁保护 |
| 任务睡眠 | 持有期间禁止睡眠 | 持有期间可以睡眠 |
| 优先级继承 | 不支持 | 支持 |
| 递归持有 | 不支持 | 支持 |
| 开销 | 无上下文切换开销 | 有上下文切换开销 |
1.3 SMP 与非 SMP 行为
| 配置 | 自旋锁行为 |
|---|---|
OS_USING_SMP 启用 | 锁包含架构相关的原子操作,真正保护多核临界区 |
OS_USING_SMP 未启用 | 自旋锁退化为几乎空操作(只有调试检查),结构体只有 padding 字段 |
二、API 详解
以下 API 定义在 kernel/include/os_spinlock.h 中:
#include <os_spinlock.h>2.1 静态定义
#define OS_DEFINE_SPINLOCK(var) os_spinlock_t var = {SPIN_LOCK_INIT SPINLOCK_DEBUG_INIT SPINLOCK_PADDING_INIT}编译时定义并初始化一个自旋锁变量。
2.2 初始化自旋锁
void os_spin_lock_init(os_spinlock_t *lock);2.3 基本锁操作
void os_spin_lock(os_spinlock_t *lock); /* 获取自旋锁(忙等) */
os_bool_t os_spin_trylock(os_spinlock_t *lock); /* 尝试获取自旋锁(非阻塞) */
void os_spin_unlock(os_spinlock_t *lock); /* 释放自旋锁 */| 函数 | 说明 |
|---|---|
os_spin_lock | 获取自旋锁,如果锁被占用则自旋等待 |
os_spin_trylock | 尝试获取自旋锁,成功返回 OS_TRUE,失败返回 OS_FALSE(不等待) |
os_spin_unlock | 释放自旋锁 |
2.4 IRQ 安全版本
void os_spin_lock_irqsave(os_spinlock_t *lock, os_ubase_t *irq_save);
void os_spin_unlock_irqrestore(os_spinlock_t *lock, os_ubase_t irq_save);| 函数 | 说明 |
|---|---|
os_spin_lock_irqsave | 获取自旋锁,同时保存当前中断状态到 irq_save,然后禁用中断 |
os_spin_unlock_irqrestore | 释放自旋锁,同时恢复之前保存的中断状态 |
这是最常用的版本,用于保护任务和中断共享的临界区。配对使用。
void os_spin_lock_irq(os_spinlock_t *lock, os_ubase_t *irq_save);
void os_spin_lock_irq_rt(os_spinlock_t *lock, os_ubase_t *irq_save);
void os_spin_unlock_irq(os_spinlock_t *lock, os_ubase_t irq_save);| 函数 | 说明 |
|---|---|
os_spin_lock_irq | 获取自旋锁并禁用中断 |
os_spin_lock_irq_rt | 与 os_spin_lock_irq 类似,但使用实时中断禁用 |
os_spin_unlock_irq | 释放自旋锁并恢复中断 |
2.5 简化 IRQ 版本
void os_spin_lock_irq_disable(os_spinlock_t *lock); /* 获取自旋锁并禁用中断 */
void os_spin_unlock_irq_enable(os_spinlock_t *lock); /* 释放自旋锁并启用中断 */注意:这个版本无条件启用中断,不会恢复之前的中断状态。如果调用前中断已禁用,
unlock后会被错误地启用。
2.6 调试检查
在 oneos_config.h 中启用 OS_USING_SPINLOCK_CHECK 后,自旋锁结构体会增加调试字段:
struct os_spinlock
{
#ifdef OS_USING_SMP
arch_spinlock_t arch_lock;
#endif
#ifdef OS_USING_SPINLOCK_CHECK
int32_t owner_cpu; /* 持有锁的 CPU 编号 */
uint32_t object_inited; /* 初始化标志:OS_SPINLOCK_INIT_FLAG (0x5A5AA5A5) */
void *owner; /* 持有锁的任务 */
#endif
/* ... */
};调试检查可以检测:
- 锁是否已初始化(
object_inited == 0x5A5AA5A5) - 同一 CPU 是否重复获取锁
- 锁是否被正确的 CPU 释放
三、代码示例
3.1 基本自旋锁:保护共享变量
#include <oneos_config.h>
#include <os_spinlock.h>
/* 静态定义自旋锁 */
OS_DEFINE_SPINLOCK(g_data_lock);
static int g_shared_data = 0;
/* 任务 A 中访问共享数据 */
void task_a_access(void)
{
os_spin_lock(&g_data_lock);
/* 临界区:操作共享数据(极短!) */
g_shared_data++;
if (g_shared_data > 100)
g_shared_data = 0;
os_spin_unlock(&g_data_lock);
}
/* 任务 B 中访问共享数据 */
void task_b_access(void)
{
os_spin_lock(&g_data_lock);
g_shared_data += 10;
os_spin_unlock(&g_data_lock);
}3.2 IRQ 安全版本:任务与中断共享数据
OS_DEFINE_SPINLOCK(g_uart_lock);
static char g_uart_buffer[256];
static int g_uart_index = 0;
/* 中断服务程序:写入数据 */
void uart_rx_isr(void)
{
os_ubase_t irq_save;
char data;
data = read_uart_data();
/* 加锁并禁用中断(保存中断状态) */
os_spin_lock_irqsave(&g_uart_lock, &irq_save);
/* 临界区 */
g_uart_buffer[g_uart_index++] = data;
if (g_uart_index >= 256)
g_uart_index = 0;
/* 解锁并恢复中断状态 */
os_spin_unlock_irqrestore(&g_uart_lock, irq_save);
}
/* 任务:读取数据 */
void uart_reader_task(void *parameter)
{
os_ubase_t irq_save;
while (1)
{
int local_index;
/* 加锁并禁用中断(保存中断状态) */
os_spin_lock_irqsave(&g_uart_lock, &irq_save);
local_index = g_uart_index;
/* 解锁并恢复中断状态 */
os_spin_unlock_irqrestore(&g_uart_lock, irq_save);
printf("Current buffer index: %d\n", local_index);
os_task_sleep(100);
}
}3.3 trylock:非阻塞尝试
OS_DEFINE_SPINLOCK(g_try_lock);
void trylock_example(void)
{
os_bool_t acquired;
/* 尝试获取锁,不等待 */
acquired = os_spin_trylock(&g_try_lock);
if (acquired)
{
/* 成功获取锁,执行临界区操作 */
printf("Acquired spinlock, doing critical work...\n");
/* 必须释放 */
os_spin_unlock(&g_try_lock);
}
else
{
/* 锁被占用,执行其他操作 */
printf("Spinlock busy, doing alternative work...\n");
}
}3.4 简化 IRQ 版本:明确知道中断状态
OS_DEFINE_SPINLOCK(g_simple_lock);
void simple_irq_example(void)
{
/* 确保调用前中断是启用的 */
/* 加锁并禁用中断 */
os_spin_lock_irq_disable(&g_simple_lock);
/* 临界区(中断已禁用) */
do_critical_work();
/* 解锁并启用中断 */
os_spin_unlock_irq_enable(&g_simple_lock);
}3.5 动态初始化
void dynamic_spinlock_example(void)
{
os_spinlock_t *lock;
/* 动态分配锁结构体 */
lock = (os_spinlock_t *)malloc(sizeof(os_spinlock_t));
if (lock == OS_NULL)
return;
/* 初始化自旋锁 */
os_spin_lock_init(lock);
/* 使用锁 */
os_spin_lock(lock);
/* ... 临界区 ... */
os_spin_unlock(lock);
/* 释放 */
free(lock);
}四、使用原则
4.1 何时使用自旋锁
| 场景 | 推荐 |
|---|---|
| SMP 多核,极短临界区(几行代码) | ✅ 自旋锁 |
| 中断与任务共享数据 | ✅ 自旋锁 + irqsave |
| 单核,任务间互斥 | 使用互斥锁 |
| 临界区较长(可能阻塞) | 使用互斥锁 |
| 需要优先级继承 | 使用互斥锁 |
4.2 自旋锁使用注意事项
- 持有时间要极短:持有自旋锁期间不能做任何可能阻塞的操作(睡眠、等待信号量等)
- 不能递归:自旋锁不支持递归持有
- 中断安全:如果数据可能被中断访问,必须使用
irqsave版本 - 释放顺序:必须由获取锁的 CPU 释放锁
- 避免死锁:如果同时持有多个锁,必须按固定顺序获取
📝 本节小结
自旋锁是 OneOS 中用于 SMP 多核保护和中断安全的关键机制,核心要点:
- 忙等待:获取不到锁时 CPU 自旋等待,不阻塞任务
- 极短临界区:适用于微秒级的临界区保护
- 中断安全:
os_spin_lock_irqsave/os_spin_unlock_irqrestore配对使用,保护任务与中断共享的数据 - SMP 核心:主要在 SMP 系统中发挥作用,非 SMP 系统退化为调试检查
- 调试检查:启用
OS_USING_SPINLOCK_CHECK可检测锁的误用 - 与互斥锁互补:长时间临界区用互斥锁,极短/中断临界区用自旋锁