Genius 发表于 2018-7-17 14:29:31

杰杰带你解读【机智云】环形缓冲区源码

本文转载自:https://mp.weixin.qq.com/s/iBrIu6RyEx_s-MVVkv1RRQ

前言大家晚上好,我是杰杰,上个星期,研究了一下机智云的源码,也不能说是研究吧,就是看了看,人家既然能拿来做商业用,还是有很厉害的地方的,如果还不知道什么叫环形缓冲区(环形队列)的同学,请看——STM32进阶之串口环形缓冲区实现好啦。多余的话不多说,看看他们的东西比我写的好在哪吧,原理都是一样的,但是效率会比我的搞,可能应用的地方也不一样,所以,先看看吧。
ringbuffer.h先看看头文件:ringbuffer.h。主要是用宏实现了一个求最小值的函数。还有就是定义了一个环形缓冲区的结构体。#define min(a, b) (a)<(b)?(a):(b)                   ///< Calculate the minimum value

typedef struct {
    size_t rbCapacity;
    uint8_t*rbHead;
    uint8_t*rbTail;
    uint8_t*rbBuff;
}rb_t;

看英文就能知道意思了,rb是ringbuff的缩写,意思就是环形缓冲区,结构体中rbCapacity是缓冲区的容量,也就是大小。结构体中rbHead是缓冲区的头指针,rbTail是缓冲区的尾指针,而rBuff是缓冲区的首地址,在创建的时候就用到。ringbuffer.c环形缓冲区的创建下面来看看源文件:int8_t ICACHE_FLASH_ATTR rbCreate(rb_t* rb)
{
    if(NULL == rb)
    {
      return -1;
    }

    rb->rbHead = rb->rbBuff;
    rb->rbTail = rb->rbBuff;
    return 0;
}

这是个创建环形缓冲区的函数,就是初始化了环形缓冲区的头尾指针,这个函数的通用性很强,因为很多时候不只创建一个缓冲区。每个缓冲区的首地址都保存在了rbBuff,这个在后面的通用性会很好用。但是杰杰还是觉得不够好,因为我们在结构体中定义了缓冲区的容量,但是在这里并没有给他初始化,我觉得应该传入应该参数,给缓冲区的容量进行初始化一下。但是无所谓啦。环形缓冲区的删除int8_t ICACHE_FLASH_ATTR rbDelete(rb_t* rb)
{
    if(NULL == rb)
    {
      return -1;
    }

    rb->rbBuff = NULL;
    rb->rbHead = NULL;
    rb->rbTail = NULL;
    rb->rbCapacity = 0;
      return 0;
}
把这些指针指向NULL,但是环形缓冲区本身地址的数据是不会被清除的,只是表明了这些地址可以被重复使用了而已。int32_t ICACHE_FLASH_ATTR rbCapacity(rb_t *rb)
{
    if(NULL == rb)
    {
      return -1;
    }

    return rb->rbCapacity;
}
获取环形缓冲区的容量int32_t ICACHE_FLASH_ATTR rbCapacity(rb_t *rb)
{
    if(NULL == rb)
    {
      return -1;
    }

    return rb->rbCapacity;
}
因为可能有多个环形缓冲区,但是容量我们不一定会知道,所以还是写一个获取它容量的函数比较好。环形缓冲区可读数据大小int32_t ICACHE_FLASH_ATTR rbCanRead(rb_t *rb)
{
    if(NULL == rb)
    {
      return -1;
    }

    if (rb->rbHead == rb->rbTail)
    {
      return 0;
    }

    if (rb->rbHead < rb->rbTail)
    {
      return rb->rbTail - rb->rbHead;
    }

    return rbCapacity(rb) - (rb->rbHead - rb->rbTail);
}
如果缓冲区是没有被创建的,那么返回-1,表示非法,如果环形缓冲区的首尾都在一个位置,那么表面环形缓冲区没有数据,那么是不可读的,否则就返回正常的数据,rb->rbTail - rb->rbHead / rbCapacity(rb) - (rb->rbHead - rb->rbTail),请用数学的方法理解这段代码。获取环形缓冲区可写数据大小同理获取可写数据也是一样的int32_t ICACHE_FLASH_ATTR rbCanWrite(rb_t *rb)
{
    if(NULL == rb)
    {
      return -1;
    }

    return rbCapacity(rb) - rbCanRead(rb);
}
环形缓冲区读数据int32_t ICACHE_FLASH_ATTR rbRead(rb_t *rb, void *data, size_t count)
{
    int32_t copySz = 0;

    if(NULL == rb)
    {
      return -1;
    }

    if(NULL == data)
    {
      return -1;
    }

    if (rb->rbHead < rb->rbTail)
    {
      copySz = min(count, rbCanRead(rb));
      memcpy(data, rb->rbHead, copySz);
      rb->rbHead += copySz;
      return copySz;
    }
    else
    {
      if (count < rbCapacity(rb)-(rb->rbHead - rb->rbBuff))
      {
            copySz = count;
            memcpy(data, rb->rbHead, copySz);
            rb->rbHead += copySz;
            return copySz;
      }
      else
      {
            copySz = rbCapacity(rb) - (rb->rbHead - rb->rbBuff);
            memcpy(data, rb->rbHead, copySz);
            rb->rbHead = rb->rbBuff;
            copySz += rbRead(rb, (char*)data+copySz, count-copySz);
            return copySz;
      }
    }
}
如果是缓冲区没被创建或者是读数据地址非法(NULL)都将返回错误。如果rb->rbHead < rb->rbTail,就是可读数据的地址是递增的,那么可以直接读数据,读取的最大数据不能超过缓冲区可读最大数据,所以要用copySz = min(count, rbCanRead(rb));限制一下读取数据的大小,因为是直接拷贝数据,所以,在较多数据面前的话,这种做法很好,比如像网络上的数据,更是适合用这种方法。读完之后把rbHead 头指针重新更新,rb->rbHead += copySz;因为环形缓冲区在数据存储(软件地址上)是环形的,所以,假如数据地址不是递增的,那么无法直接拷贝,需要分段拷贝,count < rbCapacity(rb)-(rb->rbHead - rb->rbBuff)如果要读取的数据小于从环形缓冲区的首地址开始到环形缓冲区大小的地址,那么这段地址还是递增的,所以可以直接拷贝过去,并且把头指针更新一下。 copySz = count;

