《LwIP协议栈源码详解——TCP/IP协议的实现》数据包pbu
姓名:朱小鹏 学号:16010130023
转载:http://blog.sina.com.cn/s/blog_62a85b950101am5v.html
【嵌牛导读】:WIP中常用到的内存分配策略有两种,一种是内存堆分配,一种是内存池分配,在LWIP中,将这两种分配策略混合使用,达到了很好的内存使用效率。
【嵌牛鼻子】:LWIP的数据包缓冲的实现
【嵌牛提问】:LWIP中如何实现数据包缓冲?
【嵌牛正文】:
昨天讲过了LWIP的内存分配机制。再来总之一下,LWIP中常用到的内存分配策略有两种,一种是内存堆分配,一种是内存池分配。前者可以说能随心所欲的分配我们需要的合理大小的内存块(又是‘的’),缺点是当经过多次的分配释放后,内存堆中间会出现很多碎片,使得需要分配较大内存块时分配失败;后者分配速度快,就是简单的链表操作,因为各种类型的POOL是我们事先建立好的,但是采用POOL会有些情况下会浪费掉一定的内存空间。在LWIP中,将这两种分配策略混合使用,达到了很好的内存使用效率。
下面我们将来看看LWIP中是怎样合理利用这两种分配策略的。这就顺利的过渡到了这节要讨论的话题:LWIP的数据包缓冲的实现。
在协议栈中移动的数据包,最无疑的是整个内存管理中最重要的部分了。数据包的种类和大小也可以说是五花八门,数数,首先从网卡上来的原始数据包,它可以是长达上千个字节的TCP数据包,也可以是仅有几个字节的ICMP数据包;再从要发送的数据包看,上层应用可能将自己要发送的千奇百怪形态各异的数据包递交给LWIP协议栈发送,这些数据可能存在于应用程序管理的内存空间内,也可能存在于某个ROM上。注意,这里有个核心的东西是当数据在各层之间传递时,LWIP极力禁止数据的拷贝工作,因为这样会耗费大量的时间和内存。综上,LWIP必须有个高效的数据包管理核心,它即能海纳百川似的兼容各种类型的数据,又能避免在各层之间的复制数据的巨大开销。
数据包管理机构采用数据结构pbuf来描述数据包,其源码如下,
struct pbuf {
struct pbuf *next;
void *payload;
u16_t tot_len;
u16_t len;
u8_ttype;
u8_t flags;
u16_t ref;
};
这个看似简单的数据结构,却够我讲一大歇的了!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头和相应数据在同一片连续的内存区种,注意payload并没有指向pbuf头结束即ref字段之后,而是隔了一定的区域。这段区域就是上面的offset的大小,这段区域用来存储数据的包头,如TCP包头,IP包头等。当然,offset也可以是0,具体值是多少,那就要看你是怎么个申请法了。如果还要深究,你肯定会更晕了。
PBUF_POOL类型和PBUF_RAM类型的pbuf有很大的相似之处,但它主要通过内存池分配得到的。这种类型的pbuf可以在极短的时间内得到分配。在接受数据包时,LWIP一般采用这种方式包装数据。申请PBUF_POOL类型时,协议栈会在内存池中分配适当的内存池个数以满足需要的申请大小。下面来看看源代码是怎样申请PBUF_POOL型的。其中p是pbuf型指针。
p = memp_malloc(MEMP_PBUF_POOL);
可以看出,系统是调用内存池分配函数memp_malloc进行内存分配的。分配成功的PBUF_POOL类型的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的内存释放问题。