修改记录:
3-29 初稿
介绍完类型与变量之后,就可以开始看程序的主轴了。我们从使用event-test.c入手可以看到的是:(略去之前创建命名管道和socket)
/* 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();
首先是
event_init进行了全体初始化。
在0.1中这个函数很简单,就是把之前的四个全局队列头结点置空。
在0.2中,除了初始化队列之外,选择第一个系统支持的函数处理程序,其顺序是kqueue, event_ports, epoll, poll, select(存疑。没去看现在的顺序是怎样,但肯定不是随机选择的)
在找到了合适的程序支持,比如发现了select函数可以使用之后,把select的结构体selectop的地址传给evbase。selectop的结构体是
struct selectop {
int event_fds; /* fd_set最大值 */
int event_fdsz; //fd_set的大小
fd_set *event_readset;
fd_set *event_writeset;
} sop;
其次是用
event_set初始化一个事件
event_set(&evfifo, socket, EV_READ, fifo_read, &evfifo);
这一步也很简单,其实就是把socket以及事件、标志通通给结构体evfifo赋值,没有其它东西。
再次是用
event_add给之前初始化的事件配置定时器
第一步先看event_add之时传入的时间是否为空,如果不为空的话已现在的时间为准,并加上传入的时间作为结束时间。
第二步是看event中的标志是否是EVLIST_TIMEOUT,如果是EVLIST_TIMEOUT则先从timequeue中移除此事件,这个标志
第三步是从头遍历timequeue,按照从近到远的顺序找到event该被插入的位置。
第四步是给event挂上EVLIST_TIMEOUT的标志
第五步是判断libevent是否正在循环中,如果在而且标志了EVLIST_ADD,则返回;否则插进addqueue队尾,并标志EVLIST_ADD。可以看出EVLIST_ADD表明此event是否已经added进addqueue。我们可以把addqueue理解成待处理事件。如果不在循环中,则调用
event_add_post(ev)——
在0.1中,事件被插入队列后会被标记上EVLIST_READ或者EVLIST_WRITE;
而在0.2中,readqueue和writequeue被合并成了eventqueue,并统一标记为EVLIST_INSERTED
注:LOG_DBG是log.h中的一个宏,用于记录日志
最后是进入处理循环
event_dispatch
//这个是0.2版本的处理循环
while (1) {
timeout_next(&tv);
event_inloop = 1;
res = evsel->dispatch(evbase, &tv);
event_inloop = 0;
if (res == -1)
return (-1);
maxfd = 0;
for (ev = TAILQ_FIRST(&addqueue); ev;
ev = TAILQ_FIRST(&addqueue)) {
TAILQ_REMOVE(&addqueue, ev, ev_add_next);
ev->ev_flags &= ~EVLIST_ADD;
event_add_post(ev);
if (ev->ev_fd > maxfd)
maxfd = ev->ev_fd;
}
if (evsel->recalc(evbase, maxfd) == -1)
return (-1);
timeout_process();
}
dispatch是重中之重,里面进行的操作如下
- 重新计算fd_set也就是readset或者sop->event_readset变量所需要的空间大小,并把fd_set清零。
- 把readqueue与writequeue中的所有事件的套接字放入fd_set中。
- 从timequeue队列中取出第一个定时器到时时间tv。在tv时间之前select读写的fd_set
- 锁上循环区(event_inloop=1),一一取readqueue或eventqueue的事件,先把这些事件从所有队列中移除之后,在一一调用各事件的回调函数即fifo_read。传入fifo_read的参数包括该事件的指针。fifo_read中再次调用event_set把event加入队列中,下次循环便会继续监听此事件。
- 解锁循环区(event_inloop=0),把addqueue中的event全部取出来,放进readqueue或者0.2版本中的eventqueue
- 调用recalc重新计算fd_set,也就是2中的步骤
- 最后,对照当前时间,把timequeue队列中还没有超时的事件取出来调用回调函数。
注:0.1和0.2版本的处理顺序有所不同(在进入循环区前后所调用的东西不同。0.1是select之后再循环,0.2反之),但基本上一致
至此除event_pending外的主要函数全部介绍完毕(event_pending主要用来检查某个事件是否已被标记在计划中)
总结:
libevent0.x主要使用了Tail Queue这一数据结构存放系统各种待处理的事件队列。(现在觉得这种类型蛮奇葩的,用双向链表应该也是可以,不知Niels当初有何考量)其中一个事件(event)包含了对应的套接字和当时队列中下一个事件的指针。
每次在回调之后,都需要重新把事件加入到队列中,这是因为每次select的对象都是队列。
在0.1的一个循环中,程序先处理readqueue再处理writequeue。在0.2中,libevent把read和write一视同仁,都合并成了eventqueue