LWIP pbuf结构体深度解析:嵌入式网络数据包内存管理核心
pbuf是LWIP轻量级TCP/IP协议栈中实现高效数据包内存管理的核心抽象,本质是一种支持链表组织、类型区分与引用计数的复合缓冲区结构。其设计原理源于嵌入式系统对零拷贝、低内存占用和硬实时响应的严苛要求,通过PBUF_RAM、PBUF_POOL、PBUF_ROM和PBUF_REF四类模型,分别适配动态堆分配、网卡接收、只读固件数据及DMA直传等典型场景。技术价值体现在避免大块连续内存分配失败、消
1. LWIP数据包管理的核心机制:pbuf结构体深度解析
在嵌入式TCP/IP协议栈的工程实践中,数据包(Packet)的内存管理是整个网络通信性能与稳定性的基石。LWIP作为轻量级、可裁剪的协议栈,其设计哲学并非追求功能完备性,而是以极小的资源开销实现可靠的网络协议处理。而这一目标的达成,高度依赖于其独创且精巧的数据包抽象层—— pbuf (Packet Buffer)结构体。它并非一个简单的内存缓冲区,而是一个融合了内存布局控制、链表组织、类型区分与引用计数的复合型数据结构。理解 pbuf ,就是理解LWIP如何在有限的MCU资源下,高效、安全地完成从网卡驱动接收到应用层数据交付的全过程。
1.1 pbuf结构体的官方定义与内存布局
pbuf 结构体的定义位于LWIP源码的 src/include/lwip/pbuf.h 头文件中。其核心成员如下所示,每一个字段都承载着明确的工程目的:
struct pbuf {
/** next pbuf in singly linked pbuf chain */
struct pbuf *next;
/** pointer to the actual data buffer */
void *payload;
/** total length of this pbuf and all in the chain */
u16_t tot_len;
/** length of this pbuf only */
u16_t len;
/** pbuf type */
u8_t type;
/** misc flags */
u8_t flags;
/** reference count */
u16_t ref;
};
-
next:单向链表指针
这是pbuf结构体最基础的组织方式。当一个网络数据包的长度超过单个pbuf所能承载的最大容量时,LWIP不会进行耗时的内存拷贝,而是将数据分散存储在多个pbuf中,并通过next指针将它们逻辑上串联成一个链表。这种设计完美契合了OSI模型中“分片”与“重组”的思想,避免了大块连续内存分配失败的风险,也极大降低了内存碎片化程度。在STM32等资源受限平台,连续大内存块的申请失败是常态,而链表式管理则提供了优雅的解决方案。 -
payload:数据区起始地址payload是一个void*类型的指针,它指向的是该pbuf所承载的有效数据的起始地址。关键在于,这个地址 并非总是紧邻pbuf结构体之后 。其具体位置由pbuf的类型和申请时指定的layer参数共同决定。payload的设计赋予了LWIP极大的灵活性:它可以指向RAM中的动态分配区、ROM中的常量数据区,甚至是外设DMA缓冲区的物理地址,从而实现零拷贝(Zero-Copy)的数据传递,这是提升网络吞吐量的关键。 -
tot_len与len:总长与本段长度的精确分离len表示当前pbuf节点自身所承载的数据长度。而tot_len则是一个全局视角的度量,它等于当前pbuf的len加上其next链表中所有后续pbuf的len之和。例如,一个1500字节的以太网帧被拆分为三个pbuf:第一个承载512字节,第二个承载512字节,第三个承载476字节。那么: - 第一个
pbuf的len = 512,tot_len = 1500 - 第二个
pbuf的len = 512,tot_len = 988(512 + 476) - 第三个
pbuf的len = 476,tot_len = 476
这种设计使得协议栈的上层函数(如TCP发送)无需遍历整个链表即可获知待发送数据的完整长度,简化了接口,提升了效率。
-
type:四类内存模型的策略标识type字段是pbuf的灵魂所在,它决定了该pbuf的内存来源、生命周期管理策略以及数据区的组织方式。LWIP定义了四种核心类型:PBUF_RAM、PBUF_POOL、PBUF_ROM和PBUF_REF。每一种类型都针对特定的使用场景进行了优化,工程师必须根据数据包的来源(网卡接收、应用层发送、协议栈内部生成)和生命周期(瞬时、持久、只读)来选择最合适的类型,这是LWIP性能调优的第一步。 -
ref:引用计数保障内存安全
在多任务并发的环境中,一个数据包可能同时被网卡驱动、IP层、TCP层甚至应用层任务所引用。ref字段记录了当前有多少个指针或逻辑实体正在使用这个pbuf。当某个模块完成处理并调用pbuf_free()时,它并不会立即释放内存,而是将ref减1;只有当ref减至0时,内存才真正被归还给系统。这种机制彻底消除了因“提前释放”或“重复释放”导致的内存崩溃风险,是LWIP在FreeRTOS等实时操作系统上稳定运行的底层保障。
1.2 pbuf类型详解:四种内存模型的工程选型指南
LWIP的四种 pbuf 类型并非并列关系,而是构成了一个层次化的内存管理策略体系。它们的差异主要体现在内存的申请来源、数据区的归属以及适用场景上。工程师在开发中,必须精准匹配数据流的特性,否则将直接导致性能瓶颈或内存泄漏。
1.2.1 PBUF_RAM:动态堆内存的通用方案
PBUF_RAM 是最直观、最通用的 pbuf 类型。当调用 pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM) 时,LWIP会从其内部的 mem_heap (内存堆)中申请一块连续的内存。这块内存的布局如下图所示:
+---------------------+
| pbuf struct | <-- p 指向此处 (pbuf结构体起始地址)
+---------------------+
| [padding] | <-- 字节对齐填充 (align_size)
+---------------------+
| Protocol Header | <-- offsize 偏移区 (TCP/IP/Ethernet Header)
+---------------------+
| Payload Data | <-- payload 指向此处 (有效载荷起始地址)
+---------------------+
- 内存来源 :
mem_heap,即LWIP自己维护的一块动态内存池。 - 数据区归属 :
payload指向的内存区域与pbuf结构体本身是 同一块连续内存 的一部分,由pbuf_alloc一次性申请。 - 适用场景 :适用于协议栈内部生成的数据包,如ICMP响应、ARP请求/应答,以及应用程序通过
netconn或socketAPI发送的、生命周期较短的数据。其优势在于内存布局紧凑,管理简单;劣势在于堆内存分配存在碎片化风险,且分配/释放时间相对固定,无法满足极致的实时性要求。
1.2.2 PBUF_POOL:网卡接收的高性能基石
PBUF_POOL 是为网卡(NIC)驱动量身定制的 pbuf 类型。它的核心价值在于 极高的分配速度 。当网卡硬件接收到一帧数据并触发中断时,驱动程序必须在毫秒级甚至微秒级的时间内完成 pbuf 的申请,否则将导致后续数据包被丢弃(RX Overflow)。
- 内存来源 :
memp_pbuf_pool,这是一个预先创建好的、大小固定的内存池。在LWIP初始化阶段,memp_init()会根据lwipopts.h中的配置(如MEMP_NUM_PBUF_POOL),一次性从mem_heap中申请大量相同大小的内存块,并将其组织成一个空闲链表。 - 数据区归属 :
payload指向的内存区域是 独立于pbuf结构体 的。pbuf结构体本身来自memp_pbuf_pool,而payload则指向另一个同样来自memp_pbuf_pool的、专门用于存放数据的内存块(通常命名为PBUF_POOL_BUFSIZE)。 - 适用场景 : 仅限网卡接收路径 。在
low_level_input()函数中,驱动程序会首先调用pbuf_alloc(PBUF_RAW, packet_len, PBUF_POOL)来获取一个pbuf,然后将网卡DMA缓冲区中的数据拷贝到该pbuf的payload所指向的内存中。由于内存池的分配是O(1)时间复杂度,这确保了接收路径的硬实时性。
1.2.3 PBUF_ROM 与 PBUF_REF:零拷贝的终极武器
PBUF_ROM 和 PBUF_REF 是LWIP实现零拷贝(Zero-Copy)传输的两大支柱。它们的共同点是: payload 指向的内存 完全由用户代码或外设提供 ,LWIP只负责管理 pbuf 结构体本身,不参与数据区的内存分配与释放。
-
PBUF_ROM:payload指向的内存区域位于 只读存储器(ROM/Flash) 中。这通常用于发送固化的协议报文,如HTTP服务器返回的静态HTML页面、固件升级包的头部信息等。由于数据不可修改,LWIP在发送时只需读取,无需担心数据一致性问题。 -
PBUF_REF:payload指向的内存区域位于 可读写存储器(RAM) 中,但该内存的生命周期完全由应用程序控制。这是最常用、也最强大的类型。例如,在一个视频流服务器中,摄像头DMA控制器将一帧YUV数据直接写入一块RAM缓冲区。应用程序可以创建一个PBUF_REF类型的pbuf,让其payload直接指向这块DMA缓冲区的起始地址,然后将此pbuf提交给LWIP发送。整个过程 没有一次内存拷贝 ,CPU带宽被最大程度地释放出来。 -
内存来源 :
pbuf结构体本身来自memp_pbuf内存池(注意,不是memp_pbuf_pool),这是一个专门用于存放pbuf结构体的小型内存池。 - 数据区归属 :“无”。
payload是纯粹的用户指针。 - 适用场景 :高吞吐量、低延迟的应用场景,尤其是涉及DMA、大文件传输或多媒体流的场合。工程师必须严格保证,在
pbuf被LWIP完全处理完毕(pbuf_free()被调用且ref归零)之前,payload所指向的内存区域不能被覆盖或释放。
1.3 pbuf的申请与释放:生命周期管理的黄金法则
pbuf 的生命周期管理围绕两个核心API展开: pbuf_alloc() 和 pbuf_free() 。它们是LWIP内存安全的“守门人”,任何违背其使用规范的行为都将引发灾难性后果。
1.3.1 pbuf_alloc():三层决策的精密构造
pbuf_alloc() 的函数原型为:
struct pbuf* pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type);
其内部执行流程是一个精密的三层决策过程:
-
layer层决策(协议头预留空间) :layer参数(PBUF_TRANSPORT,PBUF_IP,PBUF_LINK,PBUF_RAW)决定了pbuf需要为哪一层的协议头预留空间(offsize)。LWIP会根据layer查表得到对应的offsize值:PBUF_TRANSPORT: 预留TCP/UDP头空间(20字节)PBUF_IP: 预留IP头空间(20字节)PBUF_LINK: 预留以太网头空间(14字节)PBUF_RAW: 不预留,offsize = 0
这个
offsize值会在后续的内存布局计算中被加入,确保payload指针之后有足够空间供协议栈添加头部。 -
type类型决策(内存池选择) :
根据type参数,pbuf_alloc()会进入不同的分支,调用底层的内存分配函数:PBUF_RAM→mem_malloc()PBUF_POOL→memp_malloc(MEMP_PBUF_POOL)PBUF_ROM/PBUF_REF→memp_malloc(MEMP_PBUF)
-
链表构建决策(多块拼接) :
对于PBUF_POOL类型,如果请求的length超过了单个PBUF_POOL_BUFSIZE的大小,pbuf_alloc()会自动进行循环申请,将多个pbuf通过next指针链接起来,并正确设置每个pbuf的len和tot_len。值得注意的是, 只有第一个pbuf的payload会包含offsize的偏移 ,后续pbuf的payload直接指向数据区起始,因为协议头只需要一份。
1.3.2 pbuf_free():引用计数驱动的安全回收
pbuf_free() 的职责远非简单的“释放内存”。其核心逻辑是原子性地将 pbuf 及其整个链表的 ref 字段减1,并在 ref 归零时,将内存归还给其所属的内存池。
void pbuf_free(struct pbuf *p) {
if (p == NULL) return;
/* Decrement reference count */
if (--p->ref == 0) {
/* If this is a PBUF_POOL, free both the pbuf and its payload */
if (p->type == PBUF_POOL) {
memp_free(MEMP_PBUF_POOL, p);
memp_free(MEMP_PBUF_POOL, p->payload);
return;
}
/* For PBUF_RAM, free the entire block */
if (p->type == PBUF_RAM) {
mem_free(p);
return;
}
/* For PBUF_ROM/REF, only free the pbuf struct itself */
if (p->type == PBUF_ROM || p->type == PBUF_REF) {
memp_free(MEMP_PBUF, p);
return;
}
}
}
关键实践准则 :
- 永远不要手动 free() payload : pbuf_free() 会根据 type 自动处理。
- 在中断服务程序(ISR)中谨慎使用 : pbuf_free() 内部可能涉及内存池操作,应确保其在中断上下文中是安全的(LWIP默认配置通常是安全的)。
- 应用层发送后,切勿再访问 pbuf :一旦将 pbuf 交给 tcp_write() 或 udp_send() ,其所有权即移交LWIP,应用层代码必须视为无效。
2. 网卡驱动与pbuf的协同:从硬件到协议栈的数据流
pbuf 结构体的价值,最终要在与硬件的交互中得以体现。网卡驱动是LWIP数据包管理的“第一道关口”,其质量直接决定了整个网络栈的性能上限。一个符合LWIP最佳实践的驱动,其核心就是围绕 PBUF_POOL 类型构建的高效、无阻塞的数据接收流程。
2.1 low_level_input():数据包注入的标准化入口
在LWIP的网络接口( netif )结构体中, input 函数指针是连接驱动与协议栈的桥梁。标准的 low_level_input() 函数模板如下:
static err_t low_level_input(struct netif *netif, struct pbuf *p) {
/* This function is called by the driver when a packet is received.
It should be called with the pbuf already allocated and filled. */
return tcpip_input(p, netif);
}
该函数的输入参数 p ,正是由驱动程序在中断服务程序中完成申请、填充并准备就绪的 pbuf 。这个设计将硬件细节(如DMA描述符管理、寄存器读写)与协议栈逻辑(如IP校验、路由查找)完全解耦,是模块化设计的典范。
2.2 驱动层的典型实现流程
以STM32的ETH外设为例,一个健壮的接收流程应严格遵循以下步骤:
-
中断触发与状态检查 :
ETH外设的接收中断(RX ISR)被触发后,首先读取DMA接收描述符的状态寄存器,确认是否有新的数据包到达,并检查是否有错误(如CRC错误、帧过长)。 -
PBUF_POOL申请 :
调用pbuf_alloc(PBUF_RAW, frame_length, PBUF_POOL)。这是整个流程中最关键的一步。PBUF_RAW表明这是一个原始以太网帧,不需要为上层协议头预留空间。frame_length是DMA描述符中报告的实际接收到的字节数。 -
数据拷贝 :
如果pbuf_alloc()成功,驱动程序将DMA接收缓冲区中的数据,通过memcpy()或更高效的HAL_ETH_ReadData()函数, 拷贝 到p->payload所指向的内存区域。这是PBUF_POOL类型唯一的拷贝操作,也是LWIP设计中唯一允许的、必要的拷贝。 -
调用low_level_input() :
将填充完毕的pbuf指针p作为参数,调用low_level_input(netif, p)。此函数内部会调用tcpip_input(),将pbuf推入LWIP的TCP/IP线程(tcpip_thread)的消息队列中,等待协议栈的后续处理。 -
错误处理与资源清理 :
如果pbuf_alloc()失败(返回NULL),意味着PBUF_POOL已耗尽。此时,驱动程序必须 丢弃当前帧 ,并更新DMA描述符,使其重新指向下一个可用的缓冲区。这是防止系统因内存不足而死锁的必要措施。绝不能在此处阻塞等待,也不能尝试其他类型的pbuf申请,因为接收路径必须是硬实时的。
2.3 性能瓶颈分析:为什么PBUF_POOL是唯一选择?
在网卡接收路径上, PBUF_RAM 或 PBUF_REF 为何是禁忌?答案在于其固有的性能缺陷:
-
PBUF_RAM的缺陷 :mem_malloc()基于内存堆,其分配时间是 非确定性 的。在最坏情况下,它需要遍历整个空闲链表以寻找合适大小的块,这可能导致毫秒级的延迟。对于一个千兆以太网接口,这意味着在高负载下,每秒可能丢失数千个数据包。 -
PBUF_REF的陷阱 :虽然它避免了拷贝,但payload指向的内存必须由驱动程序自行管理。这意味着驱动需要维护一个与DMA描述符环形缓冲区一一对应的pbuf数组。这不仅增加了驱动的复杂度,而且在DMA缓冲区被重用时,必须确保LWIP已经完成了对该pbuf的处理,否则将发生数据覆盖。这种同步逻辑极易出错,且难以调试。
相比之下, PBUF_POOL 通过预分配、固定大小、链表管理,将分配时间稳定在几十纳秒级别,完美匹配了硬件中断的实时性要求。因此,在 low_level_input() 中, PBUF_POOL 是经过工程验证的、唯一可靠的选择。
3. 应用层与pbuf:数据包的创建、发送与生命周期管理
如果说网卡驱动是 pbuf 的“消费者”,那么应用层就是其“生产者”。应用层通过LWIP提供的高级API(如 netconn 或 socket )与网络栈交互,而这些API的背后,正是 pbuf 的申请、填充与提交。
3.1 应用层发送流程:从应用数据到pbuf链表
当一个应用程序调用 netconn_write() 或 send() 时,LWIP的处理流程如下:
-
数据分片 :
应用层传入的数据长度可能远超MSS(Maximum Segment Size)。LWIP的TCP层会根据当前连接的MSS,将大数据块分割成多个TCP段。 -
pbuf申请 :
对于每一个TCP段,TCP层会调用pbuf_alloc(PBUF_TRANSPORT, segment_len, PBUF_RAM)。PBUF_TRANSPORT层确保offsize包含了TCP头的空间。 -
数据填充 :
TCP层将TCP头信息(源端口、目的端口、序列号等)写入pbuf的offsize区域,然后将应用层数据拷贝到pbuf->payload之后的区域。 -
提交至发送队列 :
构造完成的pbuf被挂接到TCP控制块(tcp_pcb)的unsent队列中,等待被发送。
3.2 零拷贝发送:PBUF_REF的实战应用
对于追求极致性能的应用,工程师可以绕过LWIP的 netconn / socket API,直接操作 pbuf 。一个典型的零拷贝发送示例如下:
// 假设我们有一块由DMA填充的音频数据缓冲区
extern uint8_t audio_dma_buffer[1024];
extern uint16_t audio_data_len;
// 创建一个PBUF_REF类型的pbuf,payload直接指向DMA缓冲区
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, audio_data_len, PBUF_REF);
if (p != NULL) {
// 关键:将payload指向我们的DMA缓冲区
p->payload = audio_dma_buffer;
p->len = audio_data_len;
p->tot_len = audio_data_len;
// 设置TCP头的预留空间,以便LWIP后续填充
p->flags = PBUF_FLAG_IS_CUSTOM; // 标记为自定义pbuf
// 直接调用UDP发送,跳过netconn层
err_t err = udp_send(udp_pcb, p);
if (err != ERR_OK) {
// 发送失败,需要手动释放pbuf结构体
pbuf_free(p);
}
// 注意:audio_dma_buffer的内存仍由DMA控制器管理,我们不释放它
}
在此例中, audio_dma_buffer 的生命周期由DMA控制器和音频驱动管理。 pbuf_free() 在 udp_send() 内部完成处理后,只会释放 pbuf 结构体本身(来自 MEMP_PBUF 池),而不会触碰 audio_dma_buffer 。这节省了1024字节的内存拷贝时间,对于实时音频/视频流至关重要。
3.3 生命周期陷阱与调试技巧
pbuf 的生命周期管理是嵌入式网络开发中最容易出错的环节之一。以下是几个高频陷阱及应对技巧:
-
陷阱1:悬空指针(Dangling Pointer)
应用层在调用netconn_write()后,立即对pbuf的payload进行读写。这是致命错误,因为netconn_write()会立即将pbuf的所有权转移给LWIP,后者可能在任意时刻(如在tcpip_thread中)对其进行释放。
调试技巧 :在pbuf_free()的开头添加断点或日志,观察pbuf被释放的时间点,并与应用层代码的执行时间线比对。 -
陷阱2:内存泄漏(Memory Leak)
在错误处理路径中,忘记调用pbuf_free()。例如,在pbuf_alloc()成功后,但在memcpy()拷贝数据时发生了异常,导致pbuf未被释放。
调试技巧 :利用LWIP的MEMP_STATS宏启用内存池统计,在main()函数末尾打印memp_stats[MEMP_PBUF_POOL].used的值。一个健康的系统,在长时间运行后,该值应稳定在一个合理的基线附近,而非持续增长。 -
陷阱3:引用计数不匹配
在pbuf链表中,对某个中间节点调用了pbuf_free(),导致链表断裂,后续节点永远无法被释放。
调试技巧 :编写一个pbuf_dump()辅助函数,递归遍历链表,打印每个pbuf的len、tot_len、ref和payload地址,用于快速定位链表结构是否完整。
4. 内存池配置:lwipopts.h中的性能调优开关
LWIP的内存行为,最终由 lwipopts.h 头文件中的一系列宏定义所控制。这些配置项不是简单的“开关”,而是工程师根据具体应用场景进行性能与资源权衡的“调音旋钮”。
4.1 核心内存池配置详解
-
MEMP_NUM_PBUF:
定义memp_pbuf内存池的大小,该池用于存放PBUF_ROM和PBUF_REF类型的pbuf结构体。其值通常较小(如10-20),因为这类pbuf的创建频率不高。 -
MEMP_NUM_PBUF_POOL:
定义memp_pbuf_pool内存池的大小,该池用于存放PBUF_POOL类型的pbuf结构体及其对应的数据缓冲区。这是 最关键的配置项 。其值应大于等于网卡DMA接收描述符的数量(如STM32 ETH通常为4-8个),并留有一定余量(建议乘以2-3倍)以应对突发流量。 -
PBUF_POOL_SIZE:
定义memp_pbuf_pool中每个内存块的大小。它必须大于等于最大以太网帧长度(1514字节)加上PBUF_POOL_BUFSIZE所需的offsize。一个常见的安全值是1536(1514 + 22字节的协议头预留)。 -
MEM_SIZE:
定义mem_heap的总大小,该堆用于PBUF_RAM、PBUF_TX等类型的内存分配。其值需根据应用层发送数据的平均大小和并发连接数进行估算。一个经验法则是:MEM_SIZE >= (Average_Send_Size * Max_Concurrent_Sessions) * 2。
4.2 配置不当的典型症状与诊断
-
症状:网络接收丢包率高,
MIB2_INC_COUNTER(mib2_counters.ip_inreceives);计数器增长缓慢
诊断 :MEMP_NUM_PBUF_POOL过小。使用pbuf_free()的调试技巧,观察MEMP_PBUF_POOL的used计数器是否频繁达到峰值。 -
症状:系统在高负载下出现随机崩溃,堆栈溢出
诊断 :MEM_SIZE过小,导致mem_malloc()返回NULL,后续代码未做空指针检查而直接解引用。应检查所有pbuf_alloc()、mem_malloc()的返回值。 -
症状:TCP连接建立缓慢,握手超时
诊断 :MEMP_NUM_TCP_PCB(TCP控制块数量)或MEMP_NUM_PBUF过小,导致无法为新的连接分配必要的资源。
5. 实战案例:一个完整的pbuf生命周期剖析
为了将前述所有概念融会贯通,我们以一个具体的、可复现的STM32F4项目为例,追踪一个HTTP GET请求从应用层发出,到最终通过以太网PHY芯片发送出去的完整 pbuf 生命周期。
5.1 场景设定
- 硬件平台:正点原子STM32F407ZGT6开发板,搭载LAN8720 PHY。
- 软件环境:LWIP 2.1.2,FreeRTOS 10.3.1,HAL库。
- 应用逻辑:一个FreeRTOS任务周期性地向
www.example.com发送HTTP GET请求。
5.2 生命周期追踪
-
应用层发起(Task A) :
任务A调用netconn_connect()建立TCP连接,随后调用netconn_write(conn, "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", 45, NETCONN_COPY)。NETCONN_COPY标志告诉LWIP,需要将这45字节的数据拷贝到新分配的pbuf中。 -
TCP层分片与申请(tcpip_thread) :
tcpip_thread接收到NETCONN_WRITE消息后,TCP层根据当前MSS(假设为1460字节)判断,45字节无需分片。于是调用pbuf_alloc(PBUF_TRANSPORT, 45, PBUF_RAM)。PBUF_TRANSPORT意味着offsize为20(TCP头),因此pbuf_alloc()实际申请的内存大小为:sizeof(struct pbuf) + 20 + 45。申请成功后,TCP层将TCP头信息写入offsize区域,并将45字节的HTTP请求字符串拷贝到payload之后。 -
数据包组装(tcp_output.c) :
此时,pbuf的payload指向的是TCP头的起始地址,而payload + 20才是HTTP数据的起始地址。pbuf的len为45,tot_len也为45。 -
IP层封装(ip4_output.c) :
TCP层将pbuf传递给IP层。IP层调用pbuf_header(p, IP_HLEN),该函数将payload指针向前移动IP_HLEN(20字节),使payload现在指向IP头的起始地址。pbuf的len和tot_len均增加20。IP层随后填充IP头字段。 -
链路层封装(etharp.c) :
IP层将pbuf传递给ARP模块。ARP模块查询ARP缓存,若找到目标MAC地址,则调用pbuf_header(p, ETHERNET_HEADER_LEN),再次将payload向前移动14字节,使其指向以太网头的起始地址。len和tot_len再增加14。最后,ARP模块填充以太网头,并调用netif->linkoutput(netif, p)。 -
驱动层提交(low_level_output) :
low_level_output()函数将pbuf的payload地址和tot_len长度传递给ETH外设的DMA发送描述符。DMA控制器开始将这段内存中的数据(以太网头+IP头+TCP头+HTTP数据)通过PHY芯片发送出去。 -
生命周期终结(pbuf_free) :
当DMA发送完成中断触发后,驱动程序调用pbuf_free(p)。由于这是一个PBUF_RAM类型的pbuf,pbuf_free()会调用mem_free(p),将整块内存(包括pbuf结构体、offsize区域和payload数据)一次性归还给mem_heap。
在整个过程中, pbuf 经历了三次 pbuf_header() 操作,其 payload 指针被反复调整,但 pbuf 结构体本身始终未变, next 指针始终保持为 NULL (因为数据包很小,无需链表)。这清晰地展示了 pbuf 作为一个灵活、可变的“数据视窗”,如何在协议栈各层之间无缝传递,而无需移动底层数据。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)