Reactos 第 5 章 进程与线程 — 5.8 Windows 的 APC 机制

第 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)放到后厨队列里。当厨师(目标线程)空闲时(合适的执行环境),再按顺序处理这些订单。

本节内容概览

  1. 5.8.0 框架图:APC 从投递到分发的完整流程总览;
  2. 5.8.1 APC 数据结构:KAPC、KAPC_STATE 的结构与作用;
  3. 5.8.2 APC 类型与分类:内核 APC vs 用户 APC、特殊 APC vs 普通 APC;
  4. 5.8.3 APC 的投递机制:KeInitializeApc、KeInsertQueueApc、KiInsertQueueApc;
  5. 5.8.4 APC 的分发机制:KiDeliverApc、KiCheckForKernelApcDelivery;
  6. 5.8.5 APC 与 IRQL:APC_LEVEL、临界区、Guarded Region;
  7. 5.8.6 用户态 APC 实现:QueueUserAPC、NtQueueApcThread;
  8. 5.8.7 APC 的典型应用:I/O 完成、定时器、线程终止;
  9. 5.8.8 设计哲学与常见问题:性能考虑、调试技巧;
  10. 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 的执行需要特殊处理:

  1. 内核态准备KiDeliverApc 调用 KiInitializeUserApc
  2. 设置陷阱帧:修改 TrapFrame,将返回地址指向 ntdll 中的 APC 入口;
  3. 返回用户态:从中断返回时,CPU 执行 ntdll 的 APC 处理代码;
  4. 用户态执行 :ntdll 调用用户提供的 NormalRoutine
  5. 清理返回:执行完毕后返回原来的用户代码。

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 机制提供了以下优势:

  1. 非阻塞执行:发起 I/O 请求后,线程可以继续执行其他任务,无需阻塞等待;
  2. 减少线程数量:单个线程可以处理多个并发 I/O,避免创建大量线程带来的开销;
  3. 上下文切换优化:I/O 完成时直接在等待线程中执行回调,减少线程间通信。
场景二:定时器与周期性任务

APC 结合定时器实现周期性任务时的优势:

  1. 高精度定时:内核定时器可以达到毫秒级精度;
  2. 低延迟响应:定时器到期后立即投递 APC,无需轮询;
  3. 资源高效:不需要单独的线程来处理定时器事件。
场景三:跨线程通信

APC 是线程间通信的高效方式:

  1. 直接执行:APC 在目标线程的上下文中执行,可以直接访问线程的局部变量;
  2. 同步保证:APC 队列保证了执行顺序;
  3. 无需额外同步:APC 机制本身提供了线程安全保证。
场景四:线程控制与管理

在线程管理中的应用:

  1. 优雅退出:通过投递特殊 APC 实现线程的优雅终止;
  2. 状态查询:向目标线程投递 APC 以获取其运行状态;
  3. 强制中断:调试器使用 APC 强制中断目标线程的执行。

5.8.7.6 APC 与其他同步机制的对比

机制 优势 劣势 适用场景
APC 异步执行、低延迟、线程上下文执行 需要线程处于可接收状态 I/O 完成、定时器、线程控制
事件(Event) 简单直观、支持多线程等待 需要手动轮询或阻塞等待 简单同步场景
互斥锁(Mutex) 保证互斥访问 可能导致阻塞 临界区保护
信号量(Semaphore) 控制并发数量 复杂度较高 资源池管理
条件变量(Condition Variable) 条件等待机制 需要配合锁使用 生产者-消费者模式

5.8.7.7 使用 APC 的性能优势

  1. 减少上下文切换:APC 在目标线程中直接执行,避免了线程间切换的开销;
  2. 降低内存占用:无需为每个任务创建独立线程;
  3. 提高响应速度:APC 优先级高于普通线程调度,能够快速响应;
  4. 资源复用:单个线程可以处理多个异步任务,提高资源利用率。

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 设计原理

  1. 优先级分层:特殊 APC > 普通内核 APC > 用户 APC;
  2. 环境隔离:双 APC 环境机制支持线程上下文切换;
  3. 同步保护:APC 队列使用自旋锁保护,支持多线程并发操作;
  4. 延迟执行:APC 不是立即执行,而是在合适时机分发,提高系统响应性。

5.8.8.2 性能考虑

  • 锁竞争:APC 锁是全局的(按线程),高频 APC 投递可能产生竞争;
  • IRQL 提升:投递和分发过程需要提升 IRQL,会阻塞低优先级中断;
  • 队列遍历:特殊 APC 需要遍历队列找到插入位置,O(n) 复杂度。

5.8.8.3 常见 bug 与调试技巧

问题 现象 调试方法
APC 未执行 线程挂起或不响应 检查 ApcQueueableSpecialApcDisableAlertable 标志
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 子系统和调试器实现的关键。

核心要点回顾:

  1. APC 分为四种类型:内核特殊 APC、内核普通 APC、用户 APC;
  2. APC 的投递(KeInsertQueueApc)和分发(KiDeliverApc)是两个独立阶段;
  3. APC_LEVEL 是 APC 执行的关键 IRQL 级别;
  4. 特殊 APC 不受临界区保护,用于紧急任务;
  5. 用户 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 测试
相关推荐
PHP隔壁老王邻居1 小时前
windows菜单搜索栏无法显示历史记录或者无法使用修复方法
windows
道一232 小时前
Windows系统查看端口占用进程的3种实用方法
windows·笔记
半条-咸鱼2 小时前
【INACCESSIBLE_BOOT_DEVICE】安装 Config Tool 后 Windows 蓝屏,最终通过 VMware 虚拟机解决
windows·stm32·vmware·芯片
努力攻坚操作系统3 小时前
编程语言编译运行机制对比:C / Java / Python
java·c语言·python
人工小情绪3 小时前
Windows 安装 Codex 桌面版,并用 CC Switch 管理配置
人工智能·windows·codex·cc switch
学会去珍惜4 小时前
C语言简介
c语言·开发语言
凡人叶枫4 小时前
Effective C++ 条款07:为多态基类声明 virtual 析构函数
linux·c语言·开发语言·c++
matlabgoodboy5 小时前
计算机java程序代写python代码编写c/c++代做qt设计php开发matlab
java·c语言·python
caimouse5 小时前
Reactos 第 5 章 进程与线程 — 5.11 线程本地存储 TLS
c语言·windows