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
相关推荐
AxureMost7 小时前
XnConvert v1.111.0 图像格式转换调整
windows
铁皮哥8 小时前
【后端/Agent 开发】给你的项目配置一套 .claude/ 工作流:别再裸用 Claude Code 了!
java·windows·python·spring·github·maven·生活
装杯让你飞起来啊11 小时前
第 4 周 Unit 2:Jetpack Compose 状态、按钮、计数器与小费计算器
windows·microsoft·kotlin·安卓
菜鸟的日志11 小时前
【嵌入系统】嵌入式学习笔记(一)
windows·笔记·嵌入式硬件·学习·ubuntu·操作系统
深念Y11 小时前
装了 PowerShell 7 还是乱码?
windows·乱码·终端·命令行
相国12 小时前
在Windows里通过WSL安装Ubuntu 22.04
linux·windows·ubuntu·wsl
x***r15113 小时前
phpwind_UTF8_8.5部署步骤详解(附PHPWind论坛搭建与本地环境配置)
windows
酿情师14 小时前
网络攻防技术:Windows操作系统的攻防
网络·windows
倔强的石头10614 小时前
kingbase备份与恢复实战(六)—— 备份自动化与保留策略:Windows任务计划+日志追溯
运维·windows·自动化
枳实-叶14 小时前
【Linux驱动开发】第三天:上下文核心概念全解 —— 进程/中断上下文+切换开销+TLB刷新原理
linux·驱动开发