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
相关推荐
海寻山3 小时前
Java 泛型 (Generic) 入门到精通:语法 + 原理 + 实战 + 避坑
java·windows·python
idolao3 小时前
PixPin 1.5安装与配置教程 Windows版:解压+管理员运行+自定义路径+开机自启设置指南
windows
无限进步_3 小时前
【C++】私有虚函数与多态:访问权限不影响动态绑定
开发语言·c++·ide·windows·git·算法·visual studio
SuperEugene4 小时前
Vue3 前端配置驱动避坑:配置冗余、渲染性能、扩展性问题解决|配置驱动开发实战篇
前端·javascript·vue.js·驱动开发·前端框架
公子小六4 小时前
基于.NET的Windows窗体编程之WinForms事件简介
windows·microsoft·c#·.net
CyL_Cly5 小时前
帝国时代4修改器 风灵月影十一项 支持1.0-v10.0.576版本
windows
H Journey5 小时前
Windows 下 使用VSCode 编写C++程序中文乱码问题
c++·windows·vscode·cmake/gcc
2603_954138395 小时前
使用工具在 Windows 11/10/8/7 中扩展 C 盘的 3 种免费方法
windows
路溪非溪5 小时前
抓取手机的蓝牙HCI日志并分析
linux·arm开发·驱动开发·智能手机