为什么 Redis 单线程能支撑高并发?( 三 )

由于 epoll 相比 select 机制略有不同 , 在 epoll_wait 函数返回时并不需要遍历所有的 FD 查看读写情况;在 epoll_wait 函数返回时会提供一个 epoll_event 数组:
typedef union epoll_data {void*ptr;intfd; /* 文件描述符 */uint32_t u32;uint64_t u64;} epoll_data_t;struct epoll_event {uint32_tevents; /* Epoll 事件 */epoll_data_t data;};其中保存了发生的 epoll 事件(EPOLLIN、EPOLLOUT、EPOLLERR 和 EPOLLHUP)以及发生该事件的 FD 。
aeApiPoll 函数只需要将 epoll_event 数组中存储的信息加入 eventLoop 的 fired 数组中 , 将信息传递给上层模块:
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {aeApiState *state = eventLoop->apidata;int retval, numevents = 0;retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);if (retval > 0) {int j;numevents = retval;for (j = 0; j < numevents; j++) {int mask = 0;struct epoll_event *e = state->events+j;if (e->eventsif (e->eventsif (e->eventsif (e->eventseventLoop->fired[j].fd = e->data.fd;eventLoop->fired[j].mask = mask;}}return numevents;}子模块的选择因为 Redis 需要在多个平台上运行 , 同时为了最大化执行的效率与性能 , 所以会根据编译平台的不同选择不同的 I/O 多路复用函数作为子模块 , 提供给上层统一的接口;在 Redis 中 , 我们通过宏定义的使用 , 合理的选择不同的子模块:
#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因为 select 函数是作为 POSIX 标准中的系统调用 , 在不同版本的操作系统上都会实现 , 所以将其作为保底方案:
redis-choose-io-function
Redis 会优先选择时间复杂度为 的 I/O 多路复用函数作为底层实现 , 包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue , 上述的这些函数都使用了内核内部的结构 , 并且能够服务几十万的文件描述符 。
但是如果当前编译环境没有上述函数 , 就会选择 select 作为备选方案 , 由于其在使用时会扫描全部监听的描述符 , 所以其时间复杂度较差, 并且只能同时服务 1024 个文件描述符 , 所以一般并不会以 select 作为第一方案使用 。
总结Redis 对于 I/O 多路复用模块的设计非常简洁 , 通过宏保证了 I/O 多路复用模块在不同平台上都有着优异的性能 , 将不同的 I/O 多路复用函数封装成相同的 API 提供给上层使用 。
整个模块使 Redis 能以单进程运行的同时服务成千上万个文件描述符 , 避免了由于多进程应用的引入导致代码实现复杂度的提升 , 减少了出错的可能性


推荐阅读