Reactos 第 9 章 设备驱动 — 9.9 磁盘的设备驱动堆叠

第 9 章 设备驱动 --- 9.9 磁盘的设备驱动堆叠

本节剖析 Windows 磁盘 I/O 的设备驱动堆叠(device stack)架构。 磁盘 I/O 是 Windows 中最复杂的驱动堆叠之一,自下而上涉及 端口驱动 → 类驱动 → 卷管理 → 文件系统 多层。ReactOS 的 drivers/storage/(file:///d:/reactos/drivers/storage/) 目录实现了这些层次。理解磁盘堆叠的关键是把握 PDO/FDO 嵌套关系、IRP_MJ_SCSI 内部 IRP、SRB(SCSI Request Block)转发链


概述

磁盘 I/O 的设备栈远比一般字符设备复杂,它需要协调 硬件(磁盘控制器、磁盘)→ 端口驱动(port driver)→ 类驱动(class driver)→ 卷管理 → 文件系统 五层。

磁盘设备栈分层

复制代码
+------------------------+   顶层(应用)
| 文件系统 (fastfat/ntfs)  |   接收 NtReadFile/NtWriteFile
+------------------------+
| 卷管理 (volmgr)         |   装配 Volume,IOCTL_VOLUME_*
+------------------------+
| 分区管理 (partmgr)      |   IOCTL_DISK_*, Partition0/1/2...
+------------------------+
| 类驱动 (classpnp/disk)  |   接收 IRP_MJ_READ/WRITE/IOCTL
+------------------------+
| 端口驱动 (scsiport/     |   IRP_MJ_SCSI (内部 IRP)
|  storport/atapi)        |
+------------------------+
| HAL (PCI 中断/DMA)     |
+------------------------+
| 硬件控制器(HBA)        |
+------------------------+

每层只处理它关心的 IRP 类别,不关心的就直接下传(IoCallDriver)。

本节内容概览

  • 9.9.0 框架图
  • 9.9.1 总线驱动与 PDO 创建
  • 9.9.2 端口驱动(scsiport/storport)
  • 9.9.3 类驱动(classpnp.sys / disk.sys)
  • 9.9.4 分区管理(partmgr.sys)
  • 9.9.5 卷管理(volmgr.sys / mountmgr)
  • 9.9.6 文件系统驱动
  • 9.9.7 IRP_MJ_SCSI 内部 IRP 路径
  • 9.9.8 总结与代码索引

学习目标

  • 理解磁盘 I/O 的五层设备栈
  • 掌握 IRP_MJ_SCSI 与 SRB 的关系
  • 知道 classpnp 与 disk 的协作
  • 区分类驱动与端口驱动

涉及的内核子系统

子系统 头文件/源文件 核心作用
类驱动 classpnp drivers/storage/class/classpnp/(file:///d:/reactos/drivers/storage/class/classpnp/) 共用类驱动框架
类驱动 disk drivers/storage/class/disk/disk.c(file:///d:/reactos/drivers/storage/class/disk/disk.c) 磁盘类驱动
分区管理 drivers/storage/partmgr/partmgr.c(file:///d:/reactos/drivers/storage/partmgr/partmgr.c) PartMgrAddDevice、分区枚举
端口驱动 scsiport drivers/storage/port/scsiport/scsiport.c(file:///d:/reactos/drivers/storage/port/scsiport/scsiport.c) SCSI 端口驱动
端口驱动 storport drivers/storage/port/storport/storport.c(file:///d:/reactos/drivers/storage/port/storport/storport.c) 新式存储端口驱动
端口驱动 atapi drivers/storage/ide/atapi/atapi.c(file:///d:/reactos/drivers/storage/ide/atapi/atapi.c) IDE/ATAPI 端口驱动
卷管理 drivers/storage/mountmgr/mountmgr.c(file:///d:/reactos/drivers/storage/mountmgr/mountmgr.c) 卷挂载管理

磁盘设备栈的设计哲学

为什么需要如此复杂的五层架构?这是一种典型的**分层抽象(layered abstraction)**设计模式。每一层都隐藏了下层的复杂性,向上层提供更高层次的接口:

  • 硬件层面对的是寄存器、中断、DMA 通道等物理细节
  • 端口驱动将这些细节抽象为统一的 SCSI 命令接口
  • 类驱动将 SCSI 命令进一步抽象为读写/IOCTL 接口
  • 卷管理在逻辑分区层面管理空间分配
  • 文件系统则提供文件名、目录结构等最高层抽象

这种分层设计的核心价值在于解耦------任意一层都可以独立替换而不影响其他层。例如,更换磁盘控制器(从 IDE 到 AHCI)只需要更换最底层的端口驱动,上层的类驱动和文件系统不需要任何修改。

为什么 1:为什么磁盘设备栈需要五层架构而不是一层搞定?

如果使用单层驱动直接管理硬件,虽然看似简单,但在实际工程中会带来灾难性的后果:

  1. 代码爆炸:每种硬件控制器都需要独立的驱动实现完整的 I/O 栈------从 DMA 编程到文件系统解析。以 Windows 为例,如果将所有功能塞入一层,需要为每个 SCSI 控制器、IDE 控制器、AHCI 控制器、NVMe 控制器分别实现文件系统支持,代码量将膨胀数十倍。
  2. 可移植性灾难:文件系统逻辑与硬件控制逻辑混合在一起后,更换硬盘控制器就意味着要重写文件系统代码。反之,分层后任意一层的变化都被隔离在该层内部,不影响相邻层。
  3. 调试复杂度:五层架构中每层只需关注自己的职责,调试时可以逐层隔离问题。端口驱动出问题只影响 SRB 提交,类驱动出问题只影响 IRP 转换,而不会出现需要同时理解 DMA 页表和文件分配表才能调试的噩梦。

因此,五层架构并不是过度设计,而是操作系统工程中经过数十年演化形成的最优解。


9.9.0 框架图

复制代码
      用户态 NtReadFile/WriteFile
            |
            v
    +-------------------+
    | I/O Manager       | (ntoskrnl/io/iomgr/)
    +-------------------+
            |
            v
    +-------------------+
    | 顶层设备           |  命名空间:\Device\HarddiskVolumeX
    | 卷设备 (FS)        |  VPB->DeviceObject
    +-------------------+
            |
            v
    +-------------------+
    | 卷/分区设备        |  \Device\HarddiskN\PartitionM
    | (volmgr/partmgr)   |  IOCTL_DISK_*
    +-------------------+
            |
            v
    +-------------------+
    | 类驱动 disk.sys   |  PDO 来自 partmgr
    |  FDO              |  IRP_MJ_READ/WRITE
    +-------------------+
            |
            v
    +-------------------+
    | 端口驱动           |  scsiport.sys / storport.sys
    |  scsiport/        |  IRP_MJ_SCSI
    |  atapi/pciidex    |
    +-------------------+
            |
            v
    +-------------------+
    | PCI/ACPI 驱动     |  中断处理、DMA 控制器
    +-------------------+
            |
            v
    +-------------------+
    | 硬件              |  HBA(Host Bus Adapter)
    +-------------------+

9.9.1 总线驱动与 PDO 创建

最底层是 PCI/ACPI 总线驱动 ,它枚举 PCI 设备并创建 PDO。例如,IDE 控制器在 PCI 总线上的设备 ID 被 PCI 驱动识别,调用 IoCreateDevice 创建 PDO ,附带设备扩展(FDO_EXTENSION)记录 BAR、IRQ 等。

磁盘设备的 PnP 路径:

复制代码
PCI 总线 (pci.sys) -> 创建 PCI IDE/SATA/AHCI 控制器的 PDO
   |
   v
atapi.sys / pciidex.sys / storahci.sys -> 创建 子 PDO(表示连接的磁盘)
   |
   v
classpnp.sys / disk.sys -> 在 AddDevice 中创建 FDO 并附加到 PDO
   |
   v
partmgr.sys -> 在 AddDevice 中创建 自身 FDO 附加到 disk

每一层都把上层(FDO)的 IRP 转发到下层(PDO),并在返回时执行完成例程。

关键代码(atapi.c)

c 复制代码
// atapi 端口驱动注册类驱动回调
Status = AtapiRegisterController(DriverObject, ...);

为什么 8:为什么 PnP IRP 需要先下后上的传播方式?

PnP IRP 如 IRP_MN_START_DEVICE 采用先下后上 的传播路径,这是因为硬件资源的初始化和释放必须从底层开始。以磁盘设备为例,IRP_MN_START_DEVICE 到达设备栈时:

  1. 顶层(partmgr)先将 IRP 转发给下层(disk)
  2. disk 再转发给 scsiport
  3. scsiport 再转发给 atapi(PDO)
  4. atapi 在 PDO 层真正与硬件交互:分配 I/O 端口范围、设置中断向量、启用 DMA 通道
  5. 完成这些硬件级操作后,IRP 沿完成链向上返回,每层此时才使用已准备好的硬件资源

如果顺序颠倒------顶层先初始化再逐层向下------会导致上层在硬件尚未就绪时就尝试访问设备,造成系统崩溃。这种"先下后上"的策略是 PnP 系统的核心设计原则,在 ReactOS 的 PnP 管理器(ntoskrnl/pnp/pnpdrv.c)中得到了严格实施。


9.9.2 端口驱动(scsiport / storport)

端口驱动(port driver)位于 类驱动与硬件之间,负责:

  1. SRB 的构造和提交 :把上层发来的 IRP_MJ_SCSI 转换为 SRB(SCSI Request Block)
  2. 中断处理 :在 HwInterrupt 中处理硬件中断
  3. DMA 编程 :使用 AllocateCommonBuffer 分配 DMA 一致性内存
  4. 超时管理ScsiPortIoTimer 监控 SRB 超时

ScsiPort 入口点

c 复制代码
NTSTATUS NTAPI
ScsiPortInitialize(IN PVOID Argument1,  // DriverObject
                   IN PVOID Argument2,  // RegistryPath
                   IN struct _HW_INITIALIZATION_DATA *HwInitData,
                   IN PVOID HwContext);

Miniport 驱动调用 ScsiPortInitialize 注册一组回调(HwFindAdapterHwInitializeHwStartIoHwInterrupt)。

Storport(替代方案)

Windows XP 之后引入 storport,提供 更优的队列管理 (并行 SCSI 命令)和 Message-Signaled Interrupt (MSI) 支持。ReactOS 的 storport/(file:///d:/reactos/drivers/storage/port/storport/) 是框架实现,Miniport 驱动 storahci(AHCI)使用它。

IRP_MJ_SCSI 路径

c 复制代码
// 类驱动 disk 创建 IRP_MJ_SCSI
IoBuildDeviceIoControlRequest(IOCTL_SCSI_EXECUTE_IN, TargetDevice, ...);

IRP 的 Parameters.Scsi.Srb 字段指向 SRB

c 复制代码
typedef struct _SCSI_REQUEST_BLOCK {
    USHORT Length;
    UCHAR Function;                  // SRB_FUNCTION_EXECUTE_SCSI 等
    UCHAR SrbStatus;
    UCHAR ScsiStatus;
    UCHAR PathId;
    UCHAR TargetId;
    UCHAR Lun;
    UCHAR QueueTag;
    UCHAR QueueAction;                // 队列深度
    UCHAR Reserved;
    ULONG DataTransferLength;
    PVOID DataBuffer;                 // 数据缓冲(系统内存或 MDL)
    ULONG SenseInfoBufferLength;
    PVOID SenseInfoBuffer;
    struct _SCSI_REQUEST_BLOCK *NextSrb;
    PVOID OriginalRequest;
    PVOID SrbExtension;               // Miniport 私有
    // ... 端口驱动扩展
} SCSI_REQUEST_BLOCK, *PSCSI_REQUEST_BLOCK;

为什么 3:为什么端口驱动使用 SRB 而不是直接使用 IRP?

SRB 并不替代 IRP,而是作为 IRP 的有效载荷存在。使用 SRB 而非直接在 IRP 中编码 SCSI 参数的原因有三:

  1. SCSI 命令的复杂性 :一条 SCSI 命令包含操作码(OP Code)、逻辑块地址(LBA)、传输长度、控制字节等大量参数。将这些参数嵌入 IRP 的 Parameters 联合体不够灵活,而 SRB 可以精确描述 SCSI 协议的所有细节,包括 CDB(命令描述块)、Sense 缓冲区和数据缓冲区。
  2. 队列管理需求 :现代存储设备支持多个未完成的 I/O 请求(NCQ、TCQ)。SRB 通过 NextSrb 指针形成链表,端口驱动可以管理多个待处理的 SCSI 命令。每个 IRP 只能携带一个 SRB,但端口驱动可以在内部维护一个 SRB 链表,实现 I/O 合并和排队。
  3. Miniport 接口标准化 :SRB 是端口驱动与 Miniport 驱动之间的标准化接口。无论底层是 ATA、SATA 还是 SCSI 控制器,Miniport 驱动都通过处理 SRB 来完成 I/O。这种抽象使得微软能够用 storport.sys 替换 scsiport.sys,而所有现有 Miniport 驱动只需重新编译即可迁移。在 ReactOS 中,SpiBuildSrb 函数(scsiport.c(file:///d:/reactos/drivers/storage/port/scsiport/scsiport.c))负责从 IRP 中提取参数并填充 SRB。

9.9.3 类驱动(classpnp.sys / disk.sys)

类驱动是 类特定 的驱动,它把通用的 IRP_MJ_READ/WRITE 翻译为 IRP_MJ_SCSI,并处理 缓存、几何信息、SMART、IOCTL_DISK_* 等。

disk.sys 入口

c 复制代码
NTSTATUS NTAPI
DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
    CLASS_INIT_DATA InitializationData = { 0 };
    InitializationData.InitializationDataSize = sizeof(CLASS_INIT_DATA);
    InitializationData.FdoData.DeviceExtensionSize = FUNCTIONAL_EXTENSION_SIZE;
    // ... 填写回调

    return ClassInitialize(DriverObject, RegistryPath, &InitializationData);
}

ClassInitializeclasspnp.sys 导出。它注册了 AddDevice 回调(ClassAddDevice),PnP 触发时创建 FDO。

classpnp 通用 AddDevice

ClassAddDeviceclasspnp/class.c(file:///d:/reactos/drivers/storage/class/classpnp/class.c)):

  1. 接收总线驱动传来的 PDO(来自 scsiport)
  2. 创建 FDO,FdoData.DeviceExtensionSize 由类驱动指定
  3. 调用 IoAttachDeviceToDeviceStack 把 FDO 附加到 PDO
  4. 设置 Flags |= DO_POWER_PAGABLE
  5. DO_DEVICE_INITIALIZING 标志

IRP_MJ_READ 处理

c 复制代码
// ClassReadWrite 内核函数
NTSTATUS ClassReadWrite(IN PDEVICE_OBJECT Fdo, IN PIRP Irp) {
    PIO_STACK_LOCATION IoStack = IoGetCurrentIrpStackLocation(Irp);
    PDEVICE_EXTENSION Ext = (PDEVICE_EXTENSION)Fdo->DeviceExtension;

    // 1. 检查分区表
    // 2. 构造 SRB (SRB_FUNCTION_READ/WRITE)
    // 3. IoCallDriver(LowerDevice, Irp)  // 转发到下层
}

为什么 2:为什么类驱动要将 IRP_MJ_READ/WRITE 转换为 IRP_MJ_SCSI?

类驱动处于"文件系统 ↔ 端口驱动"的中间层,承担着协议转换的关键角色:

  • 上层视角 :文件系统驱动只关心逻辑块读写,它发送 IRP_MJ_READ/WRITE,参数是起始扇区号和传输长度。
  • 下层视角 :端口驱动只理解 SCSI 命令,它需要 SCSIOP_READ/SCSIOP_WRITE 命令描述块(CDB),包含操作码、LBA、传输长度等 SCSI 协议参数。

类驱动的 ClassReadWrite 函数完成这一转换:它从 IRP 栈的 Parameters.Read.ByteOffset 提取 LBA,构造 SCSI CDB,填充 SRB,然后将原始 IRP 重新打包为 IRP_MJ_SCSI(通过 IRP_MJ_INTERNAL_DEVICE_CONTROL + IOCTL_SCSI_EXECUTE_IN)发送到下层。

这种转换的核心价值在于接口隔离------文件系统不需要了解磁盘接口协议(SCSI/ATA/NVMe),端口驱动也不需要理解文件系统的缓存策略或扇区大小。当磁盘从 512 字节扇区切换到 4K 扇区时,类驱动负责在逻辑 LBA 和物理 LBA 之间做映射,上下层都不受影响。


9.9.4 分区管理(partmgr.sys)

partmgr.sys 位于 disk 之上,文件系统之下,主要职责

  1. 磁盘签名读取 :通过 IOCTL_DISK_GET_DRIVE_LAYOUT_EX 获得 MBR/GPT 分区表
  2. 创建分区 PDO\Device\Harddisk0\Partition0(整个磁盘)和 Partition1..N
  3. 卷/分区 IOCTL 分发 :把 IOCTL_VOLUME_*IOCTL_MOUNTMGR_* 转发到合适层

PartMgrAddDevice

c 复制代码
// partmgr.c
static
NTSTATUS
PartMgrAddDevice(
    _In_ PDRIVER_OBJECT DriverObject,
    _In_ PDEVICE_OBJECT Pdo)
{
    // 1. 创建设备对象 \Device\HarddiskN
    // 2. 附加到 disk.sys 的 FDO
    // 3. 调用 IoInvalidateDeviceRelations 通知 PnP
    // 4. PartMgrCreatePdo 列出所有分区
}

关键 IOCTL

IOCTL 处理者 作用
IOCTL_DISK_GET_DRIVE_LAYOUT_EX disk 读取分区表
IOCTL_DISK_GET_PARTITION_INFO_EX partmgr/disk 单分区信息
IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS partmgr 卷的磁盘盘区
IOCTL_MOUNTMGR_QUERY_DEFAULT mountmgr 查询默认卷

为什么 4:为什么分区管理需要为每个分区创建独立的 PDO?

partmgr 的核心职责是将物理磁盘的扇区空间划分为逻辑分区,并为每个分区创建独立的 PDO。这背后的设计考量包括:

  1. 独立挂载不同文件系统:同一个磁盘的不同分区可以挂载不同的文件系统。例如,Partition1 是 FAT32(C: 盘),Partition2 是 NTFS(D: 盘)。如果没有独立的分区 PDO,文件系统驱动无法区分同一物理磁盘上的不同分区,整个磁盘只能使用一个文件系统。
  2. 独立的 PnP 管理粒度 :每个分区 PDO 可以独立响应 PnP 事件。动态磁盘的分区扩展、收缩、删除都需要在 PDO 级别进行管理。当一个分区被删除时,只有该分区的 PDO 收到 IRP_MN_REMOVE_DEVICE,其他分区不受影响。
  3. 独立的电源与安全策略:某些系统支持对单个分区设置独立的电源策略(如 Spin-down 超时)和访问权限(如 BitLocker 加密每个分区独立)。

在 ReactOS 的 PartMgrCreatePdo 实现中,每个分区 PDO 的设备名称为 \Device\HarddiskN\PartitionM,其设备扩展中记录了分区的起始偏移和大小。这些信息通过 IOCTL_DISK_GET_DRIVE_LAYOUT_EX 从 disk 类驱动获取,该 IOCTL 由 disk 驱动负责读取 MBR 或 GPT 分区表并返回给 partmgr。


9.9.5 卷管理(volmgr.sys / mountmgr)

ReactOS 的 partmgr 兼任 volmgr 角色(volmgr 尚未独立)。它的功能:

  1. 卷设备对象 :当 IOCTL_DISK_GET_DRIVE_LAYOUT_EX 显示有有效分区时,partmgr 为每个分区创建 PDO
  2. 卷符号链接 :在 mountmgr 中创建 \DosDevices\C:\Device\HarddiskVolume1
  3. 挂载协调 :mountmgr 接收 IOCTL_MOUNTMGR_CHECK_DLNP 决定哪个盘符对应哪个卷

MountMgr 关键函数

c 复制代码
// mountmgr.c
NTSTATUS MountMgrCheckDes MountMgrCheckDLNP(
    IN PIO_STACK_LOCATION IrpSp, OUT PVOID OutData);

挂载管理器协议(Mount Manager Protocol)见 MSDN:

  • IOCTL_MOUNTMGR_CREATE_POINT
  • IOCTL_MOUNTMGR_DELETE_POINTS
  • IOCTL_MOUNTMGR_QUERY_POINTS
  • IOCTL_MOUNTMGR_SET_AUTO_MOUNT

设备名空间

复制代码
\Device\Harddisk0          <- 物理磁盘 0
\Device\Harddisk0\Partition0  <- 整个磁盘(无文件系统)
\Device\Harddisk0\Partition1  <- 第一分区
\Device\HarddiskVolume1    <- 卷管理器创建的卷对象
\DosDevices\C:             <- DOS 盘符
\??\C:                     <- 内部 NT 视图

为什么 7:为什么 ReactOS 将 volmgr 合并到 partmgr 中?

这是 ReactOS 存储栈与 Windows 的一个重要差异。在 Windows 中,volmgr.sys 负责创建卷设备对象(\Device\HarddiskVolumeX)并管理简单卷、跨区卷等逻辑卷。ReactOS 选择将 volmgr 的功能合并到 partmgr 中,原因如下:

  1. 开发优先级:ReactOS 的目标是与 Windows Server 2003 兼容,而 Server 2003 的卷管理功能相对简单。动态磁盘、镜像卷、RAID-5 卷等高级功能在 ReactOS 的早期阶段不是必须的。将 volmgr 合并到 partmgr 减少了需要独立开发和测试的驱动数量。
  2. 消除层间通信开销 :合并后 partmgr 直接创建卷 PDO,磁盘 I/O 路径减少了一层设备栈。在 Windows 中,IRP 需要穿越 partmgr → volmgr 两个模块,每多一层就多一次 IoCallDriver 和完成例程的开销。ReactOS 的合并将 IRP 路径缩短了至少一层。
  3. 代码复用:partmgr 已具备读取分区表的能力,合并 volmgr 后可以在同一个设备扩展中管理分区信息和卷信息,无需跨模块通信。

然而,这种合并也有代价:ReactOS 目前不支持 Windows 的动态磁盘管理功能(如跨区卷、镜像卷、RAID-5 卷),因为 volmgr 的核心------卷的在线扩展和容错------需要独立的卷管理器实现。从 partmgr.c 源码注释可以看到:"Here it acts like both partition and volume manager, because volmgr.sys does not (yet) exist in ReactOS。"


9.9.6 文件系统驱动

文件系统驱动(fastfat、ntfs、cdfs)位于设备栈的 最顶层 。它通过 IoCreateDevice 创建 卷设备对象(VOD)

c 复制代码
// fastfat 的 VCB 创建
PDEVICE_OBJECT VolumeDevice;
IoCreateDevice(DriverObject, sizeof(VCB), &VolumeName,
               FILE_DEVICE_DISK, ...,
               FALSE, &VolumeDevice);

Vpb->DeviceObject = VolumeDevice 把 FS 卷设备与卷管理器创建的卷设备链接起来。

挂载流程

复制代码
IopMountVolume (ntoskrnl/io/iomgr/volume.c)
  |
  v
IoCallDriver(Vpb->DeviceObject, Irp)  // IRP_MJ_FILE_SYSTEM_CONTROL with MOUNT
  |
  v
FS Driver: FatMountVolume / NtfsMountVolume
  |
  v
填充 VCB, 创建 CCB
  |
  v
返回 STATUS_SUCCESS

为什么 5:为什么文件系统挂载需要 VPB 这个中间结构?

VPB(Volume Parameter Block)是 DEVICE_OBJECT 结构体的内嵌字段,它在文件系统驱动与卷管理器之间扮演着桥梁角色。VPB 存在的根本原因在于:

  1. 文件系统的可替换性 :同一个卷设备可以被不同的文件系统挂载。VPB 作为间接层,使得卷设备对象可以动态关联到任意文件系统卷设备。例如,一个 U 盘插入时先被 fastfat 尝试挂载,如果 fastfat 返回 STATUS_UNRECOGNIZED_VOLUME,系统可以继续尝试 ntfs 或 cdfs------每次尝试都通过 VPB 重新建立关联,不影响卷设备对象本身。
  2. I/O 路径的快速路由 :当应用程序打开 C:\file.txt 时,I/O 管理器通过 \DosDevices\C: 定位到卷设备对象,然后通过 Vpb->DeviceObject 立即找到文件系统设备对象,无需遍历挂载点数据库。这个指针直接指向文件系统驱动的卷设备,I/O 请求可以一步直达。
  3. 卸载与重载的支持 :VPB 的状态字段(VpbFlags)记录了卷的当前状态------是否已挂载(VPB_MOUNTED)、是否已锁定(VPB_LOCKED)、是否为原始卷(VPB_RAW_MOUNT)。当卷被卸载时,Vpb->DeviceObject 被置空,文件系统驱动安全地清理资源。下次访问时,I/O 管理器检测到 VPB 未挂载,自动触发重新挂载。

在 ReactOS 的 IopMountVolume 实现中,挂载流程遍历已注册的文件系统驱动列表,依次尝试挂载。第一个成功的文件系统驱动通过 IoMountVolume 将自身的卷设备对象注册到 VPB 中。如果所有文件系统都失败,I/O 管理器会提示"未格式化的磁盘"并挂载 RAW 文件系统。


9.9.7 IRP_MJ_SCSI 内部 IRP 路径

类驱动通过 IOCTL_SCSI_EXECUTE_IN 构造 内部 IRP(Irp->Flags |= IRP_DEFER_IO_COMPLETION),并把它传递给端口驱动。

完整路径

复制代码
用户 NtReadFile -> IoCallDriver(vol, Irp)
                      |
                      v
                  file system (fastfat)
                      |  缓存未命中
                      v
                  IoCallDriver(disk, Irp)
                      |
                      v
                  disk.sys (classpnp)
                      |  构造 IRP_MJ_SCSI
                      v
                  IoCallDriver(scsiport, Irp)
                      |
                      v
                  scsiport.sys
                      |  SpiBuildSrb -> 构造 SRB
                      v
                  IoCallDriver(atapi, Irp) [IRP_MJ_SCSI]
                      |
                      v
                  atapi.sys
                      |  编程 ATA 控制器
                      v
                  HalStartInterruptTransfer
                      |
                      v
                  中断到来 -> ISR -> DPC -> 完成 IRP
                      |
                      v
                  沿完成例程链向上返回

每一层都注册了 IoSetCompletionRoutine,形成 完成例程链(completion routine chain)

为什么 6:为什么完成例程要从底层向上层传播?

完成例程链的传播方向(从底层到上层)由 I/O 操作的固有顺序决定:

  1. 数据流向决定顺序:底层驱动最先完成与硬件的交互,此时数据已从磁盘读取到缓冲区,或已从缓冲区写入磁盘。完成例程从底层开始执行,每个上层完成例程处理的数据都是下层已经处理完毕的------这是典型的"后进先出"(LIFO)模式。如果从上到下执行,上层完成例程会在数据尚未就绪时访问缓冲区,导致数据不一致。
  2. 资源释放的顺序依赖:DMA 缓冲区的解除映射、中断资源的释放必须从底层开始。例如,底层先释放 DMA 通道和取消 MDL 映射,然后上层才能安全地释放与 DMA 相关的高层数据结构。顺序颠倒在多核系统上会导致竞争条件。
  3. 错误恢复机制 :如果某一层返回错误状态(如 STATUS_UNSUCCESSFUL),上层的完成例程根据 SL_INVOKE_ON_ERROR 标志被调用,可以执行重试或降级处理。例如,classpnp 的 ClassInterpretSenseInfo 在收到 NOT_READY 感觉数据时,在完成例程中自动构造 REQUEST_SENSE 命令重新发送。如果传播方向颠倒,底层错误会被底层完成例程自行消化,上层完全不知情。

为什么 10:为什么设备栈之间使用 IoCallDriver 而不是直接函数调用?

设备栈的各层之间通过 IoCallDriver 发送 IRP,而不是直接函数调用,这背后的设计思想是控制反转(IoC)可插拔性

  1. 过滤驱动的透明插入 :Windows 的过滤驱动(filter driver)可以透明地插入设备栈的任意两层之间。例如,磁盘加密驱动可以插入在 classpnp 与端口驱动之间,对写入数据自动加密,对读取数据自动解密。如果使用直接函数调用,过滤驱动无法拦截 I/O 请求------它不知道应该在哪两个函数之间插入自己。而 IRP 机制使得过滤驱动只需注册 AddDevice、调用 IoAttachDeviceToDeviceStack 附加到栈中,即可处理、修改甚至拦截任意 IRP。
  2. 完成例程的自动管理IoCallDriver 不仅负责将 IRP 传递到下层,还自动管理 IRP 的 StackLocation 索引和完成例程链。每次 IoCallDriver 调用都会推进栈位置,下次完成时自动从正确的位置开始回调。直接函数调用要求每层手动管理完成回调队列,代码复杂度呈指数级增长。
  3. 统一的异步/同步模型IoCallDriver 的返回值统一为 NTSTATUS:如果下层同步完成返回 STATUS_SUCCESS,如果异步处理返回 STATUS_PENDING。上层驱动不需要关心下层是同步还是异步实现------这种统一接口简化了每层的实现。例如,scsiport 的 StartIo 队列可以挂起 IRP 等待 SRB 完成,而上面的 disk 驱动完全不需要感知这种异步性。

深入剖析:磁盘设备栈的内部机制

VPB(卷参数块)的核心角色

在磁盘设备栈中,VPB(Volume Parameter Block) 是连接文件系统驱动与下层卷管理设备的关键纽带。VPB 是 DEVICE_OBJECT 结构体内嵌的一个字段(Vpb),它指向一个 VPB 结构体。当文件系统成功挂载一个卷时,I/O 管理器会创建文件系统的卷设备对象(VOD),并将 Vpb->DeviceObject 指向该 VOD。这样,当应用层对 C:\path\file.txt 发起 I/O 请求时,I/O 管理器通过 DOS 设备名 \DosDevices\C: 找到卷设备对象,再通过 VPB 定位到文件系统驱动的设备对象,从而将 IRP 发送到正确的文件系统驱动。

在 ReactOS 的实现中,VPB 的创建和关联发生在 ntoskrnl/io/iomgr/volume.cIopMountVolume 函数中。该函数遍历所有已注册的文件系统驱动(通过 IoRegisterFileSystem 注册的驱动链表),依次向每个文件系统发送 IRP_MJ_FILE_SYSTEM_CONTROL(带有 FSCTL_MOUNT_VOLUME 控制码)。第一个成功返回 STATUS_SUCCESS 的文件系统驱动就"拥有"了这个卷。ReactOS 中 fastfat 和 ntfs 是主要的文件系统驱动,它们各自实现了 FatMountVolumeNtfsMountVolume 函数来完成挂载过程。

IRP 完成例程链的深层机制

磁盘 I/O 路径上的每一层驱动都通过 IoSetCompletionRoutine 注册完成回调,形成一条完成例程链。这条链的工作机制比表面看起来更为复杂:

  1. 完成例程的调用条件 :每个完成例程可以设置三个条件标志------SL_INVOKE_ON_SUCCESSSL_INVOKE_ON_ERRORSL_INVOKE_ON_CANCEL。只有当 IRP 的完成状态匹配设定的条件时,完成例程才会被调用。

  2. 完成例程的返回值 :完成例程返回 STATUS_MORE_PROCESSING_REQUIRED 表示"我处理完了,不要再往上走了";返回其他状态则继续向上传播。这个机制允许某一层"截获"IRP 的完成过程,例如用于异步重试或错误恢复。

  3. ReactOS 中的实现细节 :在 ntoskrnl/io/iomgr/iocomp.c 中,IopCompleteRequest 函数负责遍历 IRP 的 IoStackLocation 数组,从当前栈位置向上逐一检查并完成每一层的回调。由于每个 IO_STACK_LOCATION 中的 CompletionRoutineContext 字段是在 IoCallDriver 之前通过 IoSetCompletionRoutine 设置的,因此完成链与发送链是严格对称的。

  4. 完成例程与 IRP 重用 :在某些场景下(如 SCSI 超时重试),端口驱动会在完成例程中截获 IRP,重置 SRB 状态后重新发送到下层。这种模式在 classpnpClassInterpretSenseInfo 函数中尤为常见------当磁盘返回 NOT_READY 感觉数据时,classpnp 会自动构造 REQUEST_SENSE 命令并重新发送。

设备栈中的 PnP 传播路径

PnP IRP(如 IRP_MN_START_DEVICEIRP_MN_REMOVE_DEVICE)在设备栈中的传播方式与普通 I/O IRP 不同。PnP IRP 遵循先下后上的传播规则:

  • 向下传播 :PnP 管理器(ntoskrnl/pnp/pnpdrv.c)将 PnP IRP 发送到设备栈的顶层 设备对象,然后每层驱动在处理完毕后必须将 IRP 传递给下层(通过 PoCallDriverIoCallDriver),直到到达最底层的 PDO。
  • 向上传播:当底层 PDO 完成 IRP 后,完成结果沿着完成例程链逐层向上传播,每层的完成例程可以在此时执行自己的后处理逻辑。

在 ReactOS 的磁盘栈中,这意味着 partmgr.sysIRP_MN_START_DEVICE 处理流程是:partmgr 先做自身的初始化(如读取分区表),然后将 IRP 传递给 disk.sys 的 FDO,disk 再传递给 scsiport.sys,最终到达 atapi 或 storahci 的 PDO。每一层在"向下传递"之前完成自己的初始化工作。

与 Windows 实现的对比分析

ReactOS 的磁盘设备栈设计力求与 Windows Server 2003 保持二进制兼容,但在以下方面存在差异:

  1. volmgr.sys 的缺失 :在 Windows 中,volmgr.sys 负责基本卷管理(简单卷),volsnap.sys 负责卷影 copy。ReactOS 目前将这两者的功能合并到了 partmgr.sys 中。从 ReactOS 的 partmgr.c 源码注释可以看到:"Here is acts like both partition and volume manager, because volmgr.sys does not (yet) exist in ReactOS."这意味着 ReactOS 暂时不支持动态磁盘、跨区卷等高级卷管理功能。

  2. classpnp 的版本差异 :ReactOS 的 classpnp 来源于 Windows 10 的 classpnp.sys(从 #if (NTDDI_VERSION >= NTDDI_WIN8) 条件编译可以看到),而磁盘类驱动 disk.sys 也相应地使用了较新的接口。但 ReactOS 的 scsiport 则是独立实现的,这导致了一些兼容性问题------例如 STORAGE_ADAPTER_DESCRIPTOR_WIN8 结构体在 scsiport 中需要特殊定义才能与 classpnp 兼容。

  3. storport 的成熟度:Windows 的 storport 已经发展到支持 NVMe、SCSI 等现代协议,而 ReactOS 的 storport 实现相对初级。storahci(AHCI Miniport)虽然能够工作,但在多队列深度、NCQ(Native Command Queuing)等高级特性上还有差距。

调试技巧与常见问题

  1. 设备栈查看 :使用 WinDbg 的 !devstack 命令可以查看某个设备对象的完整栈。例如 !devstack \Device\Harddisk0\Partition1 可以显示从文件系统到端口驱动的完整设备栈。

  2. IRP 跟踪 :使用 !irp <irp_address> 可以查看 IRP 的当前栈位置、完成状态和已完成的层数。在调试磁盘 I/O 挂起问题时,检查 IRP 是否卡在某一层的完成例程中非常有用。

  3. 常见挂起场景 :磁盘 I/O 挂起通常发生在以下情况------SRB 超时但端口驱动未正确处理(ScsiPortIoTimer 未触发);完成例程返回了错误的状态码导致 IRP 不再向上传播;DMA 映射失败导致 SRB 无法启动。

  4. ReactOS 特有的调试输出 :ReactOS 的存储栈启用了 DPRINT 宏进行调试输出。设置 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter 中对应组件的级别为 0xFF,可以在调试串口看到详细的 IRP 流转日志。

为什么 9:为什么磁盘 I/O 超时处理如此复杂?

磁盘 I/O 的超时处理是存储栈中最容易出错的环节之一。其复杂性源于多个因素的叠加:

  1. 硬件的不可预测性:磁盘是机电设备,其响应时间受多种因素影响------寻道时间、旋转延迟、总线争用、坏道重映射等。一个正常的磁盘在重负载下可能需要数秒才能完成一个 I/O。因此,超时阈值的设定本身就是一种权衡:太短会导致误判(正常慢速 I/O 被中止),太长会导致系统长时间挂起。
  2. 三级恢复链 :当 SRB 超时时,端口驱动的 ScsiPortIoTimer 触发超时回调。恢复过程通常分三步执行:先发送 SRB_FUNCTION_ABORT_COMMAND 中止当前命令;如果失败,发送 SRB_FUNCTION_RESET_DEVICE 重置目标设备;如果仍然失败,发送 SRB_FUNCTION_RESET_BUS 重置整个 SCSI 总线。每一步都涉及新的 SRB 提交和等待,且每一步都可能再次超时,形成递归式的超时处理。
  3. IRP 完整性的保护 :超时恢复过程中,原始 IRP 不能被简单地丢弃。类驱动的完成例程(如 ClassInterpretSenseInfo)需要在超时后处理 SCSI 感觉数据(Sense Data),判断设备是否返回了 NOT_READYUNIT_ATTENTION,并决定是否自动重试。如果在三级恢复过程中 IRP 被意外完成,应用程序可能收到错误的 I/O 结果(如读取到不完整的数据)。
  4. 电源状态转换的干扰 :磁盘在待机状态(PowerDeviceD1/D2)下对任何 I/O 的响应时间会显著增加。端口驱动需要区分"正常的电源恢复延迟"和"真正的硬件超时",避免在电源状态转换期间误报超时。ReactOS 的 scsiport 通过检查 SRB 时间戳和电源状态标志来实现这一区分。

在 ReactOS 的 scsiport 中,超时处理由 SpiProcessTimeout 函数实现,它遍历所有未完成的 SRB 链表,检查每个 SRB 的时间戳,对超时的 SRB 依次执行中止→重置设备→重置总线的三级恢复策略。理解这一机制对于调试 ReactOS 的磁盘 I/O 挂起问题至关重要。


9.9.8 总结

磁盘设备栈的 关键要点

  1. 五层架构:硬件 → 总线 → 端口 → 类 → 卷管理 → 文件系统
  2. IRP_MJ_SCSI 内部 IRP:类驱动通过 IOCTL_SCSI_EXECUTE 构造
  3. SRB:SCSI Request Block,端口驱动与 Miniport 的接口
  4. 完成例程链 :每层都通过 IoSetCompletionRoutine 注册完成处理
  5. partmgr 与 volmgr:ReactOS 中 partmgr 兼任两者
  6. mountmgr 协议:卷挂载与盘符分配

下一节 9.9.1 将详细剖析类驱动 disk.sys


本章代码索引

文件 内容
disk.c(file:///d:/reactos/drivers/storage/class/disk/disk.c) 类驱动 disk.sys 入口
disk.h(file:///d:/reactos/drivers/storage/class/disk/disk.h) disk 设备扩展定义
pnp.c(file:///d:/reactos/drivers/storage/class/disk/pnp.c) disk PnP 处理
class.c(file:///d:/reactos/drivers/storage/class/classpnp/class.c) classpnp 框架
classp.h(file:///d:/reactos/drivers/storage/class/classpnp/classp.h) classpnp 头文件
partmgr.c(file:///d:/reactos/drivers/storage/partmgr/partmgr.c) 分区管理器
partmgr.h(file:///d:/reactos/drivers/storage/partmgr/partmgr.h) partmgr 头文件
scsiport.c(file:///d:/reactos/drivers/storage/port/scsiport/scsiport.c) SCSI 端口驱动
storport.c(file:///d:/reactos/drivers/storage/port/storport/storport.c) 新式存储端口驱动
atapi.c(file:///d:/reactos/drivers/storage/ide/atapi/atapi.c) ATAPI 端口驱动
mountmgr.c(file:///d:/reactos/drivers/storage/mountmgr/mountmgr.c) 卷挂载管理器
相关推荐
阿维的博客日记2 小时前
Windows自由切换jdk版本
java·windows
振南的单片机世界2 小时前
PWM调压调速,H桥换向:直流电机四象限控制
arm开发·stm32·单片机·嵌入式硬件
济6172 小时前
BMS系统专栏:BQ76920 锂电 AFE 芯片深度解析
嵌入式硬件·嵌入式·bms电池管理
iCxhust2 小时前
C# 生成命令行程序 将hex格式烧录程序转换成bin烧录格式
开发语言·汇编·单片机·嵌入式硬件·c#·微机原理
不脱发的程序猿2 小时前
DLL文件缺失怎么办?
单片机·嵌入式硬件·嵌入式
ACP广源盛139246256733 小时前
GSV6155@ACP#DP 1.4a 重定时器芯片,物理 AI 信号长距传输的稳定保障
大数据·人工智能·分布式·嵌入式硬件·spark
weixin_446260853 小时前
TinyML 在 STM32 与 ESP32 上的完整部署指南:从模型训练、量化到推理优化
stm32·单片机·嵌入式硬件
资深流水灯工程师3 小时前
基于 STM32L476 + SAI1 Block A + DMA 循环乒乓缓冲 实现 4 路加速度计 TDM 采集
stm32·单片机·嵌入式硬件