ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

windows 驱动实例分析系列: wintun驱动分析-api篇(三)

windows 驱动实例分析系列: wintun驱动分析-api篇(三)

Wintun API 模块深度解析(文档三):会话管理与数据路径

一、概述

本文档聚焦session.c,它实现了 Wintun 数据会话的全部功能,包括会话的启动与结束、数据包的接收与发送。数据路径是整个 Wintun 性能的关键所在,其设计充分利用了 Windows 内核驱动与用户态通过共享环形缓冲区通信的机制,并实现了高效的无锁并发。

主要文件:session.c,以及公开头文件wintun.h中对应的函数声明。


二、会话结构(TUN_SESSION

typedefstruct_TUN_SESSION{ULONG Capacity;struct{ULONG Tail;ULONG TailRelease;ULONG PacketsToRelease;CRITICAL_SECTION Lock;}Receive;struct{ULONG Head;ULONG HeadRelease;ULONG PacketsToRelease;CRITICAL_SECTION Lock;}Send;TUN_REGISTER_RINGS Descriptor;HANDLE Handle;}TUN_SESSION;
  • Capacity:环形缓冲区容量(用户指定的值,必须是 2 的幂)。
  • ReceiveSend分别对应发送方向接收方向(注意命名容易混淆:应用程序调用WintunAllocateSendPacket准备发送数据包时,实际上是向接收环(驱动读出的环)写入数据;而WintunReceivePacket是从发送环(驱动写入的环)读取。之所以这样命名,是因为从驱动的视角看,应用程序“接收”的包来自驱动“发送”的环,反之亦然。为清晰起见,下文将按功能描述:
    • 应用程序发送AllocateSendPacket/SendPacket)操作的是Receive方向(因为驱动会从这个环“接收”应用程序的数据)。
    • 应用程序接收ReceivePacket/ReleaseReceivePacket)操作的是Send方向(驱动将数据“发送”到这个环供应用程序读取)。
  • 每个方向都有自己的环形缓冲区指针(Descriptor.Send.Ring/Descriptor.Receive.Ring)、尾指针(Tail)、释放指针(TailReleaseHeadRelease)、待释放包计数,以及一个临界区锁保护内部状态。
  • Descriptor包含了发送环和接收环的描述信息(基址、大小、尾移动事件句柄),用于传递给驱动。
  • Handle是设备对象句柄,用于执行 IOCTL。

