第 9 章 设备驱动 — 9.3 DPC函数及其执行

本节深入剖析 DPC(Deferred Procedure Call,延迟过程调用)机制。 DPC 是 NT 内核用于在 低 IRQL(DISPATCH_LEVEL = 2) 上执行中断后处理的关键机制。理解 DPC 的关键是把握它与 中断服务例程(ISR) 的协作:ISR 在 DIRQL 上只做最少量工作(确认中断、排队 DPC),DPC 在 DISPATCH_LEVEL 上做实际的数据处理。ReactOS 在 [ntoskrnl/ke/dpc.c](file:///d:/reactos/ntoskrnl/ke/dpc.c) 和 [ntoskrnl/ke/dpcobj.c](file:///d:/reactos/ntoskrnl/ke/dpcobj.c) 中实现了完整的 DPC 子系统。


概述

DPC 是 NT 内核 中断处理的两阶段模型 的第二阶段:

  • 阶段 1:ISR(DIRQL):最快速确认中断、访问硬件寄存器、把 DPC 排队
  • 阶段 2:DPC(DISPATCH_LEVEL):处理实际数据、更新设备状态、调用 IoStartNextPacket 等

DPC 队列在每个 CPU 上有一个 全局 DPC 队列KiDpcListHead),DPC 分发器在所有中断处理完成后、CPU IRQL 降到 DISPATCH_LEVEL 以下之前被调用。

本节内容概览

  • 9.3.0 框架图
  • 9.3.1 DPC 的核心设计目标
  • 9.3.2 KDPC 数据结构
  • 9.3.3 KeInitializeDpc / KeInsertQueueDpc
  • 9.3.4 KiDispatchInterruptKiDpcInterrupt 触发链
  • 9.3.5 IoRequestDpc 驱动级 API
  • 9.3.6 DPC 与 ISR 的协作
  • 9.3.7 DPC 相关的同步与注意事项
  • 9.3.8 总结与代码索引

学习目标

  • 能够描述 ISR-DPC 两阶段模型的优势
  • 理解 KDPC 结构的字段含义
  • 掌握 KeInsertQueueDpc / KeRemoveQueueDpc / IoRequestDpc 的使用
  • 理解 DPC 队列在每个 CPU 上的位置

涉及的内核子系统

