LWIP接收零拷贝:无MPU配置下的实现与Cache维护
2026/8/30 18:23:29 网站建设 项目流程

LWIP的接收路径默认并不是零拷贝的,DMA把帧搬进内存后,驱动还要再用memcpy把数据挪进pbuf,一次转发就要多付一次几十微秒的CPU开销。项目标题里的[LWIP] Rx buffers zero copy no MPU config,说白了就是在不配置MPU(Memory Protection Unit)的条件下,把接收缓冲区直接交给协议栈,省掉中间那次拷贝,同时还得保证Cache一致性不出问题。我这次是在GD32F407平台上做的,跑FreeRTOS + LwIP,配合LAN8720A,后面也会提到带D-Cache的Cortex-M7平台怎么做,因为无MPU场景下真正的坑全在Cache那边。

这篇文章面向的是已经在MCU上移植过LWIP、想进一步榨取网络性能的开发者。零拷贝的原理不难,但真正落地的时候,描述符管理、内存池回收、Cache维护时机、对齐问题,任何一个细节没顾到,表现出来的就是丢包、乱码、甚至硬错误。我把自己调试过程中的思路和踩坑记录整理出来,重点放在“无MPU配置”这个前提下怎么把接收路径做干净。

1. LWIP接收零拷贝项目:到底在解决什么问题

1.1 传统接收路径里“多出来的那次拷贝”

先看默认的接收流程。以太网帧到达后,MAC的DMA会把数据写到驱动预先设置好的接收缓冲区(通常是一个静态数组或者DMA描述符自带的buffer),然后产生接收中断。驱动在中断里把数据从DMA缓冲区拷贝到pbuf的payload区域,再调用netif->input把pbuf交给协议栈。这个memcpy就是接收路径上最大的不必要开销。

假设你跑100M以太网,线速12.5MB/s,每个包平均500字节,每秒大概2.5万个包。如果每个包都多拷贝一次500字节,那就是每秒12.5MB的额外内存搬移。对跑在168MHz的Cortex-M4来说,这个负担占到CPU资源的一大部分,而且它本身不产生任何业务价值。

零拷贝的思路很直接:DMA描述符的buffer地址直接指向pbuf的payload区域。DMA收到帧后自动把数据写进这个pbuf,驱动在中断里只需要检查描述符状态、做必要的Cache维护、然后把pbuf上送协议栈。数据从头到尾只有一次写入(DMA写入),CPU不会再搬动它。

1.2 “不配MPU”为什么是个值得讨论的话题

在带Cache的高性能M核(比如Cortex-M7)上,MPU通常是用来解决DMA与Cache一致性问题的常用手段。手段很简单:用MPU把DMA缓冲区所在的SRAM区域配置成non-cacheable,这样CPU不缓存这块内存,DMA写入后CPU直接读到的就是真实数据。

但在很多实际工程里,项目没有启用MPU,也可能是产品代码里压根没人写过MPU配置。这时候麻烦就来了:如果D-Cache是打开的,默认情况下所有SRAM区域都是cacheable,DMA和CPU之间没有自动同步机制。这种情况还能不能做零拷贝?答案是可以,但你必须自己维护Cache一致性,而且需要注意的细节比配MPU多得多。

如果你的平台本身是Cortex-M4(比如GD32F407),内部没有L1 D-Cache,那“无MPU配置”做零拷贝反而没有任何额外负担,只需要处理描述符和内存池逻辑即可。但如果你迁移到STM32H7这类M7芯片,同样的代码跑起来就可能会乱码或死机,原因就是Cache。所以这篇文章会把两种情况都讲清楚,避免你在平台切换时掉坑。

2. LWIP接收路径与零拷贝的原理解读

2.1 pbuf结构和接收数据流

要理解零拷贝,先得知道pbuf是什么。pbuf是LWIP管理网络数据的基本单元,分PBUF_RAM、PBUF_POOL、PBUF_ROM等类型。接收路径上最常用的是PBUF_POOL,它由内存池分配,每个pbuf包含一个管理头(pbuf结构体)和一段数据区,payload指针指向数据区起始位置。

