一、从event-test.c示例代码开始
前面剖析了同一个文件夹下的定时器事件和信号事件,然而这两个事件基本都和多路复用技术的关系不是太密切,定时器事件依赖于多路复用技术的超时选项的设计,而信号事件则依赖于人为的触发或者机器中断的产生,所以对于某些技术我们的研究还不够到位,对于事件处理逻辑走得还不够完全接下来就看一看对于普通事件,libevent的处理流程。
static void fifo_read(int fd, short event, void *arg){}
int main (int argc, char **argv){
struct event evfifo;
。。。
const char *fifo = "event.fifo";
int socket;
/*lstat不具有穿透性,若为符号链接直接处理之*/
if (lstat (fifo, &st) == 0) {}
/*校验fifo文件,若存在则删除*/
unlink (fifo);
if (mkfifo (fifo, 0600) == -1) {.../*创建fifo*/}
/* Linux pipes are broken, we need O_RDWR instead of O_RDONLY */
#ifdef __linux
socket = open (fifo, O_RDWR | O_NONBLOCK, 0);
#else
socket = open (fifo, O_RDONLY | O_NONBLOCK, 0);
#endif
...
/* Initalize the event library */
event_init();
/* Initalize one event */
event_set(&evfifo, socket, EV_READ, fifo_read, &evfifo);
/* Add it to the active events, without a timeout */
event_add(&evfifo, NULL);
event_dispatch();
return (0);
}
无比熟悉的开头,令人兴奋的打开方式,首先调用unix的一系列原生函数处理判断和创建将被用来做测试的命名管道fifo,其实网络编程socket更有代表性,呵呵!接下来,调用event_init()创建当前线程的event_base结构,初始全局变量current_base,具体过程在相关文章二中有提到,此处不做剖析。接下来调用event_set初始化该fifo事件,此处将此event具体值贴出,以便下面查看:event_set(&evfifo, socket, EV_READ, fifo_read, &evfifo);
{
ev->ev_base = current_base;
ev->ev_callback = fifo_read;
ev->ev_arg = &evfifo;
ev->ev_fd = socket;
ev->ev_events = EV_READ;
ev->ev_res = 0;
ev->ev_flags = EVLIST_INIT;
ev->ev_ncalls = 0;
ev->ev_pncalls = NULL;
ev->min_heap_idx = -1;
ev->ev_pri = current_base->nactivequeues/2;
}
二、普通IO事件的加入
示例:event_add(&evfifo, NULL);
,剔除信号和定时事件处理部分得:
int event_add(struct event *ev, const struct timeval *tv)
{
struct event_base *base = ev->ev_base;
const struct eventop *evsel = base->evsel;
void *evbase = base->evbase;
int res = 0;
。。。
if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) &&
!(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE))) {
res = evsel->add(evbase, ev);
if (res != -1)
event_queue_insert(base, ev, EVLIST_INSERTED);
}
。。。
}
发现仍然不过是老套路,无非是调用事件机制的add函数,然后将该事件加入到event_base中的eventqueue队列上,此处需要注意的是在调用 event_queue_insert插入事件时会调用TAILQ_INSERT_TAIL宏,它是为监听事件队列特别设计的宏,与上一篇讲的event_list密切相关,非常值得一看。之后我们以epoll为例聊一聊就当是复习了:
static int epoll_add(void *arg, struct event *ev)
{
struct epollop *epollop = arg;
struct epoll_event epev = {0, {0}};
struct evepoll *evep;
int fd, op, events;
...
fd = ev->ev_fd;
if (fd >= epollop->nfds) {
/* Extent the file descriptor array as necessary */
if (epoll_recalc(ev->ev_base, epollop, fd) == -1)
return (-1);
}
evep = &epollop->fds[fd]; /*记录的fd对应的监听事件,便于触发后快速查找,*/
op = EPOLL_CTL_ADD; /*刚开始应该为空*/
events = 0;
if (evep->evread != NULL) { /*下面几行还处理了编辑事件*/
events |= EPOLLIN;
op = EPOLL_CTL_MOD;
}
if (evep->evwrite != NULL) {
events |= EPOLLOUT;
op = EPOLL_CTL_MOD;
}
if (ev->ev_events & EV_READ) /*显然本处应该为这个读事件*/
events |= EPOLLIN;
if (ev->ev_events & EV_WRITE)
events |= EPOLLOUT;
epev.data.fd = fd;
epev.events = events;
if (epoll_ctl(epollop->epfd, op, ev->ev_fd, &epev) == -1)
return (-1);/*熟悉的epoll_ctl将事件加入监听*/
/* Update events responsible */
if (ev->ev_events & EV_READ)/*记录fd对应的监听事件,便于触发后快速查找*/
evep->evread = ev;
if (ev->ev_events & EV_WRITE)
evep->evwrite = ev;
return (0);
}
epoll_add及其简单看注释即可知道,其目的就是设置好监听事件与fd的对应关系,并将其注册到内核而已。至此加入事件任务完成,仅仅等着下一步的event_dispatch()即可。
三、event_dispatch()魅力犹在
老套路而已,还是会调用到event_base_loop(current_base, 0);
int
event_base_loop(struct event_base *base, int flags)
{
const struct eventop *evsel = base->evsel;
void *evbase = base->evbase; ...
int res, done;
...
done = 0;
while (!done) {
...
res = evsel->dispatch(base, evbase, tv_p);
...
if (base->event_count_active) {
event_process_active(base);
if (!base->event_count_active && (flags & EVLOOP_ONCE))
done = 1;
} else if (flags & EVLOOP_NONBLOCK)
done = 1;
}
...
}
显然真正其作用的无非是epoll->dispatch(base, evbase, 0);它会将发生事件加入到current_base->activequeues[0]中去,然后等待后面的event_process_active(base)函数将那些发生的事件处理也!可以看一看epoll->dispatch,后面的event_process_active函数在一二篇文章中皆有介绍,故不在介绍。
static int epoll_dispatch(current_base, evbase, 0)
{
struct epollop *epollop = arg;
struct epoll_event *events = epollop->events;
struct evepoll *evep;
int i, res, timeout = -1;
...
res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);
.../*省略处处理了信号类型的事件*/
for (i = 0; i < res; i++) {
/*获取发生事件的类型和文件描述符*/
int what = events[i].events;
struct event *evread = NULL, *evwrite = NULL;
int fd = events[i].data.fd;
。。。
/*获取事件fd对应的事件evepoll*/
evep = &epollop->fds[fd];
/*获取事件fd对应的具体事件event*/
if (what & (EPOLLHUP|EPOLLERR)) {
evread = evep->evread;
evwrite = evep->evwrite;
} else {
if (what & EPOLLIN) {
evread = evep->evread;
} /*本例为读事件应该获取evread */
if (what & EPOLLOUT) {
evwrite = evep->evwrite;
}
}
。。。
/*下面的event_active()函数将读事件加到current_base->activequeues[0]中
*此处主要就是在玩尾链表的插入方面值得学习,此处不在深入*/
if (evread != NULL)
event_active(evread, EV_READ, 1);
if (evwrite != NULL)
event_active(evwrite, EV_WRITE, 1);
}
if (res == epollop->nevents && epollop->nevents < MAX_NEVENTS) {
/* We used all of the event space this time. We should
be ready for more events next time. */
//这个循环简而言之就是怕此时空间可能并不够用,需要阔容。
int new_nevents = epollop->nevents * 2;
struct epoll_event *new_events;
new_events = realloc(epollop->events,
new_nevents * sizeof(struct epoll_event));
if (new_events) {
epollop->events = new_events;
epollop->nevents = new_nevents;
}
}
return (0);
}
四、总结
至此,libevent源码剖析就可以告一个段落了,四篇文章,从信号到定时事件再到普通事件让我见识到了广受欢迎的libevent源码是怎样造就的!活动事件链表的插入和删除,宏定义的高效,对事件的高度抽象与具体化,出神入化的函数回调机制。。。最后再来一起看一看事件处理函数fifo_read:
static void
fifo_read(int fd, short event, void *arg)
{
char buf[255];
int len;
struct event *ev = arg;
/* Reschedule this event */
event_add(ev, NULL); //继续
fprintf(stderr, "fifo_read called with fd: %d, event: %d, arg: %p\n",
fd, event, arg);
len = read(fd, buf, sizeof(buf) - 1);
if (len == -1) {
perror("read");
return;
} else if (len == 0) {
fprintf(stderr, "Connection closed\n");
return;
}
buf[len] = '\0';
fprintf(stdout, "Read: %s\n", buf);
}