SerenityOS 内核 AHCI 驱动中的软锁与硬锁:Lock/Mutex 与 Spinlock 的协同使用指南
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
AHCI(Advanced Host Controller Interface)是 SerenityOS 访问 SATA 硬盘的核心驱动路径。为了保证并发安全,驱动代码同时使用了两种内核锁:可让出调度的软锁(Lock/Mutex)与关闭中断的硬锁(Spinlock)。本篇基于仓库文档 Documentation/Kernel/AHCILocking.md,结合 AHCIPort 与 AHCIController 的真实实现,系统讲解两类锁的语义、正确加锁顺序,以及它们在 AHCI 硬件访问与 IOWorkQueue场景中的落地用法,帮助你写出既不会死锁、也不会漏关中断的内核并发代码。
引言:为什么 AHCI 驱动要区分软锁与硬锁
AHCI 控制器通过 HBA(Host Bus Adapter)寄存器与端口寄存器与硬盘交互,中断、DMA、热插拔事件随时可能发生。内核在此类临界区中需要回答两个问题:
- 如何保证只有一个 CPU 在临界区内执行,且不被中断打断?(对应硬锁)
- 如何在允许中断、允许调度的上下文(如 IO 工作队列)中仍保证互斥?(对应软锁)
SerenityOS 的答案是同时引入两类锁:Mutex(软锁)与Spinlock(硬锁),二者在 Kernel/Locking 目录下实现,AHCI 驱动则在 Kernel/Devices/Storage/AHCI 中组合使用。下面先从语义讲起。
软锁:Lock/Mutex
语义与特性
软锁本质上就是内核中的普通锁。在原文档的术语中它被称为Lock,在当前仓库源码中对应的实现是 Mutex(AHCI 代码里写作Mutex m_lock { "AHCIPort"sv },见 Port.h)。
其关键特性是:
- 不会关闭中断:临界区执行期间中断依然可以到来并被处理;
- 失败时让出调度:如果锁已被占用,调度器会从该临界区让出(yield)执行权,直到下次有机会重新尝试加锁;
- 可阻塞、可休眠:正因为允许让出,软锁可以被长时间持有而不阻塞其他 CPU 的中断响应。
作用域加锁方式
原文档给出的用法是借助Locker类实现作用域化的加锁:
Locker locker(m_lock); ... ... return true;在当前的仓库实现中,Mutex对应的作用域锁类是MutexLocker(定义于 Kernel/Locking/Mutex.h),AHCI 代码中实际写作:
MutexLocker locker(m_lock);MutexLocker在构造时调用m_lock->lock()加锁,在析构时自动unlock(),因此函数无论从哪个分支return,锁都会被正确释放,避免遗漏。
AHCI 端口在 Port.cpp 的start_request、handle_interrupt排队的 work item、initialize等路径上都以软锁保护共享状态,例如:
void AHCIPort::start_request(AsyncBlockDeviceRequest& request) { MutexLocker locker(m_lock); ... m_current_request = request; ... }这里持锁期间驱动会构造散列列表(scatter-gather list)并访问内核缓冲区,这些操作可能触发页错误(page fault),因此必须在中断开启、可调度的软锁上下文下进行——这正是选择软锁的核心理由。
硬锁:Spinlock
语义与特性
硬锁用于内核中的关键临界区(critical section)。原文档中它被称为ScopedSpinLock,在当前仓库实现中对应 Spinlock 配合SpinlockLocker使用。
其关键特性是:
- 关闭中断:加锁时调用
Processor::disable_interrupts(),并进入临界区计数(enter_critical); - 自旋等待:若锁已被占用,当前 CPU 在
while (m_lock.exchange(1, ...) != 0)上自旋,配合Processor::wait_check()等待,而不是让出调度; - 保证独占:同一时刻只有一个 CPU 能进入被保护的代码段,且期间不会被中断打扰。
这一点对硬件访问至关重要:AHCI 的 HBA/端口寄存器读写、SATA 复位序列(initiate_sata_reset)等都是不容打断的时序敏感操作,任何中断都可能在临界区中造成致命后果。
作用域加锁方式
原文档给出的写法:
ScopedSpinLock lock(m_lock); ... ... return true;仓库当前对应的实际写法是:
SpinlockLocker lock(m_hard_lock);Spinlock::lock()会保存进入前的InterruptsState,SpinlockLocker析构时通过unlock(previous_interrupts_state)恢复之前的中断状态,因此即使调用方此前已关闭中断,释放后也会正确还原,而不是粗暴地无条件开启中断。
在 Controller.cpp 的reset()中,HBA 全局控制寄存器的操作就完全由硬锁保护:
ErrorOr<void> AHCIController::reset() { dmesgln_pci(*this, "{}: AHCI controller reset", device_identifier().address()); { SpinlockLocker locker(m_hba_control_lock); hba().control_regs.ghc = 1; ... // Note: The HBA is locked or hung if we waited more than 1 second! while (true) { if (retry > 1000) return Error::from_errno(ETIMEDOUT); if (!(hba().control_regs.ghc & 1)) break; microseconds_delay(1000); retry++; } ... } ... }这里m_hba_control_lock的类型在 Controller.h 中声明为:
// Note: This lock is intended to be locked when doing changes to HBA registers // that affect its core functionality in a manner that controls all attached storage devices // to the HBA SATA ports. mutable Spinlock<LockRank::None> m_hba_control_lock {};该锁只被用于改动会影响所有挂载存储设备的核心 HBA 寄存器,属于典型的硬锁场景。
加锁顺序:先软后硬,绝不可颠倒
当一段代码需要同时持有软锁与硬锁时,原文档明确给出了正确的取锁顺序:
Locker locker(m_soft_lock); ScopedSpinLock lock(m_spinlock); ... ... return true;这一顺序适用于内核中任何同时获取软锁与硬锁的模式。原因如下:
Spinlock会关闭中断,而Lock不会;当软锁暂时拿不到时,由于中断仍然开启,系统可以安全地让出执行权给其他线程,之后再回来重试;- 如果先取
Spinlock再取Lock,则属于bug:此时中断已被关闭,这段代码将无法再让出执行权,一旦软锁被占用就进退两难(既不能阻塞等待,又不该无限自旋),破坏调度与响应性。
AHCI 驱动严格遵循该顺序。例如 Port.cpp 中的reset():
bool AHCIPort::reset() { MutexLocker locker(m_lock); SpinlockLocker lock(m_hard_lock); dbgln_if(AHCI_DEBUG, "AHCI Port {}: Resetting", representative_port_index()); ... }recover_from_fatal_error()、initialize_without_reset()、shutdown()同样采用“先MutexLocker后SpinlockLocker”的结构。而access_device()、rebase()、start_command_list_processing()等函数内部则通过VERIFY(m_lock.is_locked())与VERIFY(m_hard_lock.is_locked())断言来强制校验两把锁均已按正确顺序持有——这是从代码层面固化“先软后硬”约定的一种手段。
端口自身的两把锁声明在 Port.h:
Spinlock<LockRank::None> m_hard_lock {}; Mutex m_lock { "AHCIPort"sv };为什么 AHCI 需要两种锁同时存在
原文档给出的答案是:硬件访问的安全实现需要二者配合。
Spinlock保证独占与不被中断:它确保同一时刻只有一个 CPU 能运行该代码段且完全不受任何中断干扰。由于中断在关键临界区中可能是致命的,硬锁是不可或缺的底线;Lock覆盖其余一切场景:大多数时候它与Spinlock配对使用,但在把 IO 工作调度进WorkQueue时尤为重要。
IO WorkQueue 场景:只用软锁
当 AHCI 的中断处理程序需要把耗时的收尾工作(如把 DMA 缓冲区数据拷贝到请求缓冲区、处理热插拔/复位)推迟到稍后执行时,它会向全局 IO 工作队列g_io_work投递 work item。g_io_work定义于 Kernel/Tasks/WorkQueue.cpp:
WorkQueue* g_io_work; void WorkQueue::initialize() { g_io_work = new WorkQueue("IO WorkQueue Task"sv); ... }在WorkQueue中运行时,保证中断是开启的。这意味着:
- 不能使用
Spinlock,因为内核需要能够处理页错误中断(例如在把 DMA 数据写回请求方缓冲区时可能触发页错误); - 但仍需要保证没有其他并发操作同时进行,所以继续持有软锁
Lock。
这正对应 Port.cpp 中handle_interrupt()的处理:收到DHR(Device to Host Register FIS)或PS(PIO Setup FIS)中断后,把后续的缓冲区读写工作投递到g_io_work,在 work item 内部再取软锁:
if (m_interrupt_status.is_set(AHCI::PortInterruptFlag::DHR) || m_interrupt_status.is_set(AHCI::PortInterruptFlag::PS)) { m_wait_for_completion = false; // Now schedule reading/writing the buffer as soon as we leave the irq handler. // This is important so that we can safely access the buffers, which could // trigger page faults if (!m_current_request) { ... } else { auto work_item_creation_result = g_io_work->try_queue([this]() { ... MutexLocker locker(m_lock); VERIFY(m_current_request); VERIFY(m_current_scatter_list); ... complete_current_request(AsyncDeviceRequest::Success); }); ... } }同理,热插拔(PRC/PC)与致命错误恢复(IF/TFE/HBD/HBF)的 work item 也都投递到g_io_work中执行,其中recover_from_fatal_error()依然采用“软锁 + 硬锁”的双重保护,因为错误恢复本身需要改写命令列表与 FIS 接收等硬件寄存器。
从源码看锁的完整生命周期
综合 Port.cpp 与 Controller.cpp,AHCI 驱动中两把锁的职责边界可总结为:
| 锁 | 类型 | 作用域 | 保护对象 | 典型函数 |
|---|---|---|---|---|
AHCIPort::m_lock | Mutex(软锁) | 整个端口对象 | 当前请求、散列列表、连接设备、命令列表/FIS 状态 | start_request、access_device、handle_interrupt(work item)、reset |
AHCIPort::m_hard_lock | Spinlock(硬锁) | 端口寄存器操作 | PxCMD/PxSCTL/PxCI等寄存器、SATA 复位时序 | reset、rebase、start/stop_command_list_processing、initiate_sata_reset |
AHCIController::m_hba_control_lock | Spinlock(硬锁) | HBA 全局控制 | GHC(全局主机控制)、PI端口实现位图等 | AHCIController::reset、devices_count、device_by_port |
从调用结构可以推断出一个清晰的分层:
- 上层(请求提交、完成回调)→ 只取软锁
MutexLocker,允许调度与页错误; - 中层(需要动硬件寄存器但同样涉及共享状态)→ 先软锁后硬锁;
- 底层(HBA 全局寄存器、纯寄存器时序)→ 只取硬锁
SpinlockLocker,绝对不容打断。
这套分层既保证了响应性(长路径可让出调度),又保证了硬件时序的正确性(短临界区关闭中断),是编写设备驱动并发代码时可复用的模板。
实践要点
- 默认先取软锁:凡是要同时持有两类锁,一律
MutexLocker locker(m_lock);在前、SpinlockLocker lock(m_hard_lock);在后; - 不要在关中断后拿软锁:
Spinlock之后不允许再取Lock,否则代码失去让出能力,属于内核 bug; - 区分场景选锁:需要处理页错误或长时间持有时用软锁;只做短小、时序敏感的寄存器操作时用硬锁;
- 用断言固化约定:在需要双锁的函数中通过
VERIFY(m_lock.is_locked())与VERIFY(m_hard_lock.is_locked())校验持有状态,防止未来重构破坏顺序; - WorkQueue 中只用软锁:IO 工作队列保证中断开启,页错误中断必须可被处理,此时只能依赖
Mutex保证互斥。
如需进一步阅读,原文档位于 Documentation/Kernel/AHCILocking.md,锁的实现可查看 Kernel/Locking/Mutex.h 与 Kernel/Locking/Spinlock.h,AHCI 驱动的完整落地示例见 Kernel/Devices/Storage/AHCI/Port.cpp 和 Kernel/Devices/Storage/AHCI/Controller.cpp。
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考