如何解决跨进程使用DeviceIoControl时的读写阻塞问题?
KMDF驱动读写IOCTL阻塞问题分析与修复
问题根源
先读后写时请求阻塞的核心问题集中在共享资源未同步、挂起请求参数误用两个方面:
1. 共享资源无同步保护
设备扩展中的PendingRequest、isDataAdded属于多IO请求共享的变量,但代码中未使用任何同步机制(如自旋锁),并发访问时会出现竞态条件,导致请求状态混乱。
2. 挂起读请求的参数误用
在IOCTL_WRITE分支处理挂起的读请求时,错误使用了当前写请求的OutputBufferLength,而非从挂起的读请求本身获取输出缓冲区长度。这会导致ZwReadFile调用时传入错误的缓冲区大小,要么读取失败,要么请求无法正常完成。
3. 挂起请求未支持取消
读请求挂起时未调用WdfRequestMarkCancelable,若用户取消请求,驱动无法响应,会导致请求永久阻塞。
修复方案
步骤1:为设备扩展添加同步锁
在设备扩展结构体中添加自旋锁,保护共享变量的访问:
typedef struct _CONTROL_DEVICE_EXTENSION { // ... 原有成员 WDFSPINLOCK RequestLock; // 新增:保护PendingRequest、isDataAdded的自旋锁 WDFREQUEST PendingRequest; BOOLEAN isDataAdded; HANDLE FileHandle; // ... 其他成员 } CONTROL_DEVICE_EXTENSION, *PCONTROL_DEVICE_EXTENSION;
在设备初始化时创建自旋锁:
NTSTATUS ControlDeviceCreate(...) { // ... 原有代码 WDF_OBJECT_ATTRIBUTES attributes; WDF_OBJECT_ATTRIBUTES_INIT(&attributes); attributes.ParentObject = Device; status = WdfSpinLockCreate(&attributes, &devExt->RequestLock); if (!NT_SUCCESS(status)) { return status; } // ... 其他初始化 }
步骤2:修复读请求挂起逻辑
在IOCTL_READ分支中,使用自旋锁保护共享变量,并为挂起请求添加取消支持:
case IOCTL_READ: status = WdfRequestRetrieveOutputBuffer(Request, 0, &outBuf, &bufSize); if(!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); return; } devExt = ControlGetData(WdfIoQueueGetDevice(Queue)); WdfSpinLockAcquire(devExt->RequestLock); if(devExt->FileHandle) { if (!devExt->isDataAdded) { if (!devExt->PendingRequest) { KdPrint(("FileEvtIoRead, data not available...\n")); // 标记请求可取消 status = WdfRequestMarkCancelable(Request, EvtRequestCancel); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); WdfSpinLockRelease(devExt->RequestLock); return; } devExt->PendingRequest = Request; WdfSpinLockRelease(devExt->RequestLock); return; } else { WdfRequestComplete(Request, STATUS_UNSUCCESSFUL); WdfSpinLockRelease(devExt->RequestLock); return; } } // 有数据,直接读取 position.CurrentByteOffset.QuadPart = 0; WdfSpinLockRelease(devExt->RequestLock); // 释放锁后执行文件操作 status = ZwSetInformationFile(devExt->FileHandle, &ioStatus, &position, sizeof(FILE_POSITION_INFORMATION), FilePositionInformation); if (NT_SUCCESS(status)) { status = ZwReadFile(devExt->FileHandle, NULL, NULL, NULL, &ioStatus, outBuf, (ULONG)bufSize, 0, NULL); if (NT_SUCCESS(status)) { devExt->isDataAdded = FALSE; bytesRead = ioStatus.Information; } else { status = ioStatus.Status; } } else { status = ioStatus.Status; } } else { WdfSpinLockRelease(devExt->RequestLock); status = STATUS_INVALID_HANDLE; } WdfRequestSetInformation(Request, bytesRead); WdfRequestComplete(Request, status); break;
同时添加取消请求的回调函数:
VOID EvtRequestCancel( IN WDFREQUEST Request ) { PCONTROL_DEVICE_EXTENSION devExt = ControlGetData(WdfRequestGetDevice(Request)); WdfSpinLockAcquire(devExt->RequestLock); if (devExt->PendingRequest == Request) { devExt->PendingRequest = NULL; WdfRequestUnmarkCancelable(Request); WdfRequestComplete(Request, STATUS_CANCELLED); } WdfSpinLockRelease(devExt->RequestLock); }
步骤3:修复写请求处理挂起读请求的逻辑
在IOCTL_WRITE分支中,使用自旋锁保护共享变量,并从挂起的读请求中获取正确的输出缓冲区参数:
case IOCTL_WRITE: status = WdfRequestRetrieveInputBuffer(Request, 0, &inBuf, &bufSize); if(!NT_SUCCESS(status)) { status = STATUS_INSUFFICIENT_RESOURCES; WdfRequestCompleteWithInformation(Request, status, 0); return; } devExt = ControlGetData(WdfIoQueueGetDevice(Queue)); if(devExt->FileHandle) { position.CurrentByteOffset.QuadPart = 0; status = ZwSetInformationFile(devExt->FileHandle, &ioStatus, &position, sizeof(FILE_POSITION_INFORMATION), FilePositionInformation); if (NT_SUCCESS(status)) { status = ZwWriteFile(devExt->FileHandle, NULL, NULL, NULL, &ioStatus, inBuf, (ULONG)InputBufferLength, 0, NULL); status = ioStatus.Status; bytesWritten = ioStatus.Information; } else { status = ioStatus.Status; } } else { status = STATUS_INVALID_HANDLE; } // 完成当前写请求 WdfRequestCompleteWithInformation(Request, status, bytesWritten); // 检查并处理挂起的读请求 WdfSpinLockAcquire(devExt->RequestLock); WDFREQUEST pendingReadReq = devExt->PendingRequest; if (pendingReadReq != NULL) { devExt->PendingRequest = NULL; WdfRequestUnmarkCancelable(pendingReadReq); WdfSpinLockRelease(devExt->RequestLock); // 从读请求中获取输出缓冲区和长度 PCHAR readOutBuf = NULL; size_t readBufSize = 0; rstatus = WdfRequestRetrieveOutputBuffer(pendingReadReq, 0, &readOutBuf, &readBufSize); if (!NT_SUCCESS(rstatus)) { WdfRequestComplete(pendingReadReq, rstatus); return; } position.CurrentByteOffset.QuadPart = 0; rstatus = ZwSetInformationFile(devExt->FileHandle, &ioStatus, &position, sizeof(FILE_POSITION_INFORMATION), FilePositionInformation); if (NT_SUCCESS(rstatus)) { rstatus = ZwReadFile(devExt->FileHandle, NULL, NULL, NULL, &ioStatus, readOutBuf, (ULONG)readBufSize, 0, NULL); if (NT_SUCCESS(rstatus)) { WdfRequestCompleteWithInformation(pendingReadReq, rstatus, ioStatus.Information); WdfSpinLockAcquire(devExt->RequestLock); devExt->isDataAdded = FALSE; WdfSpinLockRelease(devExt->RequestLock); } else { WdfRequestComplete(pendingReadReq, ioStatus.Status); } } else { WdfRequestComplete(pendingReadReq, ioStatus.Status); } } else { devExt->isDataAdded = TRUE; WdfSpinLockRelease(devExt->RequestLock); } return;
关键修复点说明
- 同步锁:确保
PendingRequest和isDataAdded在多IO请求并发访问时的状态一致性,避免竞态条件。 - 请求取消支持:防止挂起的读请求因用户取消而永久阻塞。
- 参数修正:从挂起的读请求本身获取输出缓冲区长度,而非复用写请求的参数,确保
ZwReadFile调用参数正确。
内容的提问来源于stack exchange,提问作者Артём Варнавский
相关产品推荐
相关产品推荐

