我们的游戏后端一直以来用的都是libev,之前尝试过去读源码,因为里面用了大量宏和自己也不够耐心的原因,一直没有看懂。这次终于痛下决心,一定要啃下它,于是在这个星期调整自己的工作学习方式(在读源码的过程也发现平时一些不利于学习的习惯),结合别人的文章与源码,终于看懂了它的脉络,然后解答了一些困惑我已久的问题,其它的细节部分再看就相对简单了。
libev是一个事件驱动库,可用select, poll, epoll等做为底层支持。关于如何选择的目前还不是很清楚,只知道它是根据对应宏是否定义来做出选择。比如epoll就要看EV_USE_EPOLL这个宏是否定义,但是EV_USE_EPOLL又依赖于宏HAVE_EPOLL_CTL和宏HAVE_SYS_EPOLL_H是否定义,这两个宏的位置我还没找到在哪里设置的,目前猜测是在生成makefile时通过检测系统环境动态生成。经过实验也知道了在linux下,libev默认使用的是epoll。
这里说下libev的主循环,libev的主循环在函数ev_run中,大致的流程是
检测是否有新事件加入或者老事件修改 ---> 有就通过底层调用epoll_ctl进行设置,没有则进入下一步 ---> 计算epoll_wait的time_out时间(其实里面还有个io_block时间貌似会先sleep,因为暂时涉及不到就先不关注) ---> 调用epoll_wait-->阻塞结束,处理所有发生的事件,然后回到第一步
在知道libev的底层在linux下是使用epoll以后,对于文件描述符的读写事件的实现,其实也就大概猜得到了。一直困惑于我的是libev的记时器是如何实现的,这里详细记录一下。
在libev中计时器的结构体是ev_timer,里面有两个比较重要的变量是after和repeat。after是指这个计时器第一次是多久之后触发;repeat是指第一次触发之后,多长周期再次触发。把定时器的回调、after、repeat设置好之后,就可以调用ev_timer_start开启计时器。libev对于一个ev_loop会维护一个包含所有计时器的最小堆,将距离此刻最近的计时器放在堆顶。在调用epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)函数之前,算出timeout = 计时器堆顶元素到期时刻 - 当前时刻,这样一来就实现了计时器的功能。计时器触发之后会根据是否存在repeat重新设置最小堆,如存在则修改触发时间并调整最小堆,如不存在则调用ev_timer_stop。
在了解了上面的知识后,我想到了一个问题,那要是在epoll_wait的阻塞期间我想加入一个新的ev_io或者ev_timer怎么办呢?按照上面的实现的实现方式是不可能实时检测到的。实际上这就是我想岔了,既然是事件驱动,那么在阻塞期间就说明没有事件发生,没有事件发生又怎么可能会产生新ev_io或ev_timer呢?哈哈哈哈,我老是会想到这些个奇怪的问题。
当然看源码到现在,了解的肯定不只这么点,这里只记录了点我个人的问题。如果要把libev的整个框架说完那么估计要写很长了。我也还没有看完,继续努力,继续学习。
最后附上我看源码时结合的文章链接:https://blog.csdn.net/drdairen/article/details/53785447