【嵌入式linux学习】自旋锁 互斥锁 原子变量

发布时间:2026/10/13 9:33:55
【嵌入式linux学习】自旋锁 互斥锁 原子变量 【嵌入式Linux学习】自旋锁、互斥锁、原子变量从一个高频问题出发自旋锁为什么不能睡眠mutex 为什么可以睡眠本质spinlock 是忙等锁、mutex 是睡眠锁、atomic 是硬件原子指令——三者定位完全不同。核心问题为什么 spinlock 不能睡眠死锁 中断上下文mutex 为什么可以睡眠等待队列机制。atomic单条指令原子、多条逻辑不原子它的边界在哪。落地选型对比表、场景规则。文章目录【嵌入式Linux学习】自旋锁、互斥锁、原子变量1. 本质三种同步手段的定位2. 为什么 spinlock 不能睡眠2.1 原因一持有者一旦睡眠就会死锁2.2 原因二自旋锁很多场景运行在中断上下文2.3 spinlock 的三条使用约束3. 为什么 mutex 可以睡眠4. spinlock vs mutex 对比表5. atomic 原子变量5.1 是什么5.2 原理CPU 硬件原子指令5.3 边界只能保护单个整数不能保护多语句临界区5.4 拓展refcount_t6. 总对比7. 什么场景用什么8. 互斥锁完整流程1. 本质三种同步手段的定位手段本质拿不到锁 / 资源时的行为一句话总结spinlock 自旋锁忙等锁CPU 原地循环自旋不放弃 CPU持有者必须一直在 CPU 上尽快释放锁mutex 互斥锁睡眠锁进程加入等待队列调用 schedule 让出 CPU进入睡眠拿不到就睡锁释放后被唤醒atomic 原子变量硬件原子指令不涉及等待操作直接完成单个整数操作的天然互斥设计前提不同决定了它们的行为完全不同spinlock 假设「持有锁的线程必须一直在 CPU 上运行尽快释放锁」mutex 假设「持有者和等待者都可以睡眠」。2. 为什么 spinlock 不能睡眠2.1 原因一持有者一旦睡眠就会死锁先看代码spin_lock(lock);// 临界区拿到锁了xxx_func();// 假设这个函数内部发生睡眠spin_unlock(lock);场景推演CPU0 上任务 A 拿到 spinlockpreempt_count关闭内核抢占A 在持有锁的情况下进入睡眠主动调用schedule()让出 CPU——A 被切出 CPU休眠了没有机会执行spin_unlock释放锁CPU1 上任务 B 执行spin_lock(lock)想要同一把锁锁还被 A 持有而 A 在睡觉、不在 CPU 上跑永远不会解锁B 就在 CPU1 上无限循环自旋忙等。结果CPU1 100% 占用空转系统卡死。锁持有者睡着了等待者空转——这就是死锁关键补充自旋锁持有期间内核抢占已被preempt_count关闭本 CPU 不会被别人抢占但代码自己主动调用schedule()睡眠是不受preempt_count拦截的preempt_count只是「禁止被别人抢占」拦不住代码自己主动睡觉——这正是持有 spinlock 不能睡眠的根源。2.2 原因二自旋锁很多场景运行在中断上下文自旋锁经常用来和中断配合比如spin_lock_irqsave()获取锁的同时关闭本地中断防止中断上下文竞争同一把锁。中断上下文是不能睡眠的中断上下文没有进程上下文、没有 task_struct不能睡眠、不能调度。如果 spinlock 允许睡眠一旦在中断上下文拿到锁后触发睡眠直接内核 Oops 崩溃。2.3 spinlock 的三条使用约束持有者不能睡眠不能主动schedule持有者不能被抢占靠preempt_count关闭内核抢占临界区代码要短小、快速执行完立刻 unlock。内核有调试检测如果检测到持有 spinlock 时发生睡眠直接打印 BUG、Oops。3. 为什么 mutex 可以睡眠mutex 不是忙等。拿不到锁的时候进程去等待队列睡觉、让出 CPU不会原地自旋。mutex_lock(mtx);// 临界区mutex_unlock(mtx);机制推演任务 A 拿到 mutex 锁。A可以被抢占、可以睡眠——哪怕 A 被调度切出 CPU 去睡觉也没关系锁依旧属于 A任务 B 调用mutex_lock()拿不到锁B 把自己加入 mutex 的等待队列设置进程睡眠状态调用schedule()主动让出 CPUCPU 去跑其他任务。B 休眠不消耗 CPU后续 A 执行mutex_unlock()释放锁会唤醒等待队列上的 BB 被唤醒后继续执行、拿到锁。关键点等待者不自旋直接睡觉不会空耗 CPU持有者就算被切走睡眠也没问题等持有者再次被调度上 CPU会继续运行到mutex_unlock做解锁再唤醒等待者。⚠️ 但 mutex 有自己的限制mutex 不能在中断上下文使用因为中断上下文不允许睡眠而mutex_lock拿不到锁会睡眠中断里调用mutex_lock直接 Oops。中断上下文只能用 spinlock 系列锁。4. spinlock vs mutex 对比表特性spinlock 自旋锁mutex 互斥锁拿不到锁行为CPU 原地自旋忙等进程加入等待队列睡眠让出 CPU持有者是否允许睡眠❌ 绝对禁止会死锁✅ 允许持有者睡眠、被抢占是否关闭内核抢占✅ 获取锁时preempt_count关闭抢占❌ 不操作 preempt_count不关闭抢占临界区长短适合短小、快速执行的临界区适合可以长时间操作的临界区能否用于中断上下文✅ 可以spin_lock_irqsave❌ 禁止拿不到锁会睡眠中断不能睡眠性能临界区很短时性能好自旋消耗 CPU上下文切换有开销适合长临界区5. atomic 原子变量5.1 是什么atomic_t32 位/atomic64_t64 位内核提供的原子整数类型atomic_tcnt;atomic_set(cnt,0);atomic_inc(cnt);// 原子自增atomic_dec(cnt);atomic_read(cnt);原子的含义这一条操作CPU 硬件层面不可分割——要么完整做完要么完全不做中间不会被打断。5.2 原理CPU 硬件原子指令普通的cntC 语言一行代码CPU 会拆成 3 条汇编读寄存器 ← cnt 寄存器 1 cnt ← 寄存器多核 SMP 下两个 CPU 同时执行这 3 步会发生数据竞争结果出错。而atomic_inc(cnt)是 CPU 提供的原子指令比如 x86 的lock add整个「读-改-写」作为一条硬件原子操作多核之间不会被打断不需要锁保护。✅ 核心atomic 依靠CPU 硬件指令实现互斥不需要锁没有锁开销。5.3 边界只能保护单个整数不能保护多语句临界区atomic 只适用于简单的整数算术操作自增、自减、加减、比较交换。不能保护一段由多条语句组成的临界区❌ 错误示例很多人踩坑// 想保护一段逻辑atomic 做不到if(atomic_read(flag)1){do_a();do_b();atomic_set(flag,0);}虽然每一条 atomic 函数本身是原子的但atomic_read()和后面的do_a()、atomic_set()之间会被打断多核其他 CPU 可以穿插执行整个 if 分支不是原子的。atomic 只能保护单个整数操作不能保护多条业务逻辑、多个变量。如果临界区是好几行代码、要改多个变量 → 不能用 atomic必须 spinlock / mutex。5.4 拓展refcount_t内核现在推荐用refcount_t代替单纯的atomic_t做引用计数它在原子操作之上增加了溢出防护防止整数溢出漏洞底层仍然是硬件原子指令。6. 总对比特性atomic 原子变量spinlock 自旋锁mutex 互斥锁实现方式CPU 硬件原子指令忙等自旋循环等待队列 睡眠调度保护范围仅单个整数变量任意代码临界区多行代码、多个变量任意代码临界区拿不到资源行为不涉及等待操作直接完成CPU 原地自旋忙等进程睡眠让出 CPU是否修改preempt_count❌ 不修改✅preempt_count关闭内核抢占❌ 完全不操作抢占计数器持有者能否睡眠本来就没有持有者概念❌ 严禁睡眠死锁风险✅ 允许睡眠、允许被抢占中断上下文使用✅ 可以✅ 可以irqsave 版本❌ 禁止会睡眠开销极小硬件指令几乎无开销临界区短时开销小自旋会耗 CPU存在上下文切换开销偏大适用场景计数器、引用计数refcount、简单 flag 标记临界区简短可能在中断访问共享数据临界区较长中间可能睡眠 / 等待 IO典型 atomic 使用场景内核引用计数模块引用、refcount_t、统计计数器。atomic 原子变量依靠 CPU 硬件原子指令只能完成单个整数的原子加减适合计数器。不能保护多行代码临界区没有锁的概念可以在中断上下文使用。spinlock 自旋锁是忙等锁拿不到锁 CPU 原地自旋。可以保护任意临界区获取锁会增加preempt_count关闭内核抢占临界区必须短小快速持有者不能睡眠支持中断上下文如果持有锁睡眠会死锁。mutex 互斥锁是睡眠锁。拿不到锁时进程进入等待队列睡眠、让出 CPU。持有者允许抢占、允许睡眠适合比较长的临界区但 mutex 不能用于中断上下文因为拿不到锁会睡眠中断上下文不允许睡眠。自旋锁是忙等锁拿不到锁会原地循环自旋不释放 CPU。它的前提是锁持有者必须持续在 CPU 上运行、尽快释放锁。如果持有自旋锁时睡眠持有者被切出 CPU、无法执行解锁其他 CPU 上等待该锁的执行流会无限自旋发生死锁系统卡死。另外自旋锁可用于中断上下文而中断上下文本身不允许睡眠。mutex 是睡眠锁拿不到 mutex 锁时进程不会自旋而是加入等待队列、调用schedule让出 CPU 进入睡眠不消耗 CPU。即使锁持有者被调度切出去睡眠也没关系等持有者再次被调度运行后最终会执行解锁解锁时会唤醒等待队列上休眠的进程。注意区别spin_lock会操作preempt_count关闭内核抢占mutex 不修改抢占计数器允许内核抢占。同时 mutex 不能在中断上下文使用因为拿不到锁会睡眠而中断上下文禁止睡眠。7. 什么场景用什么仅仅做计数器ref / ref--→atomic_t / refcount_t需要保护一小段代码会被中断访问代码执行很快 →spinlock保护一大段逻辑中间可能调用kmalloc、wait、IO 等待、会睡眠 →mutex追问为什么atomic_inc之后还需要锁单条atomic_inc本身安全。但如果你的逻辑是「读原子变量 → 做判断 → 再修改」多条操作组合在一起整体不具备原子性这时仍然需要 spinlock / mutex 保护整个逻辑块。8. 互斥锁完整流程mutex_lock()核心简化流程尝试原子操作抢占锁fast path成功直接返回。抢锁失败 →把当前进程封装成mutex_waiter加入这个 mutex 的等待队列。设置进程状态为睡眠TASK_UNINTERRUPTIBLE。调用schedule()主动让出 CPU进程从运行队列摘除进入睡眠不再占用 CPU。此时这个进程就卡在等待队列上不会自己跑必须别人唤醒。谁来唤醒mutex_unlock()的释放者当持有锁的进程调用mutex_unlock()释放锁原子修改 mutex owner 状态。检查 mutex 的等待队列上有没有等待者。如果有等待进程调用wake_up_process()唤醒等待队列里的等待者。唤醒动作是当前正在执行 unlock 的进程上下文做的不是中断不是内核后台线程。被唤醒的进程状态变为 TASK_RUNNABLE被丢进 CPU 就绪队列等待调度器选中它运行。⚠️ 重点区分唤醒者释放锁的进程unlock 的调用方被唤醒者之前在 mutex 等待队列上睡眠的进程调度器只负责 “选进程上 CPU”调度器不会主动唤醒 mutex 等待进程。唤醒之后就能拿到锁吗❌唤醒 ≠ 拿到锁唤醒只是把进程变成就绪态等待被调度运行。等这个进程重新被调度上 CPU回到mutex_lock剩下的代码再次尝试抢锁。有可能在它被唤醒、还没来得及运行的间隙又有别的进程fast path抢先拿到锁那它又会再次睡眠。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询