SerenityOS 内核 AHCI 驱动中的软锁与硬锁:Lock/Mutex 与 Spinlock 的协同使用指南
2026/9/10 13:37:46 网站建设 项目流程

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、热插拔事件随时可能发生。内核在此类临界区中需要回答两个问题:

  1. 如何保证只有一个 CPU 在临界区内执行,且不被中断打断?(对应硬锁)
  2. 如何在允许中断、允许调度的上下文(如 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_requesthandle_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()会保存进入前的InterruptsStateSpinlockLocker析构时通过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()同样采用“先MutexLockerSpinlockLocker”的结构。而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_lockMutex(软锁)整个端口对象当前请求、散列列表、连接设备、命令列表/FIS 状态start_requestaccess_devicehandle_interrupt(work item)、reset
AHCIPort::m_hard_lockSpinlock(硬锁)端口寄存器操作PxCMD/PxSCTL/PxCI等寄存器、SATA 复位时序resetrebasestart/stop_command_list_processinginitiate_sata_reset
AHCIController::m_hba_control_lockSpinlock(硬锁)HBA 全局控制GHC(全局主机控制)、PI端口实现位图等AHCIController::resetdevices_countdevice_by_port

从调用结构可以推断出一个清晰的分层:

  1. 上层(请求提交、完成回调)→ 只取软锁MutexLocker,允许调度与页错误;
  2. 中层(需要动硬件寄存器但同样涉及共享状态)→ 先软锁后硬锁;
  3. 底层(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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询