本文讲述了Rime协议栈缓冲区管理,先是结合示意图理清各变量间关系,接着给出函数API描述,并详细剖析各个函数,最后分析了发出和接收的数据包存放方式不同的原因。
1. 概述及相关变量
Rime缓冲区管理比较简单,将发出和收到的数据包(包括应用程序数据和数据包属性packet attributes,即所有数据都经过该缓冲区)都存储在一个单一的缓冲区packetbuf
,由头部和数据两部分组成。
既然发出和接收的数据包都放在同一样缓冲区,那么他们的存放格式是不是一样呢?不是的,对于发出的数据包(outbound packets),头部放在packetbuf
的头部,数据放在packetbuf
的数据;然而,对于接收的数据包(incoming packets),头部和数据都放在packetbuf
的数据区域里。(为什么发出和接收的数据包不以同样的格式存放呢,见本文第三部分)
图1 outbound、incoming packet示意图
Rime缓冲区管理相关代码在文件contiki/core/net/packetbuf.h packetbuf.c
,涉及到很多变量(如hdrptr
、bufptr
、buflen
、packetbufptr
),理清这些变量间关系非常有助于阅读packetbuf.c
的源码,这些变量关系如下图:
图2 Rime缓冲区管理示意图
注:
从名字上看,
hdrptr
、bufptr
是指针类型变量,事实上不是,而是整型变量(前者为uint8_t
,后者为uint16_t
)。阅读以下内容,结合图2将非常有助于理解(事实上,我几乎通读了
packetbuf.c
的源码,才抽象出图2)。
1.1 packetbuf定义
Rime缓冲区存放应用程序数据和数据包属性,大小是这两部分的和,Rime缓冲区由静态全局变量packetbuf
定义,源代码如下:
static uint16_t packetbuf_aligned[(PACKETBUF_SIZE + PACKETBUF_HDR_SIZE)/2+1];
static uint8_t *packetbuf = (uint8_t*)packetbuf_aligned;
这样定义确保了甚至在16位边界(如MSP430)也能对齐,这是因为一些平台(如MSP430)访问没对齐Rime缓冲区的16位数据可能会有问题。
(1)PACKETBUF_SIZE
用户可以通过PACKETBUF_CONF_SIZ
E自行设置数据包缓冲区大小PACKETBUF_SIZE
,PACKETBUF_SIZE
默认大小是128字节,源代码如下:
#ifdef PACKETBUF_CONF_SIZE
#define PACKETBUF_SIZE PACKETBUF_CONF_SIZE
#else
#define PACKETBUF_SIZE 128
#endif
(2)PACKETBUF_HDR_SIZE
用户可以通过PACKETBUF_CONF_HDR_SIZE
自行设置数据包缓冲区头部大小PACKETBUF_HDR_SIZE
,PACKETBUF_HDR_SIZE
默认大小是48字节,源代码如下:
#ifdef PACKETBUF_CONF_HDR_SIZE
#define PACKETBUF_HDR_SIZE PACKETBUF_CONF_HDR_SIZE
#else
#define PACKETBUF_HDR_SIZE 48
#endif
1.2 变量buflen、bufptr、hdrptr、packetbufptr
(结合图2理解!)hdrptr
为头部指针,初始化为PACKETBUF_HDR_SIZE
;packetbufptr
是整型指针,指向Rime缓冲区数据部分起始外;bufptr
初始值为0,即Rime缓冲区数据部分起始处,随着数据增加,bufptr
往后移;buflen
指Rime缓冲区使用的空间长度。这些变量的定义源代码如下:
static uint16_t buflen, bufptr;
static uint8_t hdrptr;
static uint8_t *packetbufptr;
1.3 数组packetbuf_attrs、packetbuf_addrs
(1)packetbuf_attrs
为了兼容其他协议,Rime不定义任何头部格式,而是将所有头部字段抽象为类型type
和长度len
,这恰是结构体packetbuf_attrlist
两个成员变量。详情见博文《单跳单播头部》。
数据包缓冲属性类型(packet buffer attribute type)是用枚举类型变量组织的,类型的值从0开始以1递增(因为枚举元素没有赋值)。很容易想到的是,数组的下标值等同于类型值,只需保存长度即可,结构体packetbuf_attr
正是这样的,源代码如下:
struct packetbuf_attr packetbuf_attrs[PACKETBUF_NUM_ATTRS];
typedef uint16_t packetbuf_attr_t;
struct packetbuf_attr
{
/* uint8_t type; */
packetbuf_attr_t val;
};
宏PACKETBUF_NUM_ATTRS
由其他宏定义,其值实际上就是系统定义的数据包缓冲属性类型数目,每个类型定义对应于一个值,相关定义源码如下:
#define PACKETBUF_NUM_ATTRS (PACKETBUF_ATTR_MAX - PACKETBUF_NUM_ADDRS)
#define PACKETBUF_NUM_ADDRS 4
PACKETBUF_ATTR_MAX
是数据包缓冲属性类型枚举变量的最后一个枚举元素,其值为28(详情见博文《单跳单播头部》),所以PACKETBUF_NUM_ATTRS
值为24。
(2)packetbuf_addrs
将数据包缓冲属性类型枚举变量关于地址的四个枚举变量单独列出来,理由很简单,缘于数据类型不同。相关源代码如下:
/*四个与Rim地址相关的枚举元素*/
PACKETBUF_ADDR_SENDER,
PACKETBUF_ADDR_RECEIVER,
PACKETBUF_ADDR_ESENDER,
PACKETBUF_ADDR_ERECEIVER,
struct packetbuf_addr packetbuf_addrs[PACKETBUF_NUM_ADDRS];
struct packetbuf_addr
{
/* uint8_t type; */
rimeaddr_t addr;
};
(3) packetbuf_attr_clear
函数packetbuf_attr_clear
初始化数组packetbuf_attrs
和packetbuf_addrs
,源代码如下:
void packetbuf_attr_clear(void)
{
int i;
for(i = 0; i < PACKETBUF_NUM_ATTRS; ++i)
{
packetbuf_attrs[i].val = 0;
}
for(i = 0; i < PACKETBUF_NUM_ADDRS; ++i)
{
rimeaddr_copy(&packetbuf_addrs[i].addr, &rimeaddr_null);
}
}
2. 相关函数
Rime缓冲区管理的函数API如下,截取自,该链接内容是以一种更直观的形式将源代码的注释显示出来,函数API如下:
图2 Rime缓冲区管理函数API
PS:有的函数代码很简单(比如packetbuf_datalen
,简单返回变量buflen
),但实际含义得根据数据包类型判断(很费解这种编码风格,二义性),所以还是整理下。
2.1 关于长度的计算
Rime缓冲区管理给了3个函数计算长度,分别是:头部长度packetbuf_hdrlen
、数据长度packetbuf_datalen
、总长度packetbuf_totllen
。还有设置数据区长度packetbuf_set_datalen
函数。
2.1.1 packetbuf_datalen
尽管发出和接收的数据包都放在同一样缓冲区,但他们的存放格式是不一样的。对于发出的数据包(outbound packets),头部放在packetbuf
的头部,数据放在packetbuf
的数据;然而,对于接收的数据包(incoming packets),头部和数据都放在packetbuf
的数据区域里。
基于上述分析,对于发出的数据包packetbuf_datalen
返回的是数据区域的长度,而对于接收的数据包packetbuf_datalen
返回的是整个数据包的长度(包括头部和数据)。packetbuf_datalen
源代码如下:
uint16_t packetbuf_datalen(void)
{
return buflen;
}
2.1.2 packetbuf_hdrlen
因为发出和接收的数据包在Rime缓冲区存放方式不同(对于接收的数据包,将头部和数据都放入packetbuf的数据区里),所以packetbuf_hdrlen
对于接收的数据包是没有意义的。对于发出的数据包,packetbuf_hdrlen
返回已使用的头部大小,源代码如下(结合图2看):
uint8_t packetbuf_hdrlen(void)
{
return PACKETBUF_HDR_SIZE - hdrptr;
}
2.1.3 packetbuf_totlen
packetbuf_totlen
返回整个已使用的缓冲区大小,源代码如下:
2.1.4 packetbuf_set_datalen
packetbuf_set_datalen
设置Rime缓冲区中数据部分的大小(Rime缓冲区存放头部和数据),packetbuf_set_datalen
源代码如下:
uint16_t packetbuf_totlen(void)
{
return packetbuf_hdrlen() + packetbuf_datalen();
}
2.2 关于头部操作
2.2.1 packetbuf_hdralloc
当发送数据包时,Rime缓冲区会从头部区域分配空间给他。packetbuf_hdralloc
从头部空间分配size
字节(即hdrptr
往前移),成功返回1,否则返回0,源代码如下:
int packetbuf_hdralloc(int size)
{
if(hdrptr >= size && packetbuf_totlen() + size <= PACKETBUF_SIZE)
{
hdrptr -= size;
return 1;
}
return 0;
}
2.2.2 packetbuf_hdr_remove
注意到hdrptr
初始值是PACKETBUF_HDR_SIZE
,也就是说使用头部空间,是从后向前使用的(想想包的封装过程,可以看成是模拟堆栈结构)。所以,从头部空间移除size
个字节函数packetbuf_hdr_remove
,只要将hdrptr
往后移size
字节即可(即退回来),源代码如下:
void packetbuf_hdr_remove(int size)
{
hdrptr += size;
}
2.2.3 packetbuf_hdrreduce
`packetbuf_hdrreduce
对于接收的数据包才有意义,当处理接收到数据包(processing incoming packets)时,注意到接收的数据包头部和数据都放在packetbuf
的数据区域,移除packetbuf
头部区域的第一部分,即移植数据区的第一部分(想想拆包过程,比如去掉第一个头部),这样buflen
显然减少了size
字节,但bufptr
却增加size
字节,我实在没想明白,acketbuf_hdrreduce
源代码如下:
int packetbuf_hdrreduce(int size)
{
if(buflen < size)
{
return 0;
}
bufptr += size;
buflen -= size;
return 1;
}
2.2.4 packetbuf_clear_hdr
packetbuf_clear_hdr
将头部空间清空重置,发送数据包前后使用,为了后续的重发,源代码如下:
/*It is used before after sending a packet in the packetbuf, to be able to reuse the packet buffer fora later retransmission.*/
void packetbuf_clear_hdr(void)
{
hdrptr = PACKETBUF_HDR_SIZE;
}
2.3 返回相关指针
2.3.1 packetbuf_dataptr
尽管发出和接收的数据包都放在同一样缓冲区,但他们的存放格式是不一样的。对于发出的数据包(outbound packets),头部放在packetbuf
的头部,数据放在packetbuf
的数据;然而,对于接收的数据包(incoming packets),头部和数据都放在packetbuf
的数据区域里。
可见,packetbuf_dataptr
函数功能分两种情况。对于发出的数据包packetbuf_dataptr
返回的是其数据区的指针,而对于接收的数据包packetbuf_dataptr
返回的是其头部指针(结合图1理解),函数packetbuf_dataptr
源代码如下:
void *packetbuf_dataptr(void)
{
return (void*)(&packetbuf[bufptr + PACKETBUF_HDR_SIZE]);
}
2.3.2 packetbuf_hdrptr
packetbuf_hdrptr
用于返回发送数据包时Rime缓冲区的头部指针(若要返回接收数据包的头部指针,则使用packetbuf_dataptr
函数,见上一小节),源代码如下:
void *packetbuf_hdrptr(void)
{
return (void*)(&packetbuf[hdrptr]);
}
2.4 初始化函数
2.4.1 packetbuf_clear
packetbuf_clear
用于清空和重置Rime缓冲区packetbuf
所有内部状态变量,源代码如下:
/*It is used before preparing a packet in the packetbuf.*/
void packetbuf_clear(void)
{
buflen = bufptr = 0;
hdrptr = PACKETBUF_HDR_SIZE;
packetbufptr = &packetbuf[PACKETBUF_HDR_SIZE];
packetbuf_attr_clear();
}
其他的就是设置变量buflen
、bufptr
、hdrptr
、packetbufptr
为起始状态,如下图:
图4 Rime缓冲区各变量初始状态示意图
函数packetbuf_attr_clear
初始化数组packetbuf_attrs
和packetbuf_addrs
,详情见本文的1.3。
2.5 关联到外部数据
对于发出的数据包,packetbuf
数据部分可以将packetbufptr
指向其他地方,而不使用原来的数据区。这样做的目的大概是为了适应数据区大小,即可以定义一个比原来数据区更长或者更短的数组,再关联到packetbuf
数据区。函数packetbuf_reference
关联数据区、packetbuf_is_reference
判断是有external data与数据区关联、packetbuf_reference_ptr
获取数据区指针。源代码如下:
void packetbuf_reference(void *ptr, uint16_t len)
{
packetbuf_clear();
packetbufptr = ptr;
buflen = len;
}
/*---------------------------------------------*/
int packetbuf_is_reference(void)
{
return packetbufptr != &packetbuf[PACKETBUF_HDR_SIZE];
}
/*--------------------------------------------*/
void *packetbuf_reference_ptr(void)
{
return packetbufptr;
}
3. 为什么发出和接收的数据存放方式不同
现在回到第一部分给出的问题,为什么发出和接收的数据存放方式不同。问题可以转化为,如果发出和接收的数据包存放方式相同会怎样。如下图,对于发送的数据,需要增加头部(由顶向下);对于收到的数据,则需要拆包(自底向上)。注意到Rime缓冲区使用头部区域是从后往前使用的,数据区域是由前往后使用的(见前面讨论)。那么,如果接收的数据包存放方式与发送数据包一样(下图的上部分),读取报文头部将会很麻烦,相反,把收到数据包头部和数据一起放入packetbuf的数据区域,读取头部将变得很简单了(下图的下部分)。
注:该部分的内容,Contiki相关论文和源码并没有体现,只是个人理解,如果你有其他看法,期待与您交流!
图5 数据包封装与拆包示意图
参考资料:
[1] Adam Dunkels,Fredrik Osterlind,Zhitao He. An Adaptive Communication Architecture for Wireless Sensor Networks[J]
[2] Rime缓冲区管理.vsd