【LwIP】03——网络数据包
任何网络协议栈,从数据层面看,本质都是发送端与接收端之间不间断的“打包”与“拆包”过程。发送端从应用层承接数据(payload),协议栈根据通信目的为其逐层添加协议头部(header),最终交付底层驱动发送至传输介质。接收端则执行逆操作:底层驱动从介质获取数据帧并上报协议栈,协议栈逐层剥离头部,直至将原始数据还原给应用层。因此,减少数据跨层传递带来的时间和空间开销,是提升协议栈性能的核心。
在LwIP中,使用 pbuf 这种数据结构来描述一个数据包。本文将重点研究这个数据结构。
一、pbuf的结构体
/* pbuf.h */
struct pbuf {
//指向下一个pbuf,当网络数据包很大时,如果使用的是PBUF_POOL 会把这个网络数据包交给多个pbuf链在一起进行管理。
struct pbuf *next;
//指向数据区域
void *payload;
//当前pbuf及后续pbuf的数据区域长度的和
u16_t tot_len;
//当前pbuf的数据区域长度
u16_t len;
//pbuf的类型 可以是PBUF_RAM、PBUF_POOL 、PBUF_ROM、PBUF_REF中的一种
u8_t type_internal;
//flags
u8_t flags;
//被引用的次数,当有一个指针指向这个pbuf时,ref就会jia'yi
LWIP_PBUF_REF_T ref;
//对于接收的数据包,指向接收网卡(netif)的索引
u8_t if_idx;
};
二、pbuf的类型
根据前面pbuf的结构体,我们知道pbuf有四种类型。在LwIP中,这四种类型由一个枚举类型的数据结构定义它们。
/* pbuf.h */
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;
1、PBUF_RAM
PBUF_RAM类型的pbuf在LwIP中用的比较多,由内存堆分配而来。一般协议栈在发送数据时,就会申请这种类型的pbuf。这种类型的pbuf包含pbuf结构体信息和数据,是一段连续的内存。当然,PBUF_RAM在申请时,也会预留各个协议层级的头部信息区域(pbuf layer)。 PBUF_RAM 的示意图如下图所示

