You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何无需打开虚拟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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 14:25:54