尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
NDIS小端口驱动缓冲区所有权与NET_BUFFER_LIST数据通路解析
简介面向 Windows 平台网络驱动开发者的 NDIS 6.0 小端口miniport驱动实例围绕 Realtek 8111/8168/8169/8110 系列 PCI 千兆以太网控制器展开用以弥补 DDK 中仅有 E100BEX 一个参考样例、可借鉴代码过少的缺口。整包共 69 个文件、约 615KB主体为 gif、png、jpg 网页配图以及 js 脚本和 htm 说明页另含三个 zip 分包分别对应 LSO 与巨型帧支持、release 发布版本、PM 电源管理等增量实现便于按功能模块逐个对照阅读。已有 1242 人学习下载。读者可从中梳理 miniport 的初始化流程、中断处理与收发描述符管理、OID 请求响应等完整实现脉络并借助电源管理与卸载加速补丁理解驱动分层与状态机设计。内容更适合具备 C 语言与 WDM 基础、希望深入 NDIS 6.0 框架的中高级开发者研读。1. 网卡驱动真正难的地方是怎么正确安排缓冲区很多人对以太网卡驱动的第一印象是「把数据发到线上」可真写过 NDIS 小端口驱动miniport driver之后会发现难的地方在于你并不拥有那些数据。在 NDIS 模型里miniport 只是一组被调用的回调缓冲区归谁、什么时候可以碰、在哪个 IRQL 上碰全由中间层说了算。搞不清这套安排缓冲区的约定代码在单包 ping 时一切正常一上多队列、真实流量就随机蓝屏。这层机制值得深入分析它是理解内核网络栈稳定性与吞吐上限的入口。2. 驱动的装配过程NDIS 凭什么敢调用你的函数NDIS 小端口驱动没有自己的入口循环也没有main。它提交给系统的是一张静态函数表剩下的全交给中间层调度。这张表填错一个字段后面所有调试都是白费——我这方面有过实际教训花了两个小时在MiniportInitializeEx里断点最后发现是Header.Size少填了几个字节NDIS 根本没读到那个回调。2.1 函数表NDIS_MINIPORT_DRIVER_CHARACTERISTICS 里的三个字段DriverEntry里做的事非常少填表、注册、走人。真正的信息量在Header的三个字段上它们构成了 NDIS 的版本协商机制。static NDIS_MINIPORT_DRIVER_CHARACTERISTICS g_Chars; NDIS_STATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NDIS_HANDLE driverHandle NULL; NDIS_STATUS status; // Header 的三个字段必须先填NDIS 靠它们判断这张表有多大、按哪版结构解析 NdisZeroMemory(g_Chars, sizeof(g_Chars)); g_Chars.Header.Type NDIS_OBJECT_TYPE_MINIPORT_DRIVER_CHARACTERISTICS; g_Chars.Header.Revision NDIS_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_1; g_Chars.Header.Size NDIS_SIZEOF_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_1; // 生命周期回调注册/注销、启停 g_Chars.MiniportInitializeEx MiniportInitializeEx; g_Chars.MiniportHaltEx MiniportHaltEx; g_Chars.MiniportDriverUnload MiniportDriverUnload; // 控制面所有 OID 请求查询 MAC、设 RSS 参数、改卸载开关都从这里进 g_Chars.MiniportOidRequest MiniportOidRequest; g_Chars.MiniportCancelOidRequest MiniportCancelOidRequest; // 数据面发和还两个必须成对出现 g_Chars.MiniportSendNetBufferLists MiniportSendNetBufferLists; g_Chars.MiniportReturnNetBufferLists MiniportReturnNetBufferLists; g_Chars.MiniportCancelSend MiniportCancelSend; // 有硬件中断的物理网卡基本都要 g_Chars.MiniportInterruptEx MiniportInterruptEx; g_Chars.MiniportInterruptDPC MiniportInterruptDPC; status NdisMRegisterMiniportDriver(DriverObject, RegistryPath, NULL, g_Chars, driverHandle); if (status ! NDIS_STATUS_SUCCESS) { return status; // 注册失败直接退出不要继续做任何初始化 } return NDIS_STATUS_SUCCESS; }Size字段的语义是「调用方认为这个结构体有多大」。NDIS 拿到这个值后只按这个长度去读你传进来的内存多出来的部分它不看少了的它也不会替你补。这套设计让同一个二进制可以在不同版本的栈上加载老版本只解析到它认识的位置新版本按Revision决定后面那些字段是否存在。代价是驱动作者一旦手写Size就会遇到「回调明明赋值了却不被调用」的困扰。另一个容易忽略的点NdisMRegisterMiniportDriver是注册「驱动级」能力一个驱动对象只注册一次而每个网卡实例在MiniportInitializeEx里各自独立初始化。这两级生命周期分开正是后面所有状态管理问题的根源。2.2 MiniportInitializeEx 的调用顺序先交上下文再动硬件MiniportInitializeEx会被调用不止一次。资源重新平衡、驱动被禁用后重新启用、系统从休眠恢复都会再走一遍。这意味着任何「全局单例 一次性初始化」的写法都会在第二次调用时出错。里面最关键的顺序约束是必须先把 adapter context 交回去之后拿到的所有回调才有地方存放状态。NDIS_STATUS MiniportInitializeEx(NDIS_HANDLE NdisMiniportHandle, NDIS_HANDLE MiniportDriverContext, PNDIS_MINIPORT_INIT_PARAMETERS InitParameters) { PMP_ADAPTER a NULL; NDIS_STATUS status; NDIS_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES reg; // 每个网卡实例一份绝不共用NonPaged 因为后面会在 DISPATCH_LEVEL 访问 a ExAllocatePoolZero(NonPagedPoolNx, sizeof(MP_ADAPTER), MP_TAG); if (a NULL) return NDIS_STATUS_RESOURCES; a-NdisHandle NdisMiniportHandle; // NDIS 发号驱动只借不换 // 第一次 NdisMSetMiniportAttributes 只能填注册属性 // 作用是把 a 交回去之后 MiniportSendNetBufferLists 里才拿得到它 NdisZeroMemory(reg, sizeof(reg)); reg.Header.Type NDIS_OBJECT_TYPE_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES; reg.Header.Revision NDIS_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES_REVISION_1; reg.Header.Size sizeof(NDIS_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES); reg.MiniportAdapterContext a; reg.AttributeFlags NDIS_MINIPORT_ATTRIBUTES_HARDWARE_DEVICE; reg.SupportedMediaType NdisMedium802_3; // 声明这是以太网 status NdisMSetMiniportAttributes(NdisMiniportHandle, (PNDIS_MINIPORT_ADAPTER_ATTRIBUTES)reg); if (status ! NDIS_STATUS_SUCCESS) { ExFreePoolWithTag(a, MP_TAG); return status; } // 到这里之后才有资格做映射 BAR、建 DMA 通道、注册中断、初始化收发环 status MpMapHardware(a); if (status ! NDIS_STATUS_SUCCESS) goto fail; status MpInitDma(a); // NdisMRegisterScatterGatherDma 等 if (status ! NDIS_STATUS_SUCCESS) goto fail; status MpRegisterInterrupt(a); // NdisMRegisterInterruptEx if (status ! NDIS_STATUS_SUCCESS) goto fail; status MpInitRings(a); // 收发描述符环 NBL 池 if (status ! NDIS_STATUS_SUCCESS) goto fail; // 第二次 SetMiniportAttributes 填通用属性MAC 选项、链路速度、RSS 能力等 return MpSetGeneralAttributes(a); fail: MpTeardown(a); // 失败路径必须能回滚到干净状态 return status; }顺序不是随便排的。中断一定要在 DMA 和环初始化之后才注册否则硬件中断可能在描述符环还没建立时就打进来DPC 里读到的是未初始化的内存。反过来失败回滚必须严格逆序中间漏一步就会留下悬挂的 DMA 通道或未注销的中断下次MiniportInitializeEx直接失败。AttributeFlags也值得注意。HARDWARE_DEVICE表示这是真实硬件而不是虚拟网卡如果驱动要参与电源管理还得配合MiniportPnPEventNotify处理NdisDevicePnPEventSurpriseRemoved——热插拔事件不会给你打招呼收到这个通知之后还去访问硬件寄存器踩中 0x9F 只是时间问题。2.3 KMDF NDIS 双栈为什么网卡驱动要挂两个框架现代以太网 miniport 基本不是一个纯粹的 NDIS 驱动而是一个 KMDF 驱动里嵌着 NDIS 小端口。原因是职责天然分裂NDIS 管网络语义MAC、OID、NBLKMDF 管设备和电源语义PnP、电源状态机、WMI、资源分配。如果全用 NDIS 的老接口自己做 PnP代码量和出错概率都会高一个量级。衔接点是NdisMGetDevicePropertyminiport 用它从 NDIS 手里换回设备对象// 在 MiniportInitializeEx 里调用把 NDIS 适配器和 WDF 设备对象绑起来 NdisMGetDeviceProperty(a-NdisHandle, a-PhysicalDeviceObject, // PDO a-FunctionalDeviceObject, // FDO也就是 WdfDevice a-NextDeviceObject, // 下层设备DMA 要用 NULL, NULL);拿到资源列表的时机在 KMDF 的EvtDevicePrepareHardware那里给的是已翻译的物理地址而 NDIS 侧的MiniportInitializeEx通常在这个调用之后发生。于是常见的实现思路是KMDF 侧先把资源存进设备上下文NDIS 侧再从上下文里取。两个框架的回调交错执行谁先谁后取决于设备栈的启动顺序——这也是为什么「在 DriverEntry 里就把硬件摸一遍」这种写法必然失败。3. 数据通路的所有权NET_BUFFER_LIST 为什么不是「一个包」把 NBL 当成「一个数据包」是新手最容易踩的认知偏差也是很多诡异 bug 的源头。它是三层结构而且一层都不能省。3.1 NBL / NET_BUFFER / MDL 的三级结构NET_BUFFER_LIST是所有权单位也是驱动之间传递的句柄它挂着一串NET_BUFFER每个NET_BUFFER才对应一份真正的数据每个NET_BUFFER再挂一条 MDL 链描述数据在物理内存中的位置。这三级分离带来的直接结果是一条 NBL 里可能有多个包也可能一个包被拆在多个不连续的页上。层级结构职责驱动的典型操作第一级NET_BUFFER_LIST所有权与状态整条归还、设置状态码第二级NET_BUFFER单个数据单元遍历、映射到描述符第三级MDL链物理内存描述生成 SG 表、交给 DMA为什么不让一条 NBL 只放一个包因为要支持分片与合并。协议栈可能把同一个连接上的多个小包合并进一条 NBL 以减少调用次数硬件也可能把一个大包拆到多个描述符。驱动如果假设「一条 NBL 就是一个以太网帧」遇到合并后的 NBL 就会只发第一个包剩下的静默丢弃——而且不会有任何报错只是吞吐上不去。3.2 发送归还时机有三种漏一种就会队列「假死」MiniportSendNetBufferLists的返回值语义很容易被误解返回NDIS_STATUS_SUCCESS并不表示「已经发出去了」而是表示「这些 NBL 我接手了之后我会负责还」。返回失败则意味着整条链退回给 NDIS不调用 complete。VOID MiniportSendNetBufferLists(NDIS_HANDLE MiniportAdapterContext, PNET_BUFFER_LIST NetBufferList, NDIS_PORT_NUMBER PortNumber, ULONG SendFlags) { PMP_ADAPTER a (PMP_ADAPTER)MiniportAdapterContext; PNET_BUFFER_LIST cur, next; PNET_BUFFER_LIST doneHead NULL, doneTail NULL; // 立即完成的链 // SendFlags 带 DISPATCH_LEVEL 说明当前已在 DISPATCH_LEVEL // 不用再调 KeGetCurrentIrql()同时也意味着这里不能等待任何事件 cur NetBufferList; while (cur ! NULL) { next NET_BUFFER_LIST_NEXT_NBL(cur); NET_BUFFER_LIST_NEXT_NBL(cur) NULL; // 先拆链逐条处理 if (MpMapNblToTxRing(a, cur)) { // 成功挂到硬件环上所有权归驱动等发送完成后统一 complete MpKickTxRing(a); } else { // 环满写明失败原因串到立即完成链上稍后一次性还给 NDIS NDIS_SET_NET_BUFFER_LIST_STATUS(cur, NDIS_STATUS_RESOURCES); if (doneHead NULL) { doneHead doneTail cur; } else { NET_BUFFER_LIST_NEXT_NBL(doneTail) cur; doneTail cur; } } cur next; } // 关键没能挂上硬件的必须马上还不能闷着否则上层以为还在飞 if (doneHead ! NULL) { NdisMSendNetBufferListsComplete(a-NdisHandle, doneHead, NDIS_SEND_COMPLETE_FLAGS_DISPATCH_LEVEL); } return NDIS_STATUS_SUCCESS; // 剩下的我接手了 }这里有三条容易翻车的分支。第一环满时不能直接return NDIS_STATUS_RESOURCES了事——那样会把已经成功挂到硬件上的那部分 NBL 也一起退回硬件稍后 DMA 读的是已经被回收的内存结果是随机校验错误。必须先拆链再分别处理。第二NdisMSendNetBufferListsComplete对同一条 NBL 只能调用一次重复调用会让引用计数偏移最终表现为句柄泄漏或者干脆池子耗尽。第三NDIS_SET_NET_BUFFER_LIST_STATUS必须在 complete 之前设好上层就是靠这个状态字段判断是拥塞还是硬件故障的。MpMapNblToTxRing里遍历NET_BUFFER的方式也要注意NET_BUFFER_LIST_FIRST_NB拿到第一个NET_BUFFER_NEXT_NB往后走每个 NB 的NET_BUFFER_DATA_LENGTH才是长度。用NET_BUFFER_LIST_GET_DATA_LENGTH之类的高层宏在小包场景下没问题遇到合并的 NBL 就只算了第一条。3.3 接收NDIS_RECEIVE_FLAGS_RESOURCES 到底改变了什么接收方向的NdisMIndicateReceiveNetBufferLists有一个决定性的标志位NDIS_RECEIVE_FLAGS_RESOURCES。加上它NDIS 会在函数返回之前就把 NBL 通过MiniportReturnNetBufferLists还给你也就是所谓「同步归还」不加所有权就交出去了什么时候还由协议栈的节奏决定。VOID MiniportInterruptDPC(NDIS_HANDLE MiniportInterruptContext, PVOID MiniportDpcContext, PVOID NdisReserved1, PVOID NdisReserved2) { PMP_ADAPTER a (PMP_ADAPTER)MiniportDpcContext; PNET_BUFFER_LIST head NULL, nbl; ULONG count 0; // 从驱动自己预分配的 NBL 池取绝不在这里临时申请内存 while ((nbl MpPopRxNbl(a)) ! NULL) { if (!MpFillFromRxRing(a, nbl)) { // 环里没新包了 MpPushRxNbl(a, nbl); // 放回池子别泄漏 break; } NET_BUFFER_LIST_NEXT_NBL(nbl) head; head nbl; count; } if (count 0) return; // 带 RESOURCES函数返回前 NDIS 就把 NBL 还回来池子不会被掏空 // 适合 DPC 里缓冲有限的场景代价是可能多一次同步往返 NdisMIndicateReceiveNetBufferLists(a-NdisHandle, head, NDIS_DEFAULT_PORT_NUMBER, count, NDIS_RECEIVE_FLAGS_DISPATCH_LEVEL | NDIS_RECEIVE_FLAGS_RESOURCES); }选哪种模式是个真实的取舍。不带RESOURCES时驱动把 NBL 交出去就可以立刻回去收下一批吞吐更高但必须保证这些 NBL 在MiniportReturnNetBufferLists回来之前不被任何人碰——包括不能释放它们背后的 DMA 缓冲因为协议栈可能还在读。带RESOURCES时NBL 在调用返回后就回到你手上可以立刻复用但 NDIS 可能会走「拷贝到自己的缓冲区」这条慢路径。高吞吐场景的常见做法是池子足够大时不用RESOURCES靠MiniportReturnNetBufferLists回收只有在 DPC 里资源紧张、不想让 NBL 悬着时才加上。这里还有一个常被忽略的细节NDIS_RECEIVE_FLAGS_DISPATCH_LEVEL是告诉 NDIS「我现在在 DISPATCH_LEVEL」让它跳过内部的 IRQL 抬升。如果你在 PASSIVE_LEVEL 的上下文里错误地带上这个标志NDIS 会按高 IRQL 处理某些路径上会触发断言。标志位不是「优化建议」而是对当前上下文的如实申报。3.4 零拷贝的真实含义不是不拷贝是绕开 CPU「零拷贝」这个词经常被理解成「数据一次都不搬」其实它指的是让数据不经过 CPU 的通用搬移路径。网卡收发包时数据必须从设备 FIFO 到主机内存这一步 DMA 在做真正被省掉的是 CPU 在中间缓冲区之间复制的那几趟。NDIS 提供的做法是让驱动把NET_BUFFER的 MDL 链翻译成硬件能直接用的散列/聚集描述符表这样硬件按表 DMACPU 完全不参与。// 把一条 NET_BUFFER 的 MDL 链翻译成 SG 描述符数组供网卡 DMA 使用 // 注意传给它的单位是 NET_BUFFER不是整条 NBL——粒度错了 SG 表会漏包 status NdisMAllocateNetBufferSGList( a-DmaHandle, // NdisMRegisterScatterGatherDma 得到的句柄 netBuffer, // 单个 NET_BUFFER a-SgListBuffer, // 驱动预分配的 SG 描述符缓冲 a-SgListBufferSize, 0, // Flags收包路径通常要求同步返回 a-SgList); if (status p a hrefhttps://download.csdn.net/download/lzy0001sl/1807616 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
RELATED

