总结一下,LWIP中常用到的内存分配策略有两种,一种是内存堆分配,一种是内存池分配。前者可以说能随心所欲的分配我们需要的合理大小的内存块,缺点是当经过多次的分配释放后,内存堆中间会出现很多碎片,使得需要分配较大内存块时分配失败;后者分配速度快,就是简单的链表操作,因为各种类型的POOL是我们事先建立好的,但是采用POOL会有些情况下会浪费掉一定的内存空间。在LWIP中,将这两种分配策略混合使用,达到了很好的内存使用效率。
下面我们将来看看LWIP中是怎样合理利用这两种分配策略的。这就顺利的过渡到了这节要讨论的话题:LWIP的数据包缓冲的实现。 在协议栈中移动的数据包,最无疑的是整个内存管理中最重要的部分了。
数据包的种类和大小也可以说是五花八门,数数,首先从网卡上来的原始数据包,它可以是长达上千个字节的TCP
数据包,也可以是仅有几个字节的ICMP
数据包;再从要发送的数据包看,上层应用可能将自己要发送的千奇百怪形态各异的数据包递交给LWIP协议栈发送,这些数据可能存在于应用程序管理的内存空间内,也可能存在于某个ROM上。注意,这里有个核心的东西是当数据在各层之间传递时,LWIP极力禁止数据的拷贝工作,因为这样会耗费大量的时间和内存。
综上,LWIP必须有个高效的数据包管理核心,它即能海纳百川似的兼容各种类型的数据,又能避免在各层之间的复制数据的巨大开销。
数据包管理机构采用数据结构pbuf
来描述数据包,其源码如下:
struct pbuf {
struct pbuf *next; //针指向下一个pbuf结构
void *payload; //数据指针,指向该pbuf管理的数据的起始地址
u16_t tot_len; //当前pbuf中的有效数据长度
u16_t len; //当前pbuf和其后所有pbuf的有效数据的长度
u8_t type; //pbuf的类型
u8_t flags; //pbuf的类型
u16_t ref; //该pbuf被引用的次数
};
这个看似简单的数据结构,却内容丰富!
next字段指针指向下一个pbuf
结构,因为实际发送或接收的数据包可能很大,而每个pbuf能够管理的数据可能很少,所以,往往需要多个pbuf
结构才能完全描述一个数据包。所以,所有的描述同一个数据包的pbuf
结构需要链在一个链表上,这一点用next
实现。
payload是数据指针,指向该pbuf
管理的数据的起始地址,这里,数据的起始地址可以是紧跟在pbuf
结构之后的RAM
,也可能是在ROM
上的某个地址,而决定这点的是当前pbuf
是什么类型的,即type
字段的值,这在下面将继续讨论。
len字段表示当前pbuf
中的有效数据长度。
而tot_len表示当前pbuf
和其后所有pbuf
的有效数据的长度。显然,tot_len
字段是len
字段与pbuf
链中随后一个pbuf
的tot_len
字段的和;pbuf
链中第一个pbuf
的tot_len
字段表示整个数据包的长度,而最后一个pbuf
的tot_len
字段必和len字段相等。
type字段表示pbuf
的类型,主要有四种类型,这点基本上涉及到pbuf
管理中最难的部分,将在下节仔细讨论。
文档上说flags字段也表示pbuf
的类型,不懂,type
字段不是说明了pbuf
的类型吗?不过在源代码里,初始化一个pbuf
的时候,是将该字段的值设为0
,而在其他地方也没有用到该字段,所以,这里直接忽略掉。
最后ref字段表示该pbuf
被引用的次数。这里又是一个纠结的地方啊。初始化一个pbuf
的时候,ref
字段值被设置为1
,当有其他pbuf
的next
指针指向该pbuf
时,该pbuf
的ref
字段值加一。所以,要删除一个pbuf
时,ref
的值必须为1
才能删除成功,否则删除失败。
pbuf的类型很多。pbuf
有四类:PBUF_RAM、PBUF_ROM、PBUF_REF和PBUF_POOL。下面,一个一个的来看看各种类型的特点。
PBUF_RAM
类型的pbuf
主要通过内存堆分配得到的。这种类型的pbuf
在协议栈中是用得最多的。协议栈要发送的数据和应用程序要传递的数据一般都采用这个形式。申请PBUF_RAM
类型时,协议栈会在内存堆中分配相应的大小,注意,这里的大小包括如前所述的pbuf
结构头大小和相应数据缓冲区,他们是在一片连续的内存区的。下面来看看源代码是怎样申请PBUF_RAM
型的。
其中p
是pbuf
型指针。
p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length));
可以看出,系统是调用内存堆分配函数mem_malloc
进行内存分配的。分配空间的大小包括:pbuf
结构头大小SIZEOF_STRUCT_PBUF
,需要的数据存储空间大小length
,还有一个offset
。关于这个offset
,也有一大堆可以讨论的东西,不过先到此打住。总之,分配成功的PBUF_RAM
类型的pbuf
如下图:
图中是分配指定大小的数据缓冲的结果,系统调用会分配多个固定大小的PBUF_POOL
类型pbuf
,并把这些pbufs
链成一个链表,以满足用户的分配空间请求。
PBUF_ROM
和PBUF_REF
类型的pbuf
基本相同,它们的申请都是在内存堆中分配一个相应的pbuf
结构头,而不申请数据区的空间。这就是它们与PBUF_RAM
和PBUF_POOL
的最大区别。PBUF_ROM
和PBUF_REF
类型的区别在于前者指向ROM
空间内的某段数据,而后者指向RAM
空间内的某段数据。下面来看看源代码是怎样申请PBUF_ROM
和PBUF_REF
类型的。其中p
是pbuf
型指针。
p = memp_malloc(MEMP_PBUF);
可以看出,系统是调用内存池分配函数memp_malloc
进行内存分配的。而此刻请求的内存池类型为MEMP_PBUF
,而不是MEMP_PBUF_POOL
。MEMP_PBUF
类型的内存池大小恰好为一个pbuf
头的大小,因为这种池是LWIP专为PBUF_ROM
和PBUF_REF
类型的pbuf
量身制作的。LWIP还是真的很周到啊,它会为不同的数据结构量身定做不同类型的池。正确分配的PBUF_ROM
或PBUF_REF
类型的pbuf
,其结构如下图:
注:以上所有图片都来自文档《Design and Implementation of the LWIP:TCP/IP Stack》
,这些图都有个共同的错误,即len
和tot_len
字段位置搞反了,窃喜。
最后说明,对于一个数据包,它可能使用上述的任意的pbuf
类型,很可能的情况是,一大串不同类型的pbufs
连在一起,用以保存一个数据包的数据。
下节看点,关于pbuf
的内存释放问题。