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 socket API发送的、生命周期较短的数据。其优势在于内存布局紧凑,管理简单;劣势在于堆内存分配存在碎片化风险,且分配/释放时间相对固定,无法满足极致的实时性要求。
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);

其内部执行流程是一个精密的三层决策过程:

  1. 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 指针之后有足够空间供协议栈添加头部。

  2. type 类型决策(内存池选择)
    根据 type 参数, pbuf_alloc() 会进入不同的分支,调用底层的内存分配函数:

    • PBUF_RAM mem_malloc()
    • PBUF_POOL memp_malloc(MEMP_PBUF_POOL)
    • PBUF_ROM/PBUF_REF memp_malloc(MEMP_PBUF)
  3. 链表构建决策(多块拼接)
    对于 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外设为例,一个健壮的接收流程应严格遵循以下步骤:

  1. 中断触发与状态检查
    ETH外设的接收中断(RX ISR)被触发后,首先读取DMA接收描述符的状态寄存器,确认是否有新的数据包到达,并检查是否有错误(如CRC错误、帧过长)。

  2. PBUF_POOL申请
    调用 pbuf_alloc(PBUF_RAW, frame_length, PBUF_POOL) 。这是整个流程中最关键的一步。 PBUF_RAW 表明这是一个原始以太网帧,不需要为上层协议头预留空间。 frame_length 是DMA描述符中报告的实际接收到的字节数。

  3. 数据拷贝
    如果 pbuf_alloc() 成功,驱动程序将DMA接收缓冲区中的数据,通过 memcpy() 或更高效的 HAL_ETH_ReadData() 函数, 拷贝 p->payload 所指向的内存区域。这是 PBUF_POOL 类型唯一的拷贝操作,也是LWIP设计中唯一允许的、必要的拷贝。

  4. 调用low_level_input()
    将填充完毕的 pbuf 指针 p 作为参数,调用 low_level_input(netif, p) 。此函数内部会调用 tcpip_input() ,将 pbuf 推入LWIP的TCP/IP线程( tcpip_thread )的消息队列中,等待协议栈的后续处理。

  5. 错误处理与资源清理
    如果 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的处理流程如下:

  1. 数据分片
    应用层传入的数据长度可能远超MSS(Maximum Segment Size)。LWIP的TCP层会根据当前连接的MSS,将大数据块分割成多个TCP段。

  2. pbuf申请
    对于每一个TCP段,TCP层会调用 pbuf_alloc(PBUF_TRANSPORT, segment_len, PBUF_RAM) PBUF_TRANSPORT 层确保 offsize 包含了TCP头的空间。

  3. 数据填充
    TCP层将TCP头信息(源端口、目的端口、序列号等)写入 pbuf offsize 区域,然后将应用层数据拷贝到 pbuf->payload 之后的区域。

  4. 提交至发送队列
    构造完成的 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 生命周期追踪

  1. 应用层发起(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 中。

  2. 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 之后。

  3. 数据包组装(tcp_output.c)
    此时, pbuf payload 指向的是TCP头的起始地址,而 payload + 20 才是HTTP数据的起始地址。 pbuf len 为45, tot_len 也为45。

  4. 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头字段。

  5. 链路层封装(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)

  6. 驱动层提交(low_level_output)
    low_level_output() 函数将 pbuf payload 地址和 tot_len 长度传递给ETH外设的DMA发送描述符。DMA控制器开始将这段内存中的数据(以太网头+IP头+TCP头+HTTP数据)通过PHY芯片发送出去。

  7. 生命周期终结(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 作为一个灵活、可变的“数据视窗”,如何在协议栈各层之间无缝传递,而无需移动底层数据。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