三、会话启动(WintunStartSession

3.1 参数校验与内存分配

  • 检查Capacity是否在WINTUN_MIN_RING_CAPACITYWINTUN_MAX_RING_CAPACITY之间,且为 2 的幂(由调用方保证,但未显式校验)。
  • 计算每个环的大小:RingSize = TUN_RING_SIZE(Capacity),即sizeof(TUN_RING) + Capacity + (TUN_MAX_PACKET_SIZE - TUN_ALIGNMENT)。这是为了容纳环形数据、数据包对齐以及最大包大小。
  • 使用VirtualAlloc分配两倍RingSize的内存(AllocatedRegion),原因是驱动要求发送环和接收环在物理上连续?实际上,代码将这段内存分为两个连续的环:发送环位于基址,接收环位于基址 +RingSize。分配双倍大小是为了确保两个环对齐,并且VirtualFree可一次性释放。

3.2 事件对象创建

  • 为发送环和接收环各创建一个手动重置事件(CreateEventW),用于通知对方尾指针已移动。这些事件句柄被填入Descriptor.Send.TailMovedDescriptor.Receive.TailMoved,驱动会使用它们来唤醒等待的线程。

3.3 打开设备对象

  • 调用AdapterOpenDeviceObject获取设备句柄,该函数内部通过CreateFileW打开之前获得的设备接口文件。

3.4 IOCTL 注册环形缓冲区

  • 执行DeviceIoControl,控制码TUN_IOCTL_REGISTER_RINGS(自定义 CTL_CODE),传递TUN_REGISTER_RINGS结构,包含两个环的基址、大小和事件句柄。
  • 驱动收到此 IOCTL 后,将内存映射到内核地址空间,并保存事件对象引用。若成功,则驱动开始接受数据包。

3.5 初始化内部状态

  • 设置Capacity
  • 初始化两个临界区(Receive.LockSend.Lock),并指定自旋计数LOCK_SPIN_COUNT(0x10000),提高多核性能。
  • 返回TUN_SESSION指针。

四、数据包接收(应用程序读取)

4.1WintunReceivePacket

此函数从发送环(驱动写入数据的环)中取出一个包。

流程:

  1. 进入Send.Lock临界区。
  2. 检查Session->Send.Head是否已到达Capacity(若大于等于,则驱动可能已终止,返回ERROR_HANDLE_EOF)。
  3. 读取驱动更新的Tail指针(ReadULongAcquire(&Session->Descriptor.Send.Ring->Tail))。该指针由驱动更新,指向环尾。
  4. 如果Head == Tail,表示无新数据,返回ERROR_NO_MORE_ITEMS
  5. 计算BuffContent = TUN_RING_WRAP(Tail - Head, Capacity),即环中可用数据字节数。
  6. Data[Head]处读取TUN_PACKET头部(包含Size字段),检查大小是否合法(≤ WINTUN_MAX_IP_PACKET_SIZE)。
  7. 计算对齐后的包大小AlignedPacketSize = TUN_ALIGN(sizeof(TUN_PACKET) + BuffPacket->Size),确保不超过BuffContent
  8. PacketSize输出,返回BuffPacket->Data指针,并将Head向前移动AlignedPacketSize(按容量回绕)。
  9. 增加PacketsToRelease计数,表示有一个包待释放。
  10. 离开临界区。

注意:函数返回的指针直接指向共享内存,因此应用程序在释放前必须尽快处理或拷贝数据,因为后续调用可能会覆盖该区域。

4.2WintunReleaseReceivePacket

当应用程序处理完包后,必须调用此函数释放缓冲区。

流程:

  1. 进入Send.Lock
  2. 通过指针反算TUN_PACKET首地址:BuffPacket = (TUN_PACKET *)(Packet - offsetof(TUN_PACKET, Data))
  3. 将该包的SizeTUN_PACKET_RELEASE(0x80000000)进行位或操作,标记为“待释放”。这样可以延迟实际更新Head,直到有足够的待释放包可以成批更新。
  4. 然后进入一个循环,从当前HeadRelease位置开始检查每个包的释放标志,若已释放,则累加AlignedPacketSize并减少PacketsToRelease,直到遇到未释放的包。
  5. 将更新后的HeadRelease写入环形区的Head字段(WriteULongRelease),通知驱动这些位置已被释放。
  6. 离开临界区。

这种批量释放设计减少了频繁写共享内存的开销,提高了性能。


五、数据包发送(应用程序写入)

5.1WintunAllocateSendPacket

为要发送的数据包分配空间(从接收环中取空闲区域)。

流程:

  1. 进入Receive.Lock
  2. 检查Session->Receive.Tail是否超限。
  3. 计算对齐后的包大小AlignedPacketSize
  4. 读取驱动更新的Head指针(ReadULongAcquire(&Session->Descriptor.Receive.Ring->Head)),它指示驱动已处理到的位置。
  5. 计算可用空间:BuffSpace = TUN_RING_WRAP(Head - Tail - TUN_ALIGNMENT, Capacity),减去一个对齐单位是为了避免TailHead完全相等时无法区分空和满。
  6. AlignedPacketSize > BuffSpace,返回ERROR_BUFFER_OVERFLOW(环形缓冲区满)。
  7. Tail位置写入TUN_PACKET头部,设置Size = PacketSize | TUN_PACKET_RELEASE(初始标记为待释放)。
  8. 返回Data指针,并将Tail前移。
  9. 增加PacketsToRelease计数。

5.2WintunSendPacket

将填充好的数据包提交给驱动。

流程:

  1. 进入Receive.Lock
  2. 通过Packet反算TUN_PACKET,清除TUN_PACKET_RELEASE标志,表示该包已准备好被驱动读取。
  3. 进入循环,检查当前位置TailRelease处的包是否已准备就绪(即没有释放标志),若是,则累加其大小并前移TailRelease,减少PacketsToRelease
  4. 完成批量提交后,用WriteULongRelease更新环形区的Tail字段。
  5. 内存屏障MemoryBarrier())确保之前的写入对驱动可见。
  6. 检查环形区的Alertable字段,若为真,则触发SetEvent唤醒等待的驱动线程(驱动可能通过事件等待新数据)。
  7. 离开临界区。