默认接收流程是这样的:

  1. DMA收到帧,写入驱动预设的DMA buffer。
  2. 驱动在接收中断(或轮询)里发现描述符的OWN位被DMA清掉,说明帧已完成接收。
  3. 驱动调用pbuf_alloc分配一个PBUF_POOL类型的pbuf。
  4. 驱动调用memcpy,把帧数据从DMA buffer拷贝到pbuf->payload。
  5. 驱动把pbuf交给netif->input,协议栈后续处理。

零拷贝的做法是倒过来:在初始化的时候,驱动就预先分配好一批PBUF_POOL的pbuf,并且把每个RX描述符的buffer地址设置为对应pbuf的payload。这样第3、4步不再需要,DMA直接写入协议栈能用的数据区。驱动要做的只是维护描述符和pbuf之间的对应关系。

2.2 零拷贝的关键:描述符与pbuf的绑定和回收

这里有个核心设计:描述符环中的每个RX描述符,必须能通过某种方式找到它对应的pbuf。最简单的办法是在驱动里维护一个静态数组,比如buf_to_pbuf[RX_DESC_NUM],初始化时填充,接收时用描述符索引直接取出pbuf。

但这引出另一个问题:当协议栈处理完pbuf后会调用pbuf_free,释放的内存回到LWIP的内存池里。此时驱动并不知道这个pbuf已经被释放了,如果不做任何处理,对应描述符的buffer指针就悬空了,下一次DMA往这块内存写入数据时,这块内存可能已经被其他模块占用,造成数据踩踏。

常用的解决办法是在主循环或中断出口处做一次“描述符补充”操作(rx_recycle)。驱动扫描描述符环,凡是当前处于空闲状态(DMA不拥有)且之前绑定的pbuf已经被释放掉的描述符,就重新从内存池分配一个新的pbuf,把buffer指针更新后,再重新把描述符交给DMA。如果内存池里的pbuf一个都没被释放,那就说明应用层持有数据太久了,此时只能放弃补充,等下一轮再试。

2.3 Cache一致性:真正的“隐形敌人”

如果平台带D-Cache,且没有MPU配置,最容易被忽视的就是Cache一致性问题。DMA写内存绕过CPU的Cache,它把新数据写到了物理SRAM里,但如果CPU的Cache里还留着这块地址的旧数据,CPU读到的依然是旧值,表现出来就是“收到的包内容不变”或者偶发乱码。

反过来也一样:CPU把数据写进缓冲区后,数据可能还滞留在Cache里没有写回物理内存,DMA去读物理内存时读到的还是旧数据。发送路径上这个问题同样存在。

无MPU、只靠软件维护时,通常使用两条指令:Clean(写回/清理)和Invalidate(失效)。把Cache line里的数据写回物理内存叫Clean;把Cache line标记为无效,让CPU下次读的时候从物理内存重新加载叫Invalidate。在接收方向,DMA写完数据后、CPU读数据前,必须对接收缓冲区的地址范围做Invalidate。在发送方向,CPU写完数据后、DMA启动传输前,必须对发送缓冲区做Clean(必要时Clean+Invalidate),确保数据真正到达物理内存。

同时要注意对齐。Cache维护的最小单位是cache line,Cortex-M7的D-Cache line一般是32字节。如果你的buffer起始地址或长度没有对齐到32字节,Invalidate操作可能会把同一cache line里CPU刚写入的有效数据一并失效掉,造成数据丢失。这就是为什么零拷贝缓冲区的地址和大小都要做对齐处理,不能随便给个结构体指针就用。

3. 实操:无MPU配置下的LWIP零拷贝接收

3.1 平台准备与LWIP关键配置

我这次实际调通的平台是GD32F407VET6 + LAN8720A + FreeRTOS + LwIP 2.1.2。GD32F407是Cortex-M4F内核,没有L1 D-Cache,因此这个平台适合先验证零拷贝的逻辑,不需要考虑Cache维护。同套逻辑拿到Cortex-M7平台时,再补上Cache操作即可。

先看LWIP这边需要调整的宏定义。lwipopts.h里几个关键配置如下:

#define MEM_ALIGNMENT 4 #define ETH_PAD_SIZE 0 #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1524 #define LWIP_ETHERNET 1 #define LWIP_NETIF_STATUS_CALLBACK 1 #define LWIP_DHCP 0 #define LWIP_UDP 1 #define LWIP_TCP 1 #define CHECKSUM_GEN_IP 1 #define CHECKSUM_GEN_UDP 1 #define CHECKSUM_GEN_TCP 1 #define CHECKSUM_CHECK_IP 1 #define CHECKSUM_CHECK_UDP 1 #define CHECKSUM_CHECK_TCP 1

