Reactos 第 9 章 设备驱动 — 9.2 一个“老式“驱动模块的实例

第 9 章 设备驱动 --- 9.2 一个"老式"驱动模块的实例

本节以 drivers/base/beep/beep.c 为例剖析一个完整的老式(NT 4 风格)设备驱动模块。 9.1 节介绍了 WDM 框架,本节通过 beep.sys(PC 扬声器发声驱动)展示一个 真实可工作 的最小驱动实例,涵盖 DriverEntry 初始化、IRP 派发、同步 I/O、设备对象管理等所有关键概念。beep.c 是 ReactOS 内置的最小驱动之一,约 300 行,是学习 NT 驱动开发的经典教材。


概述

beep.sys 是 PC 扬声器(system speaker)的驱动,对应 Win32 Beep() API。它实现:

  • IRP_MJ_CREATE:打开扬声器
  • IRP_MJ_CLOSE:关闭扬声器
  • IRP_MJ_WRITE:发声(写特定 IOCTL 或直接 WriteFile)
  • IRP_MJ_DEVICE_CONTROL:控制频率与持续时间
  • IRP_MJ_CLEANUP:清理挂起的请求

它采用 老式驱动 模式(无 PnP),通过 DriverEntry 直接 IoCreateDevice 创建设备对象。

本节内容概览

  • 9.2.0 框架图
  • 9.2.1 beep 驱动的整体结构
  • 9.2.2 DriverEntry 完整剖析
  • 9.2.3 设备扩展结构
  • 9.2.4 IRP_MJ_CREATE/CLOSE 派发
  • 9.2.5 IRP_MJ_WRITE 派发
  • 9.2.6 IRP_MJ_DEVICE_CONTROL 派发
  • 9.2.7 IRP_MJ_CLEANUP 派发
  • 9.2.8 扬声器硬件操作(HAL)
  • 9.2.9 总结与代码索引

学习目标

  • 能够完整读懂 beep.c 的每一行
  • 理解老式驱动的初始化与设备对象管理
  • 掌握 IOCTL 码的编码与解码
  • 理解 IofCallDriver 路径与扬声器硬件交互

涉及的内核子系统

