Reactos 第 9 章 设备驱动 — 9.3 DPC函数及其执行
第 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
KiDispatchInterrupt→KiDpcInterrupt触发链 - 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) | KeInitializeDpc、KeInsertQueueDpc、KeRemoveQueueDpc |
| DPC 分发 | [ntoskrnl/ke/dpc.c](file:///d:/reactos/ntoskrnl/ke/dpc.c) | KiDpcInterrupt、KiDpcInterruptHandler |
| 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 机制的设计目标是解决 中断处理的两难问题:
- ISR 在高 IRQL(DIRQL):不能访问分页内存、不能等待锁、不能调用系统服务
- 真实工作需要低 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 |
链表节点(在 KiDpcListHead 或 DpcData[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 KiDispatchInterrupt → KiDpcInterrupt 触发链
DPC 分发由 KiDpcInterrupt(汇编入口)或 KiDpcInterruptHandler(C 实现)触发。
触发条件
DPC 分发器在以下时刻被调用:
- 中断返回前:当 IRQL 从
DISPATCH_LEVEL降到更低时 - 空闲循环:在
KiIdleLoop中(每空闲一段时间) - 线程调度:在线程上下文切换时
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();
}
关键设计
- 每次取一个 DPC:先释放锁再调用 DPC(避免长时间持锁)
- 再入队检查:调用 DPC 期间,DPC 可以再次入队
- 优先级:高 Importance 的 DPC 先运行
- 线程化 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);
}
关键规则
- ISR 极简:只做确认中断、读状态、排 DPC
- DPC 处理实际数据:可以访问分页内存、可以获取自旋锁、可以调用 IoStartNextPacket
- DPC 不能调用 KeWaitFor 等待*:会死锁
- 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();
}
这段代码揭示了几个关键设计:
-
中断禁用:在进入 DPC 分发前调用
_disable()禁用硬件中断,确保 DPC 队列的原子性。这是必要的,因为 ISR 可能在任何时刻尝试插入新的 DPC。 -
锁的粒度:每次调用 DPC 例程前都释放调度器锁(
KiReleaseDispatcherLock),调用完成后再重新获取。这种设计避免了长时间持有锁导致其他 CPU 的 DPC 操作被阻塞。 -
DPC 计数统计:
Prcb->DpcCount += Prcb->DpcQueueDepth将当前队列深度累加到总计数中,然后清零队列深度。这些统计数据可用于性能分析和调试。 -
单步执行:每次从队列中取出一个 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;
}
关键观察:
-
重复插入检查:
if (!Dpc->Inserted)确保同一个 DPC 对象不会被重复插入队列。如果 DPC 已经在队列中,KeInsertQueueDpc返回FALSE,但会更新SystemArgument1/2。 -
软件中断触发:当 DPC 首次插入队列时,需要触发软件中断(在 x86 上是
IRQL = DISPATCH_LEVEL的软件中断),确保 DPC 分发器被调度执行。 -
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);
}
}
这个复杂的函数揭示了定时器到期处理的多个关键方面:
-
定时器表遍历:ReactOS 使用哈希表(
KiTimerTableListHead)组织定时器,每个桶对应一个时间槽。KiTimerExpiration遍历从Index到Limit的所有时间槽,检查到期的定时器。 -
批量 DPC 执行:为了避免频繁获取和释放调度器锁,定时器到期的 DPC 被收集到
DpcEntry数组中,然后批量执行。当处理的定时器数量达到阈值(Timers = 24或ActiveTimers = 4)时,释放锁并执行收集的 DPC。 -
周期性定时器:如果定时器设置了
Period(周期),在到期后会调用KiInsertTreeTimer重新插入定时器表,实现周期性触发。 -
多处理器支持:对于目标处理器不是当前 CPU 的 DPC,或者线程化 DPC,会调用
KeInsertQueueDpc将其插入目标 CPU 的队列,而不是本地执行。 -
等待线程唤醒:如果有线程在等待定时器(通过
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 的关键特性:
-
设备扩展集成:假设设备扩展中包含一个
KDPC对象(Ext->Dpc),这个 DPC 在驱动初始化时通过IoInitializeDpcRequest初始化。 -
IRP 关联:将 IRP 作为
SystemArgument2传递给 DPC 例程,使 DPC 可以访问完成 IRP 所需的信息。 -
自动挂起:调用
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 执行,有以下限制:
- 不能访问分页内存
- 不能使用等待原语(
KeWaitForSingleObject等) - 不能调用可能阻塞的函数
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,系统会:
- 在调试版本中输出警告信息
- 在严重情况下触发
DPC_WATCHDOG_VIOLATIONBugCheck
性能监控
可以通过以下方式监控 DPC 性能:
- 性能计数器:
Prcb->DpcCount和Prcb->DpcQueueDepth提供 DPC 处理统计 - WinDbg 命令:
!dpc显示 DPC 队列状态,!prcb显示处理器控制块信息 - 事件跟踪:Windows 的 ETW(Event Tracing for Windows)可以记录 DPC 事件
9.3.F 与 Windows 实现的对比
DPC 结构的差异
Windows 的 KDPC 结构比 ReactOS 更复杂,包含更多字段用于:
- 线程化 DPC 支持:
ThreadDpcEnable、ThreadedDpcListEntry - 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 错误
-
DPC 运行时间过长:导致
DPC_WATCHDOG_VIOLATION蓝屏。解决方法:将耗时操作移到工作项(WorkItem)中。 -
访问分页内存:在
DISPATCH_LEVEL访问分页内存导致PAGE_FAULT_IN_NONPAGED_AREA蓝屏。确保所有 DPC 访问的内存都在非分页池中。 -
使用等待原语:在 DPC 中调用
KeWaitForSingleObject导致死锁。DPC 不能使用任何阻塞原语。 -
DPC 未执行:如果
KeInsertQueueDpc返回TRUE但 DPC 未执行,可能是 DPC 分发器未被调度。检查 IRQL 和中断状态。 -
多处理器竞争:在多处理器系统中,DPC 可能在任意 CPU 上执行。确保 DPC 例程是线程安全的,使用自旋锁保护共享数据。
总结
DPC 机制的核心要点:
- 两阶段模型:ISR(DIRQL)做最少量工作,DPC(DISPATCH_LEVEL)做实际处理
KDPC结构:DeferredRoutine/DeferredContext/SystemArgument1/2/DpcListEntry- 每 CPU 队列:每个 CPU 维护独立的 DPC 队列
- API 层次:
- 高级:
IoRequestDpc(带设备/IRP) - 中级:
KeInsertQueueDpc(带自定义 DPC) - 底层:
KeRaiseIrql+ 直接操作
- 高级:
- DPC 触发时机:中断返回前、空闲循环、线程调度
- 同步限制:DPC 上下文不能用等待原语、不能访问分页内存
- 线程化 DPC:
KeInitializeThreadedDpc在PASSIVE_LEVEL运行
下一节 9.4 介绍内核劳务线程(ExQueueWorkItem / IoQueueWorkItem),用于处理更长耗时的任务。
本章代码索引
| 文件 | 内容 |
|---|---|
| [dpcobj.c](file:///d:/reactos/ntoskrnl/ke/dpcobj.c) | KeInitializeDpc、KeInsertQueueDpc、KeRemoveQueueDpc、KeSetImportanceDpc、KeInitializeThreadedDpc |
| [dpc.c](file:///d:/reactos/ntoskrnl/ke/dpc.c) | KiDpcInterrupt、KiDpcInterruptHandler、KiDispatchInterrupt |
| [i386/trap.s](file:///d:/reactos/ntoskrnl/ke/i386/trap.s) | _KiDpcInterruptHandler 汇编入口 |
| [io/dpc.c](file:///d:/reactos/ntoskrnl/io/iomgr/dpc.c) | IoRequestDpc、IopDpcRoutine |
| [ketypes.h](file:///d:/reactos/sdk/include/xdk/ketypes.h) | KDPC、PKDEFERRED_ROUTINE |
| [beep.c](file:///d:/reactos/drivers/base/beep/beep.c) | DPC 实例(BeepStopDpcRoutine) |
| [ketypes.h](file:///d:/reactos/sdk/include/xdk/ketypes.h) | KDPC、DPC_NORMAL、DPC_THREADED |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)