六、环形缓冲区管理细节

6.1 对齐与填充

  • TUN_ALIGNMENTsizeof(ULONG)(4 字节),保证所有指针自然对齐。
  • 每个包(包括头部)在环中的存储占用的实际大小是TUN_ALIGN(sizeof(TUN_PACKET) + PacketSize),确保下一个包从对齐地址开始。
  • 环容量Capacity必须是 2 的幂,以便使用位与运算实现快速回绕。

6.2 原子操作与内存序

  • 使用ReadULongAcquire/WriteULongRelease等原语(在 Windows 中通常通过Interlockedvolatile加内存屏障实现),确保跨线程/跨进程的可见性。
  • 共享内存的HeadTail字段被声明为volatile ULONG,但为了确保多核顺序,代码中使用了显式的MemoryBarrier()ReadAcquire/WriteRelease语义(这些宏在 WDK 中定义,但在用户态需要自行实现,这里可能是通过_InterlockedCompareExchange等替代)。

6.3 事件通知

  • 当驱动有数据可读时,它会设置Send.Ring->Tail并触发Send.TailMoved事件,应用程序可以通过WintunGetReadWaitEvent获取该事件句柄,在ReceivePacket返回ERROR_NO_MORE_ITEMS后等待。
  • 当应用程序提交新数据时,SendPacket会检查Receive.Ring->Alertable并可能触发Receive.TailMoved事件,驱动同样可以等待该事件。

6.4 线程安全

  • 每个方向使用单独的临界区,允许同时进行发送和接收(互不干扰)。
  • 所有函数在必要时获取锁,确保对内部计数和指针的修改是原子的。

七、性能优化特性

  • 批量处理:释放和提交时,将多个包的指针更新合并为一次写操作,减少与驱动共享内存的交互次数。
  • 无锁头尾指针:共享内存的Head/Tail由驱动和用户态分别以顺序一致性语义更新,避免锁争用。
  • 固定容量:环形缓冲区大小在会话创建时确定,避免动态扩展带来的开销。
  • 直接内存访问:数据包指针直接指向共享内存,避免内核态与用户态间的数据拷贝(仅需一次拷贝,从网卡到共享内存或反之)。

八、与驱动交互的 IOCTL

自定义控制码TUN_IOCTL_REGISTER_RINGS的定义为CTL_CODE(51820U, 0x970U, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA),其中51820是 Wintun 的专属设备类型码。驱动解析此 IOCTL 后,将用户空间的内存地址锁定并映射到内核,同时保存事件句柄以便后续信号通知。


九、总结

session.c实现了 Wintun 最核心的数据传输路径,其设计体现了极致的性能追求:

  • 共享环形缓冲区消除了内核-用户态上下文切换和数据拷贝的开销。
  • 批量处理无锁原子操作充分利用多核 CPU。
  • 事件机制允许高效等待,避免忙等。

正是这一层的高效实现,使得 Wintun 能够达到远超传统 TAP 驱动的吞吐量(如前文所述,实测可达 700+ Mbps)。下一篇文章将介绍支撑这一切的辅助基础设施:日志、命名空间、注册表、资源提取和 WOW64 代理。


返回列表