首页 / 资讯中心 / 文章详情

NDIS小端口驱动开发:注册链路、NBL收发与硬件初始化避坑

NDIS小端口驱动开发:注册链路、NBL收发与硬件初始化避坑 ★ FEATURED ARTICLE
简介面向希望深入理解 NDIS 6.0 小端口驱动开发的 Windows 内核与网络驱动开发者尤其是在 DDK 示例之外寻找可参考代码的中高级人员这份资料以 Realtek 8111、8168、8169、8110 等 PCI 千兆以太网控制器为对象给出一个可研读的 miniport 驱动实例围绕网卡初始化、数据收发、驱动注册、寄存器访问与中断注册等核心环节展开用于弥补公开案例偏少的问题。压缩包共 69 个文件约 615KB其中 gif、png、js、css、htm 多为网页快照及页面资源zip 与 rar 包则承载源码、说明和按主题整理的版本便于从 LSO、Jumbo、电源管理等方向切入。已有 1242 人学习读者可结合驱动入口、设备初始化、DMA 描述符与收发队列、中断处理、链路状态及兼容性排查等环节阅读代码也可据此搭建实验环境、理解千兆网卡驱动框架、梳理 NDIS 6.0 编程要点并积累常见调试与排错思路和方法。1. 网卡被识别但链路不通NDIS 小端口驱动卡在哪一层设备管理器里硬件能枚举、厂商 ID 也对网络适配器列表里却始终没有那块以太网卡或者网卡出现了收发计数永远是 0。很多人第一反应是硬件没焊好实际排查到后面往往是 NDIS 小端口驱动miniport driver没把适配器注册成一块以太网卡——回调表少填一项、MiniportInitializeEx中途返回失败但资源没回滚都会表现成这种“半死不活”。这份资源给的就是一套可直接编译加载的以太网 miniport 骨架包含 INF、注册链路、NBL 收发通路和描述符环对接部分。适合正在做网卡 bring-up 的驱动工程师、需要虚拟网卡做流量注入的测试同学以及想彻底搞清NET_BUFFER_LIST所有权的人。前提是一台能开测试签名、能挂内核调试器的实验机别在生产机上试。2. 把骨架立起来DriverEntry 到 MiniportInitializeEx 的注册链路一套 miniport 能不能加载成功八成在DriverEntry返回之前就决定了。后面所有收发问题都是“加载成功之后”的事先把注册这一关走通比急着写收发划算得多。2.1 先做选型NDIS miniport 还是 NetAdapterCx如果目标是物理网卡、需要自己管描述符环、需要精确控制中断亲和性和 SG DMA 的映射粒度那 NDIS miniport 是绕不开的。它对上暴露一个 802.3 接口对下把寄存器、DMA、中断翻译成NET_BUFFER_LIST中间的缓冲区池、OID 分发、PnP 暂停/重启都由 NDIS 帮你兜住。如果只是做一块虚拟网卡、或者想在数据路径上做点轻量处理NetAdapterCx 会省事很多它架在 KMDF 之上NBL 池和大量 OID 样板代码都被类扩展吃掉了对象生命周期跟着 KMDF 走。代价是对老平台的支持面窄且你想精细控制描述符环时会发现能插手的地方不多。NDIS 版本的选择更务实一点。6.20 是个比较稳的最低公分母NBL 池、SG DMA、接收队列这些现代接口它都有6.30 往上加了 QoS 相关的东西6.50 才正式支持 SR-IOV 那套虚拟化路径再往上的版本会把可加载的最低系统版本抬高。这套骨架用的是 6.20理由不是它新而是它在“接口够用”和“兼容面宽”之间取了个平衡点。真要上 SR-IOV把MinorNdisVersion提到 50 并补齐虚拟功能相关回调工作量不小别在中途改。还有一处容易忽略INF 里的*IfType、*MediaType、*PhysicalMediaType是给网络配置界面和协议栈看的用来判断这是块以太网卡而真正决定加载哪个 NDIS 版本的是二进制里的MajorNdisVersion/MinorNdisVersion。两边描述的介质类型对不上协议栈绑定会失败现象是适配器在但没有协议绑定上去。2.2 回调表哪些必须填缺了会怎样NDIS_MINIPORT_DRIVER_CHARACTERISTICS里字段很多但不是每个都必须。判断标准很简单NDIS 会在 PnP 或数据路径上无条件调用它你没填就是空指针。按这套骨架的经验必填项和漏填后的表现大致是回调漏填后的典型表现InitializeHandlerEx/HaltHandlerEx适配器根本起不来或卸载时卡死UnloadHandler驱动无法卸载只能重启PauseHandler/RestartHandler禁用/启用网卡时系统挂起NDIS 6.20 起强制要求SendNetBufferListsHandler/ReturnNetBufferListsHandler能 ping 通网关但大包一发就断或 NBL 永不归还CancelSendHandler上层取消发送时挂起!ndiskd.pendingnbls能看到堆积CheckForHangHandlerEx/ResetHandlerEx硬件卡住后系统无法自愈只能强制断电OidRequestHandler/CancelOidRequestHandleripconfig查询不到任何信息链路状态永远是未知DevicePnPEventNotifyHandler/ShutdownHandlerEx休眠唤醒后适配器消失或关机时蓝屏Header三个字段是第一个坑Type要写NDIS_OBJECT_TYPE_MINIPORT_DRIVER_CHARACTERISTICSSize要写sizeof(NDIS_MINIPORT_DRIVER_CHARACTERISTICS)Revision要和你实际使用的结构版本一致。这三个值写错NdisMRegisterMiniportDriver直接返回NDIS_STATUS_BAD_VERSION设备管理器里只给你一个黄色感叹号什么线索都没有。2.3 DriverEntry三件事顺序不能反NDIS_HANDLE g_MiniportDriverHandle NULL; NDIS_STATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NDIS_STATUS Status; NDIS_MINIPORT_DRIVER_CHARACTERISTICS MpChars; /* 先清零再逐字段填残留的栈垃圾会让 Header 校验失败 */ NdisZeroMemory(MpChars, sizeof(MpChars)); MpChars.Header.Type NDIS_OBJECT_TYPE_MINIPORT_DRIVER_CHARACTERISTICS; MpChars.Header.Size sizeof(NDIS_MINIPORT_DRIVER_CHARACTERISTICS); MpChars.Header.Revision NDIS_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2; MpChars.MajorNdisVersion 6; MpChars.MinorNdisVersion 20; /* 与二进制里其他版本相关判断保持一致 */ MpChars.InitializeHandlerEx MpInitialize; MpChars.HaltHandlerEx MpHalt; MpChars.UnloadHandler MpUnload; MpChars.PauseHandler MpPause; MpChars.RestartHandler MpRestart; MpChars.OidRequestHandler MpOidRequest; MpChars.CancelOidRequestHandler MpCancelOidRequest; MpChars.SendNetBufferListsHandler MpSendNetBufferLists; MpChars.ReturnNetBufferListsHandler MpReturnNetBufferLists; MpChars.CancelSendHandler MpCancelSend; MpChars.CheckForHangHandlerEx MpCheckForHang; MpChars.ResetHandlerEx MpReset; MpChars.ShutdownHandlerEx MpShutdown; MpChars.DevicePnPEventNotifyHandler MpDevicePnPEventNotify; Status NdisMRegisterMiniportDriver(DriverObject, RegistryPath, (NDIS_HANDLE)0, MpChars, g_MiniportDriverHandle); if (Status ! NDIS_STATUS_SUCCESS) { return Status; /* 走到这里基本都是表结构或版本对不上别往下做任何事 */ } return STATUS_SUCCESS; }NdisMRegisterMiniportDriver一旦成功NDIS 就持有你的回调表指针了所以MpChars必须是全局或堆上长期有效的对象不能是DriverEntry栈上的临时变量——骨架里之所以写成全局就是为了避免这个。第四个参数传的 context 会原样出现在后续所有回调里很多实现拿它存驱动级配置这里留 0 表示不用。返回值直接透传NDIS_STATUS是可以的DriverEntry返回非成功值时加载器会放弃加载。注意不要在这里做硬件访问那个阶段还没有适配器实例。2.4 MiniportInitializeEx注册属性 → 映射资源 → 注册 DMA 与中断MiniportInitializeEx是整个驱动里唯一跑在PASSIVE_LEVEL、又能拿到硬件资源的地方顺序错了后面全是连锁故障。骨架里的顺序是分配适配器上下文 → 调NdisMGetDeviceProperty拿 PDO 和资源 → 先设注册属性 → 再设通用属性 → 映射寄存器 → 注册 SG DMA → 注册中断。前两步的顺序是硬性的NdisMSetMiniportAttributes的第一次调用必须是注册属性。static NDIS_STATUS MpInitialize(NDIS_HANDLE AdapterHandle, NDIS_HANDLE DriverContext) { PMP_ADAPTER p NULL; NDIS_STATUS st; NDIS_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES regAttr; NDIS_MINIPORT_ADAPTER_GENERAL_ATTRIBUTES genAttr; p (PMP_ADAPTER)NdisAllocateMemoryWithTag(g_Handle, sizeof(MP_ADAPTER), paMN); if (p NULL) return NDIS_STATUS_RESOURCES; NdisZeroMemory(p, sizeof(MP_ADAPTER)); p-AdapterHandle AdapterHandle; /* 拿到 PDO、功能设备对象、下一层设备对象和资源列表 */ st NdisMGetDeviceProperty(AdapterHandle, p-PhysicalDeviceObject, p-FunctionalDeviceObject, p-NextDeviceObject, p-ResourceList, p-ResourceListTranslated); if (st ! NDIS_STATUS_SUCCESS) goto Fail; /* 第一次 SetMiniportAttributes 必须是注册属性 */ NdisZeroMemory(regAttr, sizeof(regAttr)); regAttr.Header.Type NDIS_OBJECT_TYPE_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES; regAttr.Header.Size NDIS_SIZEOF_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES_REVISION_1; regAttr.Header.Revision NDIS_MINIPORT_ADAPTER_REGISTRATION_ATTRIBUTES_REVISION_1; regAttr.MiniportAdapterContext p; regAttr.AttributeFlags NDIS_MINIPORT_ATTRIBUTES_HARDWARE_DEVICE | NDIS_MINIPORT_ATTRIBUTES_SURPRISE_REMOVE_OK; regAttr.InterfaceType NdisInterfacePci; st NdisMSetMiniportAttributes(AdapterHandle, (PNDIS_MINIPORT_ADAPTER_ATTRIBUTES)regAttr); if (st ! NDIS_STATUS_SUCCESS) goto Fail; /* 通用属性描述这是块什么网卡 */ NdisZeroMemory(genAttr, sizeof(genAttr)); genAttr.Header.Type NDIS_OBJECT_TYPE_MINIPORT_ADAPTER_GENERAL_ATTRIBUTES; genAttr.Header.Size NDIS_SIZEOF_MINIPORT_ADAPTER_GENERAL_ATTRIBUTES_REVISION_2; genAttr.Header.Revision NDIS_MINIPORT_ADAPTER_GENERAL_ATTRIBUTES_REVISION_2; genAttr.MediaType NdisMedium802_3; genAttr.PhysicalMediumType NdisPhysicalMedium802_3; genAttr.MtuSize 1500; genAttr.MinMtuSize 1500; genAttr.MaxMtuSize 1500; genAttr.MacOptions 0; /* 6.20 起多数 MAC 选项被忽略留 0 最安全 */ genAttr.MacAddressLength 6; NdisMoveMemory(genAttr.CurrentMacAddress, p-PermanentAddress, 6); NdisMoveMemory(genAttr.PermanentMacAddress, p-PermanentAddress, 6); genAttr.MaxMulticastListSize 32; genAttr.SupportedPacketFilters NDIS_PACKET_TYPE_DIRECTED | NDIS_PACKET_TYPE_MULTICAST | NDIS_PACKET_TYPE_BROADCAST | NDIS_PACKET_TYPE_ALL_MULTICAST | NDIS_PACKET_TYPE_PROMISCUOUS; genAttr.AccessType NET_IF_ACCESS_BROADCAST; genAttr.DirectionType NET_IF_DIRECTION_SENDRECEIVE; genAttr.ConnectionType NET_IF_CONNECTION_DEDICATED; genAttr.IfType IF_TYPE_ETHERNET_CSMACD; genAttr.IfConnectorPresent TRUE; st NdisMSetMiniportAttributes(AdapterHandle, (PNDIS_MINIPORT_ADAPTER_ATTRIBUTES)genAttr); if (st ! NDIS_STATUS_SUCCESS) goto Fail; /* 映射寄存器 BAR、注册 SG DMA、注册中断、初始化描述符环 */ if ((st MpMapRegisters(p)) ! NDIS_STATUS_SUCCESS) goto Fail; if ((st MpSetupSgDma(p)) ! NDIS_STATUS_SUCCESS) goto Fail; if ((st MpRegisterInterrupt(p)) ! NDIS_STATUS_SUCCESS) goto Fail; if ((st MpInitRings(p)) ! NDIS_STATUS_SUCCESS) goto Fail; MpEnableInterrupts(p); NdisMSetMiniportAttributes(AdapterHandle, NULL); /* 骨架里用它通知初始化结束 */ return NDIS_STATUS_SUCCESS; Fail: MpHalt(AdapterHandle, 0); /* 统一走一遍逆序回滚避免半初始化状态 */ return st; }几个参数值得单独说。AttributeFlags里带NDIS_MINIPORT_ATTRIBUTES_SURPRISE_REMOVE_OK意味着你能处理设备被突然拔走的情况如果硬件确实支持最好带上否则在某些热插拔场景下 NDIS 会直接把适配器标成不可恢复。MacOptions在 6.20 之后大部分标志位都不再生效写一堆NDIS_MAC_OPTION_*反而是旧代码迁移过来的遗留骨架里留 0。MaxMulticastListSize填了 32 就要真的支持 32 个组播地址硬件只有 16 个表项的话超出部分得自己用哈希或者干脆拒绝否则上层加入组播组之后收不到包排查起来会以为是转发逻辑问题。失败路径统一跳Fail再走一次MpHalt这个写法看着别扭但特别省事回滚逻辑只有一份中间任何一步失败都不会漏掉前面已经申请的东西。血泪经验是很多“第一次加载正常、第二次加载蓝屏”的案例根因都是失败路径上少释放了一个池或者少注销了一个中断。3. 收发通路NET_BUFFER_LIST 的所有权转移与三个归还时机数据路径上的 bug本质都是所有权问题。NET_BUFFER_LIST在某个时刻只能属于一方——要么是 NDIS 的要么在 miniport 手上要么在协议栈手上——搞混了就是泄漏、重复释放或者数据错乱。3.1 NBL、NB、MDL 三层结构谁拥有什么NET_BUFFER_LIST是一个链表的节点本身不装数据它挂着的NET_BUFFER才描述一段报文NET_BUFFER再挂 MDL 链指向真正非分页内存。一个 NBL 可以挂多个 NB同一个优先级队列上的一组报文所以任何“假设一个 NBL 只有一个 NB”的代码都是定时炸弹。分配上有两条路。一是NdisAllocateNetBufferListPool建一个池再用NdisAllocateNetBufferAndNetBufferList从池里分配并绑定数据缓冲区——适合接收路径缓冲区固定、复用率高。二是只建 NBL 池NB 和数据都由别处给适合发送路径做零拷贝。骨架里两条都用了。池参数里DataSize需要想清楚填 0 表示 NB 的数据不跟着 NBL 一起分配之后要用NdisRetreatNetBufferDataStart或自己挂 MDL。填具体值就是预分配好处是省一次分配坏处是大包场景下内存占用会比想象的高。PoolTag一定要给个可读的 4 字节值之后在!poolfind里能一眼认出是不是自己泄漏的。3.2 发送MiniportSendNetBufferLists 里第一件事是摘链static VOID MpSendNetBufferLists(NDIS_HANDLE AdapterContext, PNET_BUFFER_LIST NetBufferList, NDIS_PORT_NUMBER PortNumber, ULONG SendFlags) { PMP_ADAPTER p (PMP_ADAPTER)AdapterContext; PNET_BUFFER_LIST nbl NetBufferList; PNET_BUFFER_LIST done NULL; while (nbl ! NULL) { PNET_BUFFER_LIST next NET_BUFFER_LIST_NEXT_NBL(nbl); NET_BUFFER_LIST_NEXT_NBL(nbl) NULL; /* 先摘链防止归还时链被并发修改 */ if (SendFlags NDIS_SEND_FLAGS_RESOURCES) { /* 资源语义NDIS 立刻需要这块内存必须在本次调用返回前完成 */ NdisMSendNetBufferListsComplete(p-AdapterHandle, nbl, NDIS_STATUS_SUCCESS); } else if (MpTxRingFull(p)) { /* 环满就压进软件队列等发送完成中断里补 */ MpQueuePendingTx(p, nbl); } else { if (MpProgramTxDescriptor(p, nbl) ! NDIS_STATUS_SUCCESS) { NdisMSendNetBufferListsComplete(p-AdapterHandle, nbl, NDIS_STATUS_FAILURE); } else { NET_BUFFER_LIST_NEXT_NBL(nbl) done; done nbl; /* 已提交硬件等中断里归还 */ } } nbl next; } /* done 链表上的 NBL 由发送完成路径统一归还这里不能碰 */ }SendFlags里最要命的是NDIS_SEND_FLAGS_RESOURCES。带这个标志时NDIS 的意思是“我内存紧张你必须在返回前把 NBL 还给我”此时不能入队、不能挂起、不能等中断必须同步调一次NdisMSendNetBufferListsComplete。不少实现在这里踩坑先判断环满满了就入队结果RESOURCES分支根本没走到上层内存池被耗尽整个协议栈的发送直接停摆。另一个细节是摘链。NDIS 传进来的可能是一串 NBL如果你不摘链直接调NdisMSendNetBufferListsComplete归还其中一个NDIS 会把整条链都收走剩下的指针全成了野指针。骨架里的做法是进循环就断链每个 NBL 独立处理这样无论后面是入队、提交还是直接归还都不会互相影响。NDIS_SEND_FLAGS_SINGLE_QUEUE表示这条链只属于一个队列可以整体处理如果没带这个标志说明队列可能不同需要按端口号区分。骨架只支持单端口所以直接忽略做多队列时这里必须分开。3.3 接收Indicate 的 RESOURCES 标志决定语义static VOID MpIndicateRx(PM P_ADAPTER p) { PNET_BUFFER_LIST head NULL, tail NULL; ULONG count 0; while (MpRxHasPacket(p) count p-RxIndicateBudget) { PVOID data MpRxDequeueBuffer(p, p-RxFrameLen); PNET_BUFFER_LIST nbl NdisAllocateNetBufferAndNetBufferList( p-NblPool, 0, 0, p-RxMdl, 0, p-RxFrameLen); if (nbl NULL) break; NET_BUFFER_LIST_NEXT_NBL(nbl) NULL; if (head NULL) head nbl; else NET_BUFFER_LIST_NEXT_NBL(tail) nbl; tail nbl; count; } if (head ! NULL) { NdisMIndicateReceiveNetBufferLists(p-AdapterHandle, head, NDIS_DEFAULT_PORT_NUMBER, count, 0); /* 不带 RESOURCES所有权交给协议栈等 MpReturnNetBufferLists 再回收 */ } }最后一个参数是 0 还是NDIS_RECEIVE_FLAGS_RESOURCES语义完全不同。不带RESOURCES时NdisMIndicateReceiveNetBufferLists返回后 NBL 的所有权在协议栈你不能碰它直到MiniportReturnNetBufferLists回调被触发。带RESOURCES时调用返回后所有权立刻回到你这儿语义是同步的——如果你在调用返回之后还去读那块缓冲区或者立刻把它还给描述符环协议栈可能正在用同一块内存表现就是随机的 CRC 错误或内容错乱。预算参数RxIndicateBudget控制单次 DPC 里最多上送多少个包。给太大DPC 占用时间过长系统整体响应变差给太小中断频率上去了CPU 全耗在 DPC 进出上。骨架里默认 64实测在千兆线速下比较平衡。接收路径上还要注意LookaheadSize通用属性里设了 256协议栈只会预读前 256 字节来判断协议头超过这个长度的数据靠 MDL 链后续补齐不用在 indicate 阶段全拷一遍。3.4 归还路径三个时机别搞混NET_BUFFER_LIST有三种回法对应三个不同场景发送失败或RESOURCES语义下直接调NdisMSendNetBufferListsComplete发送成功等待硬件完成的在发送完成中断里先把待归还的 NBL 从软件队列或描述符上摘下来再整条链调NdisMSendNetBufferListsComplete接收侧不带RESOURCES上送的只能在MiniportReturnNetBufferLists里回收到描述符环。这三条路径的缓冲区来源不同回收目标也不同。发送路径回收的是 NBL 本身缓冲区是上层给的接收路径回收的是 NBL 加缓冲区一起。骨架里给收发各建了一个池用PoolTag区分!ndiskd.nblpool一眼能看出哪个池在涨。最后一点容易被忽略NdisMSendNetBufferListsComplete里的SendCompleteFlags在部分场景下要带NDIS_SEND_COMPLETE_FLAGS_DISPATCH_LEVEL表示当前 IRQL 是DISPATCH_LEVEL。漏了这个标志NDIS 会按PASSIVE_LEVEL处理在某些路径上直接触发断言。4. 硬件那一层寄存器映射、SG DMA 与中断注册的先后顺序软件骨架跑通之后真正让网卡出包的是寄存器、DMA 和中断这三样。它们的初始化顺序有依赖中断里要访问描述符环描述符环要在 DMA 注册之后才能拿到物理地址所以顺序只能是映射寄存器 → 注册 DMA → 建环 → 注册中断。4.1 从 PDO 取资源NdisMGetDeviceProperty 拿到的是什么NdisMGetDeviceProperty一次性给你五样东西物理设备对象PDO、功能设备对象FDO、下一层设备对象、总线资源列表、翻译后的资源列表。寄存器基地址要从翻译后的列表里取因为总线号、地址映射在翻译阶段才定下来直接读原始列表在某些平台上拿到的是总线相对地址映射出来就是错的。for (i 0; i p-ResourceListTranslated-Count; i) { PCM_RESOURCE_LIST_ENTRY e p-ResourceListTranslated-List[0].Descriptors[i]; if (e-Type CmResourceTypeMemory) { p-RegPhysBase e-u.Memory.Start; p-RegLength e-u.Memory.Length; break; } } if (p-RegPhysBase.QuadPart 0) return NDIS_STATUS_RESOURCES; p-RegBase NdisMMapIoSpace(NULL, AdapterHandle, p-RegPhysBase, p-RegLength); if (p-RegBase NULL) return NDIS_STATUS_RESOURCES;NdisMMapIoSpace做的是把物理地址映射成内核虚拟地址成功后所有寄存器访问都走NdisReadRegisterUlong/NdisWriteRegisterUlong这类封装。直接用指针解引用在某些架构上会因为缺少内存屏障而读到旧值尤其是写完控制寄存器立刻读状态寄存器判断是否生效的那种写法一定要用带屏障的封装。CmResourceTypeMemory和CmResourceTypeMemoryLarge要分开处理后者是给大于 4GB 的 BAR 用的枚举时要判断e-Flags CM_RESOURCE_MEMORY_LARGE_40之类的位。骨架里只处理了常见的小 BAR 情形做新硬件时这块必须补。4.2 描述符环与共享内存对齐、一致性与 NdisMAllocateSharedMemory描述符环是驱动和硬件共同读写的内存必须用物理连续、非缓存的内存否则硬件 DMA 写进去的数据可能还躺在 CPU 缓存里驱动读到的就是旧值。/* 描述符环必须非缓存硬件和 CPU 都要看到最新值 */ st NdisMAllocateSharedMemory(p-AdapterHandle, p-TxRingSize * sizeof(TX_DESC), FALSE, /* Cached FALSE */ p-TxRingVa, p-TxRingPa); if (st ! NDIS_STATUS_SUCCESS) return st; /* 报文缓冲区可以缓存CPU 拷贝时走缓存DMA 前再刷 */ st NdisMAllocateSharedMemory(p-AdapterHandle, p-RxBufferSize, TRUE, /* Cached TRUE */ p-RxBufferVa, p-RxBufferPa);Cached这个参数决定了 CPU 缓存是否参与。描述符环一定要FALSE代价是每次访问都慢一点但换来的是硬件和 CPU 视图一致。报文缓冲区用TRUE可以显著提升拷贝性能前提是 DMA 前后要正确刷缓存——NdisMAllocateSharedMemory分配出来的缓存内存在交给硬件之前需要KeFlushIoBuffers或者相应的 DMA 例程做同步漏掉这一步的典型现象是“偶发丢包重发就好”。对齐也有讲究。描述符环的起始物理地址通常要求按环大小对齐比如 256 项 × 16 字节的环要求 4KB 对齐硬件手册里会写。NdisMAllocateSharedMemory返回的地址按页对齐一般够用但如果硬件要求更严格的对齐得自己多分配一点再手动对齐。SG DMA 那侧用NdisMInitializeScatterGatherDma注册MaximumPhysicalMapping设置单个 DMA 传输的最大物理跨度设小了会导致大包被拆成多段、效率下降设大了会超过硬件 DMA 引擎的能力得照手册来。4.3 中断注册与 ISR/DPC 分工NDIS_MINIPORT_INTERRUPT_CHARACTERISTICS intr; NdisZeroMemory(intr, sizeof(intr)); intr.Header.Type NDIS_OBJECT_TYPE_MINIPORT_INTERRUPT; intr.Header.Size NDIS_SIZEOF_MINIPORT_INTERRUPT_REVISION_1; intr.Header.Revision NDIS_MINIPORT_INTERRUPT_REVISION_1; intr.InterruptHandler MpIsr; intr.InterruptDpcHandler MpIntDpc; intr.EnableInterruptHandler MpEnableInterrupt; intr.DisableInterruptHandler MpDisableInterrupt; intr.MsiSupported FALSE; /* 用 MSI-X 时置 TRUE 并补 Message* 回调 */ st NdisMRegisterInterruptEx(p-AdapterHandle, p, intr, p-InterruptHandle);ISR 跑在DIRQL这个级别高到不能调用几乎任何 NDIS 服务所以它的职责只有两个判断中断是不是自己的、把中断源关掉然后返回TRUE让 NDIS 排队执行 DPC。真正干活——回收发送完成的 NBL、指示接收包——都在 DPC 里。static BOOLEAN MpIsr(NDIS_HANDLE InterruptContext, PBOOLEAN QueueDpc, PULONG TargetCpu) { PMP_ADAPTER p (PMP_ADAPTER)InterruptContext; ULONG status NdisReadRegisterUlong(p, REG_INT_STATUS); if ((status INT_OWN) 0) return FALSE; /* 不是自己的中断立刻返回 */ NdisWriteRegisterUlong(p, REG_INT_MASK, 0); /* 先关源避免中断风暴 */ *QueueDpc TRUE; return TRUE; }*QueueDpc置TRUE之后NDIS 会调度你注册的 DPC。返回FALSE表示这个中断不是给你的这在共享中断线上很常见。如果 ISR 里没有先关中断源就返回TRUE硬件会一直拉着中断线系统表现为 CPU 单核 100%、鼠标卡顿——这不是玄学是中断风暴。DPC 里做的事情要控制在一个预算内回收完成的发送、上送接收包然后重新开中断。开了中断马上又进来就形成了“中断—DPC—开中断”的循环所以在 DPC 结束前要判断环里还有没有待处理的包有就自己再调度一次 DPC而不是急急忙忙开中断。4.4 卸载与复位MpHalt 里必须逆序拆MiniportHaltEx要做的事情正好是初始化的逆序禁中断 → 注销中断NdisMDeregisterInterruptEx→ 停 DMA → 释放共享内存 → 释放 NBL 池 → 释放适配器上下文。顺序反了会出两种问题先释放上下文再注销中断中断还能进来读到已经释放的内存先释放共享内存再停 DMA硬件还在往那块物理地址写运气好是脏数据运气差就是总线错误。MiniportResetEx是另一条路径硬件卡死时 NDIS 会调它。这里不需要释放资源只需要把描述符环清空、重置硬件状态机、把挂起的 NBL 全部以失败完成、然后恢复中断。骨架里的做法是把环指针归零之后重新初始化一遍比逐个清理描述符要可靠。5. 避坑清单五次把实验机搞蓝屏的现场记录下面这些坑基本覆盖了从加载到卸载的完整生命周期每一条都是真机跑出来的。现象、原因、解决三段式对着排就行。5.1 加载阶段的三个坎现象一本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站