2、PBUF_POOL
PBUF_POOL类型和PBUF_RAM类型的pbuf都是差不多的,其pbuf的结构体信息和数据同样是存在连续的内存块中。不同的是,PBUF_POOL的内存来自于内存池,在网卡接收时,就会使用这种类型的pbuf来保存接收到的数据。
协议栈在系统内存池初始化时,会初始化以下两个内存池。
/* memp_std.h */
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,和TCP_MSS有关,一般设置为1460(TCP_MSS) + 40 + 14 ;内存块的数量为PBUF_POOL_SIZE。
如果使用PBUF_POOL类型的pbuf,则碰到大的数据包时,会使用多个pbuf链在一起进行管理。
3、PBUF_REF、PBUF_ROM
PBUF_ROM类型和PBUF_ROM的pbuf,它们的长度等于pbuf数据结构(sizeof(struct pbuf)),从MEMP_PBUF中分配得到。
其中PBUF_ROM类型的pbuf,它的数据区域(payload)指向了ROM中的一段区域;而PBUF_RAM类型的pbuf,它的数据区域指向的是RAM中的一段区域。
二、pbuf的使用
1、pbuf_alloc()
pbuf的申请有两个很重要的参数,一个就是前面提到的pbuf的类型;另外一个则是pbuf在协议栈哪一层被申请,这个参数的目的在于,只有知道了pbuf被申请的层级,才好在pbuf中预留协议的头部信息长度。例如在传输层申请的pbuf,我们需要预留传输层协议头部+网络层头部长度+链路层头部长度;而在网络层申请的pbuf,只需要预留网络层头部长度+链路层头部长度。
在LwIP中,这个参数使用枚举类型定义。
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_alloc这个函数的实现方式。
/* pbuf.c */
struct pbuf *
pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{
struct pbuf *p;
//计算需要为协议头部信息预留的空间
u16_t offset = (u16_t)layer;
switch (type) {
//如果需要申请的pbuf类型是ref和rom 不需要为其分配数据区域的空间,只需要分配pbuf结构体大小的空间即可
case PBUF_REF: /* fall through */
case PBUF_ROM:
p = pbuf_alloc_reference(NULL, length, type);
break;
//如果类型是PBUF_POOL
case PBUF_POOL: {
struct pbuf *q, *last;
u16_t rem_len; /* remaining length */
p = NULL;
last = NULL;
rem_len = length;
do { //循环 内存池中的每个内存块能够管理的数据长度有限
u16_t qlen;
//从pbuf_pool内存池中申请一个内存块
q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
if (q == NULL) {
PBUF_POOL_IS_EMPTY();
/* free chain so far allocated */
if (p) {
pbuf_free(p);
}
/* bail out unsuccessfully */
return NULL;
}
//计算当前申请到的内存块能装多少数据
qlen = LWIP_MIN(rem_len, (u16_t)(PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset)));
//初始化pbuf结构体
pbuf_init_alloced_pbuf(q, LWIP_MEM_ALIGN((void *)((u8_t *)q + SIZEOF_STRUCT_PBUF + offset)),
rem_len, qlen, type, 0);
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 );
//如果当前节点是第一个节点,那么这个pbuf节点就是头节点
if (p == NULL) {
/* allocated head of pbuf chain (into p) */
p = q;
} else {
//已经有头节点了,那么现在的节点都是后续节点,我们需要让last节点的next指向当前节点
/* make previous pbuf point to this pbuf */
last->next = q;
}
//记录一下last节点,便于与后续节点建立关系
last = q;
rem_len = (u16_t)(rem_len - qlen);
offset = 0; //关键点 offset直接清0了 意味着后面的节点不用再预留头部信息的空间了
} while (rem_len > 0); //直到所有的数据都有地方可以放
break;
}
case PBUF_RAM: {
// 计算实际需要的 payload 大小 = 头部预留(offset) + 数据长度(length) 并做内存对齐
mem_size_t payload_len = (mem_size_t)(LWIP_MEM_ALIGN_SIZE(offset) + LWIP_MEM_ALIGN_SIZE(length));
// 计算总分配大小 = pbuf结构体大小 + payload_len
mem_size_t alloc_len = (mem_size_t)(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF) + payload_len);
/* bug #50040: Check for integer overflow when calculating alloc_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
pbuf_init_alloced_pbuf(p, LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset)),
length, length, type, 0);
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;
}
2、pbuf_free()
有申请,对应就有释放。当某一pbuf中的数据已经被内核处理完时,需要对pbuf进行释放,如果pbuf一直得不到释放,将导致内存泄漏,后续将无法再申请到可用的pbuf。
pbuf_free函数将会递减pbuf()的引用次数(pbuf->ref),当pbuf的引用次数减至0时,pbuf所对应的内存空间将会被完全释放。pbuf_free函数可以处理某一pbuf或pbuf链。
下面来看看pbuf_free的代码
u8_t
pbuf_free(struct pbuf *p)
{
u8_t alloc_src;
struct pbuf *q;
u8_t count;
if (p == NULL) {
LWIP_ASSERT("p != NULL", p != NULL);
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;
//当待释放的pbuf不为空时
while (p != NULL) {
LWIP_PBUF_REF_T ref;
SYS_ARCH_DECL_PROTECT(old_level);
/* Since decrementing ref cannot be guaranteed to be a single machine operation
* we must protect it. We put the new ref into a local variable to prevent
* further protection. */
SYS_ARCH_PROTECT(old_level);
/* all pbufs in a chain are referenced at least once */
LWIP_ASSERT("pbuf_free: p->ref > 0", p->ref > 0);
//递减pbuf的引用次数
ref = --(p->ref);
SYS_ARCH_UNPROTECT(old_level);
/* this pbuf is no longer referenced to? */
if (ref == 0) {
//当引用次数减至0时,说明此时pbuf所对应的内存空间可以被释放了 记录一下当前pbuf的next
q = p->next;
LWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free: deallocating %p\n", (void *)p));
//获取pbuf的类型
alloc_src = pbuf_get_allocsrc(p);
//根据pbuf的不同,选择合适的释放方式
#if LWIP_SUPPORT_CUSTOM_PBUF
/* is this a custom pbuf? */
if ((p->flags & PBUF_FLAG_IS_CUSTOM) != 0) {
struct pbuf_custom *pc = (struct pbuf_custom *)p;
LWIP_ASSERT("pc->custom_free_function != NULL", pc->custom_free_function != NULL);
pc->custom_free_function(p);
} else
#endif /* LWIP_SUPPORT_CUSTOM_PBUF */
{
/* is this a pbuf from the pool? */
if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL) {
memp_free(MEMP_PBUF_POOL, p);
/* is this a ROM or RAM referencing pbuf? */
} else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF) {
memp_free(MEMP_PBUF, p);
/* type == PBUF_RAM */
} else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP) {
mem_free(p);
} else {
/* @todo: support freeing other types */
LWIP_ASSERT("invalid pbuf type", 0);
}
}
//被释放的pbuf数量加一
count++;
/* proceed to next pbuf */
//开始处理下一个pbuf
p = q;
/* p->ref > 0, this pbuf is still referenced to */
/* (and so the remaining pbufs in chain as well) */
} else {
LWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free: %p has ref %"U16_F", ending here.\n", (void *)p, (u16_t)ref));
/* stop walking through the chain */
p = NULL;
}
}
PERF_STOP("pbuf_free");
//将释放的pbuf数量返回
return count;
}
结合上述代码,我们假设现在对以下四条pbuf的链分别进行释放,数字代表pbuf链表中各个节点的引用次数:
1->2->3 becomes ...1->3 // 第一个释放后,指向第二个
3->3->3 becomes 2->3->3 // 只释放了第一个
1->1->2 becomes ......1 // 释放前两个
2->1->1 becomes 1->1->1 // 释放第一个
1->1->1 becomes ....... // 全部释放
3、pbuf中的常见函数接口
pbuf_realloc(struct pbuf *p, u16_t new_len) 用于将pbuf缩减为new_len的长度
pbuf_header(struct pbuf *p, s16_t header_size_increment) 之前说过,网络数据包的控制是双向的,在发包时,对网络数据包附加头部信息;在收包时,移除头部信息还原应用数据。pbuf_header函数可以用于调整pbuf的payload指针,实现在数据包前面添加或删除协议头。当header_size_increment小于0时,说明需要将payload指针向后移动,移除掉协议头部信息;当header_size_increment大于0时,说明需要将payload指针向前移动,添加协议头部信息
除此之外,pbuf的操作接口还有很多,如pbuf_copy,pbuf_take等等,后面在遇到的时候再进行介绍。