ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Windows内核移植lwIP:NDIS协议驱动下的完整TCP/IP协议栈实现

Windows内核移植lwIP:NDIS协议驱动下的完整TCP/IP协议栈实现 把 lwIP 移植到 Windows 内核这件事我最早是在一个网络设备模拟器项目里被逼出来的。当时需要在用户态和内核态之间来回倒腾自定义的二层协议帧Windows 自带协议栈在这些场景下并不灵活——它替你管好了 TCP/UDP 连接但你要想在 IP 层以下、或者在协议栈内部某个节点上做控制和改造几乎无从下手。研究了一圈决定干脆在内核驱动里用 lwIP 自己搭一套完整的 TCP/IP 协议栈按需裁剪、按需扩展彻底摆脱系统协议栈的束缚。这个方案听起来“反常识”但做下来的收益非常大。lwIP 是一个开源嵌入式 TCP/IP 协议栈结构轻、裁剪性强核心代码不依赖具体操作系统只需要补一层 OS 适配也就是 sys_arch 层就能跑在 Windows 内核态。整条链路打通之后你可以实现在内核里主动管理 TCP 连接、自定义拥塞控制逻辑、按自己的规则收发 IP 报文甚至做协议转换、透明代理、网络监控这类需要深入报文内部的工作。这篇文章就是完整复盘我在 Windows 内核里移植 lwIP、并把它通过 NDIS 协议驱动接到真实物理网卡上的全过程。内容会覆盖方案选型、lwIP 核心结构理解、sys_arch 移植层实现、NDIS 收发通路搭建、常见死锁和内存问题排查适合有一定 Windows 驱动开发基础、或者对协议栈移植感兴趣的读者。如果你只想在内核里做简单的包拦截可以直接用 WFP不用看这篇文章但如果你真的想在系统底层拥有一套完全属于自己的 TCP/IP 协议栈这篇文章应该能帮你少走不少弯路。1. 项目念起为什么要在 Windows 内核里搞一个“自研 TCPIP”1.1 系统协议栈满足不了什么大多数人对 Windows 网络编程的理解停留在 Winsock 层写 socket、调 bind/connect剩下的交给系统。绝大多数业务场景确实不需要关心底层但有一类需求系统协议栈永远给不了你没法在连接建立过程中插入自己的逻辑、没法在 TCP 收到某个特殊载荷时直接触发内核回调、也没法把一条不属于本机 IP 的数据流在内核态处理完再转发出去。举个例子我在做安全审计模块时需要把某些 TCP 流完整重放并分析但系统协议栈收到数据后头已经剥掉、连接状态也由系统管理你想拿到“最原始”的 TCP 分段只能靠注册 WFP 的流层过滤器。WFP 功能虽然强但它只给你钩子不给你协议栈。你要想真正控制重传、控制窗口、控制序列号规则就只能自己写协议栈。lwIP 的价值就在这里。它提供了从 ARP、IP、ICMP、TCP、UDP 到 socket/netconn 的完整实现而且协议核心与具体平台分离官方给出的移植思路就是“你负责实现 sys_arch 和 netif我负责干活”。这意味着只要我在 Windows 内核驱动里把线程、信号量、邮箱、网卡接口这几样东西接上就能拿到一个运行在内核态、具备完整 TCP/IP 能力的独立协议栈。1.2 底层接入方式的三种路线要在 Windows 内核里收发以太网帧不外乎三条路第一条是用TDI经典但已过时。老的传输驱动接口直接暴露 TCP/UDP 会话说白了还是在跟系统协议栈打交道不符合自研协议栈的目标而且微软官方已经在较新系统里把它移除。第二条是NDIS 过滤驱动也就是在网卡驱动和系统协议栈之间插入一层过滤。类似 WFP 但也更底层。它可以看包、改包、拦包但它自身不协调收发状态过滤驱动通常还要配合其他组件实现完整链路结构偏重。第三条是NDIS 协议驱动这是我最推荐的路线。协议驱动直接绑定到网卡适配器上通过 NdisSendNetBufferLists 发送数据通过 ProtocolReceiveNetBufferLists 接收数据。它本身就处在一个“独立协议栈应该待的位置”lwIP 完全可以作为一个普通 NDIS 客户端接收原始以太网帧然后自己处理 IP/TCP/UDP。我最终选择第三条路。原因很直接协议驱动的收发语义和 lwIP 的 netif 接口天然匹配。每个打开的适配器对应一个 lwIP 的网络接口网卡收到的包进 ProtocolReceiveNetBufferLists我只需要把帧复制成 pbuf 再喂给 tcpip_input要发包时lwIP 通过 netif-linkoutput 回调我的驱动入口我再构造 NET_BUFFER_LIST 交给 NdisSendNetBufferLists。整个链路没有多余的层逻辑最清晰。1.3 为什么是 lwIP 而不是 uIP、KTCP选型时我也对比过其他方案。uIP 更适合资源极度受限的 8 位单片机但支持的并发连接数太少报文缓冲区管理也简单放在 Windows 内核里属于杀鸡用牛刀但牛刀太钝。KTCP 这类基于 Windows 内核的协议栈实现资料少、上手门槛偏高遇到问题几乎找不到参考。lwIP 的优势非常明显代码量适中、注释清楚、模块化程度高而且社区庞大很多底层问题在嵌入式领域早就被问到烂了。它在 Windows 下也并非全无先例用户态的 Win32 移植版本很多内核态移植虽然少但原理一致唯一的难点是内核环境的各种限制这个后面会重点展开。2. 移植前必须吃透的四个核心组件2.1 协议核心和操作系统抽象层下载 lwIP 源码之后看目录你会发现最顶层存在两大块src/core和src/api下面是跟平台无关的协议实现另外就是src/arch这里放的是平台相关代码。移植的核心工作就是重写src/arch里的 sys_arch其他部分基本不用改。sys_arch 是 lwIP 对操作系统的所有依赖的抽象集合。包括信号量、互斥量、邮箱、线程创建、系统时间这几类原语。只要你把这几个原语实现成 Windows 内核版本lwIP 内部那一整套并发模型就能正常工作。这里要特别注意lwIP 的协议核心本身不依赖任何 API所有模块之间的通信全部通过 sys_arch 的原语完成。比如tcpip_thread主线程里有一个mbox外部线程把数据包投递进来它从mbox取出数据包再逐个调用协议处理函数。这个模型非常像 Windows 内核里的工作线程加队列理解上几乎没有障碍。2.2 两种 API 怎么取舍lwIP 提供了两种开发接口一种是netconn/socket API专门给用户态或者类用户态调用内部封装了阻塞、缓存、超时这些便利功能另一种是RAW API也就是回调式 API不依赖任何线程同步直接由协议栈线程在上下文中调用你的回调函数。内核态移植时我强烈建议直接禁用 netconn/socket API全部走 RAW API。原因有二。第一netconn/socket API 内部对阻塞、超时等语义的实现足够复杂它假定的运行环境一般是有用户态进程的完整操作系统。你确实可以通过 sys_arch 把它们弄到内核里用但没必要为了一批你大概率用不上的便利函数多承担一堆同步逻辑和隐藏 buffering 的开销。第二RAW API 的函数全部在tcpip_thread这个上下文中被调用你在 tcp_write、tcp_connect 回调里可以放心访问全局数据结构不需要额外加锁。对内核态这种讲究最小化锁竞争的环境来说RAW API 最干净。在lwipopts.h里对应的配置是LWIP_NETCONN 0和LWIP_SOCKET 0。如果确实需要简化接口只开LWIP_NETCONN也行但我会告诉你直接禁掉能省掉很多调试时说不清的坑。2.3 lwIP 的定时器和事件驱动模型lwIP 不是一个完全靠时钟中断驱动的协议栈。它更准确地说是“事件驱动 定时轮询”。默认情况下存在一个全局tcpip_thread消息循环外部事件通过 mbox 投递涉及到超时判断的东西比如 TCP 重传计时、ARP 老化靠的是sys_check_timeouts()这个函数被周期性调用系统会遍历超时链表有超时事件就触发。所以在 Windows 内核态里一个常见的移植错误就是把超时机制丢了。很多人实现完 mbox 和线程之后发现 ARP 表不老化、TCP 重传永远不触发就是因为sys_check_timeouts()根本没人调用。你需要专门建一个内核定时器线程或者干脆在tcpip_thread的主循环里每隔一定 tick 调用一次。更省事的实现是仿照官方做法在sys_arch_timeouts里设置一个定时器来进程唤醒只要下一个超时到来就自动继续执行。具体我会在后续的 sys_arch 章节讲到。2.4 Windows 内核环境IRQL、内存、线程与锁这是移植过程中最核心、最容易翻车的地方。Windows 内核的很多限制是嵌入式操作系统里根本没概念的。第一IRQL中断请求级别。lwIP 协议核心绝大部分代码必须在PASSIVE_LEVEL或更低优先级运行不能在DISPATCH_LEVEL上调协议函数、不能等信号量。但是 NDIS 的很多回调比如接收回调ProtocolReceiveNetBufferLists运行在DISPATCH_LEVEL。这时候直接去调 lwIP 的 API 就是找崩。对策就是把你需要处理的数据封装好通过 mbox 投递到tcpip_thread所在的普通进程协议处理全部在低 IRQL 完成。第二内存分配。lwIP 默认有自己的内存池和内存堆建立在 C 库的 malloc 之上。但在内核态你不能随便调用 C 运行时库必须把mem_malloc这类宏替换成ExAllocatePoolWithTag或者ExAllocatePool2。还要时刻注意在内核高 IRQL 下只能分配非分页内存。lwIP 的 pbuf 分配如果从接收回调里发生就必须使用非分页池但如果从tcpip_thread进程上下文发起分页池问题不大。最稳妥的做法是统一给 pbuf 使用非分页池代价是内存占用高一些。第三线程和调度。tcpip_thread用PsCreateSystemThread创建运行在一个普通系统线程里。建议把它的优先级调高但不要高过真实的时钟中断线程和 NDIS 内部线程太多否则会拖累整体收发性能甚至造成网卡中断饥饿。3. 动手移植搭建工程与核心配置3.1 版本选择与工程目录结构lwIP 新版2.1.x 及之后代码结构已经比较规整我建议直接用 2.2.x 的稳定版。版本太老会有很多无效宏和过时接口文档又少没必要给自己添堵。我当时的目录是这样的my_lwip_driver/ lwip-2.2.0/ src/ core/ netif/ api/ include/ driver/ lwip_ndis_driver.c nsi_import.c lwipopts.h arch/ sys_arch.c sys_arch.h注意src/api如果不需要就可以不编译免得引入一堆不必要符号。核心必选项是core目录下的 ipv4、tcp、udp、icmp、raw、mem、memp、pbuf、timeouts以及netif/ethernet.c和netif/etharp.c。Windows 内核驱动建议用 Visual Studio 的“内核模式驱动程序空项目”模板编译目标选 x64。lwIP 源代码整体编译但要把所有警告当错误那关关掉否则一堆C4152函数指针类型转换会让你直接崩溃。3.2 lwipopts.h 裁剪与关键配置lwIP 的配置集中在lwipopts.h里。它实际上覆盖了src/include/lwip/opt.h里的默认值。我把我的关键配置列出来并解释为什么这么设。#define NO_SYS 0 #define LWIP_NETCONN 0 #define LWIP_SOCKET 0 #define LWIP_ARP 1 #define LWIP_ETHERNET 1 #define LWIP_ICMP 1 #define LWIP_RAW 1 #define LWIP_UDP 1 #define LWIP_TCP 1 #define LWIP_DHCP 0 #define LWIP_AUTOIP 0 #define LWIP_DNS 0 #define MEM_SIZE (1024 * 1024) #define MEMP_NUM_PBUF 256 #define MEMP_NUM_TCP_SEG 512 #define PBUF_POOL_SIZE 256 #define TCP_MSS 1460 #define TCP_WND (16 * TCP_MSS) #define TCP_SND_BUF (16 * TCP_MSS) #define CHECKSUM_GEN_IP 1 #define CHECKSUM_GEN_UDP 1 #define CHECKSUM_GEN_TCP 1 #define CHECKSUM_CHECK_ICMP 0 #define LWIP_CHECKSUM_ON_COPY 0 #define LWIP_RAND() my_kernel_rand()NO_SYS是 0打开系统抽象层。LWIP_NETCONN和LWIP_SOCKET都关掉因为我们在内核态不需要高层 API。DHCP 关掉是因为内核态网卡 IP 通常是手动配置或者由另一个控制线程分配没必要引入 UDP 客户端的复杂度。DNS 同理。LWIP_RAND一定要自定义。默认的伪随机数生成器在嵌入式上都没问题但你移植到另一个平台如果没注意会导致 TCP 初始序号可预测这在安全场景下是致命伤。我在内核里直接用KeQueryPerformanceCounter的低位做随机种子再套一个轻量 xorshift简单可靠。3.3 内存相关pcb 与池的大小估算关于内存大小很多人一上来就拍脑袋。实际上可以估算每个 TCP 控制块TCB占用大约几百字节到 1KB加上发送和接收缓冲假设你要同时维持 128 个 TCP 连接且每个连接发送缓冲 16 个 MSS发送缓冲就要 16×1460×128 ≈ 3MB。这也太大了所以我实际把TCP_SND_BUF调到 8 个 MSS控制内存总量在 1MB 左右。内核内存不像用户态你申请不到就用不了必须按实际业务规模来设计。PBUF_POOL_SIZE决定接收路径能同时缓存多少帧。如果抓包时发现丢包率奇高先看这个值是不是太小。NDIS 的回调有时会一次性给你多个包如果池不够用包会直接被丢弃。这个在性能调优部分还会详细说。4. 关键动手sys_arch 移植层实现4.1 信号量与互斥量映射内核态里的信号量最简单直接用 Windows 的KSEMAPHORE即可。lwIP 的信号量操作包括创建、等待、释放、删除。映射关系非常直观err_t sys_sem_new(sys_sem_t *sem, u8_t count) { KeInitializeSemaphore(sem, count, 0x7FFFFFFF); return ERR_OK; } u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout) { LARGE_INTEGER interval; NTSTATUS status; if (timeout 0) { status KeWaitForSingleObject(sem, Executive, KernelMode, FALSE, NULL); } else { // 将毫秒转换为相对时间 interval.QuadPart -10000 * (LONGLONG)timeout; status KeWaitForSingleObject(sem, Executive, KernelMode, FALSE, interval); } return (status STATUS_SUCCESS) ? 0 : SYS_ARCH_TIMEOUT; }互斥量同理用KMUTEX初始化KeWaitForSingleObject和KeReleaseMutex对应获取和释放。唯一要记住的点是Windows 内核对象的等待超时单位是 100 纳秒负数表示相对超时时间换算别搞错。我就在这里踩过一次坑把 10ms 直接当成 10 个单位传入结果所有等待瞬间超时。4.2 邮箱mbox实现lwIP 的 mbox 是一个线程安全的消息队列用来在tcpip_thread和外部上下文之间传递指针。内核态实现 mbox 最常见的方式是“自旋锁 环形队列 信号量”。当外部比如接收回调往 mbox 投递数据时加锁放指针然后释放信号量tcpip_thread等待信号量得到只有一个消息到达加锁取指针然后解锁。这跟用户态的无锁消息队列很相似但叠加了内核态的调度优先级语义。另外要注意lwIP 有个变体操作叫sys_mbox_trypost它不阻塞。这个必须实现成真正的尝试性入队如果队列满了直接返回ERR_MEM绝对不能在接收回调里调用一个会阻塞的信号量等待那会把DISPATCH_LEVEL下的系统搞死。我见过太多人把trypost实现成post加超时这也是典型的蓝屏源头之一。4.3 线程创建与优先级lwIP 只要求一个核心线程就是tcpip_thread。如果你是 RAW API也可以不开这个线程直接把所有协议调用放在驱动工作线程里手动调用tcpip_input但那样并发模型就变了。建议标准实现保留tcpip_thread和tcpip_timeout两个线程。线程创建用PsCreateSystemThread注意回调例程运行在系统线程上下文不能在里面直接调那些和进程绑定的 API。线程优先级设置的教训是设为LOW_REALTIME_PRIORITY比如 8-10比较合适。设得太高比如 24 以上会和 NDIS 内部工作线程抢 CPU网卡中断处理可能被拖慢反而明显掉包。我还推荐单独分配一个PKTHREAD引用并保存到全局变量便于以后在线程销毁时ObDereferenceObject。驱动卸载时这两条线程如果不退出会导致系统直接保存不稳定严重时卸载卡死。4.4 时间与随机数lwIP 里有个sys_now()函数要返回毫秒级时间。内核态最稳的是用KeQueryTickCount乘系统时钟周期转换或者直接KeQueryPerformanceCounter按频率换算。注意不要依赖 RDTSC某些系统下主频会动态变化容易计算出负的时间差。随机数同样别用 C 库函数。我在上面的lwipopts.h里已经埋了宏LWIP_RAND()实现里我用的是KeQueryPerformanceCounter取低 32 位做种子再跑一个 xorshift32。不用为安全级别要求极高的系统设计加密级随机数那不是 lwIP 的职责但如果你的设备有硬件随机数发生器接入进去更好。5. 把网卡接到 lwIPNDIS 收发通路5.1 注册协议驱动并绑定适配器要在内核里收发真实网络包首先得让协议栈“看见”网卡。NDIS 6 协议驱动的流程是定义NDIS_PROTOCOL_DRIVER_CHARACTERISTICS调用NdisRegisterProtocolDriver注册驱动在ProtocolBindAdapterEx回调里调用NdisOpenAdapterEx打开网卡绑定完成后得到NDIS_HANDLE这就是后续所有收发操作的凭据。这里有个很常见的坑ProtocolBindAdapterEx是异步回调不能在这函数里直接阻塞等待打开完成。你要把绑定请求记录到一个工作队列等ProtocolOpenAdapterCompleteEx回调回来再初始化 lwIP 的 netif。如果顺序乱了净接口还没初始化网卡已经开始送包直接内核崩溃。初始化 netif 的伪代码如下err_t my_netif_init(struct netif *netif) { netif-hwaddr_len 6; memcpy(netif-hwaddr, adapter-mac, 6); netif-mtu 1500; netif-flags NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP; netif-output etharp_output; netif-linkoutput my_linkoutput; netif-state adapter; return ERR_OK; }linkoutput最终指向我自己的发送函数每次 lwIP 要发以太网帧时会调它。5.2 发送链路从 pbuf 到 NET_BUFFER_LISTlwIP 发数据时会把待发送的报文放在一个pbuf链上然后调用netif-linkoutput。我要做的是把这个 pbuf 链转换成 NDIS 能识别的NET_BUFFER_LIST。一个 pbuf 可能由多段内存组成比如协议头一段、数据一段而NET_BUFFER_LIST内部也是由多个 MDL 组成的链。好的做法是把 pbuf 链里的每段内存都映射成一个 MDL然后把所有这些 MDL 装配到同一个NET_BUFFER_LIST里。这样发送时不需要把数据整体复制到一个连续缓冲区零拷贝。NET_BUFFER_LIST *nbl my_pbuf_to_nbl(pbuf, adapter); NdisSendNetBufferLists(adapter-bindHandle, nbl, NDIS_SEND_FLAGS_DISPATCH_LEVEL);发送之后ProtocolSendNetBufferListsComplete会在异步回调里通知你发送结束。这时必须把这个NET_BUFFER_LIST释放并把关联的 refcount 减掉。lwIP 的 pbuf 生命周期管理是从你linkoutput进入那一刻起由你负责直到发送完成回调回来你告诉 lwIP 发送完成——所以这里你需要做一次 ref 计数防止在发送完成前把 pbuf 释放掉。我的做法是给每个 NBL 的MiniportReserved字段存一个驱动自用结构保存它对应的 pbuf 首地址。发送完成回调里通过NdisGetPoolFromNetBufferList找到结构然后释放 MDL、释放 NBL、最后pbuf_free(pb)。这套闭环在压力测试下跑几个月不泄漏。5.3 接收链路从网卡到 tcpip_input接收方向有点特殊。ProtocolReceiveNetBufferLists这个回调可能在DISPATCH_LEVEL被调用也可能在被动级别。它的典型通知方式有两种一种是当你注册时指定了接收不会再二次授权也就是系统告诉你这些缓冲可以随便改另一种是收到的包还可以被其他驱动看。为简化我按标准方式来处理void ProtocolReceiveNetBufferLists( NDIS_HANDLE ProtocolBindingContext, NET_BUFFER_LIST *NetBufferLists, NDIS_PORT_NUMBER PortNumber, ULONG NumberOfNetBufferLists, ULONG ReceiveFlags) { // 1. 遍历 NetBufferLists 中的每个 NET_BUFFER // 2. 把帧数据复制到 lwIP 的 pbuf // 3. 调用 tcpip_input(pbuf, netif) }不要试图直接把 Windows 的 MDL 挂到 lwIP 的 pbuf 上。原因是 NDIS 的接收缓冲一般是网卡驱动通过NdisAllocateNetBufferListPool预先分配的它的生命周期由 NDIS 管理你在一个回调里拿到一个 NBL如果只是把 MDL 的指针赋值给 pbuf让别人以为 pbuf 拥有这块内存那一旦 pbuf 被 lwIP 延迟处理底层缓冲早被网卡驱动回收了数据就烂了。最安全、最通用的是复制。用一个pbuf_alloc(PBUF_RAW, frame_len, PBUF_POOL)申请 pbuf把以太网帧复制进去然后tcpip_input(pb, netif)。复制带来的性能损失在千兆网卡上大约是 1%-3%可接受换来的是不用处理复杂的内存归属问题。复制完数据后要调用NdisReturnNetBufferLists把 NBL 还给底层。这一步别忘否则网卡驱动会发现缓冲池被耗尽直接停止接收。5.4 中断与 DPC 协作NDIS 接收回调在 DPC 级别之下执行时不能直接等待任何锁。所以接收路径里所有行为都要做到非阻塞。使用tcpip_input投递 pbuf 进 mbox是唯一推荐的方式。mbox 实现里的自旋锁在 DPC 级别也是安全的它保护的是队列操作本身不会长时间持有。自旋锁选KeAcquireSpinLockAtDpcLevel而不是普通版本因为后者会在 DPC 级别引发断言异常。另外网卡频繁中断会造成接收路径大量上下文切换。如果项目性能要求高可以考虑把接收线程绑定到一个高优先级工作线程并配合中断合并interrupt moderation来减少中断次数。但那是另一个层面的工作了不要在移植初期就做先把功能跑通再谈优化。6. 调试、踩坑与性能优化6.1 常见崩溃与排查技巧我在这套移植项目里遇到的崩溃几乎全部集中在下面几类IRQL_NOT_LESS_OR_EQUAL这是最常见的。原因基本就是你在DISPATCH_LEVEL下调用了可分页内存或者等待信号量。排查方法是打开驱动验证器Driver Verifier勾选“中断请求级别检查”和“内存一致性检查”跑一遍压力测试它会告诉你第一条违规代码在哪一行。内存泄漏在 Windows 里可以用池标记分配ExAllocatePoolWithTag第三个参数设置一个四个字符的 TAG用 poolmon 工具观察哪个标记持续增长。我自己的NDIS_NBL分配和 pbuf 分配的泄漏就是靠 poolmon 抓出来的。死锁冻结常见于并发路径中锁的顺序不对。tcpip_thread 在等待 mbox 信号量时如果接收回调又试图获取同一个自旋锁而那个锁被 tcpip_thread 持有两头互相等就会死锁。后期我把锁的获取顺序统一为“自旋锁 信号量 互斥量”再没有遇到死锁。蓝屏在发送完成回调里访问 pbuf因为发送完成回调可能在任何线程上下文执行你存的内存附属结构要保持引用。记得做 pbuf ref 计数发送完成后用pbuf_free时如果发现 refcount 不为 0说明还有一个持有者在发网络包不能急着释放。6.2 性能调优方向我做完功能之后用iperf分别测了 TCP 和 UDP 吞吐。第一批结果惨不忍睹千兆网卡只有 300Mbps-400Mbps。经过几轮调整到接近 700Mbps下面这几个改动效果最明显。第一个是关闭校验和卸载。虽然网卡硬件支持 TCP/UDP 校验卸载但 lwIP 的驱动逻辑没有很好地利用这一点有时会让网卡辅助校验导致大小端和 checksum 偏移处理不统一出现大量校验失败的重传。我直接把CHECKSUM_GEN_TCP和CHECKSUM_GEN_UDP打开让 lwIP 自己在软件里做性能虽有一点下降但重传和错误包肉眼可见地减少。第二个是扩大的 mbox 容量和 PBUF_POOL_SIZE。接收突发流量时如果 mbox 满了数据包就被直接丢弃。TCP 会重传UDP 就丢了。把 mbox 深度从 16 扩到 64PBUF_POOL_SIZE从 128 扩到 256掉包率显著下降。第三个是避免多次复制。接收路径上最开始我为了省事在ProtocolReceiveNetBufferLists里先复制到临时缓冲区再从这个临时缓冲区复制到 pbuf。优化后直接逐 MDL 复制到 pbuf省了一次 CPU 拷贝整体提升约 10%。第四是优先级调整与 DPC 延迟。网卡驱动中断合并打开后网卡会攒一批包再触发中断减少 DPC 数量有效降低 CPU 占用。对 lwIP 来说它更关心的是单次能拿到一批包所以这也是天然有利的。6.3 稳定性建议经过三个月持续运行我总结出几条稳定优先策略写在这里当作经验总结统一内存分配路径。要么全用非分页池要么明确标记哪些场景用分页池不要混用。混合使用在高 IRQL 下崩溃的概率会显著增加。协议处理只留在tcpip_thread不要在其它线程里直接调协议函数。你以为是并行增加效率实际上 lwIP 的全局锁会给你带来大量无意义的等待和死锁风险。所有 NDIS 回调里的异常都要用try/except包起来捕获不能容忍任何异常逃逸。驱动卸载顺序要和初始化的顺序严格相反先停止收发再销毁网卡绑定最后销毁 lwIP 的线程和内存池。顺序错了很容易蓝屏。内核世界没有“调试控制台”早做日志。我在每个关键入口都加了一个DbgPrint配合 WinDbg 的!analyze -v崩溃时能快速定位到最后一次操作省了大量时间。最后再分享一个小技巧调试这套驱动时如果你有一台不重要的物理机可以在 VM 里开 Windows然后把 lwIP 驱动装到虚拟机里配合虚拟网卡做测试。这样崩溃的是虚拟机宿主机不受影响还容易做快照回滚。把功能验证、性能摸底、压力测试全部完成之后再上真实物理机跑周期测试。先拿软环境把逻辑调稳再进硬件环境处理时序问题这是我做这套移植项目时最省心的一条经验。
返回列表