相关推荐

具身机械主义:工程智能的物理实现路径

具身机械主义:工程智能的物理实现路径

1. 具身机械主义的工程本质解析第一次接触具身机械主义这个概念时,我正在调试一台六轴机械臂的末端力矩传感器。当观察到机械臂在未编程情况下自主调整抓取力度以适应不同材质物体时,突然意识到这种"身体决定智能"的现象,恰恰印证了…

📅 2026/11/2 2:22:21
跨国企业软件许可证全球统一管理架构实践

跨国企业软件许可证全球统一管理架构实践

1. 跨国企业面临的许可证管理挑战在全球化运营的跨国企业中,软件许可证管理往往成为IT部门最头疼的问题之一。以Cadence这类电子设计自动化(EDA)软件为例,一套许可证的年费可能高达数十万美元,而企业通常需要为分布在不同国家的研发团队配置数…

📅 2026/11/2 2:17:21
现代前端开发技术栈与核心能力解析

现代前端开发技术栈与核心能力解析

1. 前端开发的真实强度解析熬过前端开发的人都知道,这个领域远不是简单的"切图仔"工作。从HTML/CSS基础到复杂的状态管理,从前端工程化到性能优化,现代前端开发已经形成了完整的技术体系。真正深入其中才会发现,前端工程…

