同步与锁 (Synchronization & Locking)
本文档概述了 comix 内核中用于处理并发和防止竞争条件的同步原语。
1. 简介
在多核或可抢占的内核中,当多个执行流(如不同核上的任务,或中断处理程序与被中断的任务)同时访问共享数据时,若不加协调,就会产生竞争条件(Race Condition),导致数据损坏和系统崩溃。
sync 模块提供了一系列同步原语(Synchronization Primitives),通过确保在任何时刻只有一个执行流能够访问临界区(Critical Section),来保证共享数据的完整性。
导航
- 自旋锁 (
SpinLock): 用于保护短临界区的互斥锁。 - 睡眠锁 (
SleepLock): 用于保护长临界区,会使等待者任务睡眠。 - 中断屏蔽 (
IntrGuard): 用于在单核上实现临界区的底层机制。 - 锁顺序与死锁预防: 内核中必须遵守的锁获取规则。
- SMP内核的中断与并发问题: SMP系统中的中断和并发挑战。
2. 核心概念
本内核主要采用两种策略来解决并发问题:
- 中断屏蔽 (Interrupt Disabling): 在单核处理器上,禁用中断可以防止当前代码被中断处理程序打断,从而避免了任务代码与中断代码之间的竞争。这是实现其他更复杂锁的底层基础。
- 原子操作与自旋 (Atomic Operations & Spinning): 在多核处理器上,仅屏蔽本地核心的中断是不够的,因为其他核心仍然可以访问共享数据。自旋锁利用CPU提供的原子操作(如
amoswap)来循环检查并获取锁。如果锁已被占用,它会“自旋”(在一个紧凑循环中等待),直到锁被释放。
3. 同步原语概览
comix 提供了多种同步原语,适用于不同的场景:
| 原语 | 源码链接 | 核心机制 | 适用场景 |
|---|---|---|---|
SpinLock<T> | os/src/sync/spin_lock.rs | 屏蔽中断 + 原子操作自旋 | 保护访问耗时极短的共享数据,例如修改一个计数器或链表指针。 |
SleepLock | os/src/sync/sleep_lock.rs | 原子操作 + 任务睡眠 (WaitQueue) | 保护访问耗时较长的共享数据,例如执行I/O操作或复杂的计算。 |
IntrGuard | os/src/sync/intr_guard.rs | 屏蔽/恢复中断 (RAII) | 作为其他锁的底层实现,或在确定为单核且无需锁的场景下临时屏蔽中断。 |
RawSpinLock | os/src/sync/raw_spin_lock.rs | 纯粹的原子操作自旋 | SpinLock 和 SleepLock 的内部构件,不推荐直接使用。 |
4. 设计哲学:RAII 与锁守卫
为了防止因忘记释放锁而导致的死锁,本模块广泛采用了 RAII (Resource Acquisition Is Initialization) 设计模式。
- 当调用
lock()方法时,会返回一个锁守卫 (Lock Guard) 对象(例如SpinLockGuard)。 - 这个守卫对象在其生命周期内持有锁,并提供对受保护数据的安全访问(通过
Deref和DerefMut)。 - 当守卫对象离开其作用域时,它的
drop()方法会自动被调用,从而自动释放锁。
这种设计极大地提升了锁使用的安全性。
#![allow(unused)] fn main() { // 示例: let data = SpinLock::new(0); // lock() 返回一个守卫对象 guard let mut guard = data.lock(); // 通过 guard 安全地访问被保护的数据 *guard += 1; // 当 guard 离开作用域时,锁会自动释放,无需手动调用 unlock() }
5. 注意事项:死锁
使用锁时必须警惕死锁 (Deadlock)。一个常见的死锁场景是锁顺序反转:
- 任务A:
lock(L1); lock(L2); - 任务B:
lock(L2); lock(L1);
如果任务A持有L1并等待L2,而任务B持有L2并等待L1,两个任务将永远等待下去。
规则: 在整个内核中,如果需要同时获取多个锁,必须始终按照相同的顺序获取它们。 详见锁顺序与死锁预防