屏蔽中断 (Disabling Interrupts)
- 核心概念: 一种低级同步原语,主要用于单处理器 (Uniprocessor / Single-CPU) 系统。通过在执行临界区代码前暂时禁止 CPU 响应外部硬件中断,保证一小段代码(通常是操作关键内核数据结构)的原子性执行。
- 工作原理:
- 进入临界区前: 执行特殊 CPU 指令(如
CLI
- Clear Interrupt Flag on x86)关闭中断响应。 - 执行临界区代码: CPU 此时不会响应任何外部硬件中断(如定时器中断、磁盘 I/O 完成中断、键盘中断等)。当前执行流(内核线程或进程)独占 CPU,不会被中断处理程序抢占。
- 退出临界区后: 执行另一条指令(如
STI
- Set Interrupt Flag on x86)重新开启中断响应。CPU 会处理在屏蔽期间发生但被延迟的中断。
- 进入临界区前: 执行特殊 CPU 指令(如
- 核心目的与作用:
- 保证短操作的原子性: 防止当前执行的关键代码被中断处理程序打断(中断处理程序可能访问相同的内核数据结构,导致数据不一致)。
- 防止内核抢占: 在非抢占式内核中,是实现互斥的一种简单方式(阻止可能导致内核线程切换的时钟中断)。
- 实现其他同步原语的基础: 更高级别的同步机制(如自旋锁)在单处理器上的实现,其底层通常依赖于屏蔽中断(或结合屏蔽中断)来保证锁操作本身的原子性。
- 关键特性和注意事项:
- 仅适用于单处理器系统: 在多处理器 (SMP) 系统上,屏蔽中断只能阻止当前 CPU 响应中断。其他 CPU 上的线程仍然可以并发访问共享数据。因此,在 SMP 系统中,屏蔽中断不能单独提供互斥,必须与其他机制(如自旋锁)结合使用。
- 临界区必须非常短: 屏蔽中断会延迟所有中断响应,包括重要的系统中断(如时钟中断、磁盘中断)。如果屏蔽时间过长:
- 会导致丢失中断(硬件事件未被处理)。
- 严重影响系统的实时性和**响应性如鼠标键盘无响应)。
- 可能导致硬件故障(某些设备需要及时响应中断)。
- 因此,只应用于保护执行时间极短的代码段(通常是几条指令)。
- 不能用于用户态程序: 屏蔽中断是特权指令,只能在 CPU 的内核模式 (Ring 0) 下执行。
- 典型应用场景 (在现代内核中):
- 保护每个 CPU 的私有数据: 在 SMP 系统中,修改每个 CPU 独有的数据(如当前运行的任务指针)时,只需要防止当前 CPU 被中断打断即可。
- 实现自旋锁: 在单处理器内核中,自旋锁的实现通常是在获取锁时屏蔽中断(防止中断处理程序获取同一个锁导致死锁),释放锁时再开启中断。
- 非常短的内核操作: 修改几个全局标志位、操作几个全局指针等,且操作必须原子完成,时间极短。
- 中断处理程序内部: 中断处理程序本身默认屏蔽(或部分屏蔽)其他中断。有时需要在处理程序内部进一步精细控制中断屏蔽状态。
锁变量 (Lock Variable - Naive Attempt)
- 核心思想: 使用一个共享的整型变量(例如
lock
)作为标志位。lock = 0
表示临界区空闲(解锁),lock = 1
表示临界区已被占用(加锁)。
- 设想的工作流程:
- 进程进入临界区前检查
lock
。 - 如果
lock == 0
(空闲),进程将lock
设置为1
(加锁),然后进入临界区。 - 如果
lock == 1
(已锁),进程等待(忙等或阻塞),直到lock
变为0
。 - 进程退出临界区时,将
lock
设置为0
(解锁)。
- 进程进入临界区前检查
- 致命缺陷: 检查和设置
lock
的操作不是原子的。考虑以下场景:- 进程 A 检查
lock
,发现它是0
(空闲)。 - 在进程 A 将
lock
设置为1
之前,发生时钟中断或进程切换。 - 进程 B 被调度运行,它也检查
lock
,同样发现它是0
(因为 A 还没来得及设置)。 - 进程 B 将
lock
设置为1
并进入临界区。 - 进程 A 恢复运行,它仍然认为
lock
是0
(基于之前检查的结果),于是也将lock
设置为1
并进入临界区。
- 进程 A 检查
- 结果: 两个进程(A 和 B)同时进入了临界区,违反了互斥原则。这就是一个典型的竞态条件 (Race Condition)。
- 结论: 简单的锁变量方案无法实现互斥,因为检查和设置锁状态的操作本身需要是原子的,而软件层面的普通读写操作无法保证这一点。
严格轮询法 (Strict Alternation / Spinlock with Busy Waiting)
- 核心思想: 两个进程(例如进程 0 和进程 1)通过一个共享变量
turn
严格交替进入临界区。 - 代码示例 (伪代码):
// Process 0 while (TRUE) { while (turn != 0) {} // 忙等,直到轮到进程 0 critical_region(); // 进入临界区 turn = 1; // 将轮次交给进程 1 noncritical_region(); // 非临界区 } // Process 1 while (TRUE) { while (turn != 1) {} // 忙等,直到轮到进程 1 critical_region(); // 进入临界区 turn = 0; // 将轮次交给进程 0 noncritical_region(); // 非临界区 }
- 优点: 简单,保证了互斥(因为
turn
的值强制了顺序)。 - 致命缺点:
- 忙等待 (Busy Waiting): 进程在等待
turn
改变时持续占用 CPU 循环检查,浪费 CPU 资源。 - 违反“前进条件”: 进程的执行速度被强制绑定。如果进程 0 在非临界区执行时间很长,即使进程 1 早已准备好且临界区空闲,进程 1 也必须等待进程 0 执行完非临界区并显式地将
turn
设置为1
后才能进入。这违反了“位于临界区外的进程不应被其他非临界区的进程阻塞”的原则。
- 忙等待 (Busy Waiting): 进程在等待
Peterson 解法 (Peterson's Solution)
- 核心思想: 一种软件解决方案,结合了锁变量和警告变量的概念,用于解决两个进程的互斥问题,无需硬件支持,也避免了严格轮询的缺点。它使用两个共享变量:
interested[2]
: 布尔数组,interested[i] = TRUE
表示进程i
想进入临界区。turn
: 指示哪个进程有优先权进入临界区(如果两者都想进入)。- 由于这是硬件层面上的操作所以一下代码是汇编,理解思路即可
- 代码示例 (伪代码):
#define FALSE 0 #define TRUE 1 #define N 2 // 进程数量 int turn; // 谁的轮次 int interested[N]; // 初始化为 0 (FALSE) void enter_region(int process) { // process 是 0 或 1 int other = 1 - process; // 另一个进程的编号 interested[process] = TRUE; // 表明本进程想进入 turn = process; // 设置轮次标志(表示本进程愿意让出优先权) while (turn == process && interested[other] == TRUE) { // 空循环(忙等),直到条件不满足 } } void leave_region(int process) { interested[process] = FALSE; // 表明本进程离开临界区 }
- 工作原理 (
enter_region
):- 进程
process
设置interested[process] = TRUE
,表明它想进入临界区。 - 进程
process
设置turn = process
。这相当于礼貌地说:“现在轮到我了,但如果另一个进程也想进,它可以先走”。 - 进程
process
进入忙等循环:while (turn == process && interested[other] == TRUE)
。- 这个条件的意思是:“如果现在轮到我(
turn == process
),但同时另一个进程也表示它想进入临界区(interested[other] == TRUE
),那么我就等待”。 - 只要另一个进程在临界区(
interested[other] == TRUE
)并且turn
没变,或者另一个进程也想进且turn
是对方(turn != process
),本进程就会等待。
- 这个条件的意思是:“如果现在轮到我(
- 当条件不满足时(要么
turn
不是自己了,要么另一个进程不想进了),进程退出循环,进入临界区。
- 进程
- 工作原理 (
leave_region
):- 进程
process
设置interested[process] = FALSE
,表明它已离开临界区。 - 调用TSL时,发起该指令的CPU会锁定内存总线,这意味着其他 CPU在这段时间内将无法访问内存总线
- 进程
- 关键点:
- 通过
interested
数组和turn
变量的组合,解决了锁变量方案的原子性问题。 - 避免了严格轮询法的强制交替,允许一个进程连续进入临界区(如果另一个进程不想进)。
- 仍然存在忙等待问题。
- 仅适用于两个进程。
- 在现代 CPU(多核、乱序执行、缓存)上可能由于内存可见性问题失效,需要内存屏障(Memory Barriers)保证正确性。但在理论教学和单处理器顺序一致性模型下是有效的。
- 通过
- 总结: Peterson 解法是一个经典的、纯软件的、解决两个进程互斥问题的算法。它证明了互斥可以在没有硬件原子指令的情况下实现,但存在忙等待和扩展性(仅限两进程)问题。
TSL 指令 (Test and Set Lock Instruction)
- 核心概念: 一种由硬件直接提供的原子操作 (Atomic Operation) 指令,用于解决临界区问题。它是现代同步原语(如自旋锁)的基础。
- 指令功能 (TSL RX, LOCK):
- 原子地 (Atomically) 执行以下两个操作:
- 将内存位置
LOCK
的当前值读入寄存器RX
。 - 将内存位置
LOCK
的值设置为一个非零值(通常是1
)。
- 将内存位置
- 整个操作在硬件层面保证是不可分割的(在执行期间不会被中断,其他 CPU 也无法访问
LOCK
)。
- 原子地 (Atomically) 执行以下两个操作:
- 硬件支持: CPU 在执行 TSL 指令时,会锁定内存总线 (Lock Memory Bus),确保在此期间其他 CPU 无法访问该内存地址(或任何内存地址),从而保证原子性。
- 实现互斥锁 (自旋锁):
; 假设 LOCK 是全局锁变量,初始化为 0 (解锁) enter_region: TSL REGISTER, LOCK ; 原子操作:复制LOCK到REGISTER,设置LOCK=1 CMP REGISTER, #0 ; 检查REGISTER(即LOCK的旧值)是否为0? JNE enter_region ; 如果不是0(锁已被设置),循环等待(忙等) RET ; 如果是0(成功获取锁),返回进入临界区 leave_region: MOVE LOCK, #0 ; 释放锁:将LOCK设置为0 RET ; 返回
- 工作原理 (
enter_region
):- 执行
TSL REGISTER, LOCK
:- 原子地读取
LOCK
的旧值到REGISTER
。 - 原子地将
LOCK
设置为1
(加锁状态)。
- 原子地读取
- 检查
REGISTER
(即LOCK
的旧值):- 如果
REGISTER == 0
:表示调用TSL
之前锁是空闲的(解锁状态)。进程成功获取锁,退出循环,进入临界区。 - 如果
REGISTER != 0
(通常是1
):表示调用TSL
之前锁已被占用(加锁状态)。进程进入忙等循环,不断重试TSL
指令。
- 如果
- 执行
- 工作原理 (
leave_region
):- 将
LOCK
设置为0
(解锁状态)。
- 将
- 优点:
- 简单高效: 实现简单(几条指令),在锁持有时间短且竞争不激烈时效率高。
- 适用于多处理器 (SMP): 硬件总线锁定机制保证了在 SMP 系统上的正确性。
- 避免上下文切换: 忙等避免了进程/线程切换的开销(在等待时间短时有利)。
- 缺点:
- 忙等待 (Busy Waiting): 在锁被长时间持有时,等待的进程会持续占用 CPU 循环执行
TSL
和检查,浪费 CPU 资源(尤其在单处理器系统上更严重,持有锁的进程可能因等待 CPU 而无法释放锁)。 - 优先级反转问题: 如果忙等线程优先级高,而持有锁的线程优先级低,高优先级线程会忙等,阻止低优先级线程运行释放锁,导致死锁(需要优先级继承或天花板协议解决)
- 忙等待 (Busy Waiting): 在锁被长时间持有时,等待的进程会持续占用 CPU 循环执行