libevent 源码分析:数据缓冲结构 evbuffer

277 阅读5分钟

原文

网络程序的开发少不了缓冲区,用来保存即将发送出去的数据,以及已经收到但尚未处理或 解码的数据。实现方法大同小异,libevent 的缓冲区实现也跟其它实现有很多共通之处, 了解它的实现,可以达到举一反三的目的,对今后根据业务需要实现自己的缓冲区也有一些 借鉴作用。

2 evbuffer 数据结构总览

libevent 的缓冲区结构叫 evbuffer,下图是 evbuffer 的大致结构。这里取出了一些细 节,只保留结构轮廓。

evbuffer.png

evbuffer 并不像 std::vector 一样是一块连续的内存空间,而是由一个个容纳数据的内 存块通过链表组织起来的。链表每个节点存储实际的数据,结构其其它信息记录了数据的 长度,节点的容量等信息。这里 off 这个变量名比较有意思,它表示的是这个内存块实 际已经保存的数据长度。

3 引用计数

evbuffer 比较重要的设计目标是尽可能减少数据拷贝,这就经常会出现多个对象引用同 一个 evbuffer_chain 的情况,这使得释放释放一个 evbuffer_chain 时的逻辑变得困难。 evbuffer 的解决办法是使用引用计数,只要内部有一个结构拥有指针指向它,它的引用 计数就加1,指向它的对象被释放,或者不再引用它时,引用计数减1,当引用计数为0时, 释放内存。

释放内存还要满足一个条件,就是标志位 flags 不满足 EVBUFFER_MEM_PINNED_ANY。在 别的地方没见过这么无理的要求,我觉得只用引用计数就可以满足需要的功能,不必依赖 其它标志,然而 evbuffer 不是这么做的。虽然但是,不是问题,没发现bug,效率也很 高。

4 一次申请内存

buffer 的实现少不了内存的频繁申请和释放。为了减少内存申请/释放次数,大多数实现 都会一次申请一整块内存,把缓存头部和保存数据的数组都放到这一块缓冲区里。例如, 要实现一个带长度的字符串结构,可以如下定义。

struct LenStr {
  int len;
  int cap;
  char buf[0];
};

要申请一块可以容纳长度为 n 的字符串结构,则

LenStr *str = malloc(sizeof(LenStr) + n);
str->cap = n;
str->len = 0;

这里的 buf[0] 是语言的一个小技巧,必须放在结构体的最后,申请完内存, buf[n] 就 是第 n 个数据。C语言初学者看到这个 buf[0] 可能也觉得奇怪,这相当于俚语了, C11 默认就不能这么写。

evbuffer 没有用 buf[0] 这个俚语,但基本思想差不多,它申请一整块内存,内存前部 保存长度,容量,引用计数,数据指针等信息,内存后部用来保存实际的数据。每次申请内 存后,都要计算保存数据的实际起始内存地址,将数据指针指向它。

5 evbuffer_chain 结构

evbuffer_chain 是缓存链表的一个节点。缓存做成链表结构的作用很明显,是为了尽 可能减少数据拷贝,同时节省内存空间。实际使用 evbuffer 的时候,不可能一开 始就知道数据有多长,数据也是不定期不定量产生和消费的。使用中,比较方便的想法是 每次产生多少数据,就多申请多少内存去容纳这些数据。于是就有了链表拼接内存的结构。

注意并不是所有的结构都适合这么做, std::vector 就不是这么干的。 std::vector 在标准里规定了是个连续的内存空间,不让用链表拼接结构实现。它的做 法是申请就有初始内存大小,添加数据填满了就申请一块新的比需要的内存更大的内存空 间,再把原 内存的内容拷贝过去。这个新的足够大的内存空间标准没有定义,GNU 的stl是每次翻倍, 微软的stl是每次内存变成 1.5 倍。有些链表拼接的缓冲区为了尽可能节省内存申请次数, 也是使用类似的每次内存增长一定比例的做法。 evbuffer 也借鉴了类似的做法,申请内 存的时候,内存块大小通常是2的指数,这样每次申请的内存会超过本次添加的数据长度, 下次再添加数据,很可能就不用再申请一个新的 evbuffer_chain 了。

evbuffer_chain 的结构如下:

struct evbuffer_chain {
        struct evbuffer_chain *next;
        size_t buffer_len;
        ev_misalign_t misalign;
        size_t off;
        unsigned flags;
        int refcnt;
        unsigned char *buffer;
};
  • next 指向链表中下一个 evbuffer_chain
  • buffer_len 是这个节点的数据容量。
  • misalign 表示buffer中保存的数据的起始位置。从一个链表节点清除一定量的数据之后, misalign 的值将增加,当 misalign 的值与容量相等时,这个节点可以被删除。
  • off 表示当前保存的数据字节长度。
  • flag 是位标志,表示这个内存块是不是来自文件快,是不是用于 sendfile,buffer 是 与结构同时申请的,还是引用的外部内存空间,是不是不能释放,是不是多个实例引用这 个块的内存空间。
  • refcnt 是引用计数。
  • buffer 指向容纳数据的起始位置。(buffer+misalign) 指向实际保存的第一个数据。

6 evbuffer 结构

这个结构有很多干扰单纯的缓存结构分析的成员变量,起始它是个链表头,就可以满足缓存 结构的要求,其它成员都是附加的功能,跟缓存结构没太大关系。