libevent 源码分析:数据缓冲结构 evbuffer
Table of Contents
1 前言
网络程序的开发少不了缓冲区,用来保存即将发送出去的数据,以及已经收到但尚未处理或解码的数据。实现方法大同小异,libevent 的缓冲区实现也跟其它实现有很多共通之处,了解它的实现,可以达到举一反三的目的,对今后根据业务需要实现自己的缓冲区也有一些借鉴作用。
2 evbuffer 数据结构总览
libevent 的缓冲区结构叫 evbuffer,下图是 evbuffer 的大致结构。这里取出了一些细节,只保留结构轮廓。
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 结构
这个结构有很多干扰单纯的缓存结构分析的成员变量,起始它是个链表头,就可以满足缓存结构的要求,其它成员都是附加的功能,跟缓存结构没太大关系。