尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

NDIS小端口驱动开发实战:从初始化到数据收发全解析

NDIS小端口驱动开发实战:从初始化到数据收发全解析 简介这是面向瑞昱Realtek8111、8168、8169、8110等常见PCI千兆以太网控制器编写的NDIS 6.0小端口驱动示例源码适合需要开发Windows网络驱动却缺少完整参考实例的工程师学习。压缩包采用RAR格式共69个文件大小约为615KB除核心驱动源码外还包含HTML格式说明文档、GIF与PNG截图、JS与CSS页面素材其中三个ZIP文件分别是完整工程、加入巨型帧与LSO卸载支持、加入电源管理支持的版本方便逐项对照理解功能扩展方式。源码覆盖驱动入口、初始化、收发处理、中断处理、电源管理等关键环节完整展示了NDIS小端口驱动从设备枚举到数据通路搭建再到硬件功能适配的常见实现路径参考价值较高。目前已有1238人学习下载适合具备NDIS与驱动开发基础、希望借助真实网卡控制器样例快速上手的开发者参考。 做网络驱动开发的尤其是刚接触Windows平台网卡驱动的朋友应该都绕不开 NDIS 小端口驱动这个名字。我最初接触这块的时候看着文档里一堆 MiniportXxx 回调函数和 NET_BUFFER_LIST说实话有点发怵。但真正把整个收发路径、初始化流程理顺之后才发现这套框架设计得相当规整只要理解了核心机制写出来的驱动稳定性不会差。这篇文章我就结合自己做过的一个以太网卡小端口驱动项目把从架构认知到编码实现再到调试排错的过程完整梳理一遍给正准备入坑或者已经踩坑的朋友一些实在的参考。1. 先搞清楚 NDIS 小端口驱动在网络栈里的位置1.1 从数据包旅程看驱动职责很多初学者一上来就翻 WDK 文档里 MiniportInitializeEx、MiniportSendNetBufferLists 这些函数看半天也不知道自己写的代码到底在整个系统里扮演什么角色。我习惯用一个简单的方式理解把网卡驱动想象成快递公司在某个片区的配送站上层协议栈是发货方物理网线是公路而 NDIS 库就是那个负责调度和规则制定的物流中心。你写的 NDIS 小端口驱动核心任务就是两件事把上层传下来的数据包原封不动或者说按硬件要求封装好发到线上把线上收到的数据帧拆解好交给上层。听起来简单但实际要考虑的事情远比这个多硬件怎么初始化、中断怎么处理、多核 CPU 上怎么保证并发安全、休眠唤醒怎么办、硬件出错怎么恢复。这些杂活NDIS 框架已经替你搭好了台子你只需要在对应的回调函数里填上针对你自己硬件的那部分逻辑。1.2 NDIS、小端口和网卡硬件的三角关系NDISNetwork Driver Interface Specification从 1989 年由微软和 3Com 提出到现在已经是所有 Windows 网络驱动的地基。它定义了一套统一的接口规范让协议驱动比如 TCP/IP 协议栈和硬件驱动小端口驱动能独立开发、自由组合。具体到小端口驱动这个角色它在体系里夹在 NDIS 库和网卡硬件之间。上层协议完全不关心你的网卡到底是 Realtek 还是 Intel它只调用 NDIS 提供的接口把数据往下送NDIS 库拿着你注册的回调函数表把上层的数据转交给你你要做的就是在这些回调里操作硬件寄存器、DMA 描述符完成真正的数据收发。打个比方NDIS 提供了插座和电线协议栈是插座里的电你的小端口驱动就是那个把电转换成设备能用的适配器。2. 小端口驱动的两种形态与应用场景选型2.1 无总线小端口与 WDM 小端口的区别开发之前必须先想清楚你要做哪一种。这里有个很多人忽略的概念NDIS 小端口驱动按与总线的绑定方式分成了无总线小端口Non-Bus-Specific和 WDM 小端口也叫 KMDF 小端口。无总线小端口名字听着玄乎其实意思是驱动自己管理所有硬件资源不依赖 PCI 总线驱动帮你枚举和分配资源。这类驱动常见于虚拟网卡、软件实现的网卡或者某些特殊总线接口的设备。你直接在 MiniportInitializeEx 里通过 NdisMGetBusData 之类的方式去读配置空间、自己做 IO 端口映射。WDM 小端口则是挂接在标准总线最常见的就是 PCIe上借助 KMDF 框架帮你处理 PnP、电源管理。Windows 8 之后微软主推 KMDF 小端口模型因为框架帮你解决了大量即插即用和电源管理的边缘情况。我做 PCIe 以太网卡驱动用的就是这种。选型上一句话总结有真实硬件且走标准总线的优先 KMDF纯软件虚拟设备、想轻量快速实现的考虑无总线式。当然你要是敢折腾KMDF 也能做虚拟网卡只是有点杀鸡用牛刀。2.2 为什么选择 KMDF 小端口而非传统 NDIS 5.x早年间写网卡驱动大家都是基于 NDIS 5.x自己处理 PnP 事件、自己管理电源 IRP那叫一个痛苦。NDIS 5.x 时代一个简单的网卡驱动初始化光处理 IRP 的代码就要占掉三成以上而且不同 Windows 版本行为还有差异。换成 NDIS 6.x 加 KMDF 之后幸福感直线上升。KMDF 自动处理了设备启动、停止、休眠唤醒的状态机你只需要实现 EvtDevicePrepareHardware、EvtDeviceReleaseHardware 这些框架回调。NDIS 层面也简化了数据路径NET_BUFFER_LIST 链表的操作更加清晰还加入了 RSS接收端缩放、RSC接收段合并这样的硬件卸载支持。实测下来同样功能的驱动KMDF 版本的初始化代码量能少一半左右稳定性却高一个量级特别是休眠唤醒这种边缘场景框架帮你兜底太多了。3. 核心机制逐层拆解从注册到收发3.1 驱动入口与 NDIS 注册流程每个小端口驱动都有一个 DriverEntry 入口这是老生常谈了。但里面的具体注册流程我建议你画个时序图记清楚。第一步是初始化 KMDF 驱动对象WdfDriverCreate这个会建立起驱动框架的根基。第二步就是重点了NdisMRegisterMiniportDriver这一步把你的 NDIS 版本、调用入口、关键特性全部登记到 NDIS 库里。NdisMRegisterMiniportDriver 传进去的那个 NDIS_MINIPORT_DRIVER_CHARACTERISTICS 结构体是整个驱动最核心的一张表。请注意这里面的每一个函数指针都讲究得很必须实现的MiniportInitializeEx、MiniportHaltEx、MiniportShutdownEx数据路径的MiniportSendNetBufferLists、MiniportReturnNetBufferLists查询设置的MiniportQueryInformation、MiniportSetInformation可选的MiniportDevicePnPEventNotify、MiniportResetEx、MiniportInterruptDPC 等最容易犯的错是漏掉某个可选回调。比如不确定设备是否支持 WoL局域网唤醒就不填 MiniportDevicePnPEventNotify结果休眠恢复后网卡直接失联排查到崩溃。不要偷懒该实现的别省。3.2 初始化硬件与中断处理MiniportInitializeEx 里你拿到的是系统分配好的适配器句柄接下来要做的事情大概分成这么几类读取 PCIe 配置空间确认 BAR 资源映射寄存器地址分配 DMA 描述符和收发缓冲区这一步在 KMDF 下用 WdfDmaEnabler 特别方便初始化硬件寄存器MAC 地址、流控、VLAN、多播列表配置中断MSI-X 中断向量分配、中断使能注册 NDIS 接收处理调用 NdisMIndicateReceiveNetBufferLists中断这块我要多唱几句。现代网卡几乎全走 MSI-X每个队列一个中断向量然后配合 RSS 把不同连接哈希到不同 CPU这是大吞吐量的关键。分配中断的时候要考虑 NUMA 节点让网卡中断所在 CPU 和内存所在节点一致否则跨节点访问内存延迟能差好多。我最初没注意这块64 字节小包吞吐死活上不去后来发现是中断绑在了远离 DMA 内存的 CPU 上挪近之后性能直接翻倍。3.3 收发路径的完整生命周期发送路径的关键函数是。Mac 收到上层传来的 NET_BUFFER_LIST里面挂着一串 NetBuffer。你要做的是把 NetBuffer 里的数据复制到 DMA buffer或用 DMA 直接映射写入发送描述符然后 Kick 一下硬件。这里最考验功底的是对 NET_BUFFER_LIST 生命周期和引用计数的把握。记住一句话NDIS 把 NBL 交给你你就是所有者但最终你必须调用 NdisMSendNetBufferListsComplete 把它还给 NDIS。中间无论硬件成功失败超时这个回调一定要保证调用否则上层协议栈会挂起然后卸载不了。接收路径相对简单硬件 DMA 写接收缓冲区后触发中断你在 DPC 里检查接收描述符把数据封装进 NET_BUFFER_LIST调用 NdisMIndicateReceiveNetBufferLists 通知上层。注意用 NDIS_RECEIVE_FLAGS_DISPATCH_LEVEL 标志表示在 DPC 中调用还有资源不够时可以返回 NDIS_STATUS_RESOURCES上层会退避重试。接收路径性能好坏很大程度上取决于你缓冲区回收的效率网卡用完的 buffer 要能快速归还到接收 ring 里我一般会预分配 512 个初始 buffer然后在运行中动态补充避免中断里做内存分配这种耗时的操作。3.4 NDIS 版本选择与重卸载注意点现在如果还按 NDIS 6.0 写新驱动我是不太建议的。新平台、新功能基本都往 6.80 以上走。版本上的高低不仅决定了你能用多少新特性比如 NetAdapterCx 那种面向未来的框架还决定了你能否通过 Windows 硬件认证。还有个老生常谈但特别重要的问题驱动的重入与卸载。小端口驱动支持热插拔就算了网卡还经常出现在 NDIS 重配置比如改 IP 导致协议绑定变化的场景。MiniportHaltEx 里要把之前注册的定时器、DPC、DMA 全部清理干净任何异步操作都要在返回前完成或取消。我踩过最深的坑是定时器 callback 和 HaltEx 并发跑Halt 已经把设备上下文释放了定时器回调还在操作那个野指针结果系统随机蓝屏。后来用 WdfSpinLock 把资源保护起来并在 Halt 里先停止定时器再获取锁问题彻底解决。4. 实操环境搭建与关键代码落地4.1 开发与调试环境准备开发 NDIS 小端口驱动环境配置其实相当固定但有几个细节值得写下来免得重新踩坑系统Windows 10/11 x64 专业版或企业版开启测试签名模式bcdedit /set testsigning on工具Visual Studio 2022 WDKWindows 驱动工具包版本要匹配我用的 VS2022 配 WDK 10.0.22621目标机最好准备一台独立的测试机不要在自己开发机上测试驱动蓝屏重启打断节奏不说代码都可能没保存调试器WinDbg建议用 WinDbg Preview连接方式用网络调试内核调试别再用串口了速度慢得让人崩溃还有个容易被忽略的点WDK 装完默认在 C:\Program Files (x86)\Windows Kits\10但 VS 的驱动项目模板有时候选不到原因是 VS 的 Workload 里没勾选“使用 C 的桌面开发”以及“Windows 10 SDK”。这个坑很隐蔽配环境卡了我整整半天。4.2 DriverEntry 与 Characteristic 注册实战直接甩一段我当时代码的骨架加上注释看注释就够了DriverEntry(DRIVER_OBJECT *DriverObject, UNICODE_STRING *RegistryPath) { WDF_DRIVER_CONFIG config; WDFDRIVER hDriver; NDIS_MINIPORT_DRIVER_CHARACTERISTICS ndisChars; NDIS_STATUS status; WDF_DRIVER_CONFIG_INIT(config, EvtDeviceAdd); status WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, config, hDriver); if (!NT_SUCCESS(status)) { return status; } NdisZeroMemory(ndisChars, sizeof(ndisChars)); ndisChars.Header.Type NDIS_OBJECT_TYPE_MINIPORT_DRIVER_CHARACTERISTICS; ndisChars.Header.Size sizeof(ndisChars); ndisChars.Header.Revision NDIS_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2; ndisChars.MajorNdisVersion 6; ndisChars.MinorNdisVersion 80; ndisChars.InitializeHandler MiniportInitializeEx; ndisChars.HaltHandler MiniportHaltEx; ndisChars.ShutdownHandler MiniportShutdownEx; ndisChars.SendNetBufferListsHandler MiniportSendNetBufferLists; ndisChars.ReturnNetBufferListsHandler MiniportReturnNetBufferLists; ndisChars.QueryInformationHandler MiniportQueryInformation; ndisChars.SetInformationHandler MiniportSetInformation; ndisChars.DevicePnPEventNotifyHandler MiniportDevicePnPEventNotify; ndisChars.ResetHandler MiniportResetEx; status NdisMRegisterMiniportDriver(DriverObject, RegistryPath, ndisChars, s_ndisHandle); return status; }注意 NdisMRegisterMiniportDriver 的最后一个参数是返回的 NDIS 句柄之后所有 NdisMxxx 调用都要用到它包括后面 NdisMRegisterDevice 的时候。还有一个很多人会忽略的你需要在某个时机获取一个“适配器句柄” NdisMRegisterMiniportDriver 只是注册了驱动本身真正为某个网卡实例创建适配器是在你的 KMDF EvtDeviceAdd 回调里调用 NdisMRegisterMiniport 完成的。KMDF 每枚举到一个 PCIe 设备就会触发一次 EvtDeviceAdd你就在那里创建 NDIS 适配器并设置好上下文。4.3 中断与 DPC 的配合代码示例中断回调里不适合做重活接收数据搬移都放到 DPC。KMDF 下用 WdfInterruptCreate 来创建框架管理的中断对象注册 InterruptIsr 和 InterruptDpc 两个回调框架会自动帮你在正确的 IRQL 下调用它们还能自动处理中断共享的问题。下面是一个最小实现骨架BOOLEAN InterruptIsr(WDFINTERRUPT Interrupt, ULONG MessageID) { PDEVICE_CONTEXT ctx GetContextFromInterrupt(Interrupt); // 1. 读取中断状态寄存器判断是不是自己的中断 ULONG status READ_REGISTER_ULONG(ctx-RegBase INT_STATUS_REG); // 2. 检查是否使能且 pending如果都不是返回 FALSE共享中断场景必须 if (!(status ctx-IntMask)) { return FALSE; } // 3. 写寄存器清除中断 pending 位 WRITE_REGISTER_ULONG(ctx-RegBase INT_STATUS_REG, status); // 4. 保存状态调度 DPC ctx-PendingStatus status; WdfInterruptQueueDpc(Interrupt); return TRUE; }InterruptDpc 里我会先处理基于状态位分发如果 pending 里有 RX 标志去搬接收描述符有 TX 标志回收发送完成包有链路状态变化更新 NDIS 的媒体状态。链路状态这个细节说一下网线拔了之后硬件会置位一个状态位你在 DPC 里要调用 NdisMIndicateStatus 通知 NDIS 媒体断开然后再调 NdisMIndicateStatusComplete 结束状态指示。很多驱动忘了指示链路断开导致网络连接图标显示未连接但实际收发异常用户体感很怪。5. 那些让人怀疑人生的调试与排查实录5.1 数据包发不出去DMA 描述符 cache 一致性的坑第一次调通发送的时候我遇到一个诡异的问题小包64 字节能发出去但大包大于 MTU 的 jumbo frame就会偶发丢包。后来用逻辑分析仪抓 PCIe 总线发现硬件读到的描述符内容偶尔是“旧的”。原因很简单我改完描述符之后没有做内存屏障也没有做 cache flush。PCIe 设备的 DMA 读的是内存但 CPU 的写可能还停留在 cache 里没落回主存。解决办法分两步一是描述符本身所在内存必须是非 cacheable 的可以在分配时用 MmAllocateNonCachedMemory或者使用 WdfCommonBufferCreate 分配它天然免 cache 一致性问题二是在写描述符和 Kick 硬件之间加一个内存屏障用 KeMemoryBarrier或者编译屏障硬件屏障的组合确保写操作对所有总线主控可见。5.2 接收性能上不去RSS 和中断亲和的调优组合拳吞吐量测试是最能暴露问题的。我提到过一次 NUMA 的问题但除了中断亲和性接收路径上还有一个常见的瓶颈RSS 没有开启或者哈希类型不对。如果你的网卡支持 4 队列但 RSS 没配置所有流都挤在一个队列一个 CPU 跑到 100%其他核心在围观那性能自然难看。调优步骤总结如下在 MiniportInitializeEx 里通过注册表读取 RSS 配置调用 NdisMStartAdapterRss配置哈希类型IPv4/IPv6 的 TCP/UDP 四元组中断向量与 RSS 队列一一对应并用 KeSetTargetProcessorDpc 把每个队列的 DPC 绑定到不同 CPU接收 ring 大小适度加大我一般设为 1024 个描述符避免中断风暴这几步做完配合多队列收包64B 小包的 PPS 能从最初的单队列 30 万左右提升到 120 万以上效果立竿见影。5.3 蓝屏排查三板斧网卡驱动事故频发我这里把排蓝屏的基本套路总结成三个绝招第一招抓现场用 WinDbg 连接内核调试蓝屏时执行 !analyze -v重点看 bugcheck code 和 faulting module。如果是 NDIS.sys 里面的函数崩溃十有八九是你的小端口回调传了非法参数要么是拿错句柄要么是 NBL 状态不对第二招查 NDIS 状态!ndiskd 系列命令非常好用。!ndiskd.miniport 列出所有小端口实例!ndiskd.netbuffer 查看特定 NBL 的引用计数和状态一抓一个准第三招启用驱动验证器verifier /standard /driver mydriver.sys重启后开着验证器跑测试它能监测到内存越界、DMA 错误、锁序问题虽然有时候会误报但大部分时候能帮你把潜在 bug 提前炸出来。注意开验证器之后系统会变慢生产环境切记别开有一回我遇到一个内存越界问题正常测试完全没事但一开网卡多队列高负载跑十分钟必蓝屏。!analyze 定位到是 NDIS.sys 里某处访问了野指针最后查出来是我在资源不足时错误地释放了一个还在硬件 DMA ring 里的 buffer。这类问题只有验证器加高负载压力测试才能暴露出来写驱动的一定要养成定期开验证器跑压测的习惯。6. 从能用走向好用一些实战细节补充写到这里很多人以为驱动能通、吞吐量也达标就算大功告成了。实际上作为一个经历过产品级驱动交付的人我想提醒几个“能用”和“好用”之间差距很大的细节。第一个是电源管理。笔记本合盖再打开网卡驱动如果休眠唤醒处理得不好要么直接消失设备管理器里感叹号要么链路状态不对。关键点在于设备进入 D3 之前保存好 MAC 地址、流控配置、卸载引擎参数这些硬件寄存器唤醒后重放这些配置。KMDF 下这对应 EvtDeviceArmWakeFromS0 和 EvtDeviceDisarmWakeFromS0配合 NDIS 的 MiniportDevicePnPEventNotify 收到 NetEventSetPower 事件时做完整重初始化。第二个是虚拟化环境下的问题。现在很多测试跑在 VMware/Hyper-V 上虚拟网卡的硬件行为有时候和真实网卡不完全一致比如 MSI-X 中断数量、描述符深度上限。不要因为虚拟环境测试通过就掉以轻心一定要在真实多型号硬件上跑一轮完整的兼容性测试。第三个是想清楚驱动签名和部署升级的问题。Windows 10 之后内核驱动的签名策略越来越严。企业内部分发测试签名驱动是一回事面向公众发布就必须走 WHQL 签名。签名不是开发的最后一步而是开发流程的一部分产品规划早期就要把认证预算和时间排进去不然驱动写完了也只能自己在公司里用。最后回到代码层面我建议你在驱动里多做遥测。比如用 WPP 软件跟踪WDK 自带在初始化、复位、链路切换、异常收发的关键路径上打点。问题是驱动不像应用层那样能随便打日志WPP 的 ETW 机制在性能损耗上非常低而且在客户现场出了问题只需要抓一份系统日志就能定位到具体回调有没有执行、耗时多少这在排查线上问题时价值巨大。别等到用户报障了才去抓瞎。NDIS 小端口驱动开发是个投入门槛较高、但一旦摸清套路就很有成就感的事情。从 DriverEntry 注册到数据通路的收发再到中断与电源的细节每一环都考验对 Windows 内核和网络协议栈双向的理解。真心建议新手不要直接跳过理解 NDIS 架构这一步不然写出来的代码上线之后迟早要还。等把这些都吃透了你再看 NDIS 上层那些协议驱动、过滤驱动会发现很多思想是相通的。本文还有配套的精品资源点击获取
返回列表