memcpy(data, rb->rbHead, copySz);

rb->rbHead += copySz;
最后一种情况就是,需要分段读取了,先把头指针到缓冲区最后一个地址的这部分读取了,再加上从缓冲区首地址开始读取count-copySz那么长数据的数据,就ok了。然后把两端数据拼接起来。数据保存在data中。 copySz = rbCapacity(rb) - (rb->rbHead - rb->rbBuff);
memcpy(data, rb->rbHead, copySz);
rb->rbHead = rb->rbBuff;
copySz += rbRead(rb, (char*)data+copySz, count-copySz);
环形缓冲区写数据int32_t ICACHE_FLASH_ATTR rbWrite(rb_t *rb, const void *data, size_t count)
{
    int32_t tailAvailSz = 0;

    if((NULL == rb)||(NULL == data))
    {
      return -1;
    }

    if (count >= rbCanWrite(rb))
    {
      return -2;
    }

    if (rb->rbHead <= rb->rbTail)
    {
      tailAvailSz = rbCapacity(rb) - (rb->rbTail - rb->rbBuff);
      if (count <= tailAvailSz)
      {
            memcpy(rb->rbTail, data, count);
            rb->rbTail += count;
            if (rb->rbTail == rb->rbBuff+rbCapacity(rb))
            {
                rb->rbTail = rb->rbBuff;
            }
            return count;
      }
      else
      {
            memcpy(rb->rbTail, data, tailAvailSz);
            rb->rbTail = rb->rbBuff;

            return tailAvailSz + rbWrite(rb, (char*)data+tailAvailSz, count-tailAvailSz);
      }
    }
    else
    {
      memcpy(rb->rbTail, data, count);
      rb->rbTail += count;
      return count;
    }
}
与读书同理的,将一定长度的数据从某段地(data)址写入环形缓冲区。如果数据地址非法或者是可写数据长度不够,那么就会返回错误代码。先看后面的    else
    {
      memcpy(rb->rbTail, data, count);
      rb->rbTail += count;
      return count;
    }
如果写数据的地址是地址的话,那么是可以直接写的,注意的是,写数据的地址并非读数据的地址,刚好相反的,可读数据的地址是绝对不允许写的。同理,假如写书的地址不是递增的话,那么,也是分成两段,假如写入数据的长度小于从尾指针到环形缓冲区最后一个地址的长度,那么,写入的这段数据其实其地址也是递增的,同样是可以直接写的,然后更新一下尾指针。 memcpy(rb->rbTail, data, count);
rb->rbTail += count;
if (rb->rbTail == rb->rbBuff+rbCapacity(rb))
{
    rb->rbTail = rb->rbBuff;
}
否则,也需要分段写入,先写入从尾指针到环形缓冲区最后一个地址的长度,然后从环形缓冲区的首地址开始再写入剩下的数据长度count-tailAvailSz, memcpy(rb->rbTail, data, tailAvailSz);
rb->rbTail = rb->rbBuff;
return tailAvailSz + rbWrite(rb, (char*)data+tailAvailSz, count-tailAvailSz);
好了,至此,源码基本分析完毕,现在说说为什么比我的源码写得好,
第一点,代码的效率,我写的源码是一个个数据的写入,而机智云是一系列数据的写入。读数据也是一样,一系列数据读出,而我的源码则是一个个数据读出,并且使用了求模的运算防止指针越界,这在运算中效率是不够高的。
第二代码的健壮性,还是机智云的好,我的代码是没有检查是否真正有有效的数据写入。同样的代码读出也是检查了读出数据的地址是否真正有效,防止数据非法丢失。总的来说,需要不断成长,还是要研究研究别人商业上用的源码,虽然说很多原理我们都知道,但是亲自写的话,不一定能写得出来,
还有就是,重用现有源码比创新的效率更高,因为并不是所有人都能另走捷径,做开拓者的,我们用已有的好东西足以。

END需要源码的同学可以在公众号回复“机智云源码”晚安!
页: [1]
查看完整版本: 杰杰带你解读【机智云】环形缓冲区源码