工作笔记----lwip的数据管理结构pbuf源码解析
工作笔记----lwip的数据管理结构pbuf源码解析
@@ Author:明月清了个风
@@ Date:2025.9.23
@@ PS:在上一篇中已经讲解了lwip的内存管理策略,这一篇中将对lwip的数据管理结构pbuf的源码进行解析。lwip在各模块各层之间传递数据,使用了统一的数据包管理结构pbuf,通过传递pbuf指针避免了数据进行传输时的内存拷贝.
@@ Version:First published date 2025.9.23
以前的文章
FreeRTOS中的lwIP网络任务为什么会让出CPU
lwip网络任务初始化问题排查
基于FreeRTOS的lwIP网络任接收过程,从MAC至协议栈
lwip的内存管理策略解析
pbuf结构体
struct pbuf {struct pbuf *next; /* 用于构建pbuf链 */void *payload; /* 实际数据buffer指针 */u16_t tot_len; /* 从本pbuf开始(包括)后面所有pbuf所含数据量 */u16_t len; /* 本pbuf所含数据量 */u8_t type_internal; /* 位域,用于保存一些flag位 */u8_t flags; /* 同样是位域,保存一些flag */LWIP_PBUF_REF_T ref; /* 记录有多少指针指向本pbuf */u8_t if_idx; /* 输入数据包的网络接口索引,也就是数据包从哪个netif进来的 */
};
pbuf_alloc函数
这个源码并不长,直接贴出来了,省略了一些条件编译和debug的内容
struct pbuf *
pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{struct pbuf *p;u16_t offset = (u16_t)layer;switch (type) {case PBUF_REF: /* fall through */case PBUF_ROM:p = pbuf_alloc_reference(NULL, length, type);break;case PBUF_POOL: {struct pbuf *q, *last;u16_t rem_len; /* remaining length */p = NULL;last = NULL;rem_len = length;do {u16_t qlen;q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);if (q == NULL) {PBUF_POOL_IS_EMPTY();if (p) {pbuf_free(p);}return NULL;}qlen = LWIP_MIN(rem_len, (u16_t)(PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset)));pbuf_init_alloced_pbuf(q, LWIP_MEM_ALIGN((void *)((u8_t *)q + SIZEOF_STRUCT_PBUF + offset)),rem_len, qlen, type, 0);if (p == NULL) {p = q;} else {last->next = q;}last = q;rem_len = (u16_t)(rem_len - qlen);offset = 0;} while (rem_len > 0);break;}case PBUF_RAM: {u16_t payload_len = (u16_t)(LWIP_MEM_ALIGN_SIZE(offset) + LWIP_MEM_ALIGN_SIZE(length));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_init_alloced_pbuf(p, LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset)),length, length, type, 0);break;}default:return NULL;}return p;
}
首先来看传入的三个参数,分别是pbuf_layer
,length
,pbuf_type
。
📖pbuf_layer
参数
该参数为枚举变量,共有五种,源码如下:
typedef enum {PBUF_TRANSPORT = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN,PBUF_IP = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN,PBUF_LINK = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN,PBUF_RAW_TX = PBUF_LINK_ENCAPSULATION_HLEN,PBUF_RAW = 0
} pbuf_layer;
可以看到,该参数定义的是一些长度,对于用于不同层的pbuf,所要求的长度是不同的,在pbuf_alloc()
函数中,该长度赋值给变量offset
,在后面会进一步使用这个值的地方
📖length
参数
该参数就是需要分配的数据包长度,后续会在函数中赋值给变量rem_len
📖pbuf_type
参数
该参数指定了分配的pbuf类型,也决定了pbuf的分配方式,函数中的switch
就是根据这个type来分的,总共有以下四类
typedef enum {PBUF_RAM = (PBUF_ALLOC_FLAG_DATA_CONTIGUOUS | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP),PBUF_ROM = PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF,PBUF_REF = (PBUF_TYPE_FLAG_DATA_VOLATILE | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF),PBUF_POOL = (PBUF_ALLOC_FLAG_RX | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL)
} pbuf_type;
简单来说,这个类型指定了pbuf的数据存储在哪里,且pbuf及其数据的构成也不同
对于PBUF_RAM
来说,注释原文如下
/** pbuf data is stored in RAM, used for TX mostly, struct pbuf and its payload are allocated in one piece of contiguous memory (so the first payload byte can be calculated from struct pbuf). pbuf_alloc() allocates PBUF_RAM pbufs as unchained pbufs (although that might change in future versions).
This should be used for all OUTGOING packets (TX).*/
也就是说,该类型的pbuf数据与该pbuf结构体在内存中是连续存储的,可以通过偏移pbuf指针直接找到该pbuf指向的数据起始地址,并且该类型的pbuf不是链式的,通常用于发出的数据包。
**对于PBUF_ROM
来说,**注释原文如下:
/** pbuf data is stored in ROM, i.e. struct pbuf and its payload are located in totally different memory areas. Since it points to ROM, payload does not have to be copied when queued for transmission. */
也就是说,该类型的pbuf是存储在ROM中的,他与其所指向的数据(pbuf->payload
)不是一块连续的内存空间,这意味着传输时不用去拷贝这部分数据
对于PBUF_REF
来说,注释原文如下:
/** pbuf comes from the pbuf pool. Much like PBUF_ROM but payload might change so it has to be duplicated when queued before transmitting, depending on who has a ‘ref’ to it. */
该类型的pbuf来源于pbuf pool,也就是内存池预先分配好的,但是当需要进行传输时,这一类的pbuf数据是有可能会需要拷贝的。
对于PBUF_POOL
来说,注释原文如下:
/** pbuf payload refers to RAM. This one comes from a pool and should be used for RX. Payload can be chained (scatter-gather RX) but like PBUF_RAM, struct pbuf and its payload are allocated in one piece of contiguous memory (so the first payload byte can be calculated from struct pbuf). Don’t use this for TX, if the pool becomes empty e.g. because of TCP queuing, you are unable to receive TCP acks! */
该类pbuf从内存池进行分配,被用于接收数据包,并且所包含数据与pbuf结构体本身是连续的。这也符合在之前的文章基于FreeRTOS的lwIP网络任接收过程,从MAC至协议栈中我们看到的数据包接收过程。
🏃接下来看具体的分配过程
记录了pbuf_layer
对应的offset
后就根据传入的pbuf_type
选择不同的分配方式
对于PBUF_REF
和PBUF_ROM
来说,都通过pbuf_alloc_reference(NULL, length, type);
函数进行分配,先来看这个函数,该函数源码如下:
struct pbuf *
pbuf_alloc_reference(void *payload, u16_t length, pbuf_type type)
{struct pbuf *p;p = (struct pbuf *)memp_malloc(MEMP_PBUF);if (p == NULL) {return NULL;}pbuf_init_alloced_pbuf(p, payload, length, length, type, 0);return p;
}
可以看到对于上面这两种pbuf来说都直接通过memp_malloc
从内存池分配pbuf结构体,而不分配存储数据的空间,非常简单。
对于pbuf_pool
来说,其分配过程在pbuf_alloc
函数中完成,主要的分配过程是一个do {} while
循环,循环条件就是rem_len > 0
,也就是每次循环都会分配一部分空间来容纳目标数据包,直到待分配内存大小rem_len = 0
。
在循环体中,首先从内存池中分配pbuf结构体q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
;然后比较两个数值的大小,一个是rem_len
,另一个是(u16_t)(PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset))
,这里的PBUF_POOL_BUFSIZE_ALIGNED
是一个pbuf中能够容纳的最多的数据大小,这个值还需要减去pbuf_layer
对应的offset
才是一个pbuf真实能够存储的数据大小。
算出一个pbuf要分配的数据内存大小qlen
后,就通过pbuf_init_alloced_pbuf()
函数初始化这个pbuf,并且构建pbuf链,更新rem_len =(u16_t)(rem_len - qlen)
通过不停循环上面的过程即可分配足够大小的pbuf.
对于pbuf_ram
来说,分配过程更加简单了,直接通过下面两行代码算出总共要分配多少大小的数据空间,这个空间包括了pbuf结构体本身,offset
以及数据包大小length
。
u16_t payload_len = (u16_t)(LWIP_MEM_ALIGN_SIZE(offset) + LWIP_MEM_ALIGN_SIZE(length));
mem_size_t alloc_len = (mem_size_t)(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF) + payload_len);
然后直接通过上一篇讲过的p = (struct pbuf *)mem_malloc(alloc_len);
进行分配并通过pbuf_init_alloced_pbuf()
初始化。
至此,pbuf的分配过程就讲完了
pbuf_free
函数
这个源码也并不长,直接贴出来了,去掉了内核保护和debug代码,该函数的返回值是成功释放的pbuf数量
u8_t
pbuf_free(struct pbuf *p)
{u8_t alloc_src;struct pbuf *q;u8_t count;if (p == NULL) {return 0;}count = 0;while (p != NULL) {LWIP_PBUF_REF_T ref;ref = --(p->ref);if (ref == 0) {q = p->next;alloc_src = pbuf_get_allocsrc(p);
#if LWIP_SUPPORT_CUSTOM_PBUF /* 对于自定义pbuf,会执行自定义的释放函数 */if ((p->flags & PBUF_FLAG_IS_CUSTOM) != 0) {struct pbuf_custom *pc = (struct pbuf_custom *)p;pc->custom_free_function(p);} else
#endif /* LWIP_SUPPORT_CUSTOM_PBUF */{if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL) {memp_free(MEMP_PBUF_POOL, p);} else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF) {memp_free(MEMP_PBUF, p);} else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP) {mem_free(p);} else {}}count++;p = q;} else {p = NULL;}}return count;
}
释放的过程是从pbuf链的头部开始(也可能不是链),通过对pbuf->ref
减1进行释放,如果pbuf->ref
变为0,则表示不再有任何指针指向该pbuf,因此可以进行释放,根据pbuf的不同类型调用不同的释放函数,也就是memp_free
或者mem_free
。当遍历到某个pbuf的ref自减后不为0,那么就会停止释放过程。
其他函数
可以看到其他函数大多都是链表的操作过程,且操作原理和上面两个关键函数起始差不多,因此就不展开细讲了,虽然lwip提供了很多的API,但其实常用的并不多,甚至很多时候不会需要自己操作pbuf。