📅 2026/11/2 2:17:21
MORE NEWS

更多资讯

📰

DeepSeek-V3 MLA注意力机制:KV Cache压缩与长上下文推理优化

多头潜在注意力机制(Multi-head Latent Attention,MLA)是 DeepSeek-V3 在模型架构层面最值得拿出来单独讲的一项改动。我第一次读到相关技术报告时,最直观的感受不是"又出了一个新注意力变体",而是它把推理阶…

📰

RK3588多路USB摄像头连续取流实战:从V4L2零拷贝到NPU满载

1. 项目本质与真实痛点:为什么“抓一帧”在工业场景里根本不够用香橙派RK3588跑YOLOv5s,这本身不是新鲜事——但标题里那句“从抓一帧改成循环连续取流”,才是真正在产线、安防、智能交通现场熬过夜的人才懂的硬需求。我去年在东莞一家做AGV视…

📰

LRU缓存机制:哈希表与双向链表的完美结合

1. 为什么LRU缓存机制是链表的终极试炼场当我们需要设计一个缓存系统时,最核心的问题就是:当缓存空间不足时,应该淘汰哪些数据?这就是LRU(Least Recently Used)缓存淘汰算法要解决的问题。它基于"最近…

📰

风电齿轮箱振动监测:DSP就地FFT与远程诊断实战避坑

简介:《风力发电机组状态监测系统研究》是一篇面向风电运维、状态监测与故障诊断方向学习者的期刊论文,聚焦风电机组齿轮箱、轴系等关键部件的振动信号采集与分析,讨论如何借助自主研发的嵌入式微处理器实现就地信号处理、异常报警与远程在线…

📰

OpenClaw多Agent协作平台重构跨境电商运营

1. 项目概述:OpenClaw如何重构跨境电商运营范式 跨境电商行业正面临人力成本飙升与运营效率瓶颈的双重压力。传统模式下,一个标准运营团队需要配置市场分析师、内容编辑、社媒运营、视频制作等多个岗位,完成从市场调研到内容分发的全流程工作…

📰

Python分块计算技术:大数据处理的内存优化方案

1. Python分块计算的核心价值与场景定位当数据集体积超过物理内存容量时,传统的一次性加载方式会直接触发MemoryError。我曾处理过一个电商用户行为日志分析案例,原始CSV文件38GB而服务器内存仅32GB,使用常规pd.read_csv()读取时内存占用峰值…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