"non-blocking IO + IO multiplexing(IO多路复用)"即reactor模型。Redis就采用这种模型。
Redis中单线程Reactor模式
select/epoll/poll等API分为两个阶段:①等待数据就绪阶段。这阶段不需由应用程序来监控,转而由内核替代应用程序监视文件描述符,具体内核监控的机制不同又产生了像epoll、select等API接口。②从内核拷贝数据到用户空间,这里是同步的。
- 使用IO多路复用的优势
与普通的进程线程相比较,不需要创建维护多个进程线程,使得系统开销相对较小,节省系统资源。
- 非阻塞IO
非阻塞IO只指如监听网络事件socket API的非阻塞。为什么要这样?
Linux中“man 2 select”BUGS可以看到:
“man 2 select”BUGS
上面介绍了虽然API返回可读,但是仍然可能阻塞,例如,当数据已经到达,但是在检查时出现错误的校验和并被丢弃时,就会发生这种情况。可能在其他情况下,文件描述符被错误地报告为ready。建议使用非阻塞。
在实际使用一些情形:
- 应用网络数据比较大超过发送缓冲区时,如果阻塞会造成整个进程/线程阻塞,这种情况是不能容忍的。
- 惊群效应。子父进程同时监控一文件描述符时会发生。
- epoll边缘触发模式。epoll不会返回可读数据长度,所以read API会阻塞。
Redis下伪代码如下
bool isRun = true;
while(isRun)
{
int timeoutMs = aeSearchNearestTimer(); // 寻找里目前时间最近的时间事件
int ret = aeApiPoll(maxfd, rfd, wfd, timeoutMs);
if(ret < 0){
// 错误处理
}else if(ret > 0){
// 有时间来,需处理IO,如注册的读、写回调等操作,非阻塞
}
// 处理系统要做其他事物,非阻塞。如:doBeforeSleep()等
}
- aeApiPoll()
Redis对这个接口进行了封装以适应不同平台。
// ae.c
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
上面这种写法对C/C++的人员再熟悉不过,采用宏条件语句判断,在预处理阶段把对应平台使用的ae_xxx.c文件拷贝到ae.c文件中。把各个平台提供的如:epoll、poll、select、kqueue等统一封装成aeApiPoll()。
- aeSearchNearestTimer()
寻找目前时间最近的时间事件唤醒本线程,做一些事情,如:100ms的用于删除设定了过期的key、AOF从内存buf空间系统调用write写入OS缓冲区(磁盘fsync有阻塞属性,在另外进程执行)。
aeSearchNearestTimer()在内部是通过查找timeEventHead无序链表,时间复杂度O(n),这里是由于链表较短,只有一个serverCron 时间事件,性能无影响。
在redis.c initServer()中注册的serverCron
如果时间事件较多,需要采用有序数据结构如:红黑树(nginx)查找时间复杂度O(logn)。