1引言
TCP/IP协议分层处理数据,数据要在协议层间流动,存储系统的主要用途是在网络各层接口间传递数据时保存数据。另外,存储系统还需要存放通信双方的源地址、目的地址、源端口和目的端口等信息。
基于以上目的,对存储管理能力有如下要求:(1)适合存放不同长度的数据。(2)方便地操作变长缓存。(3)尽量减少为完成这些操作所做的数据拷贝。
另外,嵌入式系统中资源有限,传统PC上的实现对于嵌入式来说过于庞大复杂,需要去掉传统实现中复杂的部分,使得缓存的分配回收简单、可靠,减少内存的使用。
LwIP是一个应用比较广泛的嵌入式TCP/IP协议栈。LwIP协议栈参考目前使用最广泛的BSD UNIX的Mbuf缓存管理系统,去掉Mbuf中实现复杂的、开销大的部分,根据嵌入式系统的实际需要,以减少内存使用量、增强实时性、可移植性为目标,设计实现了pbuf。本文分析LwIP的缓存管理系统实现,并实现在EuroDOSIS协议栈的嵌入式系统中应用。
2 PBUF的设计与实现
2.1 设计目标
Pbuf,PACket buffer的缩写。Pbuf的设计目标是既可以动态分配内存存放数据包,也可以让数据包存放在静态内存中,且可以把多个pbuf链接到一起成为一个pbuf的链,使得一个大的数据分组可以分散存储在几个pbuf中。
支持内存动态分配和回收,为了防止多次malloc/free在整个系统空间产生内存碎片,采用内存再分配算法——先从系统申请固定大小的静态内存作为协议栈的缓冲区,在这段内存上定义协议栈自身的内存分配算法,在固定协议栈所用的内存的情况下支持内存的动态分配和回收,防止了协议栈耗尽所有系统内存,提高了系统的可控性和可靠性。为了灵活使用有限内存空间,采用变长分配缓冲区和定长分配缓冲区两种方式。
为了提高速度,允许数据驻留在协议栈以外的内存中,避免数据拷贝。
针对数据源和流向的不同,设计实现四种类型的pbuf:PBUF_RAM ,PBUF_ROM ,PBUF_REF和 PBUF_POOL类型。后面对每种类型pbuf的实现目的和方法有详细介绍。
2.2 数据结构和实现
? PBUF结构
struct pbuf {
struct pbuf *next; //指向下一个pbuf;
void *payload; //指向pbuf数据区;
INT16 len; //当前pbuf中已存放数据的长度
INT16 tot_len; //pbuf链表中当前pbuf及其后所有pbuf中数据的长度的和;
INT16 flags; //pbuf类型;
INT16 ref; //pbuf被引用的次数,ref为0时,才可以释放该pbuf
};
以下讲述不同类型pbuf的设计,按照先介绍不同pbuf本身的设计思想和目的,然后给出支持该种pbuf机制的内存管理机制的顺序介绍。
? PBUF_RAM
PBUF_RAM类型缓冲区是变长分配的缓冲区,根据申请的大小进行分配。既分配存放数据所需的内存,也分配在处理过程中添加协议栈的首部信息需要的内存。该类型缓冲区通常应用于程序动态产生需要发送数据的时候,通过调整payload指针指向当前要处理的数据。
为支持PBUF_RAM类型pbuf机制,从系统中申请一段内存作为变长分配内存的区域,对这段内存定义分配、回收连续的内存区域等操作。变长分配内存区域是由一系列未分配和已分配的内存块组成,采用链表对这段内存进行管理——在每个已分配和未分配的内存块的首部放上简单的管理信息,设置prev指向前一个内存块,next指向后一个内存块,另有used字段标志内存块是否已分配——0表示未分配,1表示已分配。

变长内存分配区域最初是一个连续的未分配内存块,prev指向0,next指向最后,used为0,表示是一个只有一个未分配 图一 PBUF_RAM类型pbuf 内存块的链表。分配内存时,搜索内存块链表,采用最先适应内存分配算法找到可以满足所申请内存大小的空闲内存块,分配所需大小,该空闲内存块被分离为一个已分配内存块和未分配内存块。为了防止内存碎片,在释放内存时,检查前后的内存块的used字段是否为0,如果为0,则把它们和正在释放的内存合并。

图二 内存布局
· PBUF_ROM类型和PBUF_REF类型
在PBUF_ROM类型的pbuf中,存放数据的内存不在pbuf系统的管理中,而只有pbuf结构中的payload指针指向协议栈外存放数据的静态内存。这样设计是因为,当应用程序有数据需要发出且协议栈处理过程中不改变数据时,直接从pbuf系统中访问这段内存中的数据以减少数据拷贝的次数,提高系统性能。发送PBUF_ROM类型pbuf中数据,需要在PBUF_ROM型pbuf前链接一个PBUF_RAM型pbuf缓冲区用于存放TCP/IP协议栈各层协议首部信息。

