3 数据包 pbuf
这段数据包和《tcp/ip详解(卷二)》的开始基本相同,所以读起来在参照代码看很熟悉的感觉,理解很容易!
图片复制都丢了,日后有时间补上!
高的地方,总是很冷。孤独,可以让人疯狂。没人能懂你!昨天讲过了 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_t type;
u8_t flags;
u16_t ref;
};
这个看似简单的数据结构,却够我讲一大歇的了!next 字段指针指向下一个 pbuf 结构,因
为实际发送或接收的数据包可能很大,而每个 pbuf 能够管理的数据可能很少,所以,往往
需要多个 pbuf 结构才能完全描述一个数据包。所以,所有的描述同一个数据包的 pbuf 结构
E-mail:[email protected] 老衲五木出品
需要链在一个链表上,这一点用 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 如下图:
E-mail:[email protected] 老衲五木出品
从图中可看出 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,其结构
如下图:
E-mail:[email protected] 老衲五木出品
注:以上所有图片都来自文档《Design and Implementation of the LWIP:TCP/IP Stack》,这些图都
有个共同的错误,即 len 和 tot_len 字段位置搞反了,窃喜。
最后说明,对于一个数据包,它可能使用上述的任意的 pbuf 类型,很可能的情况是,一大串
不同类型的 pbufs 连在一起,用以保存一个数据包的数据。
广告……
下节看点,关于 pbuf 的内存释放问题。