Windows 驱动开发(四)—— IRP Pending

概述

通常驱动收到 IRP 后会立即处理并完成。但在某些场景下,驱动无法立即响应------例如用户态程序等待内核事件时,内核可能暂时没有数据。这时需要用到 IRP Pending 机制:将 IRP 挂起,等到数据就绪时再完成它。

本文以 src/drv/ioctl.cc 的实现为例,分析 IRP Pending 在实际使用中需要考虑的问题。


问题一:如何标记和返回 Pending 状态

挂起 IRP 需要两个配合动作,缺一不可:

cpp 复制代码
static void PendingIrpRoutine(PIRP irp) {
    IoMarkIrpPending(irp);  // 在 IRP 栈位置设置 SL_PENDING_RETURNED 标志
    // ...
}

NTSTATUS Dispatcher(PIRP irp) {
    // ...
    PendingIrpRoutine(irp);
    return STATUS_PENDING;  // 分发函数必须返回 STATUS_PENDING
}

IoMarkIrpPending 告知 I/O 管理器此 IRP 将异步完成;分发函数必须同步返回 STATUS_PENDING。两者必须同时满足,否则 I/O 管理器的状态与驱动行为不一致,会导致系统崩溃或数据损坏。


问题二:挂起前与取消之间的竞态

在调用 IoMarkIrpPending 之后、注册取消例程之前,存在一个窗口期。如果 I/O 管理器在这个窗口期取消 IRP,由于还没有取消例程,I/O 管理器什么也不做,而驱动随后将这个已被取消的 IRP 加入挂起队列------IRP 将永远不会被完成,造成泄漏。

解决方法是在 Cancel SpinLock 的保护下,先检查 irp->Cancel 再注册取消例程:

cpp 复制代码
static void PendingIrpRoutine(PIRP irp) {
    IoMarkIrpPending(irp);

    KIRQL irql;
    IoAcquireCancelSpinLock(&irql);

    if (irp->Cancel) {
        // IRP 在进入这里之前已被取消,直接完成,不入队
        IoReleaseCancelSpinLock(irql);
        CompleteIrp(irp, STATUS_CANCELLED, 0);
        return;
    }

    IoSetCancelRoutine(irp, NotifyIrpCancelRoutine);
    ioctl_ctx_->irp_list.Push(irp);

    IoReleaseCancelSpinLock(irql);
}

Cancel SpinLock 是系统全局锁,I/O 管理器在调用取消例程前也必须持有它。因此,在持有该锁期间检查 irp->Cancel,能保证检查与注册之间不会被取消逻辑插入。


问题三:取消例程的约定

取消例程进入时,I/O 管理器已持有 Cancel SpinLock驱动必须在例程内释放它,释放后再处理 IRP:

cpp 复制代码
static VOID NotifyIrpCancelRoutine(PDEVICE_OBJECT, PIRP irp) {
    IoReleaseCancelSpinLock(irp->CancelIrql);  // 必须先释放锁
    if (!ioctl_ctx_ || !ioctl_ctx_->irp_list.Pop(irp)) return;
    CompleteIrp((PIRP)irp, STATUS_CANCELLED, 0);
}

取消例程的职责:从挂起队列中移除该 IRP,并以 STATUS_CANCELLED 完成它。


问题四:完成挂起 IRP 时与取消的竞态

当数据到来时,SendMessageirp_list 取出 IRP 并准备完成它。与此同时,I/O 管理器可能也在并发地触发取消流程。两条路径如下:

路径 A:SendMessage 先到达

  1. SendMessage 通过 irp_list.Pop() 取出 IRP;
  2. I/O 管理器随后调用取消例程,取消例程执行 irp_list.Pop(irp) 按指针查找,但 IRP 已不在队列中,返回 nullptr,直接退出;
  3. SendMessage 独占 IRP,写入数据并以 STATUS_SUCCESS 完成。

路径 B:I/O 管理器先到达

  1. I/O 管理器先于 SendMessage 清空 IRP 的取消例程槽,并调用取消例程;
  2. 取消例程执行 irp_list.Pop(irp),从队列中取出 IRP,并以 STATUS_CANCELLED 完成它;
  3. SendMessage 随后执行 irp_list.Pop(),队列已空,返回 nullptr,不操作任何 IRP。

