一、数据路径概述数据路径是 Wintun 驱动的核心性能所在。它实现了零拷贝或极低拷贝的数据交换通过用户态共享的环形缓冲区驱动直接读写 IP 数据包然后与 NDIS 网络栈交换NET_BUFFER_LISTNBL结构。数据路径包括两个方向发送方向应用程序 → 网络栈应用程序调用WintunAllocateSendPacket/SendPacket将数据写入接收环驱动视角的 Receive Ring驱动从中读取并构造 NBL 提交给 NDIS。接收方向网络栈 → 应用程序NDIS 可能产生数据包实际应用中数据包来自系统路由或上层协议驱动从 NDIS 接收 NBL将其复制到发送环驱动视角的 Send Ring供应用程序读取。注意名称容易混淆但代码中清晰定义了Ctx-Device.Send.Ring驱动向应用程序提供数据即应用程序读取。Ctx-Device.Receive.Ring驱动从应用程序接收数据即应用程序写入。本文档遵循代码命名按“发送”驱动发送给 NDIS和“接收”驱动从 NDIS 接收来描述。二、发送路径应用程序 → 驱动 → NDIS此路径由TunSendNetBufferLists处理当 NDIS 调用驱动的发送例程时触发实际上NDIS 不会主动调用而是由上层协议驱动发起但 Wintun 作为 miniport其发送例程是由 NDIS 调用的用于将上层的数据包传递给 miniport而 Wintun 的发送例程是将这些数据包放入环形缓冲区供应用程序读取——需要澄清在 NDIS miniport 中SendNetBufferLists是 miniport 向协议层发送数据的函数还是协议层向 miniport 发送数据的函数实际上在 NDIS 中MiniportSendNetBufferLists是由协议驱动调用用于将数据包发送给 miniport即发送到网络。而 Wintun 作为虚拟适配器其发送方向从系统网络栈到虚拟适配器应该是将数据包送给用户态应用。但 Wintun 的设计是系统网络栈发送数据包给 Wintun 适配器时驱动应将这些包放入环形缓冲区供用户态读取。因此TunSendNetBufferLists正是这个功能——它接收来自协议层的 NBL将其数据复制到Send.Ring即应用程序读取的环并唤醒用户态。所以TunSendNetBufferLists系统要发送数据给 Wintun 适配器即出站流量驱动将数据放入Send.Ring应用程序通过WintunReceivePacket读取。TunReturnNetBufferLists当应用程序处理完接收的数据后通过WintunReleaseReceivePacket驱动释放对应的 NBL。TunProcessReceiveData内核线程负责从Receive.Ring应用程序写入的环读取数据构造 NBL 并调用NdisMIndicateReceiveNetBufferLists向网络栈指示收到数据即入站流量。因此数据流向是系统 → 驱动 → 用户态通过TunSendNetBufferLists将 NBL 写入Send.Ring。用户态 → 驱动 → 系统通过TunProcessReceiveData从Receive.Ring读取构造 NBL 并指示给系统。下面分别剖析。2.1 发送 NBL 到用户态TunSendNetBufferLists功能将从协议层收到的 NBL即系统要发送到虚拟网卡的数据包放入Send.Ring供用户态应用程序读取。流程遍历 NBL 计算总大小统计包数和所需环空间每个包对齐后占TUN_ALIGN(sizeof(TUN_PACKET)PacketSize)。获取共享锁ExAcquireSpinLockShared(Ctx-TransitionLock)检查Running和Device.Disconnected状态若不可用则直接完成 NBL 并标记失败。获取发送环自旋锁KeAcquireInStackQueuedSpinLock(Ctx-Device.Send.Lock, LockHandle)。分配环空间读取Ring-Head驱动已消费位置计算可用空间RingSpace TUN_RING_WRAP(Head - RingTail - TUN_ALIGNMENT, Capacity)。若空间不足返回NDIS_STATUS_BUFFER_OVERFLOW。更新Ctx-Device.Send.RingTail为RingTail RequiredRingSpace回绕。将 NBL 加入活动列表通过TunNblInitialize设置偏移NBL 在环中的尾指针位置并链接到ActiveNbls链表尾部。释放自旋锁但继续持有共享锁。复制数据到环遍历每个 NBL 的每个 NET_BUFFER从NdisGetDataBuffer获取数据指针拷贝到环中对应位置TUN_PACKET头部包含大小数据紧随其后。若拷贝失败数据指针不可用则填充零并标记丢弃。更新本地RingTail指针。标记 NBL 为已完成TunNblMarkCompleted(NetBufferLists)。批量更新环的 Tail 指针重新获取自旋锁遍历ActiveNbls链表将已完成且连续的 NBL 依次写出Ring-TailWriteULongRelease并触发Send.TailMoved事件唤醒用户态等待。调用NdisMSendNetBufferListsComplete完成这些 NBL。释放共享锁更新统计信息ifHCOutOctets等。关键点批量提交多个 NBL 可一次性复制到环然后一次性更新 Tail减少用户态唤醒次数。错误处理若复制失败仍占据环空间写入零包避免碎片。2.2 返回接收 NBLTunReturnNetBufferLists当应用程序通过WintunReleaseReceivePacket释放了一个已读取的包时驱动会收到ReturnNetBufferLists调用从而可以更新环的Head指针释放空间供后续写入。流程遍历返回的 NBL标记为已完成TunNblMarkCompleted。在自旋锁保护下遍历Receive.ActiveNbls链表找出所有已完成且连续的 NBL将其从链表中移除并更新Ring-HeadWriteULongRelease。若ActiveNbls.Head变为空设置Empty事件通知等待的接收线程。释放 NBL 相关的 MDL 和 NBL 内存使用NdisFreeNetBufferList。三、接收路径用户态 → 驱动 → 系统此路径由内核线程TunProcessReceiveData驱动它不断轮询Receive.Ring应用程序写入数据构造 NBL 并指示给 NDIS。3.1 接收线程TunProcessReceiveData初始化线程优先级设为 1KeSetPriorityThread计算自旋超时1/10 ms。主循环读取Ring-Head驱动已处理位置和Ring-Tail应用程序已写入位置。若Head Tail则进入等待设置Ring-Alertable TRUE内存屏障。重新检查Tail若无变化则调用KeWaitForMultipleObjects等待Disconnected或Receive.TailMoved事件。唤醒后清除Alertable继续循环。否则从Head位置读取一个TUN_PACKET获取大小校验合法性。分配 MDL 和 NBL使用IoAllocateMdl为数据分配 MDL并通过IoBuildPartialMdl使其指向环中数据避免拷贝。调用NdisAllocateNetBufferAndNetBufferList分配 NBL。设置帧类型IPv4 或 IPv6和标志。将 NBL 加入Receive.ActiveNbls链表自旋锁保护。调用NdisMIndicateReceiveNetBufferLists向 NDIS 协议层指示收到数据包。更新本地Head指针继续循环。错误处理若分配失败则丢弃该包增加ifInDiscards并等待ActiveNbls.Empty事件确保之前的 NBL 已返回后才更新Ring-Head避免资源不足时死循环。3.2 接收线程的退出当Device.Disconnected事件被触发注销时线程会退出循环并将Ring-Head设为MAXULONG表示环已失效。随后线程等待所有 NBL 返回ActiveNbls.Empty然后结束。四、环形缓冲区管理细节4.1 数据结构TUN_RINGtypedefstruct_TUN_RING{volatileULONG Head;// 消费者已读位置驱动或用户态volatileULONG Tail;// 生产者已写位置volatileLONG Alertable;// 是否处于可唤醒状态UCHAR Data[];}TUN_RING;在共享内存中Head和Tail是原子访问的使用ReadULongAcquire/WriteULongRelease确保顺序一致性。4.2 容量计算TUN_RING_CAPACITY(Size)返回实际可用于数据的字节数减去头部开销和一个最大包对齐填充。容量必须为 2 的幂以便使用 (Capacity-1)回绕。4.3 包对齐与填充每个包在环中存储时起始地址对齐到TUN_ALIGNMENT4 字节。包结构为TUN_PACKET4 字节大小 数据总大小向上对齐到 4 的倍数。环末尾预留了TUN_MAX_PACKET_SIZE - TUN_ALIGNMENT的额外空间确保最大包不需要回绕。4.4 事件通知当驱动将数据写入Send.Ring并更新Tail后它会调用KeSetEvent触发Send.TailMoved用户态可通过WintunGetReadWaitEvent获取该事件句柄并等待。当应用程序将数据写入Receive.Ring并更新Tail后应用程序需设置Receive.TailMoved事件此事件句柄已在注册时传递给驱动驱动线程会等待该事件通过WaitForMultipleObjects。五、性能优化零拷贝接收方向使用IoBuildPartialMdl直接映射用户态环中的数据无需额外拷贝。批量处理发送方向累积多个 NBL 一次提交接收方向循环处理多个包。无锁竞争读路径TunSendNetBufferLists和TunProcessReceiveData使用共享锁写路径注册/注销使用独占锁以及自旋锁保护链表操作。自旋等待接收线程在无数据时会短暂自旋最多 0.1 ms再等待事件减少上下文切换开销。六、总结数据路径是 Wintun 高性能的关键。通过共享环形缓冲区和细致的并发控制驱动与用户态进程实现了高效的数据交换。发送方向将系统网络包传递给用户态应用接收方向将用户态数据注入系统网络栈二者相互独立且并发充分利用了多核 CPU 的能力。下一篇文章将涵盖 OID 请求处理、统计信息和辅助功能。