PBUF_POOL_SIZE决定接收缓冲池大小,PBUF_POOL_BUFSIZE决定每个pbuf的容量。以太网最大帧1518字节,算上VLAN标签1522,1524足够覆盖,再多出一点余量给对齐用。如果你的应用需要接收超长帧,这个值得同步调大。

DMA描述符数量方面,GD32的以太网控制器支持可配置的描述符数量,我设置为16个,与PBUF_POOL_SIZE保持一致,这样每个描述符对应一个pbuf,管理最简单。如果描述符数量比pool数量多,就会有描述符缺少pbuf可绑定的情况,处理起来会麻烦很多。

3.2 驱动层改造:把pbuf挂到DMA描述符上

核心思路是:在描述符初始化时,不创建独立的DMA buffer,而是从LWIP内存池里拿pbuf,然后把描述符的buffer地址指向pbuf->payload。以GD32的enet驱动为例,改造后的初始化流程类似这样:

#define RX_DESC_NUM 16 #define TX_DESC_NUM 4 struct eth_dma_desc { uint32_t status; uint32_t control; uint32_t buffer_addr; uint32_t next_desc_addr; void *pbuf_ptr; /* 驱动自定义字段,保存pbuf指针 */ }; static struct eth_dma_desc rx_desc[RX_DESC_NUM]; static struct pbuf *rx_pbuf_pool[RX_DESC_NUM]; static void low_level_init(struct netif *netif) { int i; /* ... MAC地址配置、PHY复位、DMA全局配置等 ... */ for (i = 0; i < RX_DESC_NUM; i++) { struct pbuf *p = pbuf_alloc(PBUF_POOL, PBUF_POOL_BUFSIZE, PBUF_POOL); if (p == NULL) { /* 分配失败要处理,实际代码里这里应该报错并停止初始化 */ while (1); } rx_pbuf_pool[i] = p; rx_desc[i].pbuf_ptr = p; rx_desc[i].buffer_addr = (uint32_t)(p->payload); /* 描述符给DMA,清除OWN位前配置好控制字 */ rx_desc[i].status = ENET_RX_DESC_OWN; rx_desc[i].control = (RX_DESC_BUFSIZE & ENET_RX_DESC_BUFFER_SIZE_MASK) | ENET_RX_DESC_CTRL_RX_EN; } /* 建立描述符链表 */ for (i = 0; i < RX_DESC_NUM; i++) { rx_desc[i].next_desc_addr = (i < RX_DESC_NUM - 1) ? (uint32_t)&rx_desc[i + 1] : (uint32_t)&rx_desc[0]; } enet_dma_rx_desc_chain_init(rx_desc); enet_dma_enable(); }

这里要特别注意,描述符的buffer_addr必须直接取p->payload,而不是取pbuf结构体首地址。payload才是数据区,pbuf结构体头里存的是next指针、len、ref等管理字段,DMA往那里写会把协议栈内部数据踩坏。

另外,PBUF_POOL类型的pbuf,其payload默认会做对齐,但不同平台对齐粒度不同。如果你后续要在带Cache的M7上跑,建议把pbuf的payload地址手动强制对齐到32字节,或者用宏#define LWIP_MEM_ALIGN 32将全局内存对齐调整到Cache line大小,这样Cache操作会省心很多。

3.3 low_level_input的实现:从描述符直接上送pbuf

接收数据时,驱动要做的事情比传统方式少很多。low_level_input的基本逻辑如下:

static struct pbuf *low_level_input(struct netif *netif) { uint32_t desc_idx; struct pbuf *p; uint32_t len; uint32_t status; desc_idx = rx_current_index; status = rx_desc[desc_idx].status; if (status & ENET_RX_DESC_OWN) { /* DMA还拥有这个描述符,没有新数据 */ return NULL; } p = (struct pbuf *)rx_desc[desc_idx].pbuf_ptr; if (status & ENET_RX_DESC_ERR_MASK) { /* CRC错误、长度错误等,直接释放重配 */ pbuf_free(p); p = pbuf_alloc(PBUF_POOL, PBUF_POOL_BUFSIZE, PBUF_POOL); if (p == NULL) { return NULL; } rx_pbuf_pool[desc_idx] = p; rx_desc[desc_idx].pbuf_ptr = p; rx_desc[desc_idx].buffer_addr = (uint32_t)(p->payload); rx_desc[desc_idx].status = ENET_RX_DESC_OWN; rx_current_index = (desc_idx + 1) % RX_DESC_NUM; return NULL; } /* 帧长度 */ len = (status & ENET_RX_DESC_FRAME_LEN_MASK) >> ENET_RX_DESC_FRAME_LEN_SHIFT; /* 无MPU且开启D-Cache的平台上,这里必须加Invalidate操作。 Cortex-M4/M4F没有D-Cache,这步直接跳过 */ /* SCB_InvalidateDCache_by_Addr((uint32_t *)(p->payload), len); */ p->len = len; p->tot_len = len; /* 当前描述符暂时交给协议栈,驱动侧不再拥有这个pbuf。 重新分配新pbuf并补齐描述符的操作放到rx_recycle里统一做。 */ rx_desc[desc_idx].pbuf_ptr = NULL; rx_pbuf_pool[desc_idx] = NULL; rx_desc[desc_idx].buffer_addr = 0; rx_desc[desc_idx].status = 0; rx_current_index = (desc_idx + 1) % RX_DESC_NUM; return p; }

注意这里有个细节:描述符一旦上送协议栈,驱动必须立刻把该描述符标记为无效,“暂时退出环”。否则DMA拥有这个描述符后,会往一块已经被协议栈占用的内存区域写数据,轻则覆盖数据,重则踩坏协议栈内部指针。

3.4 描述符回收:防止内核池枯竭和硬件停顿

当协议栈处理完pbuf后,会调用pbuf_free释放它回内存池。但驱动没有直接的回调钩子,所以需要在主循环里做回收。最简单的方式是在ethernetif_input外面再加一个rx_recycle函数,每次poll完所有接收帧后调用一次:

static void rx_recycle(void) { int i; for (i = 0; i < RX_DESC_NUM; i++) { if (rx_desc[i].pbuf_ptr == NULL) { /* 这个描述符空闲,重新分配pbuf绑定 */ struct pbuf *p = pbuf_alloc(PBUF_POOL, PBUF_POOL_BUFSIZE, PBUF_POOL); if (p == NULL) { /* 池子被占满了,说明协议栈还没释放足够的pbuf,稍后再试 */ break; } rx_pbuf_pool[i] = p; rx_desc[i].pbuf_ptr = p; rx_desc[i].buffer_addr = (uint32_t)(p->payload); /* 如果上层带Cache,这里最好做一次Clean+Invalidate, 把该缓冲区中可能残留的脏Cache行清干净再交给DMA */ /* SCB_CleanInvalidateDCache_by_Addr((uint32_t *)(p->payload), PBUF_POOL_BUFSIZE); */ rx_desc[i].status = ENET_RX_DESC_OWN; } } }

主循环结构变成:

for ( ;; ) { ethernetif_input(&g_netif); rx_recycle(); /* 其他任务 */ }

这个recycle放在主循环里有个好处:不会在中断上下文里做pbuf_alloc这类耗时操作,避免中断关闭时间过长。中断里收到帧后只把事件记录,真正处理放在ethernetif_input的轮询逻辑中,符合高实时性系统的常规做法。

3.5 带D-Cache平台:无MPU下的Cache维护示范

如果你把上面的代码直接搬到一个带D-Cache的Cortex-M7平台(比如STM32H750、STM32H743),并且D-Cache是打开的,那么必须补上Cache维护。核心位置就三处:

第一处,low_level_input里取到帧长度后、返回到协议栈之前,对payload区域做Invalidate:

SCB_InvalidateDCache_by_Addr((uint32_t *)(p->payload), len);

第二处,rx_recycle里把新分配的pbuf重新挂到描述符之前,对整块缓冲区做Clean+Invalidate:

SCB_CleanInvalidateDCache_by_Addr((uint32_t *)(p->payload), PBUF_POOL_BUFSIZE);

为什么recycle这里要用Clean加Invalidate?因为这块pbuf在上一次被协议栈使用期间,CPU可能往里面写过数据(比如应用层修改了payload),这些数据可能还残留在Cache里。如果不Clean掉,DMA可能把残留数据发出或覆盖掉新数据。做一个Clean+Invalidate能把脏数据写回物理内存,同时清掉Cache里的旧内容,保证接下来DMA写入时CPU不会读到陈旧数据。

