简介:NDIS 6.0小端口驱动开发长期缺乏可参考的完整实例,DDK自带示例仅E100BEX一个,这套针对Realtek 8111/8168/8169/8110等PCI千兆以太网卡编写的miniport驱动源码,覆盖了驱动框架、硬件访问与数据路径等核心部分,为需要编写NDIS 6.0驱动的开发者提供了难得范本。压缩包共69个文件,除主要源码外,还附带PM电源管理、LSO巨帧支持等3个zip附加模块,以及用于辅助说明的htm、js、css与图片文件,整体体积仅615KB,小巧紧凑,便于快速下载和按需查阅。目前已有1240人学习,其价值受到不少开发者认可。资源从适配器初始化、发送接收队列管理到中断处理均有完整实现,并可结合附加模块对比不同功能特性的集成方式;对于正在开发或维护NDIS 6.0小端口驱动的工程技术人员,这套代码既能帮助理解底层运转机制,也能显著缩短底层驱动开发中的调研和排错时间。
1. 把 NDIS 小端口驱动拆开:从一个看不见的以太网卡驱动说起
网卡驱动在 Windows 里不是随便写个 WDM 或 KMDF 驱动就完事,它必须经过 NDIS(Network Driver Interface Specification)这个中间层来和协议栈通信。刚接触这块的人最容易懵的是:明明照着示例代码写完了,设备也枚举出来了,但网络连接死活起不来,网上邻居里那个“未识别的网络”消不掉。这事我当初调试了快一周,最后才发现是 OID 查询的返回状态没处理好,NDIS 直接判定 miniport 初始化失败。
NDIS 小端口驱动(Miniport Driver)本质上是一个“被 NDIS 管理的适配器驱动”。它自己不直接操作 TCP/IP 协议栈,而是通过一组 NDIS 定义的回调函数,把硬件的收发能力、链路状态、电源管理等细节暴露给系统。你需要关心的核心问题只有一个:如何用一套标准的回调模型去驱动一块具体的以太网硬件。这个资源适合谁说清——不是在写 PCIe 网卡固件的嵌入式工程师,也不是只调应用层 Socket 的开发,而是那些需要让一块自定义网卡(或虚拟网卡)在 Windows 设备管理器里以“网络适配器”身份工作的人。
2. NDIS 的层次与核心理念:为什么必须走 miniport 这一层
2.1 NDIS 驱动栈:不是所有驱动都能叫 miniport
Windows 网络驱动从下往上分布着物理网卡、小端口驱动、NDIS 库、协议驱动(TCP/IP 栈),以及上面的各种过滤驱动(如 WFP、LWF)。NDIS 库本身不处理任何数据包,它只是个调度中心,负责把上层的发送请求转给小端口,把小端口收到、完成的接收包再递交回上层。
这里最常见的误用是:有人以为自己可以写一个直接接管网卡硬件、再自己实现一套类似 Socket 的接口的驱动。这么说吧,那你会失去 TCP/IP 协议栈、DHCP、无线上网、防火墙过滤等所有系统自带能力。走 NDIS miniport 的好处在于:
- 协议栈和驱动之间是解耦的,你做的是“硬件适配”,不用重写网络语义。
- NDIS 库帮你管理了大部分琐碎的同步与并发问题,比如数据路径上的锁、预取、队列。
- 调用了
NdisMRegisterMiniportDriver之后,PNP 管理器、电源管理器会通过 NDIS 间接引导你的驱动,配套的 inf 也有一套成熟的写法。
打个不严谨的比喻:NDIS 是江湖中间人,miniport 是你在中间人面前演的“硬件代理人”。你不需要知道 TCP 粘包拆包,但你必须知道如何把你的网卡 DMA 描述符里的数据搬到NET_BUFFER_LIST里。
2.2 你逃不掉的两条主线:控制路径与数据路径
在开发 miniport 时,我习惯把代码分成两条线来想。
控制路径,也叫慢路径,是 PNP 事件、OID 查询/设置、暂停/重启、电源状态迁移等操作。它们的特点是频率低、但讲究正确性和状态同步。比如MiniportInitializeEx里要完成适配器初始化、寄存器映射、中断注册、广播初始链路状态;MiniportQueryOid和MiniportSetOid处理上层(比如IP Helper、DHCP 客户端)发来的各种属性请求。
数据路径,是收发数据的快路径。这里只关心怎么高效地把包从硬件 DMA 引擎里取出来,以及把上层的NET_BUFFER_LIST链描述成硬件能理解的 DMA 描述符数组。数据路径是不能随便调函数、不能做复杂计算的,因为每一微秒都可能影响吞吐。
理解这两条路径的区分是读懂任何一份 NDIS 示例代码的钥匙。如果你发现自己在MiniportSendNetBufferLists里做同步等待或者分配大量内存,那说明设计已经歪了。
2.3 选择全功能 miniport 还是仅控制路径的 miniport?
NDIS 支持两种 miniport:无线的、有线的全功能驱动,以及所谓的“仅控制路径”的驱动(比如一些虚拟交换机扩展)。对以太网卡来说,你需要的是完整的数据路径支持,即:
MiniportSendNetBufferLists并支持分片MiniportReturnNetBufferListsMiniportReceiveNetBufferLists(通过NdisMIndicateReceiveNetBufferLists上报)- 中断处理函数
MiniportInterrupt和MiniportInterruptDPC
如果你的硬件本身不是太复杂,很多厂商会选择把全部路径写在一个驱动里,但并不代表结构可以简化。你依然要把初始化、暂停、重启、OID 处理等钩子全部实现,否则 NDIS 库在遇到特定状态时会直接给你返回驱动不支持——没有商量的余地。
3. 搭起一个能加载的 miniport:DriverEntry 与注册特征结构
3.1 DriverEntry 的真实使命
NDIS miniport 的 DriverEntry 比普通 WDM 驱动多一个关键动作:调用NdisMRegisterMiniportDriver注册一组特征函数。它和 WDF 的WdfDriverCreate不是一个套路——你不需要在这里创建设备对象,因为设备对象的创建完全由 NDIS 在 PNP 事件到来时替你完成。
一个典型的 DriverEntry 骨架如下:
NDIS_STATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ) { NDIS_MINIPORT_DRIVER_CHARACTERISTICS Chars; NDIS_STATUS Status; // NDIS 要求使用该宏安全填充版本信息 NdisInitMiniportDriverCharacteristics( &Chars, NDIS_OBJECT_TYPE_MINIPORT_DRIVER_CHARACTERISTICS, NDIS_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2, 0); Chars.AdapterInitializationHandler = MiniportInitializeEx; Chars.AdapterHaltHandler = MiniportHaltEx; Chars.AdapterPauseHandler = MiniportPauseEx; Chars.AdapterRestartHandler = MiniportRestartEx; Chars.QueryOidHandler = MiniportQueryOid; Chars.SetOidHandler = MiniportSetOid; Chars.SendNetBufferListsHandler = MiniportSendNetBufferLists; Chars.ReturnNetBufferListsHandler = MiniportReturnNetBufferLists; Chars.CancelSendHandler = MiniportCancelSend; Chars.InterruptHandler = MiniportInterrupt; Chars.InterruptDpcHandler = MiniportInterruptDPC; Status = NdisMRegisterMiniportDriver( DriverObject, RegistryPath, &Chars, NDIS_SIZEOF_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2, &AdapterDriverHandle); return Status; }注意Chars内部的对象头(Header)字段在第一处调用里已经用宏填好了,所以再单独去改Header.Type/Size/Revision是多余的。很多新手是从 WDK 老示例ndislwf或e1000抄代码,能看到那里多了几个InitializePutHandle之类的调用,但如果你用的是 Windows 10+ 的 WDK,直接按上面的骨架来即可。
参数上,NdisMRegisterMiniportDriver最后的AdapterDriverHandle是一个NDIS_HANDLE,后续几乎所有全局性调用(申请内存、注册中断、读寄存器)都要以它作为NdisWrapperHandle传入。丢了这个句柄,你在其它函数里就寸步难行。
3.2 注册特征结构时最容易犯的版本错误
NDIS_MINIPORT_DRIVER_CHARACTERISTICS这个结构有多个版本,不同版本允许注册的回调函数数量不同。WDK 里REVISION_1到REVISION_2的主要区别,在于Revision_2支持更新的回调集(比如DirectOidRequest和SynchronousOidRequest,后者用于异步 OID 请求处理)。
但注意:这个结构的Size字段必须使用NDIS_SIZEOF_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2这个宏,不能自己算。如果你用的是旧点的 WDK,硬编码sizeof(NDIS_MINIPORT_DRIVER_CHARACTERISTICS)通常也能编译过,但它可能与 NDIS 库期望的结构大小不一致——NDIS 在运行时校验,一旦失败,它会拒绝加载,并且Event Log里给你一个非常不明确的The driver is invalid错误。
3.3 初始化适配器:MiniportInitializeEx 里必须做到的六件事
当一个网卡设备被 PNP 枚举出来后,NDIS 会调用你注册的MiniportInitializeEx。这个函数失败,设备将无法启用,Windows 网络服务直接不可用。我在做虚拟网卡时总结的固定流程:
NDIS_STATUS MiniportInitializeEx( _In_ NDIS_HANDLE NdisAdapterHandle, _In_ NDIS_HANDLE MiniportDriverHandle, _In_ PNDIS_MINIPORT_INIT_PARAMETERS InitParams) { PADAPTER Adapter = NULL; // 1. 分配适配器结构,该结构的第一个字段必须是 NDIS_OBJECT_HEADER Adapter = NdisAllocateMemoryWithTagPriority( NdisAdapterHandle, sizeof(ADAPTER), 'paNx', LowMemoryPriority); RtlZeroMemory(Adapter, sizeof(ADAPTER)); Adapter->Header.Type = NDIS_OBJECT_TYPE_ADAPTER; Adapter->Header.Revision = NDIS_ADAPTER_REVISION_1; Adapter->Header.Size = NDIS_SIZEOF_ADAPTER_REVISION_1; NdisMSetMiniportAttributes( NdisAdapterHandle, &Adapter->Header); // 2. 读取注册表中的参数(中断亲和性、环形容量等) // 3. 映射 BAR 寄存器,获取物理地址 // 4. 分配 DMA(通过 NdisMRegisterDmaChannel 或直接使用硬件固有的片段) // 5. 注册中断:NdisMRegisterInterruptEx // 6. 初始化本地状态后,设置“媒体连接”的初始状态再返回 NDIS_STATUS_SUCCESS Adapter->MediaState = MediaConnectStateConnected; *((PNDIS_STATUS)None) = NDIS_STATUS_SUCCESS; // 伪代码示意,实际是通过 NdisM... 调用上报 return NDIS_STATUS_SUCCESS; }在初始化里最隐蔽的一个坑是:你需要调用NdisMSetMiniportAttributes并传入一个NDIS_MINIPORT_ADAPTER_ATTRIBUTES结构,里面至少要有MediaType(NdisMedium802_3是绝大多数以太网适配器的选择)和PhysicalMediumType。如果你漏设MediaType为NdisMedium802_3,系统可能把该设备识别成某种未知介质类型的网络设备,DHCP 从上层就把你“拒绝服务”了。
另一个坑是初始化期间不能直接向协议栈发送数据包,也不能主动调用NdisMIndicateReceiveNetBufferLists——那必须等MiniportRestartEx之后。我看到过有人试图在初始化里探测对端链接状态,导致 NDIS 库抛异常。初始化阶段就应该把精力放在“让设备可被操纵”上,而不是抢跑数据路径。
4. 把包发出去、把包收回来:数据路径的关键实现
4.1 发送路径:从 NET_BUFFER_LIST 到 DMA 描述符
数据发送是验证一个 miniport 是否靠谱的第一道考试。当 TCP/IP 栈要发包时,NDIS 会调用你的MiniportSendNetBufferLists。你要做的是遍历传入的NET_BUFFER_LIST链,再遍历每个NET_BUFFER的NET_BUFFER_LIST上挂着的各段内存。
VOID MiniportSendNetBufferLists( _In_ NDIS_HANDLE MiniportAdapterContext, _In_ PNET_BUFFER_LIST NetBufferLists, _In_ ULONG PortNumber, _In_ ULONG SendFlags) { PADAPTER Adapter = (PADAPTER)MiniportAdapterContext; PNET_BUFFER_LIST CurrentNbl; BOOLEAN fSendComplete = TRUE; CurrentNbl = NetBufferLists; while (CurrentNbl != NULL) { PNET_BUFFER CurrentNb = NET_BUFFER_LIST_FIRST_NB(CurrentNbl); // 把 NET_BUFFER 中数据地址映射给硬件 // 大多数网卡支持散列分散收集,但我们一般先检查段数 ULONG nbCount = NET_BUFFER_LIST_QUERY_NBL_NB(CurrentNbl); if (nbCount > 32) { // 超过硬件DMA描述符上限:走分片逻辑或直接丢弃并上报发送失败 } // 这里必须记住 NBL 的上下文字段,用来在 "完成" 时把它还回去 NET_BUFFER_LIST_SET_CONTEXT_STRUCT( CurrentNbl, &Adapter->SendContext[CurrentIndex], ADAPTER_SEND_CTEXT); CurrentNbl = NET_BUFFER_LIST_NEXT_NBL(CurrentNbl); } // 将描述符列表写入硬件 TX 队列,然后写 Doorbell 寄存器触发发送 // 如果该驱动支持“即时完成”,可以在此处循环完成 // 但注意:不要在 NDIS 的数据路径里做阻塞等待 —— 哪怕你用 KeDelay 也不行 NdisMSendNetBufferListsComplete( Adapter->AdapterHandle, NetBufferLists, NDIS_STATUS_SUCCESS); }这个逻辑里最重要的一个环节是:NBL 的所有权在你手上,完成发送后必须调用NdisMSendNetBufferListsComplete把它还给 NDIS。如果你忘记调用,协议栈会认为该包还在途中,最终导致发送队列内存泄漏,系统内存池一点点被耗尽,直到蓝屏。
参数上,SendFlags是NDIS_SEND_FLAGS_DISPATCH_LEVEL或NDIS_SEND_FLAGS_CHECK_FOR_LOOPBACK等标志位。如果你在中断 DPC 里处理发送完成,不再对原 NBL 做额外操作,一般直接原样传给完成函数即可。但如果是切到线程上下文里完成,需要自行处理DISPATCH_LEVEL的限制,不能把 IRQL 随意降级而不加保护。
4.2 接收路径:中断进来之后的三段式处理
接收路径上,网卡硬件通过 DMA 把包写进 RAM,然后触发中断。你要做到的事情是:从中断服务例程(ISR)快速判断这是自己设备的包、清中断、把处理交到 DPC。在 DPC 里从 DMA 描述符环里取包,构造NET_BUFFER_LIST,再通过NdisMIndicateReceiveNetBufferLists告诉协议栈。
BOOLEAN MiniportInterrupt( _In_ NDIS_HANDLE MiniportAdapterContext, _In_ PVOID InterruptContext, _In_ PVOID MessageId) { PADAPTER Adapter = (PADAPTER)MiniportAdapterContext; ULONG statusReg = READ_REGISTER_ULONG( Adapter->RegBase + REG_STATUS); if (!(statusReg & INT_STATUS_RX_DONE)) { return FALSE; // 不是本设备的中断,必须返回FALSE } // 立刻关闭中断/或写入Mask,防重入 WRITE_REGISTER_ULONG(Adapter->RegBase + REG_INT_MASK, 0); // 在ISR里绝不可以分配内存、调用NdisMIndicateReceiveNetBufferLists // 只需记录有事件,让DPC去跑 NdisMQueueDpc(Adapter->DpcHandle, NULL, 0, 0); return TRUE; }DPC 里的操作:
VOID MiniportInterruptDPC( _In_ NDIS_HANDLE MiniportAdapterContext, _In_ PVOID InterruptContext, _In_ PVOID MessageId, _In_ PVOID DpcContext) { PADAPTER Adapter = (PADAPTER)MiniportAdapterContext; // 1. 遍历DMA描述符环,找到已经由硬件标记为"owned by software"的包 // 2. 对收到的每个包,把缓冲区的物理地址对应的系统虚拟地址填入 NBL // 3. 记录 DataLength,并更新 DMA 完成计数 // 4. 如果 RSS 开启,根据哈希值计算 CPU,调用带 CPU 编号的NdisMIndicateReceiveNetBufferLists // 普通版本:第一个参数传 AdapterHandle // 5. 重新开启中断 }这里最常说的一句“血泪经验”是:不要在 ISR 里做任何重量级操作。我不止一次看到有刚转来做驱动的同事在MiniportInterrupt里直接调用NdisMIndicateReceiveNetBufferLists,结果 IRQL 不匹配,系统立即蓝屏。如果厂商硬件没有 MSI-X 多队列,单队列驱动更是要确保 DPC 处理速度能跟上中断频率,否则中断风暴会吃掉整机 CPU。
接收路径的绕不开的坑是 NBL 池的预分配。你必须在初始化时调用NdisAllocateNetBufferPool预留好一组 NBL 和NET_BUFFER,并且让 DMA 环的缓冲区物理地址全部提前映射好。接收完成时不是“分配内存再把数据拷贝过去”,而是直接把硬件缓冲区“上交”给协议栈,再从池中取出一个新的缓冲区放回 DMA 环。这个交接如果不顺,吞吐就崩。
5. NDIS miniport 避坑:九个前人踩出来的坑
5.1 现象:驱动安装了,设备管理器中适配器显示“无法启动(代码 10)”
原因:MiniportInitializeEx返回非NDIS_STATUS_SUCCESS。但很多人不知道的是,系统事件日志里往往不直接记录失败的具体 OID 或寄存器错误,它只显示一个笼统的状态码。第一直觉应该去看SetupAPI.dev.log以及驱动里的DbgPrint缓冲。
解决:我在MiniportInitializeEx的每个可能失败的分支前都会调用NdisWriteEventLogEntry,把失败原因记录为事件日志的EventLogDriverFailure。这个方法很老但有效,至少比一句“代码 10”强太多。再者就是检查伴生的INF文件里Characteristics=0x80这一类标志是否和驱动的 Release 类型匹配。
5.2 现象:装了驱动后,任务栏网络图标始终是感叹号,ipconfig显示媒体已断开
原因:初始化时没有设定初始的MediaConnectState,或者MiniportRestartEx之后没有上报链路状态。NDIS 默认认为适配器是断开状态,直到你调用NdisMIndicateStatusEx报NDIS_STATUS_LINK_STATE。
解决:在MiniportRestartEx成功且硬件链路已建立后,显式调用NdisMIndicateStatusEx,并在它的StatusBuffer里填一个NDIS_LINK_STATE结构。注意MediaConnectStateConnected只是字段值,携带这个状态的 OID 请求还要能正确响应——也就是说MiniportQueryOid里对于OID_GEN_MEDIA_CONNECT_STATUS必须回MediaConnectStateConnected,二者缺一不可。
5.3 现象:系统休眠唤醒后网卡断网,需禁用再启用才恢复
原因:电源状态迁移处理不完整。NDIS 会在休眠时调用MiniportDevicePnPEvent,并携带NDIS_DEVICE_PNP_EVENT_POWER_PROFILE_CHANGED等事件。如果驱动没有在 wake 后重新初始化 DMA 环,或者没有重写某些被电源切断的寄存器,就会丢包。
解决:在MiniportShutdownEx和MiniportDevicePnPEvent里做好状态保存,在MiniportRestartEx里强制对 MAC 和 PHY 做一次完全复位(soft reset),并重新推进 DMA 描述符环的读写指针。别只依靠硬件寄存器的“非易失性”,很多网卡从休眠醒来后 DMA 地址映射都没了,必须重建。
5.4 现象:吞吐量只有几 MB/s,CPU 占用却高到 30%
原因:多数情况是数据路径频繁操作 NBL 上下文和锁。比如你在发送路径里用了NdisAcquireSpinLock保护一个全程无并发问题的计数器,就是严重的性能自杀;另一个常见原因是接收中断没有合并(interrupt moderation),网络流量一大,每秒几十万次中断,CPU 全耗在中断到 DPC 的路上了。
解决:发送完成、接收指示都尽量走批处理。NDIS 提供了NDIS_RECEIVE_FLAGS_DISPATCH_LEVEL等标志位,但更重要的是设计上把“循环处理多个包”作为最小处理单元,而不是一次中断只收一个包。中断节流可以在硬件寄存器里设置阈值(比如包数量达到 4 个或时间超过 50 微秒再触发一次中断)。
5.5 现象:NdisMRegisterInterruptEx返回NDIS_STATUS_INVALID_PARAMETER
原因:NDIS_MINIPORT_INTERRUPT_CHARACTERISTICS结构里的InterruptType没填对,或者MessageInfo在 NDR 中断下没有正确填充。特别是 MSI 配置错误时,NDIS 库里表现为这个通用失败码。
解决:先确定硬件支持的是传统引脚中断还是 MSI/MSI-X。对传统的NDIS_INTERRUPT_TYPE_LEVEL_SENSITIVE,MessageInfo必须清零且MessageId设为 0;对 MSI-X 要用NdisMGetBusData或查询 PCIe 配置空间拿到中断消息号。我调试过的多数失败都发生在GetMessageInfo时没有检查PCI_CAPABILITY_MSI_X的下一个指针位置。
5.6 现象:包收发总耗一半,另一半被丢
原因:这不是随机丢包,很可能是 DMA 同步问题。网卡的 DMA 引擎把数据写到内存时,使用了 CPU 缓存标记为不可缓存或未做MmMapIoSpace映射一致性设置,导致 CPU 读到的数据是旧的。另一种是环形容量的深度问题——驱动 DMA 环只有 32 个描述符,而硬件发送队列却可以接受 128 个写请求。
解决:先在MmMapIoSpace之后用Misc工具(!pci 2 d或用 Windbg 的!devobj)确认寄存器访问没问题。再查描述符自身是否为硬件要求的格式——比如是否设置结束位和主机端标记。最后,在发送完成回路里加描述符计数,确认每个完成的描述符都被释放后,增加 RingSize 到 256 或 512。
5.7 现象:OID_GEN_RECEIVE_BLOCK_SIZE返回 0,TCP 开始反复重传
原因:有些测试工具或上层服务通过 OID 查询驱动支持的最大接收包大小。如果驱动里没有实现这个 OID 或者返回值为 0,协议栈会认为无法承载正常 MTU 流量,于是放弃发送大包,甚至把窗口协商成很小。
解决:在MiniportQueryOid里正确响应OID_GEN_MAXIMUM_FRAME_SIZE、OID_GEN_RECEIVE_BLOCK_SIZE、OID_GEN_TRANSMIT_BLOCK_SIZE等基础 OID。这个坑容易发生在“功能上能跑但 sys 老版本结构体填错”的情况下,务必回头核对 NDIS 结构体版本宏。
5.8 现象:重启后 Inf 安装不上去,在设备管理器里是黄色的“
网络适配器未找到驱动”
原因:多半是co-installer和驱动版本签名问题。Windows 10/11 强制签名,未签名的 NetCfg 安装会失败,而且失败发生在复制驱动文件之后、启动服务之前,导致设备信息残留。
解决:使用 WDK 自带的inf2cat和signtool做测试签名,并且确认 INF 里DriverVer日期不是过去的、CatalogFile指向的.cat文件真实存在。安装前记得清理pnputil /delete-driver oemXX.inf,再做新的安装。
5.9 现象:NdisMIndicateReceiveNetBufferLists导致 0xD1 蓝屏
原因:报告接收包后,NDIS 会尝试访问NET_BUFFER_LIST的上下文区域,如果你在初始化时没有正确分配上下文大小,或是在接收 DPC 里擅自释放了一块正在被协议栈引用的内存,就会触发DRIVER_IRQL_NOT_LESS_OR_EQUAL之类的问题。
解决:初始化时在NdisAllocateNetBufferPool的NBL_FLAGS中传入NDIS_NBL_FLAGS_CONTEXT对应对齐上下文空间;接收时不要手动NdisFreeNetBufferList,交给完成回调MiniportReturnNetBufferLists来做回收。铁律:数据路径上,NBL 的生命周期由 NDIS 规则决定,不是你决定的。
6. 验证你的 miniport:用 Windbg 和 WPT 让隐藏状态现形
开发完驱动只是第一步,能不能在真实环境下稳定跑起来才是关键。我自己的流程是这样:装好驱动、Network 图标正常之后,先不管吞吐,先打开 Windbg 双机调试,在MiniportInitializeEx下断点,然后用!ndiskd查看 NDIS 对这个小端口的内部状态。
kd> !ndiskd.miniport这个扩展命令会把所有已加载的 miniport 实例列出来,包含适配器的设备名、句柄、状态。接着用:
kd> !ndiskd.miniport <handle> -irp能够看到所有挂起的 OID 请求和 PNP IRP 队列,确认是否存在“队列堆积”。如果大量 OID 请求一直挂起未完成,真相就是你某条回调路径上真的阻塞了。
性能验证方面,用 WPT(Windows Performance Toolkit)里的WPA打开netsh trace或者xperf -on NDIS的 ETW 日志。重点看两个指标:DPC里的NDIS中断处理耗时,以及MiniportSendNetBufferLists的扇出是否繁忙。对了,NT 内核里有个ndis.sys的性能计数器组,用perfmon里的Networking类别也能看到小端口单队列的封装包数量。
如果要验证我前面说的功耗——无论有没有流量,先检查MiniportPauseEx和MiniportRestartEx是否能被系统正常调用。用!ndiskd.netadapter查看它当前的PnPEvents,如果没有收到NDIS_DEVICE_PNP_EVENT_SURPRISE_REMOVED就有希望。
有一条我保留到现在的“翻车后”习惯:在MiniportSendNetBufferLists里对每个NBL的NET_BUFFER做一次KdPrint,打印出它携带的源 MAC、目的 MAC、EtherType 和数据长度,把输出重定向到 Windbg 缓冲区。第一次跑发包测试时,你看到 EtherType 0x0806(ARP)能通过,才说明你的这个 miniport 真正活过来了。
从那以后,我每次调一个新的小端口,都会先把这个三步走流程强制走一遍:!ndiskd.miniport验证状态、WPT 抓收发路径耗时、再抓 ARP 和 DHCP 的包确认协议栈默认通道可用,三个都过了才敢往上层调 TCP 吞吐。这规矩看着繁琐,但一次次把“为什么网卡连不上网”这种问题从玄学变成可定位的具体寄存器值。希望这个拆解对你有用——你手头那份源码或者驱动包值不值得下,看它是否具备上面这些回调、这些 OID、这些状态处理,基本就有了判断。
本文还有配套的精品资源,点击获取