[Rime 协议栈] 缓冲管理

概述

  关于Rime的缓冲区管理这一块,能在网上搜到很多博客,但是我想说的是,99%+都是过时的,坑爹啊!Contiki的开发非常活跃,所以对代码的改进很多,而Rime的缓冲区管理这也在今年二月份进行了优化,由之前难以理解的、晦涩的“双头栈”改为了现在通俗易懂的结构。双头栈有多晦涩,你将contiki的代码reset到今年二月份之前去看看就知道了。
  不过不要马虎,虽然现在的缓冲管理变简单了,但是它很重要!很重要!!很重要!!!
  为啥说它很重要呢?

  • 其一,Rime整个协议栈里面的子协议,都得使用它。(至于uIP协议会不会使用它,我还没接触过,不知道呢)
  • 其二,底层协议,比如MAC,RDC,FRAMER,LLSEC都得使用它。
  • 其三,甚至某些应用程序也会用到它。还记得博客[Contiki协议栈Rime:引子》中的匿名广播的例程吗?里面有packetbuf_copyfrom("Hello", 6);这句话,它就使用到了packetbuf。

缓冲区的作用很简单,将发出和收到的数据包(包括数据和包属性)都存储在一个单一的缓冲区packetbuf,由头部和数据两部分组成。
相关代码位于contiki/core/net/packetbuf.[ch]

相关变量

packetbuf

packbuf就是Rime的缓冲区,它是一个指向数组的指针,其定义为:

1
2
3
/* 以下声明确保包缓冲区可以对齐32bit边界,在一些平台(最常见的如msp430或OpenRISC),在访问字节时,有可能会出现非对齐包缓冲而导致问题的发生。 */
static uint32_t packetbuf_aligned[(PACKETBUF_SIZE + 3) / 4];
static uint8_t *packetbuf = (uint8_t *)packetbuf_aligned;

其中,PACKETBUF_SIZE被定义为:

1
2
3
4
5
#ifdef PACKETBUF_CONF_SIZE
#define PACKETBUF_SIZE PACKETBUF_CONF_SIZE
#else
#define PACKETBUF_SIZE 128 // 缓冲区默认长度为128个字节
#endif

所以默认情况下,Rime的buffer是大小为128字节的连续的内存空间。

需要说明的是,这里的PACKETBUF_SIZE是指整个缓冲的长度,包括头部和数据。在以前的机制中,这个PACKETBUF_SIZE只包括数据部分,头部还另外占据PACKETBUF_HDR_SIZE个字节的缓冲。

buflen hdrlen bufptr

这三个变量的定义如下:

1
2
static uint16_t buflen, bufptr;
static uint8_t hdrlen;

其中,
buflen:已使用的数据部分的长度
hdrlen:已使用的头部部分的长度
bufptr:这不是指针,而是一个整型变量。它相当于一个索引,指向缓冲的某个地址。这个变量在今后解析buffer时非常有用。

相关函数

packetbuf_clear

1
2
3
4
5
6
7
void packetbuf_clear(void)
{
buflen = bufptr = 0;
hdrlen = 0;

packetbuf_attr_clear();
}

该函数负责清空数据,包括packetbuf指向的buffer,以及两个属性数组里的内容。在将包压入包缓冲之前,会调用该函数。

packetbuf_copyfrom

1
2
3
4
5
6
7
8
9
10
int packetbuf_copyfrom(const void *from, uint16_t len)
{
uint16_t l;

packetbuf_clear(); // 先清空属性数组和packetbuf
l = MIN(PACKETBUF_SIZE, len); // 如果len大于PACKETBUF_SIZE,则截断
memcpy(packetbuf, from, l);
buflen = l;
return l;
}

很容易理解:先清空属性数组和packetbuf,然后从from中拷贝len个长度的数据到packetbuf中。如果需要拷贝的长度但对于定义的buffer长度,则进行截断处理,只拷贝PACKETBUF_SIZE个字节。该函数返回所拷贝的数据的长度。

packetbuf_compact

1
2
3
4
5
6
7
8
9
10
11
12
void packetbuf_compact(void)
{
int16_t i;

if(bufptr) {
/* 将数据部分向左移至头部后面 */
for(i = 0; i < buflen; i++) {
packetbuf[hdrlen + i] = packetbuf[packetbuf_hdrlen() + i];
}
bufptr = 0;
}
}

该函数通过拷贝packetbuf的数据部分,使其紧紧跟随头部。头部和数据之间可能有若干个字节是隔开的,但是为啥会隔开呢?看后面的函数packetbuf_hdrreduce()和函数packetbuf_dataptr()就会明白。Rime中的协议在将包发送给设备驱动之前,会调用该函数,以确保包在内存中是连续的。
为了更容易理解,直接上图:

![这里写图片描述](http://img.blog.csdn.net/20160523194942463)

packetbuf_copyto

1
2
3
4
5
6
7
8
9
10
11
int packetbuf_copyto(void *to)
{
if(hdrlen + buflen > PACKETBUF_SIZE) { // 怎么会发生这样的情况?
return 0;
}
// 由于数据部分和头部中间可能有间隔,所以分开拷贝这两个部分,
// 否则当中间真的存在间隔时,就会拷贝错误
memcpy(to, packetbuf_hdrptr(), hdrlen);
memcpy((uint8_t *)to + hdrlen, packetbuf_dataptr(), buflen);
return hdrlen + buflen;
}

拷贝一个完整的packbuf到一个外部buffer。

packetbuf_hdralloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
packetbuf_hdralloc(int size)
{
int16_t i;

if(size + packetbuf_totlen() > PACKETBUF_SIZE) {
return 0;
}

/* shift data to the right */
for(i = packetbuf_totlen() - 1; i >= 0; i--) {
packetbuf[i + size] = packetbuf[i];
}
hdrlen += size;
return 1;
}

分配size个字节的头部空间。我们后面会看到,当需要向packetbuf中写入新的包属性时,会先调用此函数预分配空间。上图:

![这里写图片描述](http://img.blog.csdn.net/20160523194957166)

packetbuf_hdrreduce

1
2
3
4
5
6
7
8
9
10
int packetbuf_hdrreduce(int size)
{
if(buflen < size) {
return 0;
}

bufptr += size;
buflen -= size;
return 1;
}

该函数主要用于解析packetbuf。调用函数packetbuf_dataptr()会返回一个指向数据部分的第一个字节的指针。如果先调用packetbuf_hdrreduce(size1),此时bufptr会增加size个字节,如果再调用函数packetbuf_dataptr(),此时返回的函数指针就比之前的函数指针后移了size个字节。解析packetbuf的流程就是不断地调用packetbuf_hdrreduce()和packetbuf_dataptr()。

packetbuf_set_datalen

1
2
3
4
5
6
void
packetbuf_set_datalen(uint16_t len)
{
PRINTF("packetbuf_set_len: len %d\n", len);
buflen = len;
}

该函数用于设置数据部分的长度

packetbuf_dataptr

1
2
3
4
5
void *
packetbuf_dataptr(void)
{
return packetbuf + packetbuf_hdrlen();
}

该函数返回指向数据部分第一个字节的指针。

packetbuf_hdrptr

1
2
3
4
5
void *
packetbuf_hdrptr(void)
{
return packetbuf;
}

返回头部指针,即这个buf的指针

packetbuf_datalen

1
2
3
4
5
uint8_t
packetbuf_hdrlen(void)
{
return bufptr + hdrlen;
}

返回数据部分长度

packetbuf_totlen

1
2
3
4
5
uint16_t
packetbuf_totlen(void)
{
return packetbuf_hdrlen() + packetbuf_datalen();
}

返回头部和数据部分总长度

packetbuf_holds_broadcast

1
2
3
4
5
int
packetbuf_holds_broadcast(void)
{
return linkaddr_cmp(&packetbuf_addrs[PACKETBUF_ADDR_RECEIVER - PACKETBUF_ADDR_FIRST].addr, &linkaddr_null);
}

通过比较属性数组中的PACKETBUF_ADDR_RECEIVER属性的值与linkaddr_null的值是否相等,判断packetbuf中是否包含广播地址。

关于这个函数的使用,在底层协议里有,暂时先不管,今后再补充

小结

在这篇博客中,简单介绍了缓冲区管理的各个函数,通过两张图片,应该更容易理解。但是函数太多了,我不可能相关的函数都画图,太浪费时间了。第一次阅读的话可能会觉得有些函数模棱两可,但是结合后面几篇博客,就能真正知道其具体应用了。