第 5 章 进程与线程 --- 5.8 Windows 的 APC 机制
本节深入剖析 Windows/ReactOS 内核中异步过程调用(APC)的完整实现机制。
概述
APC(Asynchronous Procedure Call,异步过程调用)是 Windows 内核中实现异步执行的核心机制。它允许内核或用户态代码将一个函数(APC例程)投递到指定线程,使其在特定时机执行。APC 是理解线程调度、I/O 完成、定时器、线程终止等功能的关键基础。
「异步」在这里意味着什么?
- APC 不是立即执行,而是被「投递」到目标线程的队列中;
- 当线程处于合适的执行环境时(如 IRQL 降至 PASSIVE_LEVEL、处于 Alertable 状态等),APC 才会被分发执行;
- 这使得内核可以在不中断线程当前工作的情况下,安排后续任务。
想象一个餐厅的场景:服务员(内核)接到顾客(调用者)的点单(APC),但不会立即去厨房做,而是把订单(APC)放到后厨队列里。当厨师(目标线程)空闲时(合适的执行环境),再按顺序处理这些订单。
本节内容概览
- 5.8.0 框架图:APC 从投递到分发的完整流程总览;
- 5.8.1 APC 数据结构:KAPC、KAPC_STATE 的结构与作用;
- 5.8.2 APC 类型与分类:内核 APC vs 用户 APC、特殊 APC vs 普通 APC;
- 5.8.3 APC 的投递机制:KeInitializeApc、KeInsertQueueApc、KiInsertQueueApc;
- 5.8.4 APC 的分发机制:KiDeliverApc、KiCheckForKernelApcDelivery;
- 5.8.5 APC 与 IRQL:APC_LEVEL、临界区、Guarded Region;
- 5.8.6 用户态 APC 实现:QueueUserAPC、NtQueueApcThread;
- 5.8.7 APC 的典型应用:I/O 完成、定时器、线程终止;
- 5.8.8 设计哲学与常见问题:性能考虑、调试技巧;
- 5.8.9 为什么会这样------10 个设计哲学问答:深入理解 APC 的设计决策。
学习目标
读完本节后,读者应当能够:
- 说清 APC 从投递到分发的完整流程;
- 理解 KAPC 和 KAPC_STATE 的结构与作用;
- 区分四种 APC 类型:内核特殊 APC、内核普通 APC、用户 APC;
- 解释 APC_LEVEL 在调度中的特殊地位;
- 理解为什么用户 APC 需要 Alertable 状态;
- 分析 APC 在 I/O 完成和定时器中的应用。
涉及的内核子系统
| 子系统 | 职责 |
|---|---|
| ntoskrnl!Ke | APC 对象管理、投递、分发(核心) |
| ntoskrnl!Ps | 线程状态管理、线程终止 APC |
| ntoskrnl!Io | I/O 完成 APC 的触发 |
| ntdll | 用户 APC 的用户态入口和执行环境设置 |
5.8.0 框架图
┌──────────────────────────────────────────────────────────────────────────────┐
│ Windows APC 完整执行流程 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 阶段 A:初始化 APC(KeInitializeApc) │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ KAPC 对象:Type=ApcObject, Thread=目标线程 │ │ │
│ │ │ KernelRoutine=内核例程(必选) │ │ │
│ │ │ NormalRoutine=用户态例程(可选) │ │ │
│ │ │ ApcMode=KernelMode/UserMode │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 阶段 B:投递 APC(KeInsertQueueApc → KiInsertQueueApc) │ │
│ │ ├─► 获取 APC 锁(Synch Level) │ │ │
│ │ ├─► 插入对应队列(内核 APC 队列 / 用户 APC 队列) │ │ │
│ │ ├─► 设置 KernelApcPending/UserApcPending 标志 │ │ │
│ │ └─► 若线程正在运行 → 请求 APC_LEVEL 中断 │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 阶段 C:分发 APC(KiDeliverApc) │ │
│ │ 触发时机:IRQL 从 >=APC_LEVEL 降至 PASSIVE_LEVEL │ │ │
│ │ │ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ Step 1: 处理特殊内核 APC(无 NormalRoutine) │ │ │
│ │ │ └─► 直接调用 KernelRoutine(APC_LEVEL) │ │ │
│ │ ├──────────────────────────────────────────────────────────────┤ │ │
│ │ │ Step 2: 处理普通内核 APC(有 NormalRoutine) │ │ │
│ │ │ ├─► 调用 KernelRoutine(APC_LEVEL) │ │ │
│ │ │ └─► 若 NormalRoutine 非空 → 降至 PASSIVE_LEVEL 执行 │ │ │
│ │ ├──────────────────────────────────────────────────────────────┤ │ │
│ │ │ Step 3: 处理用户 APC(仅当 DeliveryMode=UserMode) │ │ │
│ │ │ ├─► 调用 KernelRoutine(APC_LEVEL) │ │ │
│ │ │ └─► 设置用户态执行环境 → 返回用户态执行 NormalRoutine │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
5.8.1 APC 数据结构
5.8.1.1 KAPC 结构
KAPC 是 APC 对象的核心数据结构,定义于 sdk/include/ndk/ketypes.h:
c
typedef struct _KAPC {
CSHORT Type; // 类型:ApcObject (0x12)
CSHORT Size; // 大小:sizeof(KAPC)
LIST_ENTRY ApcListEntry; // 链接到 APC 队列的链表项
PKTHREAD Thread; // 目标线程
PKKERNEL_ROUTINE KernelRoutine; // 内核例程(必选)
PKRUNDOWN_ROUTINE RundownRoutine; // 清理例程(可选)
PKNORMAL_ROUTINE NormalRoutine; // 用户态例程(可选)
PVOID NormalContext; // 传递给 NormalRoutine 的上下文
PVOID SystemArgument1; // 系统参数1
PVOID SystemArgument2; // 系统参数2
CHAR ApcStateIndex; // APC 环境索引(0/1)
CHAR ApcMode; // 执行模式(KernelMode/UserMode)
BOOLEAN Inserted; // 是否已插入队列
} KAPC, *PKAPC;
关键字段说明:
| 字段 | 说明 |
|---|---|
KernelRoutine |
必须提供,所有 APC 都会执行此例程,运行于 APC_LEVEL |
NormalRoutine |
可选,若为 NULL 则为「特殊 APC」;否则为「普通 APC」 |
ApcMode |
决定 NormalRoutine 的执行模式,特殊 APC 强制为 KernelMode |
ApcStateIndex |
双环境索引(0 或 1),用于线程切换环境时保存/恢复 APC 状态 |
Inserted |
标记 APC 是否已在队列中,防止重复插入 |
5.8.1.2 KAPC_STATE 结构
每个线程维护一个或多个 KAPC_STATE,用于管理该线程的 APC 队列:
c
typedef struct _KAPC_STATE {
LIST_ENTRY ApcListHead[2]; // [0]=内核 APC 队列, [1]=用户 APC 队列
PKPROCESS Process; // 所属进程
BOOLEAN KernelApcPending; // 是否有内核 APC 待处理
BOOLEAN UserApcPending; // 是否有用户 APC 待处理
} KAPC_STATE, *PKAPC_STATE;
5.8.1.3 线程的 APC 状态管理
线程结构 KTHREAD 中包含 APC 相关的字段:
c
typedef struct _KTHREAD {
// ...
KAPC_STATE ApcState; // 当前 APC 状态
PKAPC_STATE ApcStatePointer[2]; // 双环境指针数组
CHAR ApcStateIndex; // 当前使用的环境索引
BOOLEAN KernelApcDisable; // 普通内核 APC 禁用标志
BOOLEAN SpecialApcDisable; // 特殊内核 APC 禁用标志
BOOLEAN CombinedApcDisable; // 组合禁用标志
BOOLEAN ApcQueueable; // 是否允许入队 APC
// ...
} KTHREAD, *PKTHREAD;
5.8.1.4 双 APC 环境机制
ApcStatePointer[2] 实现了双环境机制:
- 环境 0:线程的「主环境」,通常对应线程所属进程;
- 环境 1:线程的「附属环境」,用于线程临时切换到其他进程上下文时使用(如 APC 执行期间)。
当线程执行用户 APC 时,内核会保存当前 APC 状态,切换到目标进程的 APC 环境,执行完毕后再恢复。
源码位置:sdk/include/ndk/ketypes.h(file:///d:/reactos/sdk/include/ndk/ketypes.h)
5.8.2 APC 类型与分类
5.8.2.1 分类维度
APC 从两个维度进行分类:
| 分类维度 | 类型 | 特点 |
|---|---|---|
| 执行模式 | 内核模式 APC | KernelRoutine 和 NormalRoutine 都在内核态执行 |
| 用户模式 APC | KernelRoutine 在内核态执行,NormalRoutine 在用户态执行 | |
| 是否有 NormalRoutine | 特殊 APC | NormalRoutine = NULL,只有 KernelRoutine |
| 普通 APC | NormalRoutine != NULL,有完整的两阶段执行 |
5.8.2.2 四种 APC 类型对比
| 类型 | NormalRoutine | ApcMode | 执行级别 | 禁用条件 | 典型用途 |
|---|---|---|---|---|---|
| 内核特殊 APC | NULL | KernelMode | APC_LEVEL | SpecialApcDisable 或 IRQL >= DISPATCH_LEVEL | 线程终止、调试器断点 |
| 内核普通 APC | 非 NULL | KernelMode | APC_LEVEL → PASSIVE_LEVEL | KernelApcDisable 或 Critical Region | I/O 完成端口、定时器 |
| 用户 APC | 非 NULL | UserMode | APC_LEVEL → 用户态 | Alertable 状态未设置 | QueueUserAPC、异步 I/O 回调 |
5.8.2.3 特殊 APC vs 普通 APC
特殊 APC(Special APC):
NormalRoutine = NULL;- 只执行
KernelRoutine,运行于 APC_LEVEL; - 优先级最高,不受临界区保护;
- 用于紧急任务,如线程终止、调试器中断。
普通 APC(Normal APC):
NormalRoutine != NULL;- 先执行
KernelRoutine(APC_LEVEL),再执行NormalRoutine(PASSIVE_LEVEL 或用户态); - 受临界区和 APC 禁用标志保护。
5.8.3 APC 的投递机制
5.8.3.1 KeInitializeApc ------ 初始化 APC 对象
c
VOID
NTAPI
KeInitializeApc(IN PKAPC Apc,
IN PKTHREAD Thread,
IN KAPC_ENVIRONMENT TargetEnvironment,
IN PKKERNEL_ROUTINE KernelRoutine,
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine,
IN KPROCESSOR_MODE Mode,
IN PVOID Context)
{
Apc->Type = ApcObject;
Apc->Size = sizeof(KAPC);
// 设置环境索引
if (TargetEnvironment == CurrentApcEnvironment) {
Apc->ApcStateIndex = Thread->ApcStateIndex;
} else {
Apc->ApcStateIndex = TargetEnvironment;
}
// 设置线程和例程
Apc->Thread = Thread;
Apc->KernelRoutine = KernelRoutine;
Apc->RundownRoutine = RundownRoutine;
Apc->NormalRoutine = NormalRoutine;
// 根据是否有 NormalRoutine 设置模式
if (NormalRoutine) {
Apc->ApcMode = Mode;
Apc->NormalContext = Context;
} else {
// 特殊 APC 强制为内核模式
Apc->ApcMode = KernelMode;
Apc->NormalContext = NULL;
}
Apc->Inserted = FALSE;
}
源码位置:ntoskrnl/ke/apc.c#L649-L705(file:///d:/reactos/ntoskrnl/ke/apc.c#L649-L705)
5.8.3.2 KeInsertQueueApc ------ 用户可见的投递 API
c
BOOLEAN
NTAPI
KeInsertQueueApc(IN PKAPC Apc,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2,
IN KPRIORITY PriorityBoost)
{
PKTHREAD Thread = Apc->Thread;
KLOCK_QUEUE_HANDLE ApcLock;
BOOLEAN State = TRUE;
// 获取 APC 锁(提升到 SYNCH_LEVEL)
KiAcquireApcLockRaiseToSynch(Thread, &ApcLock);
// 检查是否可以入队
if (!(Thread->ApcQueueable) || (Apc->Inserted)) {
State = FALSE;
} else {
// 设置系统参数
Apc->SystemArgument1 = SystemArgument1;
Apc->SystemArgument2 = SystemArgument2;
Apc->Inserted = TRUE;
// 调用内部入队函数
KiInsertQueueApc(Apc, PriorityBoost);
}
// 释放锁并返回
KiReleaseApcLockFromSynchLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return State;
}
源码位置:ntoskrnl/ke/apc.c#L733-L770(file:///d:/reactos/ntoskrnl/ke/apc.c#L733-L770)
5.8.3.3 KiInsertQueueApc ------ 核心入队逻辑
KiInsertQueueApc 是真正执行入队操作的内部函数:
c
VOID
FASTCALL
KiInsertQueueApc(IN PKAPC Apc,
IN KPRIORITY PriorityBoost)
{
PKTHREAD Thread = Apc->Thread;
PKAPC_STATE ApcState;
KPROCESSOR_MODE ApcMode;
PLIST_ENTRY ListHead, NextEntry;
// 获取 APC 状态和模式
ApcState = Thread->ApcStatePointer[(UCHAR)Apc->ApcStateIndex];
ApcMode = Apc->ApcMode;
// 三种入队场景
if (Apc->NormalRoutine) {
// 场景1:普通 APC(有 NormalRoutine)
if ((ApcMode != KernelMode) &&
(Apc->KernelRoutine == PsExitSpecialApc)) {
// 特殊:线程终止 APC 插入到用户 APC 队列头部
Thread->ApcState.UserApcPending = TRUE;
InsertHeadList(&ApcState->ApcListHead[ApcMode], &Apc->ApcListEntry);
} else {
// 普通 APC 插入到队列尾部
InsertTailList(&ApcState->ApcListHead[ApcMode], &Apc->ApcListEntry);
}
} else {
// 场景2:特殊 APC(无 NormalRoutine)
// 插入到所有特殊 APC 的前面(保持顺序)
ListHead = &ApcState->ApcListHead[ApcMode];
NextEntry = ListHead->Blink;
while (NextEntry != ListHead) {
PKAPC QueuedApc = CONTAINING_RECORD(NextEntry, KAPC, ApcListEntry);
if (!QueuedApc->NormalRoutine) break;
NextEntry = NextEntry->Blink;
}
InsertHeadList(NextEntry, &Apc->ApcListEntry);
}
// 设置待处理标志并请求中断
if (Thread->ApcStateIndex == Apc->ApcStateIndex) {
if (Thread == KeGetCurrentThread()) {
if (ApcMode == KernelMode) {
Thread->ApcState.KernelApcPending = TRUE;
if (!Thread->SpecialApcDisable) {
HalRequestSoftwareInterrupt(APC_LEVEL);
}
}
} else {
// 对其他线程的处理逻辑...
}
}
}
源码位置:ntoskrnl/ke/apc.c#L84-L273(file:///d:/reactos/ntoskrnl/ke/apc.c#L84-L273)
5.8.3.4 入队顺序规则总结
APC 入队顺序决策树
│
▼
NormalRoutine != NULL?
│
┌─────┴─────┐
│ │
Yes No (特殊 APC)
│ │
▼ ▼
用户模式且 插入到所有
KernelRoutine 特殊 APC
= PsExitSpecialApc? 前面
│
┌──┴──┐
Yes No
│ │
▼ ▼
插入到 插入到
队列头 队列尾
5.8.4 APC 的分发机制
5.8.4.1 KiDeliverApc ------ 核心分发函数
KiDeliverApc 是 APC 分发的核心,负责按优先级处理队列中的 APC:
c
VOID
NTAPI
KiDeliverApc(IN KPROCESSOR_MODE DeliveryMode,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame)
{
PKTHREAD Thread = KeGetCurrentThread();
PKPROCESS Process = Thread->ApcState.Process;
PKTRAP_FRAME OldTrapFrame;
PLIST_ENTRY ApcListEntry;
PKAPC Apc;
KLOCK_QUEUE_HANDLE ApcLock;
PKKERNEL_ROUTINE KernelRoutine;
PVOID NormalContext;
PKNORMAL_ROUTINE NormalRoutine;
PVOID SystemArgument1, SystemArgument2;
// 保存旧的陷阱帧
OldTrapFrame = Thread->TrapFrame;
Thread->TrapFrame = TrapFrame;
// 清除内核 APC 待处理标志
Thread->ApcState.KernelApcPending = FALSE;
// 检查特殊 APC 是否禁用
if (Thread->SpecialApcDisable) goto Quickie;
// ========== Step 1: 处理内核 APC ==========
while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode])) {
KiAcquireApcLockRaiseToDpc(Thread, &ApcLock);
if (IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode])) {
KiReleaseApcLock(&ApcLock);
break;
}
// 获取队列中的第一个 APC
ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink;
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
// 保存参数
NormalRoutine = Apc->NormalRoutine;
KernelRoutine = Apc->KernelRoutine;
NormalContext = Apc->NormalContext;
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;
if (!NormalRoutine) {
// 特殊内核 APC
RemoveEntryList(ApcListEntry);
Apc->Inserted = FALSE;
KiReleaseApcLock(&ApcLock);
// 直接执行 KernelRoutine
KernelRoutine(Apc, &NormalRoutine, &NormalContext,
&SystemArgument1, &SystemArgument2);
} else {
// 普通内核 APC
if ((Thread->ApcState.KernelApcInProgress) ||
(Thread->KernelApcDisable)) {
KiReleaseApcLock(&ApcLock);
goto Quickie;
}
RemoveEntryList(ApcListEntry);
Apc->Inserted = FALSE;
KiReleaseApcLock(&ApcLock);
// 执行 KernelRoutine
KernelRoutine(Apc, &NormalRoutine, &NormalContext,
&SystemArgument1, &SystemArgument2);
// 若有 NormalRoutine,降至 PASSIVE_LEVEL 执行
if (NormalRoutine) {
Thread->ApcState.KernelApcInProgress = TRUE;
KeLowerIrql(PASSIVE_LEVEL);
NormalRoutine(NormalContext, SystemArgument1, SystemArgument2);
KeRaiseIrql(APC_LEVEL, &ApcLock.OldIrql);
Thread->ApcState.KernelApcInProgress = FALSE;
}
}
}
// ========== Step 2: 处理用户 APC ==========
if ((DeliveryMode == UserMode) &&
!(IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])) &&
(Thread->ApcState.UserApcPending)) {
KiAcquireApcLockRaiseToDpc(Thread, &ApcLock);
Thread->ApcState.UserApcPending = FALSE;
if (IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])) {
KiReleaseApcLock(&ApcLock);
goto Quickie;
}
ApcListEntry = Thread->ApcState.ApcListHead[UserMode].Flink;
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
// 保存参数
NormalRoutine = Apc->NormalRoutine;
KernelRoutine = Apc->KernelRoutine;
NormalContext = Apc->NormalContext;
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;
RemoveEntryList(ApcListEntry);
Apc->Inserted = FALSE;
KiReleaseApcLock(&ApcLock);
// 执行内核例程
KernelRoutine(Apc, &NormalRoutine, &NormalContext,
&SystemArgument1, &SystemArgument2);
if (!NormalRoutine) {
KeTestAlertThread(UserMode);
} else {
// 设置用户态执行环境
KiInitializeUserApc(ExceptionFrame, TrapFrame,
NormalRoutine, NormalContext,
SystemArgument1, SystemArgument2);
}
}
Quickie:
// 恢复陷阱帧
Thread->TrapFrame = OldTrapFrame;
}
源码位置:ntoskrnl/ke/apc.c#L300-L512(file:///d:/reactos/ntoskrnl/ke/apc.c#L300-L512)
5.8.4.2 KiCheckForKernelApcDelivery ------ 分发入口
当内核从临界区或受保护区域退出时,会调用此函数检查是否有待处理的 APC:
c
VOID
NTAPI
KiCheckForKernelApcDelivery(VOID)
{
KIRQL OldIrql;
// 只能在 PASSIVE_LEVEL 投递
if (KeGetCurrentIrql() == PASSIVE_LEVEL) {
KeRaiseIrql(APC_LEVEL, &OldIrql);
KiDeliverApc(KernelMode, 0, 0);
KeLowerIrql(PASSIVE_LEVEL);
} else {
// 非 PASSIVE_LEVEL,设置待处理标志并请求中断
KeGetCurrentThread()->ApcState.KernelApcPending = TRUE;
HalRequestSoftwareInterrupt(APC_LEVEL);
}
}
源码位置:ntoskrnl/ke/apc.c#L34-L60(file:///d:/reactos/ntoskrnl/ke/apc.c#L34-L60)
5.8.4.3 APC 分发时机
APC 分发发生在以下时机:
| 时机 | 触发条件 | 说明 |
|---|---|---|
| IRQL 下降 | 从 APC_LEVEL 或更高降至 PASSIVE_LEVEL | 最常见的触发时机 |
| 临界区退出 | KeLeaveCriticalRegion | 检查并投递内核 APC |
| Guarded Region 退出 | KeLeaveGuardedRegion | 检查并投递 APC |
| 线程唤醒 | 从等待状态被唤醒 | 检查待处理的 APC |
| 系统调用返回 | 用户态系统调用返回前 | 检查用户 APC |
5.8.5 APC 与 IRQL
5.8.5.1 APC_LEVEL 的特殊地位
APC_LEVEL(值为 1)是 Windows IRQL 体系中的一个特殊级别:
IRQL 层级示意图(从低到高)
┌─────────────────────────────────────────┐
│ PASSIVE_LEVEL (0) ────────► 用户态代码、普通内核代码 │
│ │
│ APC_LEVEL (1) ────────► APC 执行、某些同步操作 │
│ │
│ DISPATCH_LEVEL (2) ───────► DPC 执行、调度器 │
│ │
│ 中断级别 (3-31) ────────► 硬件中断处理 │
└─────────────────────────────────────────┘
关键规则:
- APC 只能在 IRQL <= APC_LEVEL 时投递;
- 当 IRQL > APC_LEVEL(如 DISPATCH_LEVEL)时,APC 会被延迟到 IRQL 下降后执行;
- 内核 APC 的 KernelRoutine 运行于 APC_LEVEL;
- 用户 APC 的 NormalRoutine 运行于用户态(通过陷阱帧切换)。
5.8.5.2 临界区与 APC 禁用
KeEnterCriticalRegion / KeLeaveCriticalRegion 用于控制普通内核 APC 的投递:
c
// 进入临界区 ------ 禁用普通内核 APC
VOID KeEnterCriticalRegion(VOID) {
KeGetCurrentThread()->KernelApcDisable++;
}
// 离开临界区 ------ 重新启用普通内核 APC,可能触发投递
VOID KeLeaveCriticalRegion(VOID) {
KeGetCurrentThread()->KernelApcDisable--;
if (KeGetCurrentThread()->KernelApcDisable == 0) {
KiCheckForKernelApcDelivery();
}
}
注意 :临界区只禁用普通内核 APC,特殊内核 APC 仍然可以投递。
5.8.5.3 Guarded Region
KeEnterGuardedRegion / KeLeaveGuardedRegion 提供更强的保护:
c
// 进入 Guarded Region ------ 禁用所有 APC
VOID KeEnterGuardedRegion(VOID) {
KeGetCurrentThread()->SpecialApcDisable++;
}
// 离开 Guarded Region
VOID KeLeaveGuardedRegion(VOID) {
KeGetCurrentThread()->SpecialApcDisable--;
if (KeGetCurrentThread()->SpecialApcDisable == 0) {
KiCheckForKernelApcDelivery();
}
}
注意 :Guarded Region 禁用所有 APC,包括特殊内核 APC。
5.8.5.4 APC 禁用状态查询
c
// 检查普通内核 APC 是否禁用
BOOLEAN KeAreApcsDisabled(VOID) {
return KeGetCurrentThread()->CombinedApcDisable ? TRUE : FALSE;
}
// 检查所有 APC 是否禁用(包括特殊 APC 和 IRQL >= APC_LEVEL)
BOOLEAN KeAreAllApcsDisabled(VOID) {
return ((KeGetCurrentThread()->SpecialApcDisable) ||
(KeGetCurrentIrql() >= APC_LEVEL)) ? TRUE : FALSE;
}
源码位置:ntoskrnl/ke/apc.c#L956-L990(file:///d:/reactos/ntoskrnl/ke/apc.c#L956-L990)
5.8.6 用户态 APC 实现
5.8.6.1 QueueUserAPC ------ 用户态 API
QueueUserAPC 是用户态程序投递 APC 的主要接口:
c
DWORD WINAPI QueueUserAPC(
IN PAPCFUNC pfnAPC, // 用户态 APC 例程
IN HANDLE hThread, // 目标线程句柄
IN ULONG_PTR dwData // 传递给 APC 例程的数据
);
5.8.6.2 NtQueueApcThread ------ 系统调用层
QueueUserAPC 内部调用 NtQueueApcThread 系统调用:
c
NTSTATUS NTAPI NtQueueApcThread(
IN HANDLE ThreadHandle,
IN PKNORMAL_ROUTINE ApcRoutine,
IN PVOID ApcArgument1 OPTIONAL,
IN PVOID ApcArgument2 OPTIONAL,
IN PVOID ApcArgument3 OPTIONAL
);
内核层会为用户 APC 创建一个 KAPC 对象,设置:
KernelRoutine= 内核提供的包装函数(负责切换到用户态);NormalRoutine= 用户提供的pfnAPC;ApcMode= UserMode。
5.8.6.3 用户 APC 的执行环境
用户 APC 的执行需要特殊处理:
- 内核态准备 :
KiDeliverApc调用KiInitializeUserApc; - 设置陷阱帧:修改 TrapFrame,将返回地址指向 ntdll 中的 APC 入口;
- 返回用户态:从中断返回时,CPU 执行 ntdll 的 APC 处理代码;
- 用户态执行 :ntdll 调用用户提供的
NormalRoutine; - 清理返回:执行完毕后返回原来的用户代码。
5.8.6.4 Alertable 状态
用户 APC 只有在线程处于 Alertable 状态时才能被投递:
c
// 使线程进入 Alertable 等待状态
DWORD WaitForSingleObjectEx(
HANDLE hHandle,
DWORD dwMilliseconds,
BOOL bAlertable // TRUE = Alertable 状态
);
当 bAlertable = TRUE 时:
- 线程在等待期间可以被用户 APC 唤醒;
KiDeliverApc会检查Thread->Alertable标志;- 若有用户 APC 待处理,线程会被唤醒并执行 APC。
5.8.7 APC 的典型应用
5.8.7.1 I/O 完成端口
I/O 完成端口是 APC 的核心应用场景:
I/O 完成 APC 流程
用户程序
│
├─► CreateIoCompletionPort() 创建完成端口
│
├─► CreateFile() 打开文件(FILE_FLAG_OVERLAPPED)
│
├─► ReadFile() / WriteFile() 发起异步 I/O
│ │
│ └─► 内核将 I/O 请求放入设备队列
│
└─► GetQueuedCompletionStatus() 等待完成
│
▼
设备完成 I/O
│
└─► IoCompleteRequest()
│
└─► 若线程在 Alertable 等待 → 投递用户 APC
│
└─► 用户 APC 回调被执行
5.8.7.2 定时器 APC
内核定时器到期时会投递 APC:
c
// 创建定时器并设置 APC 回调
HANDLE CreateWaitableTimer(..., TIMER_ALL_ACCESS, ...);
SetWaitableTimer(hTimer, &dueTime, period,
TimerRoutine, // APC 例程
NULL, FALSE);
5.8.7.3 线程终止 APC
线程终止时,系统会投递一个特殊的用户 APC(PsExitSpecialApc):
c
// PsExitSpecialApc 是内核提供的特殊 APC 例程
// 它会在用户态执行,负责清理线程资源
VOID PsExitSpecialApc(
IN PKAPC Apc,
IN OUT PKNORMAL_ROUTINE *NormalRoutine,
IN OUT PVOID *NormalContext,
IN OUT PVOID *SystemArgument1,
IN OUT PVOID *SystemArgument2
)
{
// 线程终止处理...
}
5.8.7.4 调试器 APC
调试器使用 APC 实现断点和单步执行:
- 调试器向目标线程投递 APC;
- APC 执行时检查断点条件;
- 若命中断点,通知调试器。
5.8.7.5 异步 Procedure Call 的优势场景
场景一:高性能 I/O 编程
在高并发服务器场景中,APC 机制提供了以下优势:
- 非阻塞执行:发起 I/O 请求后,线程可以继续执行其他任务,无需阻塞等待;
- 减少线程数量:单个线程可以处理多个并发 I/O,避免创建大量线程带来的开销;
- 上下文切换优化:I/O 完成时直接在等待线程中执行回调,减少线程间通信。
场景二:定时器与周期性任务
APC 结合定时器实现周期性任务时的优势:
- 高精度定时:内核定时器可以达到毫秒级精度;
- 低延迟响应:定时器到期后立即投递 APC,无需轮询;
- 资源高效:不需要单独的线程来处理定时器事件。
场景三:跨线程通信
APC 是线程间通信的高效方式:
- 直接执行:APC 在目标线程的上下文中执行,可以直接访问线程的局部变量;
- 同步保证:APC 队列保证了执行顺序;
- 无需额外同步:APC 机制本身提供了线程安全保证。
场景四:线程控制与管理
在线程管理中的应用:
- 优雅退出:通过投递特殊 APC 实现线程的优雅终止;
- 状态查询:向目标线程投递 APC 以获取其运行状态;
- 强制中断:调试器使用 APC 强制中断目标线程的执行。
5.8.7.6 APC 与其他同步机制的对比
| 机制 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| APC | 异步执行、低延迟、线程上下文执行 | 需要线程处于可接收状态 | I/O 完成、定时器、线程控制 |
| 事件(Event) | 简单直观、支持多线程等待 | 需要手动轮询或阻塞等待 | 简单同步场景 |
| 互斥锁(Mutex) | 保证互斥访问 | 可能导致阻塞 | 临界区保护 |
| 信号量(Semaphore) | 控制并发数量 | 复杂度较高 | 资源池管理 |
| 条件变量(Condition Variable) | 条件等待机制 | 需要配合锁使用 | 生产者-消费者模式 |
5.8.7.7 使用 APC 的性能优势
- 减少上下文切换:APC 在目标线程中直接执行,避免了线程间切换的开销;
- 降低内存占用:无需为每个任务创建独立线程;
- 提高响应速度:APC 优先级高于普通线程调度,能够快速响应;
- 资源复用:单个线程可以处理多个异步任务,提高资源利用率。
5.8.7.8 实际应用案例分析
案例一:高性能网络服务器
一个典型的高性能网络服务器使用 APC 的流程:
┌──────────────────────────────────────────────────────────────┐
│ 高性能网络服务器架构 │
├──────────────────────────────────────────────────────────────┤
│ │
│ AcceptEx() 接收连接 │
│ │ │
│ ▼ │
│ WSARecv() 异步接收数据 │
│ │ │
│ ▼ │
│ 数据到达 → 投递用户 APC │
│ │ │
│ ▼ │
│ APC 回调处理数据 │
│ │ │
│ ├─► 解析协议 │
│ ├─► 业务逻辑处理 │
│ └─► WSASend() 发送响应 │
│ │
└──────────────────────────────────────────────────────────────┘
案例二:后台任务调度
c
// 使用 APC 实现后台任务调度
void ScheduleBackgroundTask(HANDLE hThread, PVOID TaskData)
{
// 投递用户 APC,在目标线程中执行任务
QueueUserAPC(BackgroundTaskCallback, hThread, (ULONG_PTR)TaskData);
}
VOID WINAPI BackgroundTaskCallback(ULONG_PTR Parameter)
{
PVOID TaskData = (PVOID)Parameter;
// 执行后台任务
ProcessBackgroundTask(TaskData);
// 任务完成后的清理工作
CleanupTaskData(TaskData);
}
案例三:异步文件操作
c
// 异步读取文件并通过 APC 回调通知完成
HANDLE hFile = CreateFile(L"data.dat", GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
OVERLAPPED overlapped = {0};
overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 发起异步读取,指定 APC 回调
ReadFileEx(hFile, buffer, bufferSize, &overlapped, ReadCompletionRoutine);
// 线程可以继续执行其他任务...
// APC 回调函数
VOID CALLBACK ReadCompletionRoutine(DWORD dwErrorCode,
DWORD dwNumberOfBytesTransfered,
LPOVERLAPPED lpOverlapped)
{
// 文件读取完成,处理数据
ProcessFileData(buffer, dwNumberOfBytesTransfered);
}
5.8.8 设计哲学与常见问题
5.8.8.1 设计原理
- 优先级分层:特殊 APC > 普通内核 APC > 用户 APC;
- 环境隔离:双 APC 环境机制支持线程上下文切换;
- 同步保护:APC 队列使用自旋锁保护,支持多线程并发操作;
- 延迟执行:APC 不是立即执行,而是在合适时机分发,提高系统响应性。
5.8.8.2 性能考虑
- 锁竞争:APC 锁是全局的(按线程),高频 APC 投递可能产生竞争;
- IRQL 提升:投递和分发过程需要提升 IRQL,会阻塞低优先级中断;
- 队列遍历:特殊 APC 需要遍历队列找到插入位置,O(n) 复杂度。
5.8.8.3 常见 bug 与调试技巧
| 问题 | 现象 | 调试方法 |
|---|---|---|
| APC 未执行 | 线程挂起或不响应 | 检查 ApcQueueable、SpecialApcDisable、Alertable 标志 |
| APC 重复入队 | Inserted 检查失败 |
使用 KeRemoveQueueApc 确保 APC 已从队列移除 |
| 用户 APC 不执行 | 线程未进入 Alertable 状态 | 确认使用了 WaitForSingleObjectEx(..., TRUE) |
| 死锁 | 临界区内等待事件 | 避免在 Critical Region 内阻塞 |
5.8.9 为什么会这样------10 个设计哲学问答
Q1:为什么 APC 需要区分内核模式和用户模式?
A:安全隔离是核心原因。内核模式 APC 运行在内核地址空间,可以访问所有系统资源;用户模式 APC 运行在用户地址空间,受页表保护。这种区分防止了用户态代码通过 APC 访问内核资源,是 Windows 安全模型的重要组成部分。
Q2:为什么特殊 APC 可以在 Critical Region 内执行?
A :特殊 APC 用于处理紧急且不可延迟的任务,如线程终止、调试器中断。如果这些任务被延迟到临界区结束,可能导致系统死锁或调试器无法响应。因此特殊 APC 不受 KernelApcDisable 标志影响。
Q3:为什么需要 APC_LEVEL 这个特殊的 IRQL?
A:APC_LEVEL 提供了一个「半抢占」的执行环境:
- 高于 PASSIVE_LEVEL:可以禁用普通线程调度;
- 低于 DISPATCH_LEVEL:允许 DPC 和中断执行;
- 这使得 APC 可以在相对安全的环境中执行,同时不阻塞关键的系统中断。
Q4:为什么用户 APC 需要 Alertable 状态才能投递?
A:这是一种显式的协作机制。用户线程通过设置 Alertable 状态,表示「我愿意接受异步通知」。这避免了用户 APC 在不合适的时机(如持有锁时)被强制执行,减少了死锁风险。
Q5:为什么 APC 要设计双环境机制(ApcStateIndex)?
A:双环境机制支持线程在不同进程上下文间切换。当线程执行用户 APC 时,需要临时切换到目标进程的地址空间;执行完毕后再恢复原进程上下文。这在调试器、I/O 完成等场景中至关重要。
Q6:为什么 KiDeliverApc 要先处理特殊 APC?
A:特殊 APC 通常处理紧急任务(如线程终止),需要优先执行。如果先处理普通 APC,可能导致特殊 APC 延迟,进而影响系统响应性或导致死锁。
Q7:为什么 KeEnterCriticalRegion 只禁用普通内核 APC?
A:临界区保护的是共享数据结构的一致性。如果完全禁用所有 APC,包括特殊 APC,可能导致线程无法被终止,造成系统挂起。保留特殊 APC 的投递能力确保了系统的可管理性。
Q8:为什么用户 APC 需要通过 NTDLL 中转执行?
A :用户 APC 的 NormalRoutine 需要在用户态执行,但 CPU 在内核态中断返回时只能回到内核预定的入口点。NTDLL 提供了这个入口点(KiUserApcDispatcher),负责设置用户态执行环境并调用用户提供的例程。
Q9:为什么 APC 队列需要同步保护?
A:APC 队列可能被多个线程并发访问:一个线程投递 APC,另一个线程分发 APC,第三个线程可能移除 APC。使用自旋锁(APC Lock)确保队列操作的原子性,防止链表损坏。
Q10:为什么线程终止需要使用特殊 APC(PsExitSpecialApc)?
A:线程终止需要在用户态清理资源(如 TLS、用户态堆)。使用特殊 APC 确保终止操作能立即执行,不受临界区或其他 APC 禁用机制的影响,保证线程能够及时退出。
总结
APC 是 Windows 内核中实现异步执行的核心机制,通过分层设计(特殊 APC、普通内核 APC、用户 APC)和精细的控制机制(IRQL、临界区、Alertable 状态),实现了高效且安全的异步任务调度。理解 APC 机制是深入掌握 Windows 线程模型、I/O 子系统和调试器实现的关键。
核心要点回顾:
- APC 分为四种类型:内核特殊 APC、内核普通 APC、用户 APC;
- APC 的投递(
KeInsertQueueApc)和分发(KiDeliverApc)是两个独立阶段; - APC_LEVEL 是 APC 执行的关键 IRQL 级别;
- 特殊 APC 不受临界区保护,用于紧急任务;
- 用户 APC 需要线程处于 Alertable 状态才能执行。
本章代码索引
| 文件 | 内容 |
|---|---|
| ntoskrnl/ke/apc.c(file:///d:/reactos/ntoskrnl/ke/apc.c) | APC 核心实现 |
| sdk/include/ndk/ketypes.h(file:///d:/reactos/sdk/include/ndk/ketypes.h) | KAPC、KAPC_STATE 结构定义 |
| modules/rostests/tests/apc/apc.c(file:///d:/reactos/modules/rostests/tests/apc/apc.c) | APC 测试代码 |
| modules/rostests/apitests/kernel32/QueueUserAPC.c(file:///d:/reactos/modules/rostests/apitests/kernel32/QueueUserAPC.c) | QueueUserAPC API 测试 |