设为首页 收藏本站
查看: 1167|回复: 0

[经验分享] Redis事件驱动库<转>

[复制链接]

尚未签到

发表于 2015-7-22 04:44:59 | 显示全部楼层 |阅读模式
  本文转自:http://www.iyunv.com/wankaizhang/archive/2010/12/27/1918111.html
  事件驱动的编程方式已经很普及了,原因自然是互联网的疾速膨胀,现在要写个服务器不用事件驱动,出门都不好意思跟人打招呼。但是实现事件库并不是那 么容易,首先它与人们亦步亦趋的思考方式有点儿冲突,其次事件库的底层实现必须平台相关,如Linux使用epoll,FreeBSD使用kqueue。
  事件驱动库是很多系统软件的基础设施,如Lighttpd、NodeJS使用了libev,Memcached使用了libevent,Nginx 和Redis自己实现了一套。通用的事件库一般比较复杂,有很多我们并不需要的功能,而自己写一个又费时间,可能Boss不会同意。本文将分析一个小巧的 事件库,它内置于Redis。如果你事先没看Redis事件库的源码(ae.h ae.c ae_epoll.c),读起来可能会有困难。
一个事件库的必要组成元素有哪些呢:
  1)事件,一般由外在因素触发,比如有网络数据到达;
2)事件处理函数,事情发生以后要靠它处理;
3)事件与处理函数之间的映射关系,将上述两种概念联系起来;
4)循环监控,基于事件驱动的程序一般主体是个循环,在每一遍循环中检查发生了哪些事儿,然后调用相应的处理函数;
  事件:
操作系统产生的事件包括文件的读写,网络接口的读写,操作系统信号,超时事件。前两个Linux统一为文件描述符,信号事件Redis没有在事件库中实现(确实没这个必要),超时事件Redis只向操作系统借了个获取系统时间的系统调用。于是库中事件只有两种:文件读写和超时。
struct aeFileEvent {
    int mask;                 /* AE_READABLE表示可读,AE_WRITABLE表示可写 */
    aeFileProc *rfileProc;    /* 处理读事件 */
    aeFileProc *wfileProc;    /* 处理写事件 */
    void *clientData;         /* 传递用户数据给读写函数,这是Linux C的惯用法 */
};
  struct aeTimeEvent {
    long long id;             /* 标识TimeEvent,实际用处下面再说 */
    long when_sec;
    long when_ms;
    aeTimeProc *timeProc;     /* 超时处理函数 */
    aeEventFinalizerProc *finalizerProc;    /* TimeEvent被删掉时会被调用,通常被设为NULL */
    void *clientData;
    aeTimeEvent *next;        /* 所有的TimeEvent组成一个链表 */
  };
  映射关系:
超时处理简单,在每一轮循环(事件驱动程序的主体是个循环)中,通过系统调用获取当前时间,然后遍历TimeEvent链表,把该处理的都给处理了。文件该怎么办呢,我怎么知道它到底能不能读写,用户程序是无法知道的,只能借助于系统调用,在Linux中使用epoll,当文件事件发生时,epoll机制会捕获它,然后我们把发生的事件保存起来,这样在每一轮主循环中就知道发生了哪些文件事件了。epoll会将发生的事件保存在 aeFiredEvent中:
struct aeFiredEvent {         /* 哪个文件发生了什么事儿 */
    int fd;
    int mask;
};
“将发生的文件事件保存在FireEvent中”这个过程主要是由操作系统帮你完成的,不是在主循环内实现的,你要是能在用户程序中完成那就是见着鬼了。  
  
  基本信息就介绍到这儿了,我不会深入到琐碎的代码细节,而是以问答的方式展现有意义的细节。读者要先读源码,并时刻对比下面这张图:
DSC0000.png
  
  Q1:Redis是先处理FileEvent,还是先处理TimeEvent?
从aeProcessEvents函数可以看出是先处理FileEvent。
  Q2:我看了下aeProcessEvents函数,确实是先FileEvent,再TimeEvent。但是在处理FileEvent前,还有一长串代码,那是干什么用的?
呵呵,这个说起来比较烦,我现在不知道怎么表达,你问些其他的问题吧,说不定等下就说清楚了。
  Q3:如果我要延迟5秒输出“Hello, World”,该怎么办?
/* 先定义好超时处理函数 */
int print5(struct aeEventLoop *loop, long long id, void *clientData)
  {
    printf("Hellow, World\n");
    return -1;                 /* 返回 -1 很重要,否则会出错 */
}
int main(void)
{
    aeEventLoop *loop = aeCreateEventLoop();
    /* 创建5秒超时事件,处理函数是print5 */
    createTimeEventProc(loop, 5000, print5, NULL, NULL);
    aeMain(loop);              /* 启动主循环 */
    aeDeleteEventLoop(loop);
    return 0;
}
  Q4:如果我要每隔5秒输出“Hello, World”,又该怎么办呢?