子系统 头文件/源文件 核心作用
beep 驱动 drivers/base/beep/beep.c(file:///d:/reactos/drivers/base/beep/beep.c) 完整驱动实例
扬声器 HAL hal/halx86/generic/beep.c(file:///d:/reactos/hal/halx86/generic/beep.c) HalMakeBeepHalStopBeep
I/O 管理器 ntoskrnl/io/iomgr/(file:///d:/reactos/ntoskrnl/io/iomgr/) IRP 派发与设备对象管理
蜂鸣 API(用户态) dll/win32/kernel32/client/console.c(file:///d:/reactos/dll/win32/kernel32/client/console.c) Beep() Win32 API
注册表 drivers/base/beep/beep_reg.inf(file:///d:/reactos/drivers/base/beep/beep_reg.inf) 设备注册项
资源文件 drivers/base/beep/beep.rc(file:///d:/reactos/drivers/base/beep/beep.rc) 版本信息

9.2.0 框架图

复制代码
         用户进程
            |
            v
    +-------------------+
    | Beep(dwFreq, dwDur)|  <- Win32 API
    +-------------------+
            |
            v
    +-------------------+
    | ntdll NtDeviceIoControlFile
    +-------------------+
            |
            v
    +-------------------+
    | 内核 NtDeviceIoControlFile
    | (syscall)         |
    +-------------------+
            |
            v  IRP_MJ_DEVICE_CONTROL
    +--------------------+
    |   beep.sys          |
    |   DispatchControl  |
    +--------------------+
            |
            v  (HalMakeBeep / HalStopBeep)
    +--------------------+
    |  HAL HalMakeBeep  |
    +--------------------+
            |
            v
    +--------------------+
    |   I/O 端口 0x43, 0x61  |  <- 直接操作 8254 计时器
    +--------------------+

  设备栈(简化):
  +-----------------+
  | \Device\Beep    |  <- beep.sys 创建
  +-----------------+

9.2.1 beep 驱动的整体结构

beep.c 的代码结构如下(精简版):

c 复制代码
/* 全局变量 */
BEEP_DEVICE_EXTENSION BeepDeviceExtension;

/* DriverEntry */
NTSTATUS NTAPI DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath);

/* 派发函数 */
static NTSTATUS BeepCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp);
static NTSTATUS BeepWrite(PDEVICE_OBJECT DeviceObject, PIRP Irp);
static NTSTATUS BeepDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp);
static NTSTATUS BeepCleanup(PDEVICE_OBJECT DeviceObject, PIRP Irp);

/* 扬声器控制 */
static BOOLEAN BeepStart(ULONG Frequency);
static VOID BeepStop(VOID);

beep.c 的核心数据是 BEEP_DEVICE_EXTENSION(设备扩展)。整个驱动仅使用一个全局扩展而非多设备,这反映其作为简单单例设备的特性。


9.2.2 DriverEntry 完整剖析

c 复制代码
NTSTATUS NTAPI
DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
    PDEVICE_OBJECT DeviceObject;
    NTSTATUS Status;
    UNICODE_STRING DeviceName;

    UNREFERENCED_PARAMETER(RegistryPath);

    /* 1. 注册派发函数 */
    DriverObject->MajorFunction[IRP_MJ_CREATE]         = BeepCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]          = BeepCreateClose;
    DriverObject->MajorFunction[IRP_MJ_WRITE]          = BeepWrite;
    DriverObject->MajorFunction[IRP_MJ_CLEANUP]        = BeepCleanup;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = BeepDeviceControl;

    /* 2. 创建设备对象 \Device\Beep */
    RtlInitUnicodeString(&DeviceName, L"\\Device\\Beep");
    Status = IoCreateDevice(DriverObject,
                            sizeof(BEEP_DEVICE_EXTENSION),
                            &DeviceName,
                            FILE_DEVICE_BEEP,
                            FILE_DEVICE_SECURE_OPEN,
                            FALSE,
                            &DeviceObject);
    if (!NT_SUCCESS(Status)) return Status;

    /* 3. 初始化设备扩展 */
    RtlZeroMemory(DeviceObject->DeviceExtension, sizeof(BEEP_DEVICE_EXTENSION));

    /* 4. 清除 DO_DEVICE_INITIALIZING 标志 */
    DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;

    return STATUS_SUCCESS;
}

关键观察

  1. 不使用 PnP :无 AddDevice、无 IRP_MJ_PNP 派发------纯老式驱动
  2. 设备路径硬编码\Device\Beep(不是符号链接)
  3. 设备类型FILE_DEVICE_BEEP(0x00000001)
  4. 共享设备Exclusive = FALSE(多进程可同时打开)
  5. 直接 I/O:默认是缓冲 I/O(write 路径不需要 MDL)

用户态如何访问

用户态通过 Win32 Beep() API 访问:

c 复制代码
// user32/console.c(伪代码)
BOOL Beep(DWORD dwFreq, DWORD dwDuration)
{
    HANDLE hBeep = CreateFileW(L"\\\\.\\Beep", GENERIC_WRITE, 0, NULL,
                               OPEN_EXISTING, 0, NULL);
    if (hBeep == INVALID_HANDLE_VALUE) return FALSE;

    BEEP_SET_PARAMETERS BeepParams;
    BeepParams.Frequency = dwFreq;
    BeepParams.Duration = dwDuration;

    DWORD BytesReturned;
    DeviceIoControl(hBeep, IOCTL_BEEP_SET, &BeepParams, sizeof(BeepParams),
                    NULL, 0, &BytesReturned, NULL);

    CloseHandle(hBeep);
    return TRUE;
}

\\\\.\\Beep 通过 Win32 子系统映射到 \Device\Beep(详见 11.3)。


9.2.3 设备扩展结构

c 复制代码
typedef struct _BEEP_DEVICE_EXTENSION {
    KDPC Dpc;              // DPC 对象(用于延迟关闭扬声器)
    KTIMER Timer;          // 定时器(持续时间到时停止发声)
    BOOLEAN BeepState;     // 扬声器是否正在发声
} BEEP_DEVICE_EXTENSION, *PBEEP_DEVICE_EXTENSION;

设备扩展的作用

  1. 设备状态保存:单例设备的运行状态
  2. DPC 关联BeepStopDpcRoutine 在定时器到时停止发声
  3. 定时器对象BeepTimerRoutine 触发 DPC
  4. 无 IOCTL 缓冲:因为是单例,不需要互斥保护

初始化

c 复制代码
RtlZeroMemory(DeviceObject->DeviceExtension, sizeof(BEEP_DEVICE_EXTENSION));
KeInitializeTimer(&Extension->Timer);
KeInitializeDpc(&Extension->Dpc, BeepStopDpcRoutine, Extension);

KeInitializeDpc 把 DPC 与其回调函数 BeepStopDpcRoutine 关联。


9.2.4 IRP_MJ_CREATE/CLOSE 派发

beep.c 把 CREATE 和 CLOSE 共用同一个派发函数 BeepCreateClose

c 复制代码
static NTSTATUS
BeepCreateClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    PIO_STACK_LOCATION IoStackLocation = IoGetCurrentIrpStackLocation(Irp);

    /* 同步完成 */
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

简化的原因

  • beep 不需要为 CREATE/CLOSE 维护任何状态
  • 真正的资源管理在 CLEANUP(IRP_MJ_CLEANUP)中

9.2.5 IRP_MJ_WRITE 派发

BeepWrite 处理直接写设备:

c 复制代码
static NTSTATUS
BeepWrite(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    PIO_STACK_LOCATION IoStackLocation = IoGetCurrentIrpStackLocation(Irp);
    NTSTATUS Status = STATUS_SUCCESS;

    /* WriteFile 风格:直接传频率作为 ULONG */
    if (IoStackLocation->Parameters.Write.Length < sizeof(ULONG))
    {
        Status = STATUS_INVALID_BUFFER_SIZE;
    }
    else
    {
        ULONG Frequency = *(PULONG)Irp->AssociatedIrp.SystemBuffer;
        BeepStart(Frequency);
    }

    Irp->IoStatus.Status = Status;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return Status;
}

缓冲 I/O

因为 beep.sys 没有设置 DO_DIRECT_IO,I/O 管理器为每次 WriteFile 分配一个内核缓冲,地址在 Irp->AssociatedIrp.SystemBuffer。驱动可安全访问该缓冲。


9.2.6 IRP_MJ_DEVICE_CONTROL 派发

c 复制代码
static NTSTATUS
BeepDeviceControl(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    PIO_STACK_LOCATION IoStackLocation = IoGetCurrentIrpStackLocation(Irp);
    ULONG IoControlCode = IoStackLocation->Parameters.DeviceIoControl.IoControlCode;
    PVOID InputBuffer = Irp->AssociatedIrp.SystemBuffer;
    ULONG InputBufferLength = IoStackLocation->Parameters.DeviceIoControl.InputBufferLength;
    NTSTATUS Status = STATUS_SUCCESS;
    LARGE_INTEGER DueTime;

    switch (IoControlCode)
    {
        case IOCTL_BEEP_SET:
            if (InputBufferLength < sizeof(BEEP_SET_PARAMETERS))
            {
                Status = STATUS_INVALID_BUFFER_SIZE;
                break;
            }
            {
                PBEEP_SET_PARAMETERS BeepParams = (PBEEP_SET_PARAMETERS)InputBuffer;
                if (BeepParams->Frequency == 0)
                {
                    /* 停止发声 */
                    BeepStop();
                    BeepDeviceExtension->BeepState = FALSE;
                }
                else
                {
                    /* 启动发声 */
                    if (BeepStart(BeepParams->Frequency))
                    {
                        BeepDeviceExtension->BeepState = TRUE;
                        /* 设置定时器 */
                        DueTime.QuadPart = -(LONGLONG)BeepParams->Duration * 10000LL;
                        KeSetTimer(&BeepDeviceExtension->Timer, DueTime, &BeepDeviceExtension->Dpc);
                    }
                    else
                    {
                        Status = STATUS_INVALID_PARAMETER;
                    }
                }
            }
            break;

        default:
            Status = STATUS_INVALID_DEVICE_REQUEST;
            break;
    }

    Irp->IoStatus.Status = Status;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return Status;
}

IOCTL 码

c 复制代码
#define IOCTL_BEEP_SET CTL_CODE(FILE_DEVICE_BEEP, 0, METHOD_BUFFERED, FILE_ANY_ACCESS)

IOCTL_BEEP_SET 的编码:

  • DeviceType = FILE_DEVICE_BEEP (1)
  • Function = 0
  • Method = METHOD_BUFFERED (0)
  • Access = FILE_ANY_ACCESS (0)

关键流程

  1. 解析 IOCTL 码IoStackLocation->Parameters.DeviceIoControl.IoControlCode
  2. 频率 0 = 停止 :调用 BeepStop(),取消定时器
  3. 频率 > 0 = 发声 :调用 BeepStart(Frequency),设置定时器
  4. 定时器到时BeepStopDpcRoutine 触发,停止发声

BEEP_SET_PARAMETERS

c 复制代码
typedef struct _BEEP_SET_PARAMETERS {
    ULONG Frequency;   // 频率(Hz),0 = 停止
    ULONG Duration;    // 持续时间(毫秒)
} BEEP_SET_PARAMETERS, *PBEEP_SET_PARAMETERS;

9.2.7 IRP_MJ_CLEANUP 派发

c 复制代码
static NTSTATUS
BeepCleanup(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    /* 取消所有挂起的 IRP */
    IoCancelIrp(Irp);  /* 实际是取消本线程所有未完成 IRP */
    BeepStop();
    BeepDeviceExtension->BeepState = FALSE;
    KeCancelTimer(&BeepDeviceExtension->Timer);

    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

CLEANUP 的语义

IRP_MJ_CLEANUP最后一个 handle 关闭 时发送,表示:"进程退出前清理"。驱动应:

  1. 取消所有挂起的 IRP(用 IoCancelIrp 标记为 canceled)
  2. 重置设备状态
  3. 完成清理 IRP

9.2.8 扬声器硬件操作(HAL)

BeepStart / BeepStop 调用 HAL:

c 复制代码
static BOOLEAN
BeepStart(IN ULONG Frequency)
{
    return HalMakeBeep(Frequency);
}

static VOID
BeepStop(VOID)
{
    HalStopBeep();
}

HalMakeBeep

定义于 hal/halx86/generic/beep.c(file:///d:/reactos/hal/halx86/generic/beep.c):

c 复制代码
BOOLEAN NTAPI
HalMakeBeep(IN ULONG Frequency)
{
    if (Frequency < 20 || Frequency > 20000) return FALSE;

    /* 设置 8254 计时器 2 的分频 */
    USHORT Divider = (USHORT)(PIT_FREQUENCY / Frequency);
    WRITE_PORT_UCHAR((PUCHAR)0x43, 0xB6);  // 选择计数器 2,方波模式
    WRITE_PORT_UCHAR((PUCHAR)0x42, LOBYTE(Divider));
    WRITE_PORT_UCHAR((PUCHAR)0x42, HIBYTE(Divider));

    /* 启用扬声器 */
    UCHAR PortValue = READ_PORT_UCHAR((PUCHAR)0x61);
    if ((PortValue & 0x03) != 0x03)
    {
        WRITE_PORT_UCHAR((PUCHAR)0x61, PortValue | 0x03);
    }
    return TRUE;
}

HalStopBeep

c 复制代码
VOID NTAPI
HalStopBeep(VOID)
{
    UCHAR PortValue = READ_PORT_UCHAR((PUCHAR)0x61);
    WRITE_PORT_UCHAR((PUCHAR)0x61, PortValue & ~0x03);
}

硬件原理

PC 扬声器由 8254 可编程间隔定时器(Programmable Interval Timer, PIT) 驱动:

  • 计数器 2(端口 0x42):方波输出
  • 分频值PIT_FREQUENCY (1.193182 MHz) / Frequency
  • 扬声器门控(端口 0x61):位 0(计数器 2 启用)、位 1(扬声器启用)

HalMakeBeep 设置计数器 2 的频率并打开扬声器,HalStopBeep 仅关闭扬声器门控。


9.2.9 BeepStopDpcRoutine

BeepStopDpcRoutine 是 DPC 回调,在定时器到时由 DPC 分发器在 DISPATCH_LEVEL 调用:

c 复制代码
static VOID NTAPI
BeepStopDpcRoutine(IN PKDPC Dpc, IN PVOID DeferredContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2)
{
    PBEEP_DEVICE_EXTENSION Extension = (PBEEP_DEVICE_EXTENSION)DeferredContext;
    UNREFERENCED_PARAMETER(Dpc);
    UNREFERENCED_PARAMETER(SystemArgument1);
    UNREFERENCED_PARAMETER(SystemArgument2);

    /* 关闭扬声器 */
    BeepStop();
    Extension->BeepState = FALSE;
}

DPC 的作用

DPC(Deferred Procedure Call)用于在 低优先级(DISPATCH_LEVEL) 上执行工作,避免中断处理在 DIRQL 上停留过久。在 beep 中:

  • 定时器到时(DPC_DISPATCH_LEVEL)→ 关闭扬声器
  • 不需要长时间操作,所以 DPC 是合适的
  • 如果工作量大,应该用工作项(IoQueueWorkItem

9.2.A ReactOS beep.c 源代码深度剖析

设备扩展结构的真实实现

在 ReactOS 源代码(drivers/base/beep/beep.c(file:///d:/reactos/drivers/base/beep/beep.c))中,DEVICE_EXTENSION 的实际定义比文档中简化的版本更为复杂:

c 复制代码
typedef struct _BEEP_DEVICE_EXTENSION
{
    LONG ReferenceCount;      // 引用计数(支持多进程同时打开)
    FAST_MUTEX Mutex;         // 快速互斥锁(保护引用计数)
    KTIMER Timer;             // 定时器对象(控制发声持续时间)
    LONG TimerActive;         // 定时器活动标志(原子操作)
    PVOID SectionHandle;      // 内存段句柄(用于分页锁定)
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

这个结构揭示了几个重要的设计决策:

  1. 引用计数机制ReferenceCount 跟踪有多少进程同时打开了 \Device\Beep。当第一个进程打开设备时(ReferenceCount 从 0 变为 1),驱动会锁定代码段以防止分页;当最后一个进程关闭设备时(ReferenceCount 从 1 变为 0),驱动解锁代码段并取消任何活动的定时器。

  2. FAST_MUTEX 保护Mutex 是一个快速互斥锁,用于保护 ReferenceCount 的并发修改。与自旋锁不同,快速互斥锁在 PASSIVE_LEVEL 运行,允许线程休眠等待。这对于 IRP_MJ_CREATEIRP_MJ_CLOSE 这类不会在高 IRQL 调用的派发函数是合适的。

  3. TimerActive 原子标志TimerActive 使用 InterlockedIncrementInterlockedDecrement 进行原子操作,确保在 DPC 和派发函数之间对定时器状态的并发访问是安全的。这是一个典型的内核同步模式。

  4. SectionHandle 分页控制SectionHandle 存储 MmLockPagableDataSection 返回的句柄。这个机制确保当设备被打开时,驱动的代码和数据不会被分页到磁盘,从而避免在 DISPATCH_LEVEL(DPC 上下文)访问分页内存导致的蓝屏。

BeepCreate:首次打开的分页锁定

c 复制代码
DRIVER_DISPATCH BeepCreate;
NTSTATUS NTAPI
BeepCreate(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    PDEVICE_EXTENSION DeviceExtension = DeviceObject->DeviceExtension;

    ExAcquireFastMutex(&DeviceExtension->Mutex);
    if (++DeviceExtension->ReferenceCount == 1)
    {
        /* 首次打开,锁定可分页代码段 */
        DeviceExtension->SectionHandle = MmLockPagableDataSection(BeepCreate);
    }
    ExReleaseFastMutex(&DeviceExtension->Mutex);

    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

MmLockPagableDataSection(BeepCreate) 的作用是锁定包含 BeepCreate 函数的整个可分页代码段。这确保了当 DPC 在 DISPATCH_LEVEL 调用 BeepDPC 时,BeepDPC 的代码一定在物理内存中,不会因为页面错误而崩溃。

这个设计反映了一个重要的内核编程原则:任何可能在 DISPATCH_LEVEL 或更高 IRQL 执行的代码,必须位于非分页内存中,或者在访问前锁定分页内存

BeepClose:最后关闭的资源清理

c 复制代码
DRIVER_DISPATCH BeepClose;
NTSTATUS NTAPI
BeepClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    PDEVICE_EXTENSION DeviceExtension = DeviceObject->DeviceExtension;

    ExAcquireFastMutex(&DeviceExtension->Mutex);
    if (!(--DeviceExtension->ReferenceCount))
    {
        /* 最后一个引用,检查定时器 */
        if (DeviceExtension->TimerActive)
        {
            if (KeCancelTimer(&DeviceExtension->Timer))
            {
                InterlockedDecrement(&DeviceExtension->TimerActive);
            }
        }
        /* 解锁分页代码段 */
        MmUnlockPagableImageSection(DeviceExtension->SectionHandle);
    }
    ExReleaseFastMutex(&DeviceExtension->Mutex);

    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

BeepClose 的关键逻辑是:当 ReferenceCount 降为 0 时,驱动必须取消任何活动的定时器并解锁分页段。KeCancelTimer 返回 TRUE 表示定时器成功取消(尚未触发),返回 FALSE 表示定时器已经触发或不在队列中。只有在成功取消时才递减 TimerActive,避免与已经执行的 DPC 产生竞争。


9.2.B StartIo 机制与异步 IRP 处理

BeepDeviceControl:异步排队的决策

c 复制代码
DRIVER_DISPATCH BeepDeviceControl;
NTSTATUS NTAPI
BeepDeviceControl(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    PIO_STACK_LOCATION Stack;
    PBEEP_SET_PARAMETERS BeepParam;
    NTSTATUS Status;

    Stack = IoGetCurrentIrpStackLocation(Irp);
    BeepParam = (PBEEP_SET_PARAMETERS)Irp->AssociatedIrp.SystemBuffer;

    if (Stack->Parameters.DeviceIoControl.IoControlCode != IOCTL_BEEP_SET)
    {
        Status = STATUS_NOT_IMPLEMENTED;
    }
    else
    {
        if (Stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(BEEP_SET_PARAMETERS))
        {
            Status = STATUS_INVALID_PARAMETER;
        }
        else if ((BeepParam->Frequency != 0) && !(BeepParam->Duration))
        {
            /* 频率非零但持续时间为零,立即完成 */
            Status = STATUS_SUCCESS;
        }
        else
        {
            /* 需要排队处理 */
            Status = STATUS_PENDING;
        }
    }

    Irp->IoStatus.Status = Status;
    Irp->IoStatus.Information = 0;

    if (Status == STATUS_PENDING)
    {
        IoMarkIrpPending(Irp);
        IoStartPacket(DeviceObject, Irp, NULL, BeepCancel);
    }
    else
    {
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
    }

    return Status;
}

这段代码展示了异步 IRP 处理的标准模式:

  1. 参数验证:首先检查 IOCTL 码和缓冲区大小
  2. 快速路径 :对于可以立即完成的操作(如频率非零但持续时间为零),直接返回 STATUS_SUCCESS
  3. 慢速路径 :对于需要排队的操作,返回 STATUS_PENDING,调用 IoMarkIrpPending 标记 IRP,然后调用 IoStartPacket 将 IRP 加入设备队列

IoStartPacket 的第四个参数 BeepCancel 是取消例程。如果 IRP 在队列中等待时被取消,I/O 管理器会调用这个取消例程。

BeepStartIo:队列处理的入口

c 复制代码
DRIVER_STARTIO BeepStartIo;
VOID NTAPI
BeepStartIo(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    PDEVICE_EXTENSION DeviceExtension = DeviceObject->DeviceExtension;
    KIRQL CancelIrql;
    PIO_STACK_LOCATION IoStack;
    PBEEP_SET_PARAMETERS BeepParam;
    LARGE_INTEGER DueTime;
    NTSTATUS Status;

    /* 获取取消锁并验证 IRP */
    IoAcquireCancelSpinLock(&CancelIrql);
    if (!Irp)
    {
        IoReleaseCancelSpinLock(CancelIrql);
        return;
    }

    /* 移除取消例程 */
    (VOID)IoSetCancelRoutine(Irp, NULL);
    IoReleaseCancelSpinLock(CancelIrql);

    BeepParam = (PBEEP_SET_PARAMETERS)Irp->AssociatedIrp.SystemBuffer;
    IoStack = IoGetCurrentIrpStackLocation(Irp);

    if (IoStack->Parameters.DeviceIoControl.IoControlCode == IOCTL_BEEP_SET)
    {
        /* 取消之前的定时器 */
        if (DeviceExtension->TimerActive)
        {
            if (KeCancelTimer(&DeviceExtension->Timer))
            {
                InterlockedDecrement(&DeviceExtension->TimerActive);
            }
        }

        /* 启动新的蜂鸣 */
        if (HalMakeBeep(BeepParam->Frequency))
        {
            Status = STATUS_SUCCESS;
            DueTime.QuadPart = BeepParam->Duration * -10000LL;
            InterlockedIncrement(&DeviceExtension->TimerActive);
            KeSetTimer(&DeviceExtension->Timer, DueTime, &DeviceObject->Dpc);
        }
        else
        {
            Status = STATUS_INVALID_PARAMETER;
        }
    }
    else
    {
        Status = STATUS_INVALID_PARAMETER;
    }

    Irp->IoStatus.Status = Status;
    Irp->IoStatus.Information = 0;
    IoStartNextPacket(DeviceObject, TRUE);
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
}

BeepStartIo 是 StartIo 机制的核心。当 IoStartPacket 将 IRP 加入设备队列后,如果设备当前没有活动的 IRP,I/O 管理器会立即调用 DriverStartIo(即 BeepStartIo)。

关键步骤:

  1. 取消锁保护:获取取消自旋锁并检查 IRP 是否为 NULL。如果 IRP 为 NULL,说明 IRP 已被取消,直接返回。
  2. 移除取消例程 :调用 IoSetCancelRoutine(Irp, NULL) 移除取消例程,防止在 StartIo 执行期间 IRP 被取消。
  3. 硬件操作 :调用 HalMakeBeep 启动蜂鸣器,设置定时器在指定持续时间后停止。
  4. 启动下一个 :调用 IoStartNextPacket 从设备队列中取出下一个 IRP 并再次调用 StartIo

BeepCancel:取消例程的实现

c 复制代码
DRIVER_CANCEL BeepCancel;
VOID NTAPI
BeepCancel(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    if (Irp == DeviceObject->CurrentIrp)
    {
        /* 这是当前正在处理的 IRP */
        DeviceObject->CurrentIrp = NULL;
        IoReleaseCancelSpinLock(Irp->CancelIrql);
        IoStartNextPacket(DeviceObject, TRUE);
    }
    else
    {
        /* 这是在队列中等待的 IRP */
        KeRemoveEntryDeviceQueue(&DeviceObject->DeviceQueue,
                                 &Irp->Tail.Overlay.DeviceQueueEntry);
        IoReleaseCancelSpinLock(Irp->CancelIrql);
    }

    Irp->IoStatus.Status = STATUS_CANCELLED;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
}

取消例程在取消自旋锁(CancelIrql)持有期间被调用。它必须区分两种情况:

  1. 当前 IRP :如果 IRP 是 DeviceObject->CurrentIrp(正在 StartIo 中处理),清除 CurrentIrp 指针并启动下一个 IRP。
  2. 队列中的 IRP :如果 IRP 还在设备队列中等待,使用 KeRemoveEntryDeviceQueue 将其从队列中移除。

无论哪种情况,最后都以 STATUS_CANCELLED 完成 IRP。


9.2.C BeepDPC:定时器到时的处理

c 复制代码
VOID NTAPI
BeepDPC(IN PKDPC Dpc,
        IN PDEVICE_OBJECT DeviceObject,
        IN PVOID SystemArgument1,
        IN PVOID SystemArgument2)
{
    PDEVICE_EXTENSION DeviceExtension = DeviceObject->DeviceExtension;

    UNREFERENCED_PARAMETER(Dpc);
    UNREFERENCED_PARAMETER(SystemArgument1);
    UNREFERENCED_PARAMETER(SystemArgument2);

    /* 停止蜂鸣 */
    HalMakeBeep(0);

    /* 禁用定时器标志 */
    InterlockedDecrement(&DeviceExtension->TimerActive);
}

BeepDPC 是定时器到时时调用的 DPC 例程。它在 DISPATCH_LEVEL 执行,因此必须遵守 DPC 的所有限制:

  • 不能访问分页内存(但 HalMakeBeep 在非分页内存中,所以安全)
  • 不能使用等待原语
  • 必须快速完成

HalMakeBeep(0) 通过向端口 0x61 写入来关闭扬声器门控信号,停止发声。InterlockedDecrement 原子地递减 TimerActive,确保与 BeepClose 中的取消操作不会竞争。


9.2.D BeepCleanup:进程退出时的清理

c 复制代码
DRIVER_DISPATCH BeepCleanup;
NTSTATUS NTAPI
BeepCleanup(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    KIRQL OldIrql, CancelIrql;
    PKDEVICE_QUEUE_ENTRY Packet;
    PIRP CurrentIrp;

    KeRaiseIrql(DISPATCH_LEVEL, &OldIrql);
    IoAcquireCancelSpinLock(&CancelIrql);

    CurrentIrp = DeviceObject->CurrentIrp;
    DeviceObject->CurrentIrp = NULL;

    while (CurrentIrp)
    {
        (VOID)IoSetCancelRoutine(CurrentIrp, NULL);
        CurrentIrp->IoStatus.Status = STATUS_CANCELLED;
        CurrentIrp->IoStatus.Information = 0;

        IoReleaseCancelSpinLock(CancelIrql);
        IoCompleteRequest(CurrentIrp, IO_NO_INCREMENT);

        IoAcquireCancelSpinLock(&CancelIrql);
        Packet = KeRemoveDeviceQueue(&DeviceObject->DeviceQueue);
        if (Packet)
        {
            CurrentIrp = CONTAINING_RECORD(Packet, IRP, Tail.Overlay.DeviceQueueEntry);
        }
        else
        {
            CurrentIrp = NULL;
        }
    }

    IoReleaseCancelSpinLock(CancelIrql);
    KeLowerIrql(OldIrql);

    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    HalMakeBeep(0);
    return STATUS_SUCCESS;
}

IRP_MJ_CLEANUP 在进程的最后一个句柄关闭时发送。BeepCleanup 的任务是取消所有挂起的 IRP:

  1. 提升到 DISPATCH_LEVEL:保护设备队列的访问
  2. 获取取消锁:确保取消操作的原子性
  3. 取消当前 IRP :如果 CurrentIrp 非空,移除其取消例程并以 STATUS_CANCELLED 完成
  4. 遍历队列 :使用 KeRemoveDeviceQueue 从设备队列中取出所有等待的 IRP,逐一取消
  5. 停止蜂鸣 :调用 HalMakeBeep(0) 确保扬声器停止

这个清理过程确保了当进程退出时,不会有挂起的 IRP 遗留,也不会继续发声。


9.2.E HalMakeBeep 的硬件操作细节

ReactOS 的 HalMakeBeep 实现(hal/halx86/generic/beep.c(file:///d:/reactos/hal/halx86/generic/beep.c))展示了直接硬件操作的典型模式:

c 复制代码
BOOLEAN NTAPI HalMakeBeep(IN ULONG Frequency)
{
    SYSTEM_CONTROL_PORT_B_REGISTER SystemControl;
    TIMER_CONTROL_PORT_REGISTER TimerControl;
    ULONG Divider;
    BOOLEAN Result = FALSE;

    HalpAcquireCmosSpinLock();

    /* 关闭定时器输出和扬声器门控 */
    SystemControl.Bits = __inbyte(SYSTEM_CONTROL_PORT_B);
    SystemControl.SpeakerDataEnable = FALSE;
    SystemControl.Timer2GateToSpeaker = FALSE;
    __outbyte(SYSTEM_CONTROL_PORT_B, SystemControl.Bits);

    if (Frequency)
    {
        Divider = PIT_FREQUENCY / Frequency;

        if (Divider <= 0x10000)
        {
            /* 编程 PIT 为方波模式(模式 3) */
            TimerControl.BcdMode = FALSE;
            TimerControl.OperatingMode = PitOperatingMode3;
            TimerControl.Channel = PitChannel2;
            TimerControl.AccessMode = PitAccessModeLowHigh;
            __outbyte(TIMER_CONTROL_PORT, TimerControl.Bits);

            /* 写入通道 2 的重载值 */
            __outbyte(TIMER_CHANNEL2_DATA_PORT, Divider & 0xFF);
            __outbyte(TIMER_CHANNEL2_DATA_PORT, (Divider >> 8) & 0xFF);

            /* 重新连接扬声器 */
            SystemControl.Bits = __inbyte(SYSTEM_CONTROL_PORT_B);
            SystemControl.SpeakerDataEnable = TRUE;
            SystemControl.Timer2GateToSpeaker = TRUE;
            __outbyte(SYSTEM_CONTROL_PORT_B, SystemControl.Bits);
            Result = TRUE;
        }
    }

    HalpReleaseCmosSpinLock();
    return Result;
}

8254 PIT 编程细节

PC 扬声器使用 8254 可编程间隔定时器(PIT)的通道 2:

  • PIT_FREQUENCY:1.193182 MHz(1193182 Hz),这是 PIT 的输入时钟频率
  • 通道 2:专门用于 PC 扬声器(通道 0 用于系统时钟 IRQ0,通道 1 用于 DRAM 刷新)
  • 模式 3(方波发生器):产生方波输出,适合驱动扬声器
  • 分频值Divider = PIT_FREQUENCY / Frequency,例如 1000 Hz 需要分频值 1193

端口操作

  • 0x43(定时器控制端口):写入控制字配置 PIT 的工作模式
  • 0x42(通道 2 数据端口):写入分频值(先低字节,后高字节)
  • 0x61(系统控制端口 B)
    • 位 0:定时器 2 门控到扬声器
    • 位 1:扬声器数据使能

CMOS 自旋锁

HalpAcquireCmosSpinLock 获取 CMOS 访问锁。这是因为端口 0x61 与 CMOS RAM 共享,需要防止并发访问导致的数据损坏。


9.2.F 与 Windows 实现的对比

分页锁定机制

Windows 的 beep.sys 实现与 ReactOS 非常相似,但在细节上有所不同:

  • Windows XP/2003 :使用 MmLockPagableDataSection 锁定代码段,与 ReactOS 一致
  • Windows Vista+:引入了 Driver Verifier 集成,可以在运行时检查驱动的正确性
  • Windows 10:beep.sys 仍然存在,但默认被禁用(需要手动启用)

StartIo 队列

Windows 和 ReactOS 都使用 IoStartPacket / IoStartNextPacket 机制管理设备的 IRP 队列。这个机制是旧式驱动(非 WDM)的标准模式。在 WDM 驱动中,通常使用驱动自定义的队列或 WDF 框架的队列。

IOCTL 码定义

IOCTL_BEEP_SET 的定义在 Windows 和 ReactOS 中完全一致:

c 复制代码
#define IOCTL_BEEP_SET CTL_CODE(FILE_DEVICE_BEEP, 0, METHOD_BUFFERED, FILE_ANY_ACCESS)

这确保了 Windows 应用程序可以使用相同的 IOCTL 码与 ReactOS 的 beep.sys 交互。


9.2.G 调试技巧与常见问题

使用 WinDbg 调试 beep 驱动

  • !devobj \Device\Beep:查看 beep 设备对象的状态
  • !devstack \Device\Beep:查看设备栈(beep 是单设备,栈很简单)
  • !pool <设备扩展地址>:查看设备扩展的内存分配
  • !timer:列出所有活动的定时器,可以看到 beep 的定时器

常见问题

  1. 无声输出 :如果 HalMakeBeep 返回 FALSE,可能是频率超出范围(< 20 Hz 或 > 20000 Hz)或分频值超过 0x10000。

  2. 持续发声不停止 :如果定时器设置失败或 DPC 未执行,扬声器会持续发声。检查 TimerActive 标志和定时器状态。

  3. 蓝屏 DISPATCH_LEVEL 违规:如果 DPC 例程访问了分页内存(例如未锁定的代码段),会触发蓝屏。确保在设备打开时正确锁定分页段。

  4. IRP 泄漏 :如果 BeepDeviceControl 返回 STATUS_PENDING 但未调用 IoMarkIrpPending,I/O 管理器会报错。必须确保异步返回前标记 IRP。

  5. 取消竞争 :如果取消例程和 StartIo 同时访问 IRP,可能导致数据损坏。使用取消自旋锁保护所有取消相关操作。


总结

beep.c 展示了一个 最小可工作 的老式驱动,其要点:

  1. 不实现 PnP :纯老式风格,DriverEntry 中创建设备
  2. 无符号链接\Device\Beep 名称(Win32 子系统通过 \\\\.\\Beep 访问)
  3. 同步完成:所有派发函数同步完成 IRP,无异步
  4. 单例设备 :用全局 BEEP_DEVICE_EXTENSION 状态
  5. IOCTL 控制IOCTL_BEEP_SET 启动/停止
  6. HAL 操作硬件HalMakeBeep / HalStopBeep 操作 8254 PIT
  7. DPC + TimerKeSetTimer 持续时间到时触发 BeepStopDpcRoutine
  8. CLEANUP 取消 IRP:进程退出时清理

虽然 beep 简单,但包含 NT 驱动的所有核心概念:派发表、设备对象、设备扩展、IRP、IOCTL、同步 I/O、IRP 完成。理解 beep 是学习更复杂驱动(PnP、电源、磁盘)的基础。


本章代码索引

文件 内容
beep.c(file:///d:/reactos/drivers/base/beep/beep.c) 完整 beep 驱动(DriverEntry + 派发 + HAL 调用)
beep.h(file:///d:/reactos/drivers/base/beep/beep.h) beep 驱动头文件
beep.rc(file:///d:/reactos/drivers/base/beep/beep.rc) 版本信息资源
beep_reg.inf(file:///d:/reactos/drivers/base/beep/beep_reg.inf) 设备注册 INF
hal/beep.c(file:///d:/reactos/hal/halx86/generic/beep.c) HalMakeBeepHalStopBeep
iotypes.h(file:///d:/reactos/sdk/include/xdk/iotypes.h) IRP_MJ_* 主功能码
dll/win32/kernel32/client/console.c(file:///d:/reactos/dll/win32/kernel32/client/console.c) Beep() Win32 API
ntdll/include/iotypes.h(file:///d:/reactos/dll/ntdll/include/iotypes.h) IOCTL_BEEP_SET 定义
iotypes.h CTL_CODE(file:///d:/reactos/sdk/include/xdk/iotypes.h) CTL_CODE
相关推荐
caimouse1 小时前
Reactos 第 9 章 设备驱动 — 9.4 内核劳务线程
开发语言·windows
星栈独行1 小时前
Rust + Makepad 应用怎么打包发布:Windows、macOS、Linux 全平台交付
windows·程序人生·macos·ui·rust
辣香牛肉面2 小时前
Windows PDF转换工具箱
windows·pdf
daly5202 小时前
PyCharm怎么下载?2026最新版PyCharm安装教程(Windows/macOS/Linux)
windows·macos·pycharm
西凉的悲伤2 小时前
redis-windows 安装 redis 到 windows 电脑
java·windows·redis·redis-windows
caimouse2 小时前
Reactos 第 8 章 结构化异常处理 — 8.4 软异常
服务器·开发语言·windows
chushiyunen2 小时前
codex笔记、thinkai中转站
windows
z落落13 小时前
C#WinForm 窗体切换与窗体传值(登录跳转案例)+WinForm 窗体传值(从上往下传、从下往上传)
开发语言·windows·c#
Dontla13 小时前
git bash打开Claude code报错:Claude Code on Windows requires git-bash.(别把git装其他位置,严格按照默认安装)找不到claude code
windows·git·bash