这一章我们来看看LWIP中网络数据包pbuf。在协议栈内核中移动的数据包,无疑是整个内核最关键的部分。数据包的种类和大小五花八门:首先是网卡上接收的原始数据包,它可以是包含TCP报文的长达数百字节的数据包,也可以是仅有几十字节的ARP数据包;然后是要发送的数据包,上层应用可能是各种各样的数据交给LWIP内核发送。
数据包管理机构采用数据结构pbuf来描述协议栈中使用的数据包,结构pbuf的定义如下:
struct pbuf {
struct pbuf *next; //pbuf链表中指向下一个pbuf结构
void *payload; //数据指针,指向该pbuf所记录的数据区域
u16_t tot_len; //当前pbuf及后续所有pbuf中所包含的数据//总长度
u16_t len; //当前pbuf中数据的长度
u8_t type; //当前pbuf的类型
u8_t flags; //状态位未用到
u16_t ref; //指向该pbuf的指针数,即该pbuf被引用//的次数
};
next: 这个指向下一个pbuf结构,因为实际发送或接收的数据包可能很大,而每个pbuf能够管理的数据有限,所以,存在需要多个pbuf结构才能完全描述一个数据包的情况。此时,所有描述同一个数据包的pbuf需要连接在一个链表上。
payload:数据指针,指向该pbuf管理的数据起始地址。数据地址可以在RAM空间中,也可以在处在ROM中的某个地址,这个与参数type相关。
totlen:表示的当前pbuf以及以后所有pbuf的有效数据的总长度。len则表示的是当前pbuf的数据长度。所以,totlen是当前len字段和pbuf链表中下一个pbuf的totl_len字段之和。pbuf链表中第一个pbuf的tol_len字段表示整个数据包的长度。而最后一个pbuf的tot_len字段同len字段相等。
type字段表示pbuf的类型,具体来说pbuf有四种类型,待会儿详细解释
flags这里暂时没有用到
ref字段表示的是该pbuf被引用的次数。引用表示有其他指针指向当前buf,这里的指针可以是其他pbuf的next指针,也可以是其他任何形式的指针。初始化一个pbuf的时候,ref字段值被设置为1。
pbuf的类型
pbuf有四种类型: PBUF_RAM, PBUF_ROM, PBUF_REF, PBUF_POOL.
typedef enum {
PBUF_RAM, /* pbuf data is stored in RAM */
PBUF_ROM, /* pbuf data is stored in ROM */
PBUF_REF, /* pbuf comes from the pbuf pool */
PBUF_POOL /* pbuf payload refers to RAM */
} pbuf_type;
对这四种类型做一下详细的解释。
PBUF_RAM是用的最多的一种类型,pbuf的空间是通过内存堆分配得到的。这也是最常用的一种类型。申请PBUF_RAM类型的pbuf时协议栈会在内存堆中分配相应空间,这里的大小包括如前面所述的pbuf结构和相应数据缓冲区的大小,并且,他们是在一片连续的内存堆存储空间的。分配完成后,结构如下
Payload指向的并不一定是数据区域的开始,可以设定一定的offset,这个offset常用来存储TCP报文首部、IP首部等等,payload指向一般是除去帧头的首部后的数据区域。
PBUF_POOL类型和PBUF_RAM类型的pbuf有很大的相似之处,但他的空间是通过内存分配池得到的。这种类型的pbuf可以在极短的时间内得到分配。在网卡接收数据包时,我们就使用了这种方式包装数据。事实上,在系统初始化内存池的时候,还会初始化两类与数据报pbuf密切相关的POOL,这个可以参考上一章讲内存池时候的内容。它的名字分别是MEMP_PBUF和MEMP_PBUF_POOL,前者是用来存储pbuf结构的,主要在后两类PBUF_REF和PBUF_POOL中使用。而后者MEMP_PBUF_POOL的空间不仅包含了pbuf结构,还包含了LWIP认为协议栈可能使用的最大TCP数据报空间,默认长度为590个字节,很显然,这个长度小于某些大的以太网数据报,此时可能需要几个MEMP+PBUF_POOL空间才能放下这样一个大的数据报。
分配成功后如上图。
PBUF_RAM和PBUF_POOL的区别在于,PBUF_RAM的数据区域是在一起的,大小也是不固定的。而PBUF_POOL的数据区域大小是固定的,可以多个pbuf数据连接在一起形成链表关系。
剩余的两个PBUF_ROM和PBUF_REF比较类似,他们都是在内存池中分配一个相应的pbuf结构,但不申请数据区的空间,他们两者的区别在于PBUF_ROM指向ROM空间内的数据,后者指向RAM空间内的某段数据。在发送某些静态数据时,可以采用这两种类型的pbuf,这可以大大节省协议栈的内存空间,结构如下
另外,对于一个数据包来讲,它可能使用上述任意的pbuf类型来描述,还可以一大串不同类型的pbuf连在一起,共同保存一个数据包的数据
数据包申请函数
数据包申请函数pbuf_alloc在系统中的许多地方都会用到,在网卡接收数据时,需要申请一个数据包,然后将网卡中的数据填入数据包中
///用于接收数据包的最底层函数
//neitif:网卡结构体指针
//返回值:pbuf数据结构体指针
static struct pbuf * low_level_input(struct netif *netif)
{
struct pbuf *p, *q;
u16_t len;
……..
len=frame.length;//得到包大小
buffer=(u8 *)frame.buffer;//得到包数据地址
p=pbuf_alloc(PBUF_RAW,len,PBUF_POOL);//pbufs内存池分配pbuf
……….
return p;
}
又例如发送数据包时,协议栈的某层会申请一个pbuf,并将相应的数据装入到数据区域,同时相关的协议首部信息也会被填入到pbuf预留数据区域内。数据包申请函数有两个重要的参数:数据包pbuf的类型和数据包在那一层被申请。数据包类型就是我们之前讲的那四种,数据包在那一层申请这个参数主要是为了确定前面所说的偏移offset值。LWIP定义了四个层次,当数据包申请时,所处的层次不同,就会导致预留空间的的offset值不同。层次的定义是通过一个枚举类型pbuf_layer来实现
typedef enum {
PBUF_TRANSPORT, //传输层
PBUF_IP, //网络层
PBUF_LINK, //链路层
PBUF_RAW /原始层,不预留任何空间
} pbuf_layer;
#define PBUF_TRANSPORT_HLEN 20 //TCP报文首部长度
#define PBUF_IP_HLEN 20 /IP数据报文长度
这里还定义了IP报文首部和TCP报文首部长度。
pbuf分配函数pbuf_alloc的实现如下:
/**
* Allocates a pbuf of the given type (possibly a chain for PBUF_POOL type).
*
* The actual memory allocated for the pbuf is determined by the
* layer at which the pbuf is allocated and the requested size
* (from the size parameter).
*
* @param layer flag to define header size
* @param length size of the pbuf's payload
* @param type this parameter decides how and where the pbuf
* should be allocated as follows:
*
* - PBUF_RAM: buffer memory for pbuf is allocated as one large
* chunk. This includes protocol headers as well.
* - PBUF_ROM: no buffer memory is allocated for the pbuf, even for
* protocol headers. Additional headers must be prepended
* by allocating another pbuf and chain in to the front of
* the ROM pbuf. It is assumed that the memory used is really
* similar to ROM in that it is immutable and will not be
* changed. Memory which is dynamic should generally not
* be attached to PBUF_ROM pbufs. Use PBUF_REF instead.
* - PBUF_REF: no buffer memory is allocated for the pbuf, even for
* protocol headers. It is assumed that the pbuf is only
* being used in a single thread. If the pbuf gets queued,
* then pbuf_take should be called to copy the buffer.
* - PBUF_POOL: the pbuf is allocated as a pbuf chain, with pbufs from
* the pbuf pool that is allocated during pbuf_init().
*
* @return the allocated pbuf. If multiple pbufs where allocated, this
* is the first pbuf of a pbuf chain.
*/
struct pbuf *
pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{
struct pbuf *p, *q, *r; //定义几个局部指针变量
u16_t offset; //预留的首部空间的长度
s32_t rem_len; //还需要申请的数据空间长度
/*根据申请的层次不同,预留不同的空间长度 */
switch (layer) {
case PBUF_TRANSPORT:
/* 传输层,预留以太网帧首部+IP层首部+TCP层首部 */
offset = PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN;
break;
case PBUF_IP:
/* 网络层,预留以太网首部+IP层首部 */
offset = PBUF_LINK_HLEN + PBUF_IP_HLEN;
break;
case PBUF_LINK:
/* 网络接口层,预留以太网首部 */
offset = PBUF_LINK_HLEN;
break;
case PBUF_RAW:
/*原始层,不用预留*/
offset = 0;
break;
default:
LWIP_ASSERT("pbuf_alloc: bad pbuf layer", 0);
return NULL;
}
switch (type) {
case PBUF_POOL:
/* PBUF_POOL类型,根据长度不同,可能需要多个POOL */
p = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_alloc: allocated pbuf %p\n", (void *)p));
if (p == NULL) {
PBUF_POOL_IS_EMPTY();
return NULL;
}
p->type = type;
p->next = NULL;
/* 初始化payload字段,指向数据其实区域,预留出首部空间 */
p->payload = LWIP_MEM_ALIGN((void *)((u8_t *)p + (SIZEOF_STRUCT_PBUF + offset)));
/* tot_len为总长度 */
p->tot_len = length;
/* 当前pbuf的长度*/
p->len = LWIP_MIN(length, PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset));
/* ref字段设置为1 */
p->ref = 1;
/* r用于记录链表p上的最后一个pbuf*/
r = p;
/* 还需分配的长度 */
rem_len = length - p->len;
/* 多次分配直到rem_len不大于0 */
while (rem_len > 0) {
/*分配一个POLL*/
q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
/*若分配失败,释放链表上的所有pbuf*/
if (q == NULL) {
PBUF_POOL_IS_EMPTY();
/* free chain so far allocated */
pbuf_free(p);
/* bail out unsuccesfully */
return NULL;
}
/*分配成功,初始化各个字段,并将q加入链表p中*/
q->type = type;
q->flags = 0;
q->next = NULL;
r->next = q;
q->tot_len = (u16_t)rem_len;
q->len = LWIP_MIN((u16_t)rem_len, PBUF_POOL_BUFSIZE_ALIGNED);
q->payload = (void *)((u8_t *)q + SIZEOF_STRUCT_PBUF);
q->ref = 1;
/* 还需申请的长度 */
rem_len -= q->len;
/* r指向链表p的最后要给pbuf */
r = q;
}
/* end of chain */
/*r->next = NULL;*/
break;
case PBUF_RAM:
/* 在内存堆中申请 */
p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length));
if (p == NULL) {
return NULL;
}
/* 申请成功,初始化各个字段 */
p->payload = LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset));
p->len = p->tot_len = length;
p->next = NULL;
p->type = type;
break;
/*对于PBUF_ROM和PBUF_REF只分配pbuf结构的空间,不申请数据空间:*/
case PBUF_ROM:
case PBUF_REF:
p = (struct pbuf *)memp_malloc(MEMP_PBUF);
if (p == NULL) {
return NULL;
}
/* 初始化各个字段,payload字段除外,需要用户来根据实际情况来设置 */
p->payload = NULL;
p->len = p->tot_len = length;
p->next = NULL;
p->type = type;
break;
default:
LWIP_ASSERT("pbuf_alloc: erroneous type", 0);
return NULL;
}
/* set reference count */
p->ref = 1;
/* set flags */
p->flags = 0;
return p;
}
这个函数的实现过程很清晰,首先根据包申请时传入的协议层参数,计算出需要在pbuf数据区前预留的长度offset,然后根据pbuf的类型进行申请。PBUF_POLL类型的申请较为麻烦,因为有可能申请的长度比较大,需要几个POOL连在一起。PBUF_REF和PBUF_ROM类型申请最简单,他们只是在内存池MEMP_PBUF中分配了一个pbuf结构空间,然后初始化相关字段,注意这两种类型的payload指针需要自己进行设置,通常在调用完函数pbuf_alloc后,调用者需要将payload指向某一个区域。
看看两个例子:
p=pbuf_alloc(PBUF_RAW,len,PBUF_RAM);//
这个调用语句申请了一个PBUF_RAM类型的pbuf,且其申请的协议层为PBUF_RAW,,所以不会在数据区预留出任何首部空间。
在TCP层申请一个数据包时
p = pbuf_alloc(PBUF_TRANSPORT,len,PBUF_RAM);
数据报分配函数使用PBUF_RAM类型的pbuf,且数据区前会预留一部分空间,预留54个字节,TCP首部20个字节,IP层首部20个字节,以太网帧首部14个字节,当数据报往下传递时,各层协议就可以直接操作这些预留中的数据,避免了数据的拷贝
数据报的释放
当底层完成数据包的发送或者上层完成数据的处理后,数据包必须释放,只有这样才能保证内存空间的持续可用。
Pbuf的ref字段表示该pbuf被引用的次数,当pbuf被创建时,该字段的初始值为1,代表被引用一次,只有当pbuf的ref字段为0时,该pbuf才能被删除。此外,能被删除的pbuf必然是某个pbuf链表的首节点或者是在其他地方被引用过的节点。这一点一定要格外注意。
举几个简单的例子,假如现在我们的pbuf链表由A\B\C三个pbuf结构连接起来,结构A---B---C,利用pfree(A)来删除pbuf结构,下面用ABC几组不同的ref值来看删除结果
1、 A(1)---B(2)---C(3),删除后变成 B(1)---C(3)
2、 A(3)---B(3)---C(3),删除后变成A(2)--- B(3)---C(3)
3、 A(1)---B(1)---C(2),删除后变成 C(1)
4、 A(2)---B(1)---C(1),删除后变成 A(1)---B(1)---C(1)
5、 A(1)---B(1)---C(1),删除后无节点
对于第4种情况,如果我们使用pbuf_free(B),那B和C节点就被删除了,但A节点仍然指向B,这就使系统埋下了隐患。
看一下释放函数的实现
/**
* Dereference a pbuf chain or queue and deallocate any no-longer-used
* pbufs at the head of this chain or queue.
*
* Decrements the pbuf reference count. If it reaches zero, the pbuf is
* deallocated.
*
* For a pbuf chain, this is repeated for each pbuf in the chain,
* up to the first pbuf which has a non-zero reference count after
* decrementing. So, when all reference counts are one, the whole
* chain is free'd.
*
* @param p The pbuf (chain) to be dereferenced.
*
* @return the number of pbufs that were de-allocated
* from the head of the chain.
*
* @note MUST NOT be called on a packet queue (Not verified to work yet).
* @note the reference counter of a pbuf equals the number of pointers
* that refer to the pbuf (or into the pbuf).
*
* @internal examples:
*
* Assuming existing chains a->b->c with the following reference
* counts, calling pbuf_free(a) results in:
*
* 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 .......
*
*/
u8_t
pbuf_free(struct pbuf *p)
{
u16_t type;
struct pbuf *q;
u8_t count;
if (p == NULL) { //p为NULL,返回
return 0;
}
count = 0; //记录值清零
/* 循环查找p链表*/
while (p != NULL) {
u16_t ref;
SYS_ARCH_DECL_PROTECT(old_level); //申请临界保护变量
SYS_ARCH_PROTECT(old_level); //进入临界区
ref = --(p->ref); //ref值自减
SYS_ARCH_UNPROTECT(old_level); //退出临界区
/*ref字段是0,说明可以删除了*/
if (ref == 0) {
/* p链表中的下一个pbuf */
q = p->next;
type = p->type;
/*根据不同的类型,申请不同的内存释放函数*/
{
/* 从内存池申请的, PBUF_POOL类型 */
if (type == PBUF_POOL) {
memp_free(MEMP_PBUF_POOL, p);
/* 是PBUF_ROM或PBUF_REF) 类型的 */
} else if (type == PBUF_ROM || type == PBUF_REF) {
memp_free(MEMP_PBUF, p);
/* 是PBUF_RAM类型的 */
} else {
mem_free(p);
}
}
count++;
/* 处理链表的下一个pbuf */
p = q;
} else {
/* ref字段不是0,p为NULL,跳出while*/
p = NULL;
}
}
return count;
}
pbuf_free检查pbuf属于哪一种类型的pbuf,调用不同的释放函数进行删除。PBUF_POOL、PBUF_ROM和PBUF_ROM都通过memp_free来实现,PBUF_RAM使用mem_free来删除