子系统 头文件/源文件 核心作用
DPC 核心 [ntoskrnl/ke/dpcobj.c](file:///d:/reactos/ntoskrnl/ke/dpcobj.c) KeInitializeDpcKeInsertQueueDpcKeRemoveQueueDpc
DPC 分发 [ntoskrnl/ke/dpc.c](file:///d:/reactos/ntoskrnl/ke/dpc.c) KiDpcInterruptKiDpcInterruptHandler
I/O DPC 接口 [ntoskrnl/io/iomgr/dpc.c](file:///d:/reactos/ntoskrnl/io/iomgr/dpc.c) IoRequestDpc
KDPC 定义 [sdk/include/xdk/ketypes.h](file:///d:/reactos/sdk/include/xdk/ketypes.h) KDPC 结构
同步 [ntoskrnl/ke/spinlock.c](file:///d:/reactos/ntoskrnl/ke/spinlock.c) 自旋锁
定时器 [ntoskrnl/ke/timerobj.c](file:///d:/reactos/ntoskrnl/ke/timerobj.c) 与 DPC 关联的定时器

9.3.0 框架图

  +-------------------+
  | 中断源(设备)     |
  +-------------------+
         |
         v
  +-------------------+  IRQL = DIRQL(高)
  | ISR(中断服务例程)|  1. 确认中断
  |                   |  2. 读取硬件状态
  |                   |  3. KeInsertQueueDpc
  |                   |  4. 退出
  +-------------------+
         |
         v  (IRQL 降低到 DISPATCH_LEVEL)
  +-------------------+  IRQL = DISPATCH_LEVEL
  | KiDpcInterrupt     |  遍历 KiDpcListHead
  | (DPC 分发器)       |  调用每个 DPC
  +-------------------+
         |
         v
  +-------------------+
  | DPC Routine        |  1. 实际数据处理
  | (KeDpcRoutine)     |  2. IoStartNextPacket
  |                   |  3. 唤醒等待线程
  +-------------------+
         |
         v  (IRQL 降低到 APC_LEVEL)
  +-------------------+  IRQL = APC_LEVEL / PASSIVE_LEVEL
  | 用户请求继续       |
  | 线程 / 系统服务   |
  +-------------------+


  队列位置(每 CPU):
  +-------------------+
  | PCR/KPCR          |
  |  - DpcQueueDepth  |
  |  - DpcRequestFlag |
  |  - DpcRoutine     |
  |  - DpcStack       |
  +-------------------+

9.3.1 DPC 的核心设计目标

DPC 机制的设计目标是解决 中断处理的两难问题

  1. ISR 在高 IRQL(DIRQL):不能访问分页内存、不能等待锁、不能调用系统服务
  2. 真实工作需要低 IRQL(DISPATCH_LEVEL 或更低)

DPC 通过以下方式解决:

  • ISR 立即返回(尽快完成)
  • 把实际工作放到 DPC(DISPATCH_LEVEL)
  • DPC 队列在 ISR 与 DPC 之间作为 缓冲
  • DPC 在所有中断处理完毕后、IRQL 降到 DISPATCH_LEVEL 之前被调度

DPC vs APC

维度 DPC APC
IRQL DISPATCH_LEVEL (2) APC_LEVEL (1) 或 PASSIVE_LEVEL (0)
线程上下文 无(系统上下文) 特定线程上下文
优先级 高(紧接 ISR) 中(线程调度时)
用途 中断后处理、StartIO 线程异步通知、IRP 完成
队列位置 每 CPU 一个 线程 APC 队列
API KeInsertQueueDpc KeInsertQueueApc

9.3.2 KDPC 数据结构

定义于 [ketypes.h](file:///d:/reactos/sdk/include/xdk/ketypes.h):

typedef struct _KDPC {
    CSHORT Type;                  // DPC 对象类型标志
    UCHAR Number;                 // DPC 编号(用于跟踪)
    UCHAR Importance;             // 重要性(用于调度)
    LIST_ENTRY DpcListEntry;      // 队列链表节点
    PKDEFERRED_ROUTINE DeferredRoutine;  // 回调函数
    PVOID DeferredContext;        // 回调上下文
    PVOID SystemArgument1;        // 系统参数 1
    PVOID SystemArgument2;        // 系统参数 2
    PULONG_PTR Lock;              // 自旋锁
} KDPC, *PKDPC;

字段详解

字段 含义
Type 对象类型(DpcObject/ThreadedDpcObject
Number 用于在 !dpc 调试中显示编号
Importance LowImportance/MediumImportance/HighImportance
DpcListEntry 链表节点(在 KiDpcListHeadDpcData[DpcQueue] 中)
DeferredRoutine 回调函数原型 VOID (*)(PKDPC, PVOID, PVOID, PVOID)
DeferredContext 驱动传入的上下文(通常是 DeviceExtension
SystemArgument1/2 驱动可放入的额外参数
Lock 用于并发保护的优化字段

KDPC 类型

#define DPC_NORMAL       0    // 普通 DPC
#define DPC_THREADED     1    // 线程化 DPC(在 PASSIVE_LEVEL 运行)

DPC_THREADED 用于需要分页内存或长时间等待的场景(Windows XP+)。


9.3.3 KeInitializeDpc / KeInsertQueueDpc

KeInitializeDpc

VOID KeInitializeDpc(
    IN PRKDPC Dpc,
    IN PKDEFERRED_ROUTINE DeferredRoutine,
    IN PVOID DeferredContext);

初始化一个 DPC 对象,关联回调函数和上下文。关键点:DPC 不会立即运行,必须调用 KeInsertQueueDpc 才能进入队列。

VOID NTAPI
KeInitializeDpc(IN PRKDPC Dpc, IN PKDEFERRED_ROUTINE DeferredRoutine, IN PVOID DeferredContext)
{
    Dpc->DeferredRoutine = DeferredRoutine;
    Dpc->DeferredContext = DeferredContext;
    Dpc->Type = DPC_NORMAL;
    Dpc->Inserted = FALSE;
    KeInitializeSpinLock(&Dpc->DpcLock);
    /* 初始化链表节点 */
    InitializeListHead(&Dpc->DpcListEntry);
}

KeInsertQueueDpc

BOOLEAN KeInsertQueueDpc(
    IN PRKDPC Dpc,
    IN PVOID SystemArgument1,
    IN PVOID SystemArgument2);

把 DPC 加入 当前 CPU 的 DPC 队列。返回 TRUE 表示 DPC 之前未在队列中(首次入队),FALSE 表示 DPC 已经在队列中。

BOOLEAN NTAPI
KeInsertQueueDpc(IN PRKDPC Dpc, IN PVOID SystemArgument1, IN PVOID SystemArgument2)
{
    KIRQL OldIrql;
    BOOLEAN Inserted = FALSE;

    /* 提升到 DISPATCH_LEVEL(必须在 >= DISPATCH_LEVEL 调用) */
    KeRaiseIrql(DISPATCH_LEVEL, &OldIrql);

    /* 加锁 */
    KiAcquireDispatcherLock();

    if (!Dpc->Inserted)
    {
        Dpc->Inserted = TRUE;
        Inserted = TRUE;
        Dpc->SystemArgument1 = SystemArgument1;
        Dpc->SystemArgument2 = SystemArgument2;

        /* 加入 CPU DPC 队列 */
        InsertTailList(&KeGetCurrentPrcb()->DpcListHead, &Dpc->DpcListEntry);
        KeGetCurrentPrcb()->DpcQueueDepth++;
        KeGetCurrentPrcb()->DpcRequest |= 0x1;
    }

    KiReleaseDispatcherLock();
    KeLowerIrql(OldIrql);

    return Inserted;
}

KeRemoveQueueDpc

BOOLEAN KeRemoveQueueDpc(IN PRKDPC Dpc);

从队列中移除 DPC。如果 DPC 已经在执行,则无法移除(返回 FALSE)。


9.3.4 KiDispatchInterruptKiDpcInterrupt 触发链

DPC 分发由 KiDpcInterrupt(汇编入口)或 KiDpcInterruptHandler(C 实现)触发。

触发条件

DPC 分发器在以下时刻被调用:

  1. 中断返回前:当 IRQL 从 DISPATCH_LEVEL 降到更低时
  2. 空闲循环:在 KiIdleLoop 中(每空闲一段时间)
  3. 线程调度:在线程上下文切换时

KiDpcInterrupt 入口

定义于 [ntoskrnl/ke/i386/trap.s](file:///d:/reactos/ntoskrnl/ke/i386/trap.s):

_KiDpcInterruptHandler:
    /* 在栈上保存中断帧 */
    push ebp
    mov ebp, esp
    /* ... */
    call _KiDpcInterruptHandler@0
    /* 退出中断 */

KiDpcInterruptHandler(C 实现)

定义于 [ntoskrnl/ke/dpc.c](file:///d:/reactos/ntoskrnl/ke/dpc.c):

VOID NTAPI
KiDpcInterruptHandler(VOID)
{
    PKDPC Dpc;
    PLIST_ENTRY ListHead, ListEntry;
    PKPRCB Prcb;
    KIRQL OldIrql;
    PVOID SystemArgument1, SystemArgument2;
    PKDEFERRED_ROUTINE DeferredRoutine;
    PVOID DeferredContext;
    BOOLEAN Safe = TRUE;
    BOOLEAN SingleDpc;
    PVOID DpcData;

    /* 禁用软中断:避免 Dpc 队列竞争 */
    _disable();

    Prcb = KeGetCurrentPrcb();
    KiAcquireDispatcherLock();

    /* 设置 DPC 运行标志 */
    Prcb->DpcRoutineActive = TRUE;
    Prcb->DpcCount += Prcb->DpcQueueDepth;
    Prcb->DpcQueueDepth = 0;

    while (Prcb->DpcListEntry.Flink != &Prcb->DpcListHead)
    {
        /* 取出队头 DPC */
        ListEntry = RemoveHeadList(&Prcb->DpcListHead);
        Dpc = CONTAINING_RECORD(ListEntry, KDPC, DpcListEntry);
        Dpc->Inserted = FALSE;

        /* 调用 DPC */
        DeferredRoutine = Dpc->DeferredRoutine;
        DeferredContext = Dpc->DeferredContext;
        SystemArgument1 = Dpc->SystemArgument1;
        SystemArgument2 = Dpc->SystemArgument2;

        KiReleaseDispatcherLock();
        DeferredRoutine(Dpc, DeferredContext, SystemArgument1, SystemArgument2);
        KiAcquireDispatcherLock();
    }

    Prcb->DpcRoutineActive = FALSE;
    KiReleaseDispatcherLock();
    _enable();
}

关键设计

  1. 每次取一个 DPC:先释放锁再调用 DPC(避免长时间持锁)
  2. 再入队检查:调用 DPC 期间,DPC 可以再次入队
  3. 优先级:高 Importance 的 DPC 先运行
  4. 线程化 DPC:在 Windows XP+ 引入,运行在 PASSIVE_LEVEL 的系统线程

9.3.5 IoRequestDpc 驱动级 API

驱动级封装 [IoRequestDpc](file:///d:/reactos/ntoskrnl/io/iomgr/dpc.c):

VOID NTAPI
IoRequestDpc(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context);

它把 DPC 与设备对象、IRP 关联,常用于 启动下一次 I/O 操作

VOID NTAPI
IoRequestDpc(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context)
{
    PKINTERRUPT InterruptObject;
    PDEVICE_EXTENSION Ext = DeviceObject->DeviceExtension;

    /* 取 DPC 对象 */
    IoMarkIrpPending(Irp);
    Ext->Dpc.DeviceObject = DeviceObject;
    Ext->Dpc.Irp = Irp;

    /* 排队 DPC */
    KeInsertQueueDpc(&Ext->Dpc, DeviceObject, Irp);
}

IoRequestDpc 对应的 DPC 回调通常是 IopDpcRoutine 或自定义 DPC:

VOID NTAPI
MyDpcRoutine(IN PKDPC Dpc, IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context)
{
    /* 调用 StartIo */
    IoStartNextPacket(DeviceObject, FALSE);
    /* 处理 IRP */
    ...
    /* 完成后启动下一个 IRP */
    IoStartPacket(DeviceObject, Irp, NULL, NULL);
}

9.3.6 DPC 与 ISR 的协作

典型的 ISR-DPC 协作:

/* ISR (DIRQL) */
BOOLEAN NTAPI
MyInterruptServiceRoutine(IN PKINTERRUPT Interrupt, IN PVOID ServiceContext)
{
    PDEVICE_EXTENSION Ext = (PDEVICE_EXTENSION)((PDEVICE_OBJECT)ServiceContext)->DeviceExtension;
    UCHAR Status;

    /* 1. 读硬件状态 */
    Status = READ_PORT_UCHAR(Ext->PortAddress);

    /* 2. 确认中断(写入 EOI) */
    WRITE_PORT_UCHAR(Ext->EoiAddress, 0);

    /* 3. 排队 DPC(最小工作) */
    KeInsertQueueDpc(&Ext->Dpc, NULL, NULL);

    return TRUE;
}

/* DPC (DISPATCH_LEVEL) */
VOID NTAPI
MyDpcRoutine(IN PKDPC Dpc, IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context)
{
    PDEVICE_EXTENSION Ext = DeviceObject->DeviceExtension;

    /* 1. 从硬件读数据 */
    /* 2. 拷贝到 IRP 缓冲区 */
    /* 3. 完成 IRP */
    /* 4. 启动下一个 IRP */
    IoStartNextPacket(DeviceObject, FALSE);
    IoCompleteRequest(Irp, IO_DISK_INCREMENT);
}

关键规则

  1. ISR 极简:只做确认中断、读状态、排 DPC
  2. DPC 处理实际数据:可以访问分页内存、可以获取自旋锁、可以调用 IoStartNextPacket
  3. DPC 不能调用 KeWaitFor 等待*:会死锁
  4. DPC 不能使用 PagedPool 内存:必须 NonPagedPool

9.3.7 DPC 相关的同步与注意事项

同步原语

DPC 上下文中可用的同步原语:

  • KeAcquireSpinLock / KeReleaseSpinLock:自旋锁(推荐)
  • KeSynchronizeExecution:与 ISR 同步
  • Interlocked* 原子操作

DPC 上下文中 不可用 的原语:

  • KeWaitForSingleObject / KeWaitForMutexObject(会死锁)
  • ExAcquireFastMutex(在 DISPATCH_LEVEL 不安全)
  • 分页内存访问(必须 NonPagedPool)
  • 任何可能换页的操作

线程化 DPC

Windows XP+ 引入 KeInitializeThreadedDpc

VOID KeInitializeThreadedDpc(
    IN PRKDPC Dpc,
    IN PKDEFERRED_ROUTINE DeferredRoutine,
    IN PVOID DeferredContext);

线程化 DPC 在 系统线程 上以 PASSIVE_LEVEL 运行,可以:

  • 访问分页内存
  • 调用等待原语
  • 长时间运行

但代价是更高的延迟(需要等待系统线程调度)。

DPC Watchdog

ReactOS 在调试版本中提供 DPC watchdog:检测 DPC 运行时间过长,触发 KeBugCheckEx 蓝屏。


9.3.A ReactOS DPC 实现的深度剖析

DPC 队列的数据结构

在 ReactOS 源代码中,DPC 队列的组织方式比文档中简化的描述更为复杂。每个处理器的 DPC 队列实际上由多个组件构成:

// 在 KPRCB(处理器控制块)中的 DPC 相关字段
typedef struct _KPRCB {
    // ... 其他字段 ...
    LIST_ENTRY DpcListHead;           // DPC 队列链表头
    ULONG DpcQueueDepth;              // 队列中 DPC 的数量
    ULONG DpcCount;                   // 已处理的 DPC 总数
    BOOLEAN DpcRoutineActive;         // DPC 例程是否正在执行
    BOOLEAN DpcRequest;               // 是否有 DPC 请求待处理
    PVOID DpcStack;                   // DPC 栈(用于线程化 DPC)
    ULONG MaximumDpcQueueDepth;       // 队列深度上限
    ULONG DpcRequestRate;             // DPC 请求速率
    ULONG MinimumDpcRate;             // 最小 DPC 速率
    ULONG AdjustDpcThreshold;         // 调整阈值
    // ... 其他字段 ...
} KPRCB, *PKPRCB;

这些字段共同管理 DPC 的生命周期和调度策略。DpcQueueDepth 跟踪当前队列中的 DPC 数量,而 DpcCount 是累积统计值,用于性能监控。

KiDpcInterruptHandler 的完整实现

ReactOS 的 KiDpcInterruptHandler([ntoskrnl/ke/dpc.c](file:///d:/reactos/ntoskrnl/ke/dpc.c))展示了 DPC 分发的核心逻辑:

VOID NTAPI
KiDpcInterruptHandler(VOID)
{
    PKDPC Dpc;
    PLIST_ENTRY ListHead, ListEntry;
    PKPRCB Prcb;
    KIRQL OldIrql;
    PVOID SystemArgument1, SystemArgument2;
    PKDEFERRED_ROUTINE DeferredRoutine;
    PVOID DeferredContext;
    BOOLEAN Safe = TRUE;
    BOOLEAN SingleDpc;
    PVOID DpcData;

    /* 禁用中断:避免 DPC 队列竞争 */
    _disable();

    Prcb = KeGetCurrentPrcb();
    KiAcquireDispatcherLock();

    /* 设置 DPC 运行标志 */
    Prcb->DpcRoutineActive = TRUE;
    Prcb->DpcCount += Prcb->DpcQueueDepth;
    Prcb->DpcQueueDepth = 0;

    /* 遍历 DPC 队列 */
    while (Prcb->DpcListEntry.Flink != &Prcb->DpcListHead)
    {
        /* 取出队头 DPC */
        ListEntry = RemoveHeadList(&Prcb->DpcListHead);
        Dpc = CONTAINING_RECORD(ListEntry, KDPC, DpcListEntry);
        Dpc->Inserted = FALSE;

        /* 提取 DPC 参数 */
        DeferredRoutine = Dpc->DeferredRoutine;
        DeferredContext = Dpc->DeferredContext;
        SystemArgument1 = Dpc->SystemArgument1;
        SystemArgument2 = Dpc->SystemArgument2;

        /* 释放锁后调用 DPC */
        KiReleaseDispatcherLock();
        DeferredRoutine(Dpc, DeferredContext, SystemArgument1, SystemArgument2);
        KiAcquireDispatcherLock();
    }

    Prcb->DpcRoutineActive = FALSE;
    KiReleaseDispatcherLock();
    _enable();
}

这段代码揭示了几个关键设计:

  1. 中断禁用:在进入 DPC 分发前调用 _disable() 禁用硬件中断,确保 DPC 队列的原子性。这是必要的,因为 ISR 可能在任何时刻尝试插入新的 DPC。

  2. 锁的粒度:每次调用 DPC 例程前都释放调度器锁(KiReleaseDispatcherLock),调用完成后再重新获取。这种设计避免了长时间持有锁导致其他 CPU 的 DPC 操作被阻塞。

  3. DPC 计数统计Prcb->DpcCount += Prcb->DpcQueueDepth 将当前队列深度累加到总计数中,然后清零队列深度。这些统计数据可用于性能分析和调试。

  4. 单步执行:每次从队列中取出一个 DPC,释放锁,执行,再获取锁。这确保了 DPC 执行期间其他 CPU 可以继续插入新的 DPC。

DPC 的重要性(Importance)机制

ReactOS 支持 DPC 的重要性级别,影响 DPC 在队列中的位置:

typedef enum _KDPC_IMPORTANCE {
    LowImportance = 0,      // 低重要性(插入队尾)
    MediumImportance = 1,   // 中等重要性(插入队尾)
    HighImportance = 2      // 高重要性(插入队头)
} KDPC_IMPORTANCE;

KeInsertQueueDpc 的变体 KeInsertHeadQueueDpc 允许将高重要性的 DPC 插入队头,使其优先执行。这在处理时间敏感的中断后处理时非常有用。

KeInsertQueueDpc 的完整实现

BOOLEAN NTAPI
KeInsertQueueDpc(IN PRKDPC Dpc,
                 IN PVOID SystemArgument1,
                 IN PVOID SystemArgument2)
{
    KIRQL OldIrql;
    BOOLEAN Inserted = FALSE;
    PKPRCB Prcb;

    /* 必须在 DISPATCH_LEVEL 或更高调用 */
    ASSERT(KeGetCurrentIrql() >= DISPATCH_LEVEL);

    Prcb = KeGetCurrentPrcb();

    /* 获取调度器锁 */
    KiAcquireDispatcherLock();

    /* 检查 DPC 是否已在队列中 */
    if (!Dpc->Inserted)
    {
        Dpc->Inserted = TRUE;
        Inserted = TRUE;
        Dpc->SystemArgument1 = SystemArgument1;
        Dpc->SystemArgument2 = SystemArgument2;

        /* 插入队尾 */
        InsertTailList(&Prcb->DpcListHead, &Dpc->DpcListEntry);
        Prcb->DpcQueueDepth++;
        
        /* 设置 DPC 请求标志 */
        Prcb->DpcRequest = TRUE;
    }

    KiReleaseDispatcherLock();

    /* 如果 DPC 请求标志刚被设置,触发软件中断 */
    if (Inserted && !Prcb->DpcRoutineActive)
    {
        /* 在 x86 上触发 DPC 软件中断 */
        // HalRequestSoftwareInterrupt(DISPATCH_LEVEL);
    }

    return Inserted;
}

关键观察:

  1. 重复插入检查if (!Dpc->Inserted) 确保同一个 DPC 对象不会被重复插入队列。如果 DPC 已经在队列中,KeInsertQueueDpc 返回 FALSE,但会更新 SystemArgument1/2

  2. 软件中断触发:当 DPC 首次插入队列时,需要触发软件中断(在 x86 上是 IRQL = DISPATCH_LEVEL 的软件中断),确保 DPC 分发器被调度执行。

  3. IRQL 要求KeInsertQueueDpc 必须在 DISPATCH_LEVEL 或更高 IRQL 调用,通常是从 ISR 或 DPC 例程中调用。


9.3.B 定时器与 DPC 的协作机制

定时器到期处理

ReactOS 的定时器系统与 DPC 紧密集成。当定时器到期时,关联的 DPC 会被插入队列。这个过程由 KiTimerExpiration([ntoskrnl/ke/dpc.c](file:///d:/reactos/ntoskrnl/ke/dpc.c))处理:

VOID NTAPI
KiTimerExpiration(IN PKDPC Dpc,
                  IN PVOID DeferredContext,
                  IN PVOID SystemArgument1,
                  IN PVOID SystemArgument2)
{
    ULARGE_INTEGER SystemTime, InterruptTime;
    LARGE_INTEGER Interval;
    LONG Limit, Index, i;
    ULONG Timers, ActiveTimers, DpcCalls;
    PLIST_ENTRY ListHead, NextEntry;
    KIRQL OldIrql;
    PKTIMER Timer;
    PKDPC TimerDpc;
    ULONG Period;
    DPC_QUEUE_ENTRY DpcEntry[MAX_TIMER_DPCS];
    PKPRCB Prcb = KeGetCurrentPrcb();

    /* 禁用中断 */
    _disable();

    /* 查询系统时间和中断时间 */
    KeQuerySystemTime((PLARGE_INTEGER)&SystemTime);
    InterruptTime.QuadPart = KeQueryInterruptTime();
    Limit = KeTickCount.LowPart;

    /* 重新启用中断 */
    _enable();

    /* 获取定时器索引 */
    Index = PtrToLong(SystemArgument1);
    if ((Limit - Index) >= TIMER_TABLE_SIZE)
    {
        Limit = Index + TIMER_TABLE_SIZE - 1;
    }

    Index--;
    Limit &= (TIMER_TABLE_SIZE - 1);

    /* 初始化统计 */
    DpcCalls = 0;
    Timers = 24;
    ActiveTimers = 4;

    /* 获取调度器锁 */
    OldIrql = KiAcquireDispatcherLock();

    /* 遍历定时器表 */
    do
    {
        Index = (Index + 1) & (TIMER_TABLE_SIZE - 1);
        ListHead = &KiTimerTableListHead[Index].Entry;

        while (ListHead != ListHead->Flink)
        {
            /* 获取定时器锁 */
            LockQueue = KiAcquireTimerLock(Index);
            NextEntry = ListHead->Flink;

            Timer = CONTAINING_RECORD(NextEntry, KTIMER, TimerListEntry);
            Timers--;

            /* 检查定时器是否到期 */
            if ((NextEntry != ListHead) &&
                (Timer->DueTime.QuadPart <= InterruptTime.QuadPart))
            {
                ActiveTimers--;
                KiRemoveEntryTimer(Timer);

                Timer->Header.Inserted = FALSE;
                KiReleaseTimerLock(LockQueue);
                Timer->Header.SignalState = 1;

                TimerDpc = Timer->Dpc;
                Period = Timer->Period;

                /* 唤醒等待线程 */
                if (!IsListEmpty(&Timer->Header.WaitListHead))
                {
                    if (Timer->Header.Type == TimerNotificationObject)
                    {
                        KxUnwaitThread(&Timer->Header, IO_NO_INCREMENT);
                    }
                    else
                    {
                        KxUnwaitThreadForEvent((PKEVENT)Timer, IO_NO_INCREMENT);
                    }
                }

                /* 如果是周期性定时器,重新插入 */
                if (Period)
                {
                    Interval.QuadPart = Int32x32To64(Period, -10000);
                    while (!KiInsertTreeTimer(Timer, Interval));
                }

                /* 如果有 DPC,排队执行 */
                if (TimerDpc)
                {
                    /* 多处理器支持 */
                    if (((TimerDpc->Number >= MAXIMUM_PROCESSORS) &&
                        ((TimerDpc->Number - MAXIMUM_PROCESSORS) != Prcb->Number)) ||
                        ((TimerDpc->Type == ThreadedDpcObject) && (Prcb->ThreadDpcEnable)))
                    {
                        KeInsertQueueDpc(TimerDpc,
                                         UlongToPtr(SystemTime.LowPart),
                                         UlongToPtr(SystemTime.HighPart));
                    }
                    else
                    {
                        /* 本地执行 DPC */
                        DpcEntry[DpcCalls].Dpc = TimerDpc;
                        DpcEntry[DpcCalls].Routine = TimerDpc->DeferredRoutine;
                        DpcEntry[DpcCalls].Context = TimerDpc->DeferredContext;
                        DpcCalls++;
                        ASSERT(DpcCalls < MAX_TIMER_DPCS);
                    }
                }

                /* 批量执行 DPC */
                if (!(ActiveTimers) || !(Timers))
                {
                    KiReleaseDispatcherLock(DISPATCH_LEVEL);

                    for (i = 0; DpcCalls; DpcCalls--, i++)
                    {
                        Prcb->DebugDpcTime = 0;
                        DpcEntry[i].Routine(DpcEntry[i].Dpc,
                                            DpcEntry[i].Context,
                                            UlongToPtr(SystemTime.LowPart),
                                            UlongToPtr(SystemTime.HighPart));
                    }

                    Timers = 24;
                    ActiveTimers = 4;
                    KiAcquireDispatcherLock();
                }
            }
            else
            {
                KiReleaseTimerLock(LockQueue);

                if (!Timers)
                {
                    KiReleaseDispatcherLock(DISPATCH_LEVEL);

                    for (i = 0; DpcCalls; DpcCalls--, i++)
                    {
                        Prcb->DebugDpcTime = 0;
                        DpcEntry[i].Routine(DpcEntry[i].Dpc,
                                            DpcEntry[i].Context,
                                            UlongToPtr(SystemTime.LowPart),
                                            UlongToPtr(SystemTime.HighPart));
                    }

                    Timers = 24;
                    ActiveTimers = 4;
                    KiAcquireDispatcherLock();
                }

                break;
            }
        }
    } while (Index != Limit);

    /* 执行剩余的 DPC */
    if (DpcCalls)
    {
        KiReleaseDispatcherLock(DISPATCH_LEVEL);

        for (i = 0; DpcCalls; DpcCalls--, i++)
        {
            Prcb->DebugDpcTime = 0;
            DpcEntry[i].Routine(DpcEntry[i].Dpc,
                                DpcEntry[i].Context,
                                UlongToPtr(SystemTime.LowPart),
                                UlongToPtr(SystemTime.HighPart));
        }

        if (OldIrql != DISPATCH_LEVEL) KeLowerIrql(OldIrql);
    }
    else
    {
        KiReleaseDispatcherLock(OldIrql);
    }
}

这个复杂的函数揭示了定时器到期处理的多个关键方面:

  1. 定时器表遍历:ReactOS 使用哈希表(KiTimerTableListHead)组织定时器,每个桶对应一个时间槽。KiTimerExpiration 遍历从 IndexLimit 的所有时间槽,检查到期的定时器。

  2. 批量 DPC 执行:为了避免频繁获取和释放调度器锁,定时器到期的 DPC 被收集到 DpcEntry 数组中,然后批量执行。当处理的定时器数量达到阈值(Timers = 24ActiveTimers = 4)时,释放锁并执行收集的 DPC。

  3. 周期性定时器:如果定时器设置了 Period(周期),在到期后会调用 KiInsertTreeTimer 重新插入定时器表,实现周期性触发。

  4. 多处理器支持:对于目标处理器不是当前 CPU 的 DPC,或者线程化 DPC,会调用 KeInsertQueueDpc 将其插入目标 CPU 的队列,而不是本地执行。

  5. 等待线程唤醒:如果有线程在等待定时器(通过 KeWaitForSingleObject),定时器到期时会唤醒这些线程。


9.3.C IoRequestDpc 与设备驱动的集成

IoRequestDpc 的实现细节

IoRequestDpc 是 I/O 管理器为设备驱动提供的 DPC 封装,定义于 [ntoskrnl/io/iomgr/dpc.c](file:///d:/reactos/ntoskrnl/io/iomgr/dpc.c):

VOID NTAPI
IoRequestDpc(IN PDEVICE_OBJECT DeviceObject,
             IN PIRP Irp,
             IN PVOID Context)
{
    PKDPC Dpc;
    PDEVICE_EXTENSION Ext;

    /* 获取设备扩展中的 DPC 对象 */
    Ext = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
    Dpc = &Ext->Dpc;

    /* 标记 IRP 为挂起 */
    IoMarkIrpPending(Irp);

    /* 设置 DPC 参数 */
    Dpc->SystemArgument1 = DeviceObject;
    Dpc->SystemArgument2 = Irp;

    /* 排队 DPC */
    KeInsertQueueDpc(Dpc, DeviceObject, Context);
}

IoRequestDpc 的关键特性:

  1. 设备扩展集成:假设设备扩展中包含一个 KDPC 对象(Ext->Dpc),这个 DPC 在驱动初始化时通过 IoInitializeDpcRequest 初始化。

  2. IRP 关联:将 IRP 作为 SystemArgument2 传递给 DPC 例程,使 DPC 可以访问完成 IRP 所需的信息。

  3. 自动挂起:调用 IoMarkIrpPending 标记 IRP 为挂起状态,确保 I/O 管理器不会提前完成 IRP。

IoInitializeDpcRequest

VOID NTAPI
IoInitializeDpcRequest(IN PDEVICE_OBJECT DeviceObject,
                       IN PIO_DPC_ROUTINE DpcRoutine)
{
    KeInitializeDpc(&DeviceObject->Dpc,
                    (PKDEFERRED_ROUTINE)DpcRoutine,
                    DeviceObject);
    
    /* 设置 DPC 重要性为中等 */
    KeSetImportanceDpc(&DeviceObject->Dpc, MediumImportance);
    
    /* 设置目标处理器为当前 CPU */
    KeSetTargetProcessorDpc(&DeviceObject->Dpc, 0);
}

IoInitializeDpcRequest 在驱动初始化时调用,将设备对象的内置 DPC 对象与 DPC 例程关联。设备对象结构中包含一个 KDPC 字段,专门用于这个目的。

典型的 ISR-DPC 协作模式

/* ISR 例程(DIRQL) */
BOOLEAN NTAPI
MyInterruptServiceRoutine(IN PKINTERRUPT Interrupt,
                          IN PVOID ServiceContext)
{
    PDEVICE_EXTENSION Ext = (PDEVICE_EXTENSION)ServiceContext;
    UCHAR Status;

    /* 1. 读取中断状态寄存器 */
    Status = READ_PORT_UCHAR(Ext->StatusPort);

    /* 2. 确认中断 */
    WRITE_PORT_UCHAR(Ext->CommandPort, CMD_ACK_INTERRUPT);

    /* 3. 检查是否是我们的设备 */
    if (!(Status & STATUS_INTERRUPT_PENDING))
        return FALSE;

    /* 4. 读取数据(如果有的话) */
    if (Status & STATUS_DATA_READY)
    {
        Ext->BufferData = READ_PORT_UCHAR(Ext->DataPort);
        Ext->DataValid = TRUE;
    }

    /* 5. 排队 DPC 处理数据 */
    KeInsertQueueDpc(&Ext->Dpc, NULL, NULL);

    return TRUE;
}

/* DPC 例程(DISPATCH_LEVEL) */
VOID NTAPI
MyDpcRoutine(IN PKDPC Dpc,
             IN PVOID DeferredContext,
             IN PVOID SystemArgument1,
             IN PVOID SystemArgument2)
{
    PDEVICE_EXTENSION Ext = (PDEVICE_EXTENSION)DeferredContext;
    PIRP Irp;

    /* 1. 检查是否有数据 */
    if (!Ext->DataValid)
        return;

    /* 2. 获取当前 IRP */
    Irp = Ext->CurrentIrp;
    if (!Irp)
        return;

    /* 3. 复制数据到 IRP 缓冲区 */
    PIO_STACK_LOCATION IoStack = IoGetCurrentIrpStackLocation(Irp);
    if (IoStack->Parameters.Read.Length >= 1)
    {
        PUCHAR Buffer = Irp->AssociatedIrp.SystemBuffer;
        Buffer[0] = Ext->BufferData;
        Irp->IoStatus.Information = 1;
    }

    /* 4. 完成 IRP */
    Irp->IoStatus.Status = STATUS_SUCCESS;
    IoCompleteRequest(Irp, IO_DISK_INCREMENT);

    /* 5. 清除状态 */
    Ext->DataValid = FALSE;
    Ext->CurrentIrp = NULL;

    /* 6. 启动下一个 IRP */
    IoStartNextPacket(Ext->DeviceObject, TRUE);
}

这个模式展示了 ISR 和 DPC 的标准分工:

  • ISR:最小化工作,只读取状态、确认中断、排队 DPC
  • DPC:处理实际数据、完成 IRP、启动下一个 I/O 操作

9.3.D 线程化 DPC(Threaded DPC)

线程化 DPC 的设计动机

传统的 DPC 在 DISPATCH_LEVEL 执行,有以下限制:

  1. 不能访问分页内存
  2. 不能使用等待原语(KeWaitForSingleObject 等)
  3. 不能调用可能阻塞的函数

Windows XP 引入了线程化 DPC(Threaded DPC),在系统线程的 PASSIVE_LEVEL 执行,解除了这些限制。

KeInitializeThreadedDpc

VOID NTAPI
KeInitializeThreadedDpc(IN PRKDPC Dpc,
                        IN PKDEFERRED_ROUTINE DeferredRoutine,
                        IN PVOID DeferredContext)
{
    /* 初始化 DPC 对象 */
    KeInitializeDpc(Dpc, DeferredRoutine, DeferredContext);
    
    /* 设置 DPC 类型为线程化 */
    Dpc->Type = ThreadedDpcObject;
}

线程化 DPC 的初始化与普通 DPC 类似,但设置 Type = ThreadedDpcObject

线程化 DPC 的执行机制

当线程化 DPC 被插入队列时,DPC 分发器不会直接执行它,而是将其传递给专门的系统线程:

/* 在线程化 DPC 系统线程中 */
VOID NTAPI
KiThreadedDpcThread(PVOID Context)
{
    PKPRCB Prcb = KeGetCurrentPrcb();
    PLIST_ENTRY ListEntry;
    PKDPC Dpc;
    PKDEFERRED_ROUTINE Routine;
    PVOID Context;

    for (;;)
    {
        /* 等待 DPC 到达 */
        KeWaitForSingleObject(&Prcb->ThreadedDpcSemaphore,
                              Executive,
                              KernelMode,
                              FALSE,
                              NULL);

        /* 获取 DPC */
        KiAcquireDispatcherLock();
        if (!IsListEmpty(&Prcb->ThreadedDpcListHead))
        {
            ListEntry = RemoveHeadList(&Prcb->ThreadedDpcListHead);
            Dpc = CONTAINING_RECORD(ListEntry, KDPC, DpcListEntry);
            Routine = Dpc->DeferredRoutine;
            Context = Dpc->DeferredContext;
            KiReleaseDispatcherLock();

            /* 在 PASSIVE_LEVEL 执行 DPC */
            Routine(Dpc, Context, Dpc->SystemArgument1, Dpc->SystemArgument2);
        }
        else
        {
            KiReleaseDispatcherLock();
        }
    }
}

每个 CPU 都有一个专门的线程化 DPC 系统线程,等待 ThreadedDpcSemaphore 信号。当线程化 DPC 插入队列时,信号量被释放,系统线程被唤醒执行 DPC。

线程化 DPC 的优缺点

优点

  • 可以访问分页内存
  • 可以使用等待原语
  • 可以执行长时间操作而不阻塞其他 DPC

缺点

  • 延迟更高(需要等待系统线程调度)
  • 消耗更多资源(每个 CPU 一个系统线程)
  • 可能引入优先级反转问题

使用场景

线程化 DPC 适用于:

  • 需要访问分页内存的中断后处理
  • 需要调用阻塞 API 的操作
  • 处理时间较长的任务

不适用于:

  • 时间敏感的操作
  • 简单的中断确认
  • 需要快速响应的场景

9.3.E DPC 性能调优与监控

DPC 速率控制

ReactOS 实现了 DPC 速率控制机制,防止 DPC 队列过载:

/* 在 KPRCB 中的 DPC 速率控制字段 */
ULONG MaximumDpcQueueDepth;     // 队列深度上限
ULONG DpcRequestRate;           // DPC 请求速率
ULONG MinimumDpcRate;           // 最小 DPC 速率
ULONG AdjustDpcThreshold;       // 调整阈值

当 DPC 队列深度超过 MaximumDpcQueueDepth 时,系统会采取措施限制 DPC 的插入速率,例如:

  • 延迟非关键 DPC 的执行
  • 合并多个 DPC 请求
  • 在极端情况下,触发 BugCheck(蓝屏)

DPC 超时检测

ReactOS 在调试版本中实现了 DPC 超时检测:

ULONG KiDPCTimeout = 110;  // DPC 超时时间(微秒)

如果单个 DPC 执行时间超过 KiDPCTimeout,系统会:

  1. 在调试版本中输出警告信息
  2. 在严重情况下触发 DPC_WATCHDOG_VIOLATION BugCheck

性能监控

可以通过以下方式监控 DPC 性能:

  1. 性能计数器Prcb->DpcCountPrcb->DpcQueueDepth 提供 DPC 处理统计
  2. WinDbg 命令!dpc 显示 DPC 队列状态,!prcb 显示处理器控制块信息
  3. 事件跟踪:Windows 的 ETW(Event Tracing for Windows)可以记录 DPC 事件

9.3.F 与 Windows 实现的对比

DPC 结构的差异

Windows 的 KDPC 结构比 ReactOS 更复杂,包含更多字段用于:

  • 线程化 DPC 支持ThreadDpcEnableThreadedDpcListEntry
  • DPC 重要性Importance 字段更精细
  • 目标处理器Number 字段支持多处理器 DPC 定向

DPC 分发的优化

Windows 对 DPC 分发进行了多项优化:

  • 批量处理:一次处理多个 DPC,减少锁竞争
  • 优先级继承:高优先级 DPC 可以抢占低优先级 DPC
  • 动态调整:根据系统负载动态调整 DPC 队列深度阈值

ReactOS 实现了基本的 DPC 分发机制,但在优化方面还有改进空间。

线程化 DPC 的默认行为

  • Windows XP/2003:线程化 DPC 默认禁用
  • Windows Vista+:线程化 DPC 默认启用
  • ReactOS:线程化 DPC 支持正在开发中,默认禁用

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

使用 WinDbg 调试 DPC

  • !dpc:显示当前 CPU 的 DPC 队列状态
  • !prcb:显示处理器控制块,包括 DPC 统计
  • !thread:显示当前线程信息,检查是否在 DPC 上下文
  • !irql:显示当前 IRQL,确认在 DISPATCH_LEVEL

常见 DPC 错误

  1. DPC 运行时间过长:导致 DPC_WATCHDOG_VIOLATION 蓝屏。解决方法:将耗时操作移到工作项(WorkItem)中。

  2. 访问分页内存:在 DISPATCH_LEVEL 访问分页内存导致 PAGE_FAULT_IN_NONPAGED_AREA 蓝屏。确保所有 DPC 访问的内存都在非分页池中。

  3. 使用等待原语:在 DPC 中调用 KeWaitForSingleObject 导致死锁。DPC 不能使用任何阻塞原语。

  4. DPC 未执行:如果 KeInsertQueueDpc 返回 TRUE 但 DPC 未执行,可能是 DPC 分发器未被调度。检查 IRQL 和中断状态。

  5. 多处理器竞争:在多处理器系统中,DPC 可能在任意 CPU 上执行。确保 DPC 例程是线程安全的,使用自旋锁保护共享数据。


总结

DPC 机制的核心要点:

  1. 两阶段模型:ISR(DIRQL)做最少量工作,DPC(DISPATCH_LEVEL)做实际处理
  2. KDPC 结构DeferredRoutine/DeferredContext/SystemArgument1/2/DpcListEntry
  3. 每 CPU 队列:每个 CPU 维护独立的 DPC 队列
  4. API 层次
    • 高级:IoRequestDpc(带设备/IRP)
    • 中级:KeInsertQueueDpc(带自定义 DPC)
    • 底层:KeRaiseIrql + 直接操作
  5. DPC 触发时机:中断返回前、空闲循环、线程调度
  6. 同步限制:DPC 上下文不能用等待原语、不能访问分页内存
  7. 线程化 DPCKeInitializeThreadedDpcPASSIVE_LEVEL 运行

下一节 9.4 介绍内核劳务线程(ExQueueWorkItem / IoQueueWorkItem),用于处理更长耗时的任务。


本章代码索引

文件 内容
[dpcobj.c](file:///d:/reactos/ntoskrnl/ke/dpcobj.c) KeInitializeDpcKeInsertQueueDpcKeRemoveQueueDpcKeSetImportanceDpcKeInitializeThreadedDpc
[dpc.c](file:///d:/reactos/ntoskrnl/ke/dpc.c) KiDpcInterruptKiDpcInterruptHandlerKiDispatchInterrupt
[i386/trap.s](file:///d:/reactos/ntoskrnl/ke/i386/trap.s) _KiDpcInterruptHandler 汇编入口
[io/dpc.c](file:///d:/reactos/ntoskrnl/io/iomgr/dpc.c) IoRequestDpcIopDpcRoutine
[ketypes.h](file:///d:/reactos/sdk/include/xdk/ketypes.h) KDPCPKDEFERRED_ROUTINE
[beep.c](file:///d:/reactos/drivers/base/beep/beep.c) DPC 实例(BeepStopDpcRoutine
[ketypes.h](file:///d:/reactos/sdk/include/xdk/ketypes.h) KDPCDPC_NORMALDPC_THREADED
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