单核 CPU 同一时间只能执行一个指令流,但中断的本质是 “抢占式打断”—— 即使是单核,正在执行的主程序(线程 / 进程)也可能被中断服务程序(ISR)打断,而如果主程序和 ISR 同时访问同一个共享资源(比如全局变量、硬件寄存器),就会导致数据竞争或不一致。
在单核下,主程序执行int count = interrupt_count时,若指令还没执行完就被中断抢占,ISR 修改了interrupt_count,会导致主程序读取到 “不完整” 的值(比如 32 位变量只读了低 16 位,高 16 位被中断修改)。
单核系统中 “锁” 的核心逻辑
单核的 “锁” 不是 “互斥”,而是 “防抢占”;
多核系统的锁(如自旋锁、互斥锁)是为了解决多个 CPU 核心同时访问同一资源的问题(比如核心 1 和核心 2 同时写一个全局变量);
单核系统的 “锁” 是为了解决中断 / 线程抢占导致的指令流打断问题(比如主程序刚读了变量的一半,就被中断服务程序(ISR)打断并修改了变量)。
核心是保护 “临界区”—— 把访问共享资源的代码段变成 “不可被打断” 的原子操作,实现方式只有两种:
方式 1:关 / 开中断(最常用)
这是单核系统的 “专属锁”,原理是禁止 CPU 响应中断,让临界区代码完整执行:
char buf[100]; int buf_wr = 0; // 临界区保护函数(底层依赖CPU架构指令) void lock() { __asm__("cli"); // x86:关闭中断;ARM用CPSID I } void unlock() { __asm__("sti"); // x86:开启中断;ARM用CPSIE I } // 主程序:安全写缓冲区 void write_buf(char c) { lock(); // 进入临界区,禁止中断打断 if (buf_wr >= 100) { unlock(); return; } buf[buf_wr] = c; buf_wr++; unlock(); // 退出临界区,恢复中断 } // ISR:本身是中断上下文,无需加锁(但要极简) void isr_uart() { lock(); // 注意:ISR中加锁是防止嵌套中断,不是防多核 if (buf_wr > 0) { uart_send(buf[0]); for (int i=0; i<buf_wr-1; i++) buf[i] = buf[i+1]; buf_wr--; } unlock(); }方式 2:原子指令(仅适用于简单操作)
如果共享资源的操作能通过一条 CPU 指令完成(即 “原子操作”),可以不用显式关中断:
// 原子操作示例:x86的inc指令(一条指令完成自增) volatile int counter = 0; // 主程序:原子自增,无需关中断 void main() { __asm__("inc %0" : "=r"(counter) : "0"(counter)); } // ISR:同样原子自增 void isr_timer() { __asm__("inc %0" : "=r"(counter) : "0"(counter)); }但这种场景非常有限(仅适用于单步读写 / 修改),大部分复杂操作(如缓冲区、链表)仍需关中断。
特殊情况:无需加锁的场景
只有满足以下所有条件时,单核系统才不需要为中断加锁:
- 共享资源的操作是原子指令(如 x86 的
inc指令,ARM 的ldrex/strex),CPU 能一条指令完成读写; - 主程序访问资源时,不依赖资源的多个状态(比如只单次读取,不做 “读 - 改 - 写”);
- 中断服务程序(ISR)中不访问该资源,或访问逻辑不会和主程序冲突。
Linux 内核中单核系统的锁机制
核心结论:Linux 内核在单核系统下仍会使用锁,但锁的底层实现会 “退化” 为轻量级的抢占控制,而非真正的多核互斥。
Linux 单核系统中,引发资源竞争的场景主要是:
- 进程 / 线程的抢占式调度(高优先级线程抢占低优先级线程);
- 中断上下文(硬件中断 / 软中断抢占进程上下文)。
锁的核心目的就是保护这两类场景下的共享数据一致性。
Linux 内核提供了多种锁(spinlock、mutex、raw_spinlock 等),但单核下的实现会大幅简化:
自旋锁(spinlock)—— 单核下的 “伪自旋”
自旋锁是内核中最基础的锁,设计初衷是多核场景下 “忙等” 不睡眠,但单核下自旋锁不会自旋:
- 多核:获取不到锁时,CPU 循环等待(自旋),直到锁释放;
- 单核:获取锁时,直接关闭内核抢占(preempt_disable ()),释放锁时开启抢占(preempt_enable ())。
// 单核下的spin_lock实现 #define spin_lock(lock) preempt_disable() // 关闭抢占,无需自旋 #define spin_unlock(lock) preempt_enable() // 开启抢占 // 多核下的spin_lock实现(对比) #define spin_lock(lock) \ while (atomic_cmpxchg(lock, 0, 1)) {} // 循环自旋,直到拿到锁互斥锁(mutex)—— 单核下仍可能睡眠
mutex 是 “睡眠锁”,即使在单核下,获取不到锁时仍会:
- 释放 CPU(调度其他进程);
- 等待锁释放后被唤醒。
但单核下 mutex 的竞争场景只有 “进程抢占”,没有多核竞争,因此底层调度逻辑更简单。
关中断类操作(底层基础)
Linux 内核提供了专门的关中断接口,是单核锁的底层支撑:
| 接口 | 作用 | 单核场景适用场景 |
|---|---|---|
| local_irq_disable() | 关闭当前 CPU 的中断(单核即全局) | 保护被中断和进程共享的资源 |
| local_irq_enable() | 开启当前 CPU 的中断 | 配合关中断使用 |
| preempt_disable() | 关闭内核抢占(不影响硬件中断) | 保护仅被进程抢占的共享资源 |
| preempt_enable() | 开启内核抢占 | 配合关抢占使用 |
内核锁的使用原则
- 中断上下文(ISR)中:必须用
spin_lock_irqsave()(关中断 + 关抢占),因为 ISR 会抢占进程上下文; - 纯进程上下文:用
spin_lock()(仅关抢占)或 mutex 即可; - 避免长时间持有锁:尤其是关中断的锁,会导致中断延迟,影响系统响应。
单核 vs 多核 Linux 锁的核心区别
| 维度 | 单核系统 | 多核系统 |
|---|---|---|
| 自旋锁 | 关闭抢占,无自旋 | 自旋等待,直到获取锁 |
| 竞争来源 | 进程抢占、中断抢占 | 多核 CPU 同时访问 + 进程 / 中断抢占 |
| 锁的核心 | 防止 “自己被打断” | 防止 “其他核心抢资源 + 自己被打断” |
| 性能开销 | 极低(仅修改抢占标志) | 较高(自旋等待或缓存一致性开销) |
总结
- 单核必须要 “锁”:核心目的是防止中断 / 线程抢占导致共享资源访问不完整,而非多核的 “互斥”;
- 锁的实现核心:单核用 “关中断→操作资源→开中断” 保护临界区,而非多核的自旋锁 / 互斥锁;
- 关键区别:多核锁是 “防别人抢”,单核锁是 “防自己被打断”,但最终目的都是保证共享资源的一致性。
- Linux 单核内核仍用锁:核心是通过 “关抢占 / 关中断” 保护临界区,防止进程 / 中断抢占导致数据不一致;
- 自旋锁是核心优化点:单核下自旋锁退化为 “关抢占”,无自旋开销,这是 Linux 内核的关键优化;
- 使用场景分上下文:中断上下文必须关中断 + 锁,纯进程上下文仅需关抢占即可。