目录
6.1 pbuf结构体
6.2 pbuf 的类型
6.2.1 PBUF_RAM 类型的pbuf
6.2.2 PBUF_POOL 类型的pbuf
6.2.3 PBUF_ROM 和 PBUF_REF 类型pbuf
6.3 pbuf
6.3.1 pbuf_alloc()
6.3.2 pbuf_free()
6.4 其它pbuf 操作函数
6.5 网卡中使用的 pbuf
6.5.1 low_level_output()
6.5.2 low_level_input()
6.5.3 ethernetif_input()
6.1 pbuf结构体
在 lwIP 协议栈中,网络数据包的处理是核心环节,其通过pbuf 结构体实现数据的封装、传递与管理,贯穿从硬件接收到底层协议解析再到应用层处理的全流程,同时兼顾内存效率与协议栈各层的协作需求。
pbuf是 lwIP 中描述网络数据包的基础数据结构,其设计采用链式结构,支持将多个内存块串联成一个完整的数据包,既适应嵌入式系统有限的内存资源,又能高效处理不同长度的网络数据。每个 pbuf 包含数据区(存储实际数据包内容)和控制信息(如指向 next 节点的指针、数据长度、 payload 指针等),其中payload
字段指向当前层级协议数据的起始位置(例如,在以太网层指向以太网帧首部,进入 IP 层后则指向 IP 首部,通过调整该指针实现协议层间的数据传递,无需复制数据)。
pbuf 就是一个描述协议栈中数据包的数据结构,LwIP 中在 pbuf.c 和 pubf.h 实现了协议栈数据包管理的所有函数与数据结构,pbuf 数据结构的定义具体见代码如下。
/*** 数据包缓冲区结构,用于在协议栈中存储和传递网络数据包* 采用链表结构,支持将多个缓冲区串联成一个完整的数据包*/
struct pbuf {struct pbuf *next; // 链表指针,指向当前缓冲区的下一个pbuf// 用于将多个pbuf串联成一个完整的数据包链void *payload; // 指向当前缓冲区中实际数据的起始地址u16_t tot_len; // 整个数据包(当前pbuf+后续所有pbuf)的总长度u16_t len; // 当前pbuf中实际存储的数据长度// 仅表示本节点的数据长度,不包含后续节点u8_t type_internal; // 内部类型标识(位字段)// 用于表示pbuf的内存分配方式和缓冲区类型// (如静态分配/动态分配、协议头/数据体等)u8_t flags; // 标志位集合// 用于存储数据包的控制信息(如是否需要分片、是否已校验等)LWIP_PBUF_REF_T ref; // 引用计数变量,记录当前pbuf被引用的次数u8_t if_idx; // 网络接口索引
};
struct pbuf *next:next 是一个pbuf 类型的指针,指向下一个 pbuf,因为网络中的数据包可能很大,而pbuf 能管理的数据包大小有限,就会采用链表的形式将所有的 pbuf 包连接起来组成一个链表。
void *payload:payload 是一个指向数据区域的指针,指向该pbuf 管理的数据区域起始地址,这里的数据区域可以是紧跟在 pbuf 结构体地址后面的RAM 空间,也可以是ROM 中的某个地址上,取决于 pbuf 的类型。
tot_len:tot_len 中记录的是当前pbuf 及其后续pbuf 所有数据的长度,例如如果当前pbuf 是pbuf 链表上第一个数据结构,那么 tot_len 就记录着整个pbuf 链表中所有pbuf 中数据的长度;如果当前 pbuf 是链表上最后一个数据结构,那就记录着当前 pbuf 的长度。
len:len 表示当前pbuf 中有效的数据长度。
type_internal:type_internal 表示pbuf 的类型,LwIP 中有 4 种pbuf 的类型,并且使用了一个枚举类型的数据结构定义他们。
flags:flags 字段在初始化的时候一般被初始化为0。
ref:ref 表示该pbuf 被引用的次数,引用表示有其他指针指向当前 pbuf这里的指针,可以是pbuf 的next 指针,也可以是其他任意形式的指针,初始化一个 pbuf 的时候,ref 会被设置为1,因为该pbuf 的地址一点会被返回一个指针变量,当有其他指针指向pbuf 的时候,就必须调用相关函数将 ref 字段加1。
if_idx:if_idx 用于记录传入的数据包中输入 netif 的索引,也就是netif 中num 字段。
6.2 pbuf 的类型
pbuf
包含四种,其差异体现在数据存储位置与管理方式:PBUF_RAM
将数据存于动态分配的 RAM,支持灵活读写,适用于需修改数据的场景(如 TCP 数据包构建);PBUF_ROM
通过指针引用 ROM/Flash 中的只读静态数据,可节省 RAM,用于发送预存报文;PBUF_REF
仅引用外部已存在的 RAM 数据,避免内存复制,适合临时使用外部缓冲区的场景(需确保数据有效性);PBUF_POOL
从预分配的固定大小内存池获取缓冲区,分配释放高效,多用于实时性要求高的网络接收场景(如以太网帧接收)。这些类型均服务于不同的数据包处理需求,且不影响pbuf
常用的链式结构,协议栈会自动适配或允许用户通过pbuf_alloc()
指定使用。
/** pbuf(数据包缓冲区)类型枚举,定义不同内存分配和管理方式 */
typedef enum {/** * 基于RAM的缓冲区:* 数据连续存储,结构体与数据在连续内存空间,内存来自标准堆 */PBUF_RAM = (PBUF_ALLOC_FLAG_DATA_CONTIGUOUS | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP),/** * 数据在ROM/Flash中的缓冲区:* 内存来自标准pbuf内存池,数据通常不可修改 */PBUF_ROM = PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF,/** * 引用外部内存的缓冲区:* 数据存于易失性内存,内存来自标准pbuf内存池,仅引用外部数据不复制 */PBUF_REF = (PBUF_TYPE_FLAG_DATA_VOLATILE | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF),/** * 从专用内存池分配的缓冲区:* 用于接收数据,结构体与数据连续,内存来自专用pbuf内存池 */PBUF_POOL = (PBUF_ALLOC_FLAG_RX | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL)
} pbuf_type;
6.2.1 PBUF_RAM 类型的pbuf
PBUF_RAM 类型的 pbuf 空间是通过内存堆分配而来的,这种类型的pbuf 在协议栈中使用得最多,一般协议栈中要发送的数据都是采用这种形式,这种 pbuf 内存块包含数据空间以及pbuf 数据结构区域,在连续的RAM 内存空间中。
很多人又会有疑问了,不是说各个协议层都有首部吗,这些内存空间在哪呢?内核申请这类型的pbuf 时,也算上了协议首部的空间,当然是根据协议栈不同层次需要的首部进行申请,LwIP 也使用一个枚举类型对不同的协议栈分层需要的首部大小进行定义。那么申请这种 pbuf 是怎么样申请的呢?
/*** 申请指定类型的pbuf数据包缓冲区* * @param layer 协议层类型(如PBUF_RAW表示原始数据层,PBUF_TRANSPORT表示传输层)* 用于标识缓冲区在协议栈中的使用层级* @param length 所需缓冲区的长度(字节数),即需要存储的数据大小* @param type 缓冲区类型(如PBUF_RAM、PBUF_POOL等),决定内存分配方式和管理策略* * @return 成功时返回指向分配的pbuf结构体的指针,失败时返回NULL*/
struct pbuf * pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type); // 示例1:分配原始数据层(PBUF_RAW)的RAM缓冲区,长度为req_len+1字节
struct pbuf *p;
p = pbuf_alloc(PBUF_RAW, (u16_t)(req_len + 1), PBUF_RAM); // 示例2:分配传输层(PBUF_TRANSPORT)的RAM缓冲区,长度为1472字节
p = pbuf_alloc(PBUF_TRANSPORT, 1472, PBUF_RAM);
PBUF_RAM 类型的 pbuf 示意图具体见图,图中可以看出整个pbuf 就是一个连续的内存区域,layer(offset)就是各层协议的首部,如TCP 报文首部、IP 首部、以太网帧首部等,预留出来的这些空间是为了在各个协议层中灵活地处理这些数据,当然 layer 的大小也可以是0,具体是多少就与数据包的申请方式有关。
图 PBUF_RAM 类型的 pbuf
6.2.2 PBUF_POOL 类型的pbuf
PBUF_POOL 类型其pbuf 结构体与数据缓冲区也是存在于连续的内存块中,但它的空间是通过内存池分配的,这种类型的pbuf 可以在极短的时间内分配得到,因为这是内存池分配策略的优势,在网卡接收数据的时候,LwIP 一般就使用这种类型的 pbuf 来存储接收到的数据,申请 PBUF_POOL 类型时,协议栈会在内存池中分配适当的内存池个数以满足需要的数据区域大小。
在系统进行内存池初始化的时候,还初始化两个与pbuf 相关的内存池,分别为MEMP_PBUF、MEMP_ PBUF_POOL。
/* pbuf 相关的内存池*/
LWIP_MEMPOOL(PBUF, MEMP_NUM_PBUF, sizeof(struct pbuf),"PBUF_REF/ROM")
LWIP_PBUF_MEMPOOL(PBUF_POOL,PBUF_POOL_SIZE,PBUF_POOL_BUFSIZE,"PBUF_POOL")
MEMP_PBUF 内存池是专门用于存放 pbuf 数据结构的内存池,主要用于 PBUF_ROM、PBUF_REF 类型的pbuf,其大小为sizeof(struct pbuf),内存块的数量为MEMP_NUM_PBUF;而MEMP_PBUF_POOL 则包含 pbuf 结构与数据区域,也就是PBUF_POOL 类型的 pbuf,内存块的大小为 PBUF_POOL_BUFSIZE,其值由用户自己定义。
如果按照默认的内存大小,对于有些很大的以太网数据包,可能就需要多个pbuf 才能将这些数据存放下来,这就需要申请多个pbuf,因为是PBUF_POOL 类型的pbuf,所以申请内存空间只需要调用 memp_malloc()函数进行申请即可。
pbuf 链表中第一个 pbuf 是有 layer 字段的,用于存放协议头部,而在它后面的pbuf 则是没有该字段。
图 PBUF_POOL 类型 pbuf(组成 pbuf 链表)
6.2.3 PBUF_ROM 和 PBUF_REF 类型pbuf
PBUF_ROM 和PBUF_REF 类型在内存池申请的pbuf 不包含数据区域,只包含pbuf 结构体。 PBUF_ROM 类型的 pbuf 的数据区域存储在ROM 中,是一段静态数据,而PBUF_REF 类型的pbuf 的数据区域存储在RAM 空间中。
图 PBUF_ROM/PBUF_REF 类型 pbuf
对于一个数据包,它可能会使用任意类型的pbuf 进行描述,也可能使用多种不同的 pbuf 一起描述,如图 所示。
图 不同类型的 pbuf 组成 pbuf 链表
6.3 pbuf
6.3.1 pbuf_alloc()
数据包申请函数pbuf_alloc()在系统中的许多地方都会用到,例如在网卡接收数据时、在发送数据的时候,同时相关的协议首部信息也会被填入到 pbuf 中的 layer 区域内,所以 pbuf 数据包的申请函数几乎无处不在,存在协议栈于各层之中。当然,在不同层的协议中,layer 字段的大小是不一样的,因为不一样的协议其首部大小是不同的,在申请的时候就把layer 需要空间的大小根据协议进行分配。
/*** @brief 定义pbuf的协议层次枚举,用于指定在分配缓冲区时需预留的各层协议首部空间*/
typedef enum {/* 传输层:包含传输层、网络层、链路层及封装层首部的总预留空间* 适用于UDP、TCP等传输层协议报文,自动预留各层协议首部所需内存*/PBUF_TRANSPORT = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN,/* 网络层:包含网络层、链路层及封装层首部的总预留空间* 适用于IP等网络层协议报文,预留对应层级首部所需内存*/PBUF_IP = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN,/* 链路层:包含链路层及封装层首部的总预留空间* 适用于以太网等链路层协议报文,预留对应层级首部所需内存*/PBUF_LINK = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN,/* 原始发送层:仅包含封装层首部的预留空间* PBUF_LINK_ENCAPSULATION_HLEN宏默认值为0,通常用于发送原始数据时使用*/PBUF_RAW_TX = PBUF_LINK_ENCAPSULATION_HLEN,/* 原始层:不预留任何协议首部空间* 适用于直接处理纯数据,无需添加各层协议首部的场景*/PBUF_RAW = 0
} pbuf_layer;
数据包申请函数有两个重要的参数:数据包pbuf 的类型和数据包在哪一层被申请,数据包在哪一层申请这个参数主要是为了预留各层协议的内存大小,也就是前面所说的 layer 值。
struct pbuf * pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{
}
pbuf_alloc()函数的思路很清晰,根据传入的 pbuf 类型及协议层次layer,去申请对应的pbuf,就能预留出对应的协议首部空间。举个例子,假设TCP 协议需要申请一个pbuf 数据包,那么就会调用下面代码进行申请:
p = pbuf_alloc(PBUF_TRANSPORT, 1472, PBUF_RAM);
内核就会根据这句代码进行分配一个PBUF_RAM 类型的pbuf,其数据区域大小是1472 字节,并且会根据协议层次进行预留协议首部空间,由于是传输层,所以内核需要预留54 个字节空间。
/*** @ingroup pbuf* 分配指定类型的pbuf(对于PBUF_POOL类型可能返回一个pbuf链表)** pbuf实际分配的内存大小由分配时指定的协议层(layer)和请求的 payload 长度(length)共同决定** @param layer 协议层,用于计算需预留的首部偏移量(不同层级对应不同首部总长度)* @param length pbuf的payload数据长度(字节数)* @param type 决定pbuf的分配方式和内存来源,具体如下:** - PBUF_RAM: 从堆中分配一整块连续内存,包含协议首部和payload* - PBUF_ROM: 不分配缓冲区内存(包括协议首部),数据通常来自只读存储(如ROM/Flash)。* 如需添加首部,需额外分配pbuf并链接到当前pbuf前。适用于不可修改的静态数据。* - PBUF_REF: 不分配缓冲区内存(包括协议首部),适用于单线程场景下引用外部动态内存。* 若需入队,需调用pbuf_take复制数据。* - PBUF_POOL: 从pbuf内存池(pbuf_init()初始化)分配,可能形成pbuf链表** @return 分配成功时返回pbuf指针(多pbuf时返回链表头);失败返回NULL*/
struct pbuf *
pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{struct pbuf *p; // 指向分配的pbuf(或链表头)u16_t offset = (u16_t)layer; // 根据协议层计算的首部偏移量(需预留的首部空间)// 调试输出:打印分配的长度LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F")\n", length));// 根据pbuf类型选择不同的分配策略switch (type) {// PBUF_REF和PBUF_ROM均引用外部内存,共用同一分配逻辑case PBUF_REF: /* fall through */case PBUF_ROM:// 调用引用型pbuf分配函数(不分配新内存,仅创建pbuf结构引用外部数据)p = pbuf_alloc_reference(NULL, length, type);break;// 从pbuf内存池分配(可能形成链表)case PBUF_POOL: {struct pbuf *q, *last; // q:当前分配的pbuf;last:链表尾指针u16_t rem_len; // 剩余待分配的长度p = NULL; // 初始化链表头为NULLlast = NULL; // 初始化链表尾为NULLrem_len = length; // 初始化剩余长度为总需求长度// 循环分配pbuf,直到满足总长度需求do {u16_t qlen; // 当前pbuf的payload长度// 从内存池分配一个pbuf结构q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);if (q == NULL) { // 内存池耗尽PBUF_POOL_IS_EMPTY(); // 触发内存池空的钩子函数// 释放已分配的pbuf链表if (p) {pbuf_free(p);}return NULL; // 分配失败}// 计算当前pbuf可容纳的payload长度(不超过内存池缓冲区大小减去偏移)qlen = LWIP_MIN(rem_len, (u16_t)(PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset)));// 初始化已分配的pbuf(设置payload指针、长度等)pbuf_init_alloced_pbuf(q, LWIP_MEM_ALIGN((void *)((u8_t *)q + SIZEOF_STRUCT_PBUF + offset)),rem_len, qlen, type, 0);// 断言:检查payload地址是否按要求对齐LWIP_ASSERT("pbuf_alloc: pbuf q->payload properly aligned",((mem_ptr_t)q->payload % MEM_ALIGNMENT) == 0);// 断言:确保内存池缓冲区大小足够LWIP_ASSERT("PBUF_POOL_BUFSIZE must be bigger than MEM_ALIGNMENT",(PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset)) > 0 );if (p == NULL) { // 第一个pbuf,作为链表头p = q;} else { // 非第一个pbuf,链接到链表尾last->next = q;}last = q; // 更新链表尾为当前pbufrem_len = (u16_t)(rem_len - qlen); // 更新剩余长度offset = 0; // 后续pbuf无需再预留首部偏移(仅第一个需考虑)} while (rem_len > 0); // 直到剩余长度为0break;}// 从堆分配连续内存的pbufcase PBUF_RAM: {// 计算payload总长度(含偏移对齐和数据长度对齐)u16_t payload_len = (u16_t)(LWIP_MEM_ALIGN_SIZE(offset) + LWIP_MEM_ALIGN_SIZE(length));// 计算总分配长度(pbuf结构体大小对齐 + payload总长度)mem_size_t alloc_len = (mem_size_t)(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF) + payload_len);// 检查整数溢出(防止分配长度计算错误)if ((payload_len < LWIP_MEM_ALIGN_SIZE(length)) ||(alloc_len < LWIP_MEM_ALIGN_SIZE(length))) {return NULL;}// 从堆分配内存p = (struct pbuf *)mem_malloc(alloc_len);if (p == NULL) { // 堆内存不足return NULL;}// 初始化已分配的pbuf(设置payload指针、长度等)pbuf_init_alloced_pbuf(p, LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset)),length, length, type, 0);// 断言:检查payload地址是否按要求对齐LWIP_ASSERT("pbuf_alloc: pbuf->payload properly aligned",((mem_ptr_t)p->payload % MEM_ALIGNMENT) == 0);break;}// 无效类型处理default:LWIP_ASSERT("pbuf_alloc: erroneous type", 0); // 断言失败(无效类型)return NULL;}// 调试输出:打印分配结果LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F") == %p\n", length, (void *)p));return p;
}
6.3.2 pbuf_free()
数据包pbuf 的释放是必须的,因为当内核处理完数据就要将这些资源进行回收,否则就会造成内存泄漏,在后续的数据处理中无法再次申请内存。
当然,既然要释放数据包,那么肯定有条件,pbuf 中 ref 字段就是记录pbuf 数据包被引用的次数,在申请pbuf 的时候,ref 字段就被初始化为 1,当释放pbuf 的时候,先将 ref减1,如果ref 减1 后为 0,则表示能释放pbuf 数据包。此外,能被内核释放的 pbuf 数据包只能是首节点或者其他地方未被引用过的节点,如果用户错误地调用pbuf 释放函数,将pbuf 链表中的某个中间节点删除了,那么必然会导致错误。
u8_t pbuf_free(struct pbuf *p)
{
}
pbuf_free
函数原型如下:
/*** @ingroup pbuf* 解除对pbuf链或队列的引用,并释放链/队列头部所有不再被使用的pbuf** 递减pbuf的引用计数。当引用计数减至0时,该pbuf将被释放** 对于pbuf链,此操作会对链中每个pbuf重复执行,直到遇到第一个递减后引用计数仍不为0的pbuf为止。* 因此,当所有pbuf的引用计数均为1时,整个链都会被释放** @param p 要解除引用的pbuf(链)** @return 从链头部释放的pbuf数量** @note 禁止在数据包队列上调用(尚未验证其在队列上的有效性)* @note pbuf的引用计数等于指向该pbuf(或pbuf内部)的指针数量** @internal 示例:* 假设现有链 a->b->c 的引用计数如下,调用pbuf_free(a)后结果为:* 1->2->3 → ...1->3(释放a)* 3->3->3 → 2->3->3(仅a的计数减1)* 1->1->2 → ......1(释放a和b)* 2->1->1 → 1->1->1(仅a的计数减1)* 1->1->1 → .......(释放整个链)*/
u8_t
pbuf_free(struct pbuf *p)
{u8_t alloc_src; // pbuf的内存分配来源(用于决定释放方式)struct pbuf *q; // 临时保存下一个pbuf的指针u8_t count; // 已释放的pbuf数量// 处理空指针情况if (p == NULL) {LWIP_ASSERT("p != NULL", p != NULL); // 断言失败(预期p不为空)/* 若断言被禁用,输出调试信息 */LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_LEVEL_SERIOUS,("pbuf_free(p == NULL) was called.\n"));return 0;}LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free(%p)\n", (void *)p)); // 调试输出PERF_START; // 性能统计开始count = 0; // 初始化释放计数为0/* 从链头部开始,释放所有引用计数递减后为0的连续pbuf */while (p != NULL) {LWIP_PBUF_REF_T ref; // 保存递减后的引用计数SYS_ARCH_DECL_PROTECT(old_level); // 声明临界区保护变量/* 由于递减引用计数可能不是原子操作,需要进行临界区保护* 将新的引用计数存入局部变量,避免后续操作再次保护 */SYS_ARCH_PROTECT(old_level); // 进入临界区/* 链中的所有pbuf至少有一个引用 */LWIP_ASSERT("pbuf_free: p->ref > 0", p->ref > 0);/* 递减引用计数(指向该pbuf的指针数量) */ref = --(p->ref);SYS_ARCH_UNPROTECT(old_level); // 退出临界区/* 该pbuf不再被引用? */if (ref == 0) {q = p->next; // 保存下一个pbuf的指针(当前pbuf即将被释放)LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free: deallocating %p\n", (void *)p)); // 调试输出alloc_src = pbuf_get_allocsrc(p); // 获取pbuf的内存分配来源#if LWIP_SUPPORT_CUSTOM_PBUF/* 是否为自定义pbuf? */if ((p->flags & PBUF_FLAG_IS_CUSTOM) != 0) {struct pbuf_custom *pc = (struct pbuf_custom *)p; // 转换为自定义pbuf类型LWIP_ASSERT("pc->custom_free_function != NULL", pc->custom_free_function != NULL); // 确保释放函数存在pc->custom_free_function(p); // 调用自定义释放函数} else
#endif /* LWIP_SUPPORT_CUSTOM_PBUF */{/* 是否为内存池(PBUF_POOL)分配的pbuf? */if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL) {memp_free(MEMP_PBUF_POOL, p); // 从pbuf池释放/* 是否为ROM或REF类型(从标准pbuf内存池分配)? */} else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF) {memp_free(MEMP_PBUF, p); // 从标准pbuf内存池释放/* 是否为RAM类型(从堆分配)? */} else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP) {mem_free(p); // 从堆释放} else {/* @todo: 支持释放其他类型 */LWIP_ASSERT("invalid pbuf type", 0); // 断言失败(无效的pbuf类型)}}count++; // 释放计数加1p = q; // 处理下一个pbuf} else {// p->ref > 0,该pbuf仍被引用(链中剩余pbuf也同理)LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free: %p has ref %"U16_F", ending here.\n", (void *)p, (u16_t)ref));p = NULL; // 停止遍历链}}PERF_STOP("pbuf_free"); // 性能统计结束return count; // 返回释放的pbuf数量
}
6.4 其它pbuf 操作函数
pbuf_realloc (pbuf *p, u16_t new_len):重新分配 p 指向的 pbuf 内存,调整其总长度为 new_len。若 new_len 小于原长度,会截断短数据;若大于原长度,会扩展空间(可能需要分配新内存并复制数据)。返回值为调整后的 pbuf 指针(可能与原指针不同),失败时返回 NULL。
pbuf_header (pbuf *p, s16_t header_size):调整 pbuf 的头部空间,header_size 为正数时增加头部空间(用于添加协议头),为负数时减少头部空间(需确保不超过当前可用头部空间)。
pbuf_take (pbuf *p, const void *dataptr, u16_t len):将 dataptr 指向的长度为 len 的数据复制到 p 指向的 pbuf 中,从 pbuf 的起始位置开始存储。要求 pbuf 的总长度至少为 len,否则可能导致数据截断或错误。
pbuf_copy (pbuf *dst, const pbuf *src):将 src 指向的 pbuf 链中的数据完整复制到 dst 指向的 pbuf 中。要求 dst 的总长度不小于 src 的总长度,否则复制会不完整。
pbuf_chain (pbuf *head, pbuf *tail):将 tail 指向的 pbuf 链链接到 head 指向的 pbuf 链末尾,形成更长的 pbuf 链。head 必须是完整的 pbuf 链(可通过 head->next 遍历至链尾),tail 也需是独立的 pbuf 链。操作后 head 的 tot_len 会更新为两链总长度之和。
6.5 网卡中使用的 pbuf
6.5.1 low_level_output()
网卡发送数据是通过 low_level_output()函数实现的,该函数是一个底层驱动函数,这要求用户熟悉网卡底层特性,还要熟悉pbuf 数据包。首先说说发送数据的过程,用户在应用层想要通过一个网卡发送数据,那么就要将数据传入 LwIP 内核中,经过内核的层层封装,存储在 pbuf 数据包中。当数据发送的时候,就要将属于一个数据包的数据全部发送出去,此处需要注意的是,属于同一个数据包中的所有数据都必须放在同一个以太网帧中发送。low_level_output()函数原型如下。
low_level_output(struct netif *netif, struct pbuf *p)
{
}
6.5.2 low_level_input()
与 low_level_output()函数相反的是 low_level_input()函数,该函数用于从网卡中接收一个数据包,并将数据包封装在 pbuf 中递交给上层。
staticstruct pbuf *low_level_input(struct netif *netif)
{
}
6.5.3 ethernetif_input()
low_level_output()函数只是完成了网卡驱动接收,但是还没将 pbuf 数据包递交给上层,那么又是谁将pbuf 数据包递交给上层的呢?
ethernetif_input()函数会被周期性调用,这样子就能接收网卡的数据,在接收完毕,就能将数据通过网卡 netif 的input 接口将pbuf 递交给上层。