第三处,发送路径上,如果要像接收路径一样做零拷贝发送,在DMA启动发送之前必须对发送缓冲区做Clean:

SCB_CleanDCache_by_Addr((uint32_t *)(tx_buf->payload), tx_len);

发送路径上只Clean还不够保险,因为发送完成后DMA可能又修改了描述符状态,但CPU读取描述符状态时也可能读到错误值,所以描述符所在内存区域也得配合Invalidate处理。这也是为什么很多人建议发送时干脆用普通拷贝方式,避免这套Cache维护的麻烦,只做接收零拷贝性价比最高。

3.6 应用层读取与pbuf生命周期管理

零拷贝接收的收益最终要体现在应用层。拿UDP接收举例,传统方式下驱动已经拷贝过一次,应用收到pbuf后如果又用pbuf_copy_partial拷贝到自己的大数组,那前面省下的开销又白费了。正确的做法是直接在pbuf的payload上做业务解析。

static void udp_echo_recv(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { /* 直接在p->payload上处理数据,不要memcpy到自己数组 */ /* 这里做一个回环示例 */ udp_sendto(upcb, p, addr, port); pbuf_free(p); }

但要注意,数据在payload里是线性的吗?PBUF_POOL的pbuf理论上可以链成多个节点,但实际上只要PBUF_POOL_BUFSIZE大于单帧长度,协议栈接收时拆包后的payload是连续的,你可以直接读p->payload。如果做了IP分片重组,情况会复杂一些,需要遍历pbuf链。普通局域网环境MTU为1500,单帧不会走到分片路径。

另一个关键点是pbuf的释放时机。零拷贝模式下,pbuf就是DMA缓冲区,你不释放它,rx_recycle就永远分不到新pbuf,整个接收环就会停转。实际项目里最常见的死机/停网都是因为这个:应用层为了性能把收到的数据指针保存到全局队列,延迟处理,结果dpbuf一直被占着。

4. 常见问题与排查技巧

4.1 数据乱码、内容不更新,先查Cache

症状:接收第一次数据正确,后续收到的包内容永远是第一个包的内容,或者偶发出现错位字节。这种情况优先怀疑Cache。在带D-Cache平台上,如果low_level_input里没有做Invalidate,CPU读到的永远是Cache里的旧数据。第一次收到数据时Cache里没有旧内容,所以还能正常工作,第二次开始就出问题了。

还有一种情况:Invalidate的长度不对。比如帧长度是250字节,你只Invalidate了前面200字节,那后面50字节可能读的是Cache旧值。注意SCB_InvalidateDCache_by_Addr要求长度按32字节向上对齐,最好这样写:

uint32_t aligned_len = (len + 31) & ~31U; SCB_InvalidateDCache_by_Addr((uint32_t *)(p->payload), aligned_len);

另外,如果pbuf的payload地址没有对齐到32字节,Invalidate会波及相邻cache line。所以建议在初始化时就把pbuf payload地址强制对齐,或者在lwipopts.h里设置:

#define MEM_ALIGNMENT 32

这样LWIP在内存池分配时会把内存地址对齐到32字节,省去后续很多麻烦。

4.2 收几个包后网络就“死了”,通常是池子耗尽了

症状:刚启动时收发正常,跑几秒或收几十个包后,网卡再也不收包了,但ping还能通或者是完全不通,复位后才能恢复。

这种问题大部分是pbuf池耗尽导致的。协议栈在处理pbuf时,如果应用迟迟不释放,内存池里的pbuf就被占光,rx_recycle分配不到新pbuf,描述符环上的所有描述符都处于空闲状态,DMA没有buffer可用,网络自然就停了。

排查方法很简单:在rx_recycle里加一个计数器,当分配失败时累加,打印出来。如果发现持续分配失败,就去看是不是应用层占着pbuf不放。

另外,PBUF_POOL_SIZE设置太小也会出现这种问题。16个pbuf在TCP多连接、高突发场景下可能不够,尤其是有TCP接收窗口数据重排时,协议栈会额外持有很多pbuf。建议先设32个看看余量,确认稳定后再往回收。

4.3 DMA描述符永远不回收,只在一个位置死循环

症状:零拷贝代码运行后,网卡能收包,但驱动里描述符状态不正常,rx_recycle里一直在扫描同一个位置,其他位置的pbuf_ptr都变成NULL了,但新pbuf分配不出来。

这种情况有两种可能。一种就是上面说的pool耗尽。另一种是mpb结构里ref计数没清干净。pbuf_free不等于真正释放,如果协议栈对pbuf做了多次引用(比如TCP重传),ref计数会大于1,pbuf_free只是ref--,不会释放内存,pbuf_ptr自然也不会变成可回收状态。

排查时可以打印每个描述符的状态和对应pbuf的ref值。如果ref一直大于1,说明协议栈还没处理完这个包,不用管它,继续等。

4.4 开启D-Cache后系统跑飞,先加屏障指令

症状:加上Cache操作后,系统偶尔死机,尤其在高负载时,Hard Fault中断频发。这通常是总线同步问题。Cache维护指令需要配合内存屏障使用,确保指令执行顺序不被编译器或CPU乱序优化破坏。

安全做法是在Cache操作前后加上:

__DSB(); __ISB();

例如:

SCB_InvalidateDCache_by_Addr((uint32_t *)(p->payload), aligned_len); __DSB();

尤其是DMA启动之前,必须确保所有Cache写入已经完成,否则DMA可能读到半写状态的数据。这块在正式代码里一定不能省。

4.5 常见问题速查表

现象可能原因排查手段
收包内容固定不变忘记Invalidate D-Cache在low_level_input里补上Invalidate
偶发乱码/丢包缓冲区地址未对齐Cache line将MEM_ALIGNMENT设为32
跑一会收不到包PBUF_POOL耗尽检查应用是否释放pbuf
Hard Fault随机出现缺DSB/ISB内存屏障Cache操作后补屏障
DMA描述符状态异常描述符被协议栈数据踩踏确认buffer_addr指向payload而非pbuf头
高负载时性能不升反降Cache维护开销过大考虑开MPU配置non-cacheable区域

5. 个人实操经验与后续扩展建议

5.1 哪些场景值得上零拷贝

并非所有项目都适合零拷贝。如果只是低速率采集、控制类通信,每秒几十个包,传统拷贝方式完全够用,没必要引入额外复杂度。零拷贝真正带来收益的场景是高吞吐、小包密集,或者CPU还需要同时跑其他计算密集任务。

我在实际测试中测过,GD32F407跑UDP echo,传统拷贝方式下百兆带宽跑到60Mbps左右CPU占用就已经很高了。改成零拷贝后,同样吞吐下CPU占用能低10到20个百分点。但这只是经验值,不同平台和协议栈版本有差异,建议按项目实际情况做对比测试。

另外,如果平台带D-Cache,做零拷贝前先想清楚能不能接受Cache维护的开销。在STM32H7上,Invalidate一个1500字节缓冲区的开销大约几微秒,一秒钟几万个包就是几十毫秒的额外耗时。这种场景下,配一个MPU region把缓冲区设为non-cacheable反而更划算,一次配置,永久省心。

5.2 后续可以继续做的事

如果零拷贝接收已经跑稳,可以继续往两个方向扩展。一是发送路径零拷贝,把协议栈下发的pbuf直接挂到DMA发送描述符上,减少一次拷贝,但要注意TCP重传时pbuf生命周期管理更复杂,建议先从UDP场景验证。二是多网口场景,比如同时跑以太网和USB网卡,LwIP的netif层天然支持多个接口,但描述符分配和回收逻辑都要跟着改成per interface,不能共用静态数组,这块需要认真设计。

5.3 最后分享一个小技巧

在调试零拷贝接收时,我习惯在代码里加一个调试钩子,统计每个描述符从“上送协议栈”到“重新挂上DMA”之间的时间。如果这个时间经常超过几毫秒,说明应用层拿住pbuf太久,接收环随时可能断。这个指标比单纯看丢包率更早暴露问题。

我个人的体会是,零拷贝接收本身不算难写,难的是让整个链路在所有边界情况下都不出错。先把描述符管理逻辑理清楚,再谈Cache优化,顺序不要反。如果你也在做类似的LWIP接收优化,卡在某一步始终调不通,可以把描述符状态、pbuf ref、Cache指令执行顺序这三个点先检查一遍,大部分问题都藏在这三处里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询