两条路径互斥,看似干净。但两者之间存在一个真正的竞态窗口:SendMessage 已从队列取出 IRP,但 I/O 管理器也已清空了取消例程槽 (清空槽和调用例程之间有间隙)。此时取消例程尚未执行,SendMessage 却已持有 IRP------如果 SendMessage 直接写入数据完成 IRP,后续取消例程再次完成同一个 IRP,就会发生双重完成(double-complete),导致崩溃。

区分这一窗口的方法是 IoSetCancelRoutine 的返回值:

cpp 复制代码
auto irp = (PIRP)ioctl_ctx_->irp_list.Pop();
if (irp) {
    if (IoSetCancelRoutine(irp, nullptr) == nullptr) {
        // 返回 nullptr:I/O 管理器已清空取消例程槽
        // 取消例程即将或正在执行,但不清理IRP
        CleanupPendingIrp(irp);
    } else {
        // 返回非 nullptr:SendMessage 抢先清空了取消例程槽
        // 取消例程不会再被调用,SendMessage 独占 IRP
        CompleteIrpWithPendingData(irp, msg);
    }
}

IoSetCancelRoutine(irp, nullptr) 将取消例程槽原子地清零并返回旧值。I/O 管理器在调用取消例程前也会做同样的原子清零操作,因此两者必然只有一个能拿到非空的旧值,从而确定唯一的 IRP 所有者。

路径 B 中 SendMessage 调用的 CleanupPendingIrp(irp) 并非防御性代码,而是这条路径下完成 IRP 的唯一出口:取消例程因队列中找不到 IRP 而退出,不会完成 IRP;SendMessage 必须负责以 STATUS_CANCELLED 将其完成,否则 IRP 泄漏。


问题五:驱动卸载时必须完成所有挂起的 IRP

驱动卸载时,irp_list 中可能仍有挂起的 IRP。这些 IRP 对应的用户态请求在等待完成,如果驱动直接卸载而不处理它们,I/O 管理器在后续操作中访问已卸载驱动的内存,必然蓝屏。

正确做法是在 finalize 时遍历队列,以 STATUS_CANCELLED 完成所有挂起 IRP:

cpp 复制代码
// IOCTLContext 析构时,SafeList::Clear() 对每个元素调用注册的清理函数
IOCTLContext(DataClean irp_clean, DataClean data_clean)
    : irp_list(irp_clean), data_list(data_clean) {}

// irp_clean 即:
static inline void CleanupPendingIrp(PVOID buffer) {
    if (!buffer) return;
    CompleteIrp((PIRP)buffer, STATUS_CANCELLED, 0);
}

通过将清理函数注入队列,析构时自动完成清理,不遗漏。


总结

IRP Pending 的实现需要在多个环节处理并发安全问题:

环节 问题 解决方法
挂起时 必须同时标记和返回 Pending IoMarkIrpPending + 返回 STATUS_PENDING
挂起前 标记后、注册取消例程前的取消竞态 持有 Cancel SpinLock 检查 irp->Cancel
取消例程 锁的所有权转移 进入后立即释放 Cancel SpinLock
完成时 完成 IRP 与取消例程的并发竞态 IoSetCancelRoutine 返回值判断归属
卸载时 遗留挂起 IRP 导致蓝屏 卸载前以 STATUS_CANCELLED 完成所有挂起 IRP
相关推荐
x***r1514 小时前
jdk-11.0.16.1_windows使用步骤详解(附JDK 11环境变量配置与验证教程)
java·开发语言·windows
玖釉-8 小时前
下一个排列:从字典序到原地算法的完整推导
数据结构·c++·windows·算法
Yunzenn8 小时前
深度分析字节最新研究cola-DLM 第 07 章:推理流水线逐行拆解 —— 从 prompt 到生成文本
人工智能·驱动开发·深度学习·chatgpt·架构·prompt·github
cddchina9 小时前
【Steps Recorder 和 Snipping Tool】
windows·效率工具·截图工具
我材不敲代码9 小时前
Python基础:列表详解、增删改查及常用高阶操作
开发语言·windows·python
KeanuReeves11 小时前
【常用操作】BAT常用脚本命令整理
windows
徐sir(徐慧阳)13 小时前
记一次生产库ORA-00257故障处理
windows·oracle·ora-00257
xiaoshuaishuai815 小时前
C# 服务注册与生命周期
开发语言·windows·c#
公子小六15 小时前
基于.NET的Windows窗体编程之WinForms打印
windows·microsoft·c#·.net·winforms
qq_4523962315 小时前
第三篇:《Docker 安装与配置指南(Linux / Windows / macOS)》
linux·windows·docker