libevent源码学习(18):缓冲区结构evbuffer

目录

evbuffer结构体

evbuffer_chain结构

evbuffer及evbuffer_chain的创建

向evbuffer中插入一个evbuffer_chain

向evbuffer中添加数据

向evbuffer的尾部添加数据

在evbuffer的头部插入数据

从evbuffer中提取数据

拷贝式提取数据

提取后删除数据

evbuffer预留一定大小的空间

从fd读取数据到缓冲区中

把缓冲区中的数据写到fd中

缓冲区的回调函数处理


以下源码均基于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

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值