图三:PBUF_ROM类型pbuf
PBUF_REF类型与PBUF_ROM类型pbuf作用和使用方法相似,用于对协议栈外动态申请的内存中数据的引用。有PBUF_REF类型和PBUF_ROM类型的不同是因为在分组暂不发出,放入缓冲队列时,对引用的是协议栈外静态内存还是动态内存中存放的数据的处理不同。
支持PBUF_ROM和PBUF_REF类型pbuf的内存管理机制很简单。在协议栈程序初始化的时候先申请一段静态内存空间,初始化成pbuf结构链表。申请PBUF_ROM或PBUF_REF类型pbuf时,从这个链表中分配一个pbuf结构给使用者。
?· PBUF_POOL
PBUF_POOL类型pbuf位于定长分配的pbuf缓冲池中,如图三所示。缓冲池中的pbuf均是固定的长度,分配缓冲区时,根据申请的大小分配一个或多个pbuf。PBUF_POOL类型pbuf通常用于网卡驱动程序中,因为从缓冲池中分配一个PBUF_POOL类型pbuf耗时很少,所以适合在中断处理程序中完成。 通常用PBUF_POOL型pbuf存放收到的数据包。
为了方便移植到不同的系统中,满足不同的应用,缓冲池中pbuf的个数和pbuf数据区的大小都是可配置的。

PBUF_POOL型pbuf的数据区不能设置的太大,太大对于短的数据包来说,造成了内存空间的浪费,也不能太小,原因有两点:1)如果数据区设置太小,pbuf结构相对于数据区来说就占用了太多的内存空间,导致内存利用率下降;2)处理报文时是通过移动payload指针来指向特定的协议首部,为了方便操作,数据分组首部信息的存放最好不要跨越pbuf,因此,数据区的大小就不能小于各层协议首部信息大小的总和。在EuroDOSIS协议栈中,因为传输层只用UDP协议,传输的数据大小在几十字节至512字节之间, PBUF_POOL_SIZE折中设置为128字节。
PBUF_POOL类型pbuf缓冲池在协议栈初始化时设置,在内存中申请一块静态存储区,初始化成pbuf链表。分配该类型pbuf时,从缓冲池首部根据申请的大小分配一个或多个pbuf。与PBUF_RAM类型需要采用最先适应算法搜索空闲内存块,花费少了很多,因此速度更快。
2.3 Pbuf的主要操作
1) struct pbuf *pbuf_alloc(pbuf_layer l, u16_t size, pbuf_flag flag);
分配pbuf。flags的值可以为PBUF_RAM、PBUF_ROM、PBUF_REF或PBUF_POOL之一。size是要申请的空间大小。
2) INT8 pbuf_header(struct pbuf *p, s16_t header_size);
调整payload指针的指向,指向或隐藏各层协议首部信息。
3)void pbuf_ref(struct pbuf *p);
增加pbuf引用次数计数。该函数对pbuf中的ref执行加1操作。只有当ref字段为0时,才可以回收该pbuf。
4)INT8 pbuf_free(struct pbuf *p);
减少一个pbuf或pbuf链表的引用计数,当ref为0时,回收该pbuf,根据其flags字段的值回收到相应的内存区域。
5) void pbuf_chain(struct pbuf *h, struct pbuf *t);
把t所指向的pbuf链接到h所指向的pbuf之后(h和t都可以是pbuf链表)。
6) void pbuf_queue(struct pbuf *p, struct pbuf *n);
用于把暂时不发送的分组放到缓冲队列中。p是指向当前缓冲队列第一个分组的指针,是要放到缓冲队列的分组。
7) struct pbuf *pbuf_dechain(struct pbuf *p);
在缓冲队列的首部取出一个分组,返回指向剩下分组第一个pbuf的指针。
3应用与评价
Pbuf实现了在EuroDOSIS协议栈的嵌入式系统中的成功应用。Pbuf的成功依赖于以下几个方面:1)实现简洁,内存占用量少。ARM7TDMI,ADS1.2环境下编译后代码只有10K,在分配2048字节给PBUF_RAM,20个128字节PBUF_POOL型pbuf的情况下正常工作。不同应用中具体分配多少的空间则根据实际的需要及网络上数据流量决定,pbuf提供了可配置的参数。2)快速,实现了“单次拷贝技术”。基本上只在协议栈和网络接口之间作一次数据拷贝,在协议栈中传递数据时,通过传递pbuf指针处理数据,尽量避免了不必要的数据拷贝。3)适合存放不同长度的数据,几十字节的ARP报文到最大的IP报文,均能在pbuf中处理。4)pbuf可以方便地操作变长缓冲区,数据分组在协议栈中流动,只需通过移动pbuf的payload指针指向相应的协议首部,可以方便的封装移去数据。
4小结
以上给出了pbuf的设计实现和对pbuf的操作,经过实际的应用,pbuf系统对内存空间的占用和实时性方面均能满足需求,运行良好。因为独立于平台,基于裸机实现,所以可移植性很强,可以很容易的把pbuf及基于pbuf写的协议栈移植到不同环境下。
在实际实现的过程中,还需要考虑内存边界对齐的问题。在多线程环境下,还需要加入对共享资源的访问控制等。




