作者:知乎用户
链接:https://www.zhihu.com/question/28594409/answer/52763082
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

题主是看redis相关书籍碰到了困惑,那就结合redis源码来回答题主这个问题。
redis源码地址:antirez/redis · GitHub

关于I/O多路复用(又被称为“事件驱动”),首先要理解的是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll/kqueue之类的系统调用函数来使用,这些函数都可以同时监视多个描述符的读写就绪状况,这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。

以select和tcp socket为例,所谓可读事件,具体的说是指以下事件:
1 socket内核接收缓冲区中的可用字节数大于或等于其低水位SO_RCVLOWAT;
2 socket通信的对方关闭了连接,这个时候在缓冲区里有个文件结束符EOF,此时读操作将返回0;
3 监听socket的backlog队列有已经完成三次握手的连接请求,可以调用accept;
4 socket上有未处理的错误,此时可以用getsockopt来读取和清除该错误。

所谓可写事件,则是指:
1 socket的内核发送缓冲区的可用字节数大于或等于其低水位SO_SNDLOWAIT;
2 socket的写端被关闭,继续写会收到SIGPIPE信号;
3 非阻塞模式下,connect返回之后,发起连接成功或失败;
4 socket上有未处理的错误,此时可以用getsockopt来读取和清除该错误。

Linux环境下,Redis数据库服务器大部分时间以单进程单线程模式运行(执行持久化BGSAVE任务时会开启子进程),网络部分属于Reactor模式,同步非阻塞模型,即非阻塞的socket文件描述符号加上监控这些描述符的I/O多路复用机制(在Linux下可以使用select/poll/epoll)。服务器运行时主要关注两大类型事件:文件事件和时间事件。文件事件指的是socket文件描述符的读写就绪情况,时间事件分为一次性定时器和周期性定时器。相比nginx和haproxy内置的高精度高性能定时器,redis的定时器机制并不那么先进复杂,它只用了一个链表来管理时间事件,而且目前链表也没有对各个事件的到点时间进行排序,也就是说,每次都要遍历链表检查每个事件是否需要到点执行。个人猜想是因为redis目前并没有太多的定时事件需要管理,redis以数据库服务器角色运行时,定时任务回调函数只有位于redis/src/redis.c下的serverCron函数,所有的定时任务都在这个函数下执行,也就是说,链表里面其实目前就一个节点元素,所以目前也无需实现高性能定时器。

Redis网络事件驱动模型代码:redis/src/目录下的ae.c, ae.h, ae_epoll.c, ae_evport.c, ae_select.c, ae_kqueue.c , ae_evport.c。其中ae.c/ae.h:头文件里定义了描述文件事件和事件时间的结构体, 即aeFileEvent和aeTimeEvent;事件驱动状态结构体aeEventLoop, 这个结构体只有一个名为eventloop的全局变量在整个服务器进程中;事件就绪回调函数指针aeFileProc和aeTimeProc;以及操作事件驱动模型的各种API(aeCreateEventLoop以及之后全部的函数声明)。ae_epoll.c, ae_select.c, ae_keque.c和ae_evport.c封装了select/epoll/kqueue等系统调用,Linux下当然不支持kqueue和evport。至于究竟选择哪一种I/O多路复用技术,在ae.c里有预处理控制,也就是说,这些源文件只有一个能最后被编译。优先选择epoll或者kqueue(FREEBSD和Mac OSX可用),其次是select。

05-11 20:14