目录
以下源码均基于libevent-2.0.21-stable。
evbuffer结构体
在Libevent缓冲管理框架一文中有说到,对于每一个fd,都相应有读和写两个缓冲区,而缓冲区管理则是由evbuffer这一结构实现的。来看看evbuffer这一结构体:
struct evbuffer {
struct evbuffer_chain *first; //指向第一个缓冲区结点
struct evbuffer_chain *last; //指向最后一个缓冲区结点
struct evbuffer_chain **last_with_datap; //指向最后一个有数据的缓冲区结点
size_t total_len; //缓冲区中的数据量
size_t n_add_for_cb; //当次缓冲区发生改变对应的数据增加量
size_t n_del_for_cb; //当次缓冲区发生改变对应的数据减少量
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
/** A lock used to mediate access to this buffer. */
void *lock; //锁变量
#endif
unsigned own_lock : 1;//设置1则在释放evbuffer之前解锁
unsigned freeze_start : 1;//设置1则禁止从缓冲区头部添加或删除数据
unsigned freeze_end : 1;//设置1则禁止从缓冲区尾部添加或删除数据
unsigned deferred_cbs : 1; //使用延迟回调标志:设为1则开启延迟回调,初始化为0,由函数evbuffer_defer_callbacks设置
ev_uint32_t flags; //缓冲区的标志,目前版本仅支持一个默认标志EVBUFFER_FLAG_DRAINS_TO_FD
struct deferred_cb_queue *cb_queue; //开启延迟回调后,cb_queue指向event_base的defer_queue
int refcnt; //缓冲区的引用计数
struct deferred_cb deferred; //作为缓冲区回调函数“代表”添加到主循环中,主循环处理它时就会调用缓冲区回调队列中的所有回调函数
TAILQ_HEAD(evbuffer_cb_queue, evbuffer_cb_entry) callbacks; //回调函数队列
struct bufferevent *parent; //evbuffer属于哪个bufferevent
};
从evbuffer的结构可以看到,它其中最主要的是前面三个指针:两个evbuffer_chain一级指针以及一个evbuffer_chain二级指针。实际上,evbuffer是用来描述整个缓冲区的,缓冲区是由一个链表构成,链表的每一个结点都是evbuffer_chain类型,而缓冲区的所有数据也是存放在这些evbuffer_chain中的。而这三个指针则分别指向了链表中的头结点、尾结点以及最后一个含数据的结点。下面就来看一下evbuffer_chain的结构。
evbuffer_chain结构
struct evbuffer_chain {//每一个evbuffer_chain都有指向下一个evbuffer_chain的指针,并且包含一个字符串
/** points to next buffer in the chain */
struct evbuffer_chain *next;//下一个evbuffer_chain地址
/** total allocation available in the buffer field. */
size_t buffer_len;//buffer的容量
/** unused space at the beginning of buffer or an offset into a
* file for sendfile buffers. */
ev_off_t misalign;//buffer前面有多少不可使用的字节
/** Offset into buffer + misalign at which to start writing.
* In other words, the total number of bytes actually stored
* in buffer. */
size_t off; //off表示当前buffer中有多少字节的数据,off+misalign表示下一次buffer中写入的位置
/** Set if special handling is required for this chain */
unsigned flags;
#define EVBUFFER_MMAP 0x0001 /**< memory in buffer is mmaped */
#define EVBUFFER_SENDFILE 0x0002 /**< a chain used for sendfile */
#define EVBUFFER_REFERENCE 0x0004 /**< a chain with a mem reference */
#define EVBUFFER_IMMUTABLE 0x0008 /**< read-only chain */
/** a chain that mustn't be reallocated or freed, or have its contents
* memmoved, until the chain is un-pinned. */
#define EVBUFFER_MEM_PINNED_R 0x0010
#define EVBUFFER_MEM_PINNED_W 0x0020
#define EVBUFFER_MEM_PINNED_ANY (EVBUFFER_MEM_PINNED_R|EVBUFFER_MEM_PINNED_W)
/** a chain that should be freed, but can't be freed until it is
* un-pinned. */
#define EVBUFFER_DANGLING 0x0040
/** Usually points to the read-write memory belonging to this
* buffer allocated as part of the evbuffer_chain allocation.
* For mmap, this can be a read-only buffer and
* EVBUFFER_IMMUTABLE will be set in flags. For sendfile, it
* may point to NULL.
*/
unsigned char *buffer;//存放数据的首地址
};
对于每个chain,也有相应的flags设置,这个flags用来描述这个chain中的数据性质。Libevent提供了以下8种flag,后面4种主要用于windows iocp中,这里就不说了。现在来说一下前面四种flag的作用:
EVBUFFER_MMAP和EVBUFFER_SENDFILE用于把文件传输给socket,EVBUFFER_MMAP表示chain中的数据是由文件内存映射而来,chain中包含了文件的相关信息以及文件映射地址;EVBUFFER_SENDFILE表示文件到socket的传输是通过sendfile进行的,chain中包含了文件的相关信息但没有数据。
EVBUFFER_REFERENCE :表示当前chain中的数据内存引用自其它数据,这样就无需拷贝;
EVBUFFER_IMMUTABLE:表示当前chain中的数据是只读的。
关于这里提到的文件向socket的传输,会在后面进行详细的分析。
evbuffer_chain的结构中还有三个非常重要的成员:buffer_len、misalign和off。这三个成员都是用来描述chain中存放数据的buffer成员的。
buffer_len是这个chain的容量;
misalign是这个chain的整体数据偏移量,也可以理解为chain的buffer头部有misalign个字节的空间不可用;
off则是描述chain的buffer现有的数据量。
由于chain中存放数据的buffer是char *类型的,因此知道每次写入的位置是非常重要的,一般来说,写入的位置应当是buffer+misalign+off。由此也可得到chain的buffer的结构,如下所示:
关于前面misalign个字节的作用,它们既可以用来在进行头插数据时,用来存放头插的数据,也可以在删除chain中数据时,如果只需要删除其中一部分数据,一个比较好的方法就是直接把需要删除的数据放到这misalign个字节中,这样就从概念上进行了删除而避免删除一部分数据时的效率降低,也还有其它很多作用,因此这misalign个字节的空间可以理解为“具有特殊作用”的区域。
evbuffer及evbuffer_chain的创建
evbuffer的创建,是通过evbuffer_new函数进行的,这是一个向用户开放的接口,其定义如下:
struct evbuffer *
evbuffer_new(void)//创建一个evbuffer
{
struct evbuffer *buffer;
buffer = mm_calloc(1, sizeof(struct evbuffer));//用的是calloc,因此first和last存储的都是0,两个指针都是NULL
if (buffer == NULL)
return (NULL);
TAILQ_INIT(&buffer->callbacks);
buffer->refcnt = 1; //初始化evbuffer的引用计数为1
buffer->last_with_datap = &buffer->first;//初始化时指向first指针
return (buffer);
}
接着再来看一下evbuffer_chain的创建,它是通过evbuffer_chain_new进行的,不过该函数并不对用户开放,但是了解它有助于后面进行其他的分析,该函数定义如下:
#define EVBUFFER_CHAIN_EXTRA(t, c) (t *)((struct evbuffer_chain *)(c) + 1)
//分配一个evbuffer_chain,并且包含一个额外大小为size,分配的size大小的额外部分与evbuffer_chain本体是连续的
static struct evbuffer_chain *
evbuffer_chain_new(size_t size)
{
struct evbuffer_chain *chain;
size_t to_alloc;
size += EVBUFFER_CHAIN_SIZE;//宏定义为evbuffer_chain的size,因此size就是evbuffer_chain本身大小加上需要分配的额外空间大小的总大小
/* get the next largest memory that can hold the buffer */
to_alloc = MIN_BUFFER_SIZE;//在32位下最小分配大小为512字节
while (to_alloc < size)
to_alloc <<= 1; //加倍保证大于size,
/* we get everything in one chunk */
if ((chain = mm_malloc(to_alloc)) == NULL)//to_alloc是evbuffer_chain加上额外空间,最终需要分配的总的大小
return (NULL);
memset(chain, 0, EVBUFFER_CHAIN_SIZE);//初始化evbuffer_chain本体部分为0
chain->buffer_len = to_alloc - EVBUFFER_CHAIN_SIZE; //buffer_len就是额外分配的空间的大小
/* this way we can manipulate the buffer to different addresses,
* which is required for mmap for example.
*/
chain->buffer = EVBUFFER_CHAIN_EXTRA(u_char, chain);//buffer指向额外分配的空间的首地址
return (chain);
}
该函数需要传入一个参数size,从程序中可以看到,在创建一个evbuffer_chain的时候,除了分配evbuffer_chain本身的大小之外,还会额外分配一个空间,这个额外分配的空间大小不会小于size,并且在chain分配之后,会用chain中的buffer_len变量去记录这个额外分配空间的大小,用buffer变量去记录这个额外分配空间的首地址。并且可以看到,额外分配的空间与evbuffer_chain本身是一起分配的,也就是说,额外分配的那一部分和evbuffer_chain本体部分是连续的。
chain是缓冲区链表中存放数据的结点,这个额外分配的空间通常就用来存放缓冲区数据,但它也可以存放其它信息,这一点在后面文件向socket的传输实现中体现。
再来说一下这里用到的一个宏:EVBUFFER_CHAIN_EXTRA。从名字上来看应该大致猜到这个宏与chain的额外空间相关。它需要有两个参数,第二个参数c传入的是一个地址,它会将这个地址强制转换为evbuffer_chain类型,然后加1就相当于在这个地址上偏移了sizeof(evbuffer_chain),前面说了,evbuffer_chain分配的额外空间实际上和它本身是连续的,也就是说,偏移后的地址实际上就是额外空间的首地址了。然后这个宏传入的第一个参数是一个类型参数,它将额外空间的首地址进行强制类型转换。比如说这里是强制类型转换为u_char类型,那么就相当于把额外空间的地址保存到了buffer变量中。
分析到这里,那么evbuffer和evbuffer_chain的结构到底是什么关系呢?这一点就可以看看向evbuffer中插入chain的过程。
向evbuffer中插入一个evbuffer_chain
由evbuffer_chain_insert函数实现,这是一个非开放接口,该函数定义如下:
static void
evbuffer_chain_insert(struct evbuffer *buf,
struct evbuffer_chain *chain)
{
ASSERT_EVBUFFER_LOCKED(buf);//前提需要持有锁
if (*buf->last_with_datap == NULL) {//由于缓冲区为空时last_with_datap是指向first的,如果它为空,说明此时缓冲区中一个chain都没有
/* There are no chains data on the buffer at all. */
EVUTIL_ASSERT(buf->last_with_datap == &buf->first);
EVUTIL_ASSERT(buf->first == NULL);
buf->first = buf->last = chain;//first和last都指向chain
} else {
struct evbuffer_chain **ch = buf->last_with_datap;//从last_with_datap的指向开始往后遍历
/* Find the first victim chain. It might be *last_with_datap */
while ((*ch) && ((*ch)->off != 0 || CHAIN_PINNED(*ch)))//找到第一个没有数据的chain
ch = &(*ch)->next;
if (*ch == NULL) {//到这里说明整个缓冲区都遍历了,所有chain都是有数据的
/* There is no victim; just append this new chain. */
buf->last->next = chain;//将当前last指向的最后一个chain与新的chain连接起来
if (chain->off)//off非0表示chain中的buffer有数据,那么last_with_datap就指向新的chain
buf->last_with_datap = &buf->last->next;//last_with_datap指向last的next
} else {//到这里说明遍历到了一个空的chain,就用新的chain来替换这个空的chain及其后面所有的chain
/* Replace all victim chains with this chain. */
EVUTIL_ASSERT(evbuffer_chains_all_empty(*ch));//确保从这个空的chain后面的所有chain都是空的
evbuffer_free_all_chains(*ch);//释放这个空的chain以及后面的空chain,不会改变ch的值
*ch = chain;//ch依然是刚才的空chain的地址,赋值为新chain。
}
buf->last = chain;//last指向新的chain
}
buf->total_len += chain->off;//evbuffer中的总字节数
}
将chain插入evbuffer的过程,首先判断evbuffer是否为空,如果为空那么新插入的chain就是唯一的,直接让evbuffer的first和last指向新的chain即可,由于last_with_datap初始化是指向first的,因此,这里也不需要改变last_with_datap;
如果evbuffer非空,那么就遍历整个evbuffer的chain链表,去寻找第一个没有数据的chain。如果遍历完整个链表都没有发现没有数据的chain,那么就让last的next指针指向新的chain,这就相当于把新的chain接入到了链表中,如果新的chain中有数据,那么last_with_datap就会指向last的next指针,而此时next指针保存的是新的chain的地址;如果在遍历过程中发现了一个空的chain,在正常情况下,这个chain包括它后面的所有chain就都应该是空的了,所以就直接把它及其后面的所有chain都释放,然后用新的chain进行替换。
最后再让last指向新插入的chain即可。
从这个过程中可以看到,evbuffer的last_with_datap指针,在缓冲区中存在数据的时候它是指向最后一个有数据的chain的前一个chain的next指针的, 也就是说*last_with_datap才是最后一个有数据的chain的地址,**last_with_datap才是最后一个有数据的chain。并且还可以看到,在正常情况下,evbuffer中不会存在大片的空chain,这是因为在每次插入chain的时候,都会对链表的末尾的空结点进行检查释放,然后再添加新的结点,因此当且仅当插入的chain本身就是一个空结点时evbuffer对应的链表中才会存在一个空结点。
至此,就不难得出evbuffer和evbuffer_chain之间的关系了,如图所示:
向evbuffer中添加数据
libevent提供了两种向缓冲区中添加数据的方法:头插和尾插。由于libevent中的缓冲区都是采取队列形式,即缓冲区中的数据都是先进先出,尾进头出的形式,因此ev