如何无需打开虚拟COM端口即可向其写入数据?(UMDF)
解决VirtualSerial2虚拟COM口多应用同时连接问题
VirtualSerial2示例默认是独占访问模式,所以无法同时被两个应用打开。要实现一个写应用+一个读应用同时连接,需要修改驱动的打开逻辑、上下文管理和读写数据转发机制,具体步骤如下:
1. 修改设备访问权限,允许共享打开
在驱动创建设备的代码中,移除独占访问标记,允许多个句柄共享设备:
- 找到调用
IoCreateDevice的代码,确保设备对象的Flags不包含DO_EXCLUSIVE - 创建符号链接时,设置共享权限为
FILE_SHARE_READ | FILE_SHARE_WRITE,示例代码:
UNICODE_STRING symbolicLinkName; RtlInitUnicodeString(&symbolicLinkName, L"\\DosDevices\\VirtualCOM"); IoCreateSymbolicLink(&symbolicLinkName, &deviceName); // 创建设备时指定共享权限 IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &deviceName, FILE_DEVICE_SERIAL_PORT, 0, FALSE, &DeviceObject); // 确保设备支持共享读写 DeviceObject->Flags &= ~DO_EXCLUSIVE;
2. 扩展设备上下文,管理多打开实例
原来的设备扩展只保存单个连接的状态,现在需要维护一个多打开实例的链表:
- 修改
DEVICE_EXTENSION结构,添加链表头和同步锁:
typedef struct _DEVICE_EXTENSION { PDEVICE_OBJECT DeviceObject; UNICODE_STRING DeviceName; LIST_ENTRY OpenContextList; // 保存所有打开的上下文 KSPIN_LOCK ContextListLock; // 保护链表的自旋锁 // 共享数据缓冲区 UCHAR* SharedBuffer; ULONG BufferSize; ULONG WritePos; ULONG ReadPos; KEVENT BufferNotEmptyEvent; // 读等待事件 } DEVICE_EXTENSION, *PDEVICE_EXTENSION;
- 每个打开的句柄对应一个
OPEN_CONTEXT结构:
typedef struct _OPEN_CONTEXT { LIST_ENTRY ListEntry; PDEVICE_EXTENSION DeviceExtension; KEVENT ReadEvent; // 该实例的读等待事件(可选) } OPEN_CONTEXT, *POPEN_CONTEXT;
3. 修改IRP_MJ_CREATE/CLOSE处理逻辑
- IRP_MJ_CREATE:创建新的
OPEN_CONTEXT,加入设备扩展的链表:
NTSTATUS VirtualSerialCreate(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; POPEN_CONTEXT openCtx = ExAllocatePoolWithTag(NonPagedPoolNx, sizeof(OPEN_CONTEXT), 'vS2C'); if (!openCtx) { Irp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_INSUFFICIENT_RESOURCES; } // 初始化上下文 InitializeListHead(&openCtx->ListEntry); openCtx->DeviceExtension = devExt; KeInitializeEvent(&openCtx->ReadEvent, SynchronizationEvent, FALSE); // 加锁插入链表 KIRQL oldIrql; KeAcquireSpinLock(&devExt->ContextListLock, &oldIrql); InsertTailList(&devExt->OpenContextList, &openCtx->ListEntry); KeReleaseSpinLock(&devExt->ContextListLock, &oldIrql); // 将上下文关联到IRP的FileObject Irp->FileObject->FsContext = openCtx; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; }
- IRP_MJ_CLOSE:从链表中移除并销毁
OPEN_CONTEXT:
NTSTATUS VirtualSerialClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { POPEN_CONTEXT openCtx = (POPEN_CONTEXT)Irp->FileObject->FsContext; PDEVICE_EXTENSION devExt = openCtx->DeviceExtension; // 加锁移除链表 KIRQL oldIrql; KeAcquireSpinLock(&devExt->ContextListLock, &oldIrql); RemoveEntryList(&openCtx->ListEntry); KeReleaseSpinLock(&devExt->ContextListLock, &oldIrql); ExFreePoolWithTag(openCtx, 'vS2C'); Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; }
4. 重写读写逻辑,实现数据转发
写操作(IRP_MJ_WRITE)
将写入的数据存入共享缓冲区,然后唤醒所有等待读的实例:
NTSTATUS VirtualSerialWrite(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp); ULONG writeLength = irpStack->Parameters.Write.Length; PUCHAR writeBuffer = Irp->AssociatedIrp.SystemBuffer; if (writeLength == 0 || !writeBuffer) { Irp->IoStatus.Status = STATUS_INVALID_PARAMETER; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_INVALID_PARAMETER; } // 加锁操作共享缓冲区 KIRQL oldIrql; KeAcquireSpinLock(&devExt->ContextListLock, &oldIrql); // 拷贝数据到环形缓冲区(简化示例,需处理溢出) ULONG copyLen = min(writeLength, devExt->BufferSize - devExt->WritePos); RtlCopyMemory(devExt->SharedBuffer + devExt->WritePos, writeBuffer, copyLen); devExt->WritePos = (devExt->WritePos + copyLen) % devExt->BufferSize; // 唤醒所有等待读的事件 KeSetEvent(&devExt->BufferNotEmptyEvent, IO_NO_INCREMENT, FALSE); KeReleaseSpinLock(&devExt->ContextListLock, &oldIrql); Irp->IoStatus.Information = copyLen; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; }
读操作(IRP_MJ_READ)
检查共享缓冲区是否有数据,有则拷贝返回;无则挂起IRP等待写操作唤醒:
NTSTATUS VirtualSerialRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp); ULONG readLength = irpStack->Parameters.Read.Length; PUCHAR readBuffer = Irp->AssociatedIrp.SystemBuffer; KIRQL oldIrql; KeAcquireSpinLock(&devExt->ContextListLock, &oldIrql); // 检查缓冲区是否有数据 ULONG available = (devExt->WritePos >= devExt->ReadPos) ? (devExt->WritePos - devExt->ReadPos) : (devExt->BufferSize - devExt->ReadPos + devExt->WritePos); if (available > 0) { ULONG copyLen = min(readLength, available); if (devExt->ReadPos + copyLen <= devExt->BufferSize) { RtlCopyMemory(readBuffer, devExt->SharedBuffer + devExt->ReadPos, copyLen); } else { ULONG firstPart = devExt->BufferSize - devExt->ReadPos; RtlCopyMemory(readBuffer, devExt->SharedBuffer + devExt->ReadPos, firstPart); RtlCopyMemory(readBuffer + firstPart, devExt->SharedBuffer, copyLen - firstPart); } devExt->ReadPos = (devExt->ReadPos + copyLen) % devExt->BufferSize; Irp->IoStatus.Information = copyLen; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); } else { // 缓冲区无数据,挂起IRP等待写事件 IoMarkIrpPending(Irp); KeInitializeEvent(&Irp->UserEvent, NotificationEvent, FALSE); IoSetCompletionRoutine(Irp, VirtualSerialReadCompletion, NULL, TRUE, TRUE, TRUE); KeWaitForSingleObject(&devExt->BufferNotEmptyEvent, Executive, KernelMode, FALSE, NULL); Irp->IoStatus.Status = STATUS_PENDING; } KeReleaseSpinLock(&devExt->ContextListLock, &oldIrql); return Irp->IoStatus.Status; } // 读操作完成例程(简化) NTSTATUS VirtualSerialReadCompletion(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context) { if (Irp->PendingReturned) { IoMarkIrpPending(Irp); } return STATUS_SUCCESS; }
5. 初始化和清理资源
- 在驱动
DriverEntry中初始化设备扩展的链表、锁和缓冲区:
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { // ... 原有代码 ... PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; InitializeListHead(&devExt->OpenContextList); KeInitializeSpinLock(&devExt->ContextListLock); devExt->BufferSize = 4096; // 设置4KB共享缓冲区 devExt->SharedBuffer = ExAllocatePoolWithTag(NonPagedPoolNx, devExt->BufferSize, 'vS2B'); KeInitializeEvent(&devExt->BufferNotEmptyEvent, SynchronizationEvent, FALSE); // ... 原有代码 ... }
- 在驱动卸载时清理共享缓冲区:
VOID VirtualSerialUnload(IN PDRIVER_OBJECT DriverObject) { PDEVICE_OBJECT DeviceObject = DriverObject->DeviceObject; PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; if (devExt->SharedBuffer) { ExFreePoolWithTag(devExt->SharedBuffer, 'vS2B'); } // ... 原有清理代码 ... }
注意事项
- 同步问题:所有访问共享缓冲区和上下文链表的操作必须加锁,避免并发冲突
- 缓冲区溢出:示例中简化了溢出处理,实际需要实现覆盖旧数据或返回错误的逻辑
- IRP挂起:确保挂起的IRP在设备卸载或关闭时能被正确唤醒,避免系统死锁
- 测试:修改后重新编译驱动,签名后安装,用Putty打开读,自己的应用打开写,验证数据是否能正常转发
内容的提问来源于stack exchange,提问作者0xReadMe
相关产品推荐
相关产品推荐