这个好办,把Q3的代码原样拷来,只做一处修改:print5函数不返回-1,而返回5000
其实返回-1代表删掉对应的超时事件,这样只会打印一次“Hello, World”;若是返回正整数n,则代表n毫秒后再触发该事件,于是就会循环打印。注意千万不能返回0或小于-1的负数,否则会陷入死循环,这也算是个瑕疵吧。
  Q5:Redis是怎么处理超时事件的?
  从图中可看出TimeEvent被组织为一个单向链表,表头指针timeEventHead保存在核心数据结构aeEventLoop中。aeMain函数在每一轮循环中都会遍历该链表,针对每个TimeEvent,先调用gettimeofday获取系统当前时间,如果它比TimeEvent中的时间要小,则说明TimeEvent还没触发,应继续前进,否则说明TimeEvent已经触发了,立即调用超时处理函数,接下来根据处理函数的返回值分两种情况讨论:
1)若处理函数返回-1,那么把这个TimeEvent删掉。
2)否则,根据返回值修改当前的TimeEvent。比如返回5000,这个TimeEvent就会在5秒后再次被触发。
好了,这个TimeEvent已经搞定了,按理说应该继续前进处理下一个TimeEvent了,但是且慢,由于情况1)我们不能由当前结点到达下一结点,于是作者就又从表头开始遍历。
  这个算法不怎么好,假如我从表头开始遍历,碰到第100个结点时才发现该调用处理函数了,这样处理完之后,又得从表头开始遍历,前面的很多结点都做了无用的重复计算。作者也承认这个算法不怎么好,应该改进。
  Q6:你已经说过在每一轮主循环中,是先处理FileEvent,再处理TimeEvent,如此往复。有没有可能这一轮的TimeEvent永远处理不完,从而导致后来发生的FileEvent得不到处理?
有可能,如果TimeEvent的处理函数返回0或者除 -1以外的负数,那么会再次无休止地调用这个处理函数。
  Q7:如果每个TimeEvent函数都会调用aeCreateTimeEvent函数,那么会不会导致和Q6一样的问题?
不会, 由aeCreateTimeEvent函数创造的TimeEvent都不会在此轮得到处理,而是会在下一轮处理完FileEvent后再处理。这个功能是由struct aeEventLoop中的timeEventNextId成员完成的,具体怎么实现的请读者看源码,很简单。
  Q8:Redis是怎么处理FileEvent的?
FileEvent和TimeEvent的处理方式差别很大,用户程序不可能去遍历文件描述符,而是在循环中调用epoll_wait系统调用。这个系统调用是阻塞式的,直到发现有文件事件触发才会返回到用户空间,进而处理FileEvent。
  Q9:假设在执行epoll_wait时,一直等不到文件事件触发,那岂不是程序就一直这样阻塞着,连后面的TimeEvent相关的代码都没机会运行了?
解决这个问题有赖于epoll_wait函数可以接受一个参数,用来确定最长等待时间,如果在这段时间一直没有文件事件触发,epoll_wait不会傻傻等待,而会返回到用户空间。问题关键是如何确定这个最长等待时间:一轮循环内,在处理FileEvent之前,会先查找最近的TimeEvent,将其时间设为最长等待时间。epoll_wait在这段时间内都没有等到文件事件触发就会返回到用户空间,继而执行后面的事件处理流程。确定最长等待时间的代码在文件事件处理之前,现在你不会有Q2的疑问了。
  Q10:上面的回答似乎令人信服,但是有一种特殊情况,如果TimeEvent链表为空,你如何确定最长等待时间?
这确实是个好问题。其实在主循环启动前我们要决定设不设AE_DONT_WAIT这个标志。当碰到TimeEvent为空的情况时, 如果设置了AE_DONT_WAIT,epoll_wait会立即返回,不再等待文件事件;如果没设此标志,epoll_wait会永远等待,直到有文件事件触发为止。
  总结:
如果把上面10个问题搞清楚了,那么对Redis事件库已经很了解了。其实Redis对事件库的功能要求很简单,完全不需要libevent和libev中的各种复杂功能,事实上很多时候我们也并不需要多强大的库,或许下次你可以把Redis的事件库运用到你的作品中去。
  移植时要注意以下几个问题:
1)ae.{c|h}实现主要的逻辑;ae_select.cae_epoll.c,ae_kqueue.c分别是三种底层实现,根据你的系统选其一即可
2)上面那些文件依赖了config.h zmalloc.{c|h},移植的话要对上面的文件做些修改。
  自己编写事件库时应注意以下问题:
1)文件读写和时间超时是两种不同的事件。我们要根据应用情况决定是处理一种,还是两者包办,甚至有没有必要处理信号事件。
2)防止饥饿现象,Q6,Q7,Q9,Q10都是关于饥饿现象的。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-89180-1-1.html 上篇帖子: Redis源码笔记五: intset 下篇帖子: 简单redis队列实现
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表