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


static int aeApiCreate(aeEventLoop *eventLoop) {aeApiState *state = zmalloc(sizeof(aeApiState));if (!state) return -1;FD_ZERO(FD_ZERO(eventLoop->apidata = http://kandian.youth.cn/index/state;return 0;}而 aeApiAddEvent 和 aeApiDelEvent 会通过 FD_SET 和 FD_CLR 修改 fd_set 中对应 FD 的标志位:
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {aeApiState *state = eventLoop->apidata;if (maskif (maskreturn 0;}整个 ae_select 子模块中最重要的函数就是 aeApiPoll , 它是实际调用 select 函数的部分 , 其作用就是在 I/O 多路复用函数返回时 , 将对应的 FD 加入 aeEventLoop 的 fired 数组中 , 并返回事件的个数:
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {aeApiState *state = eventLoop->apidata;int retval, j, numevents = 0;memcpy(memcpy(retval = select(eventLoop->maxfd+1,if (retval > 0) {for (j = 0; j <= eventLoop->maxfd; j++) {int mask = 0;aeFileEvent *fe =if (fe->mask == AE_NONE) continue;if (fe->maskif (fe->maskeventLoop->fired[numevents].fd = j;eventLoop->fired[numevents].mask = mask;numevents++;}}return numevents;}封装 epoll 函数Redis 对 epoll 的封装其实也是类似的 , 使用 epoll_create 创建 epoll 中使用的 epfd:
static int aeApiCreate(aeEventLoop *eventLoop) {aeApiState *state = zmalloc(sizeof(aeApiState));if (!state) return -1;state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);if (!state->events) {zfree(state);return -1;}state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */if (state->epfd == -1) {zfree(state->events);zfree(state);return -1;}eventLoop->apidata = http://kandian.youth.cn/index/state;return 0;}在 aeApiAddEvent 中使用 epoll_ctl 向 epfd 中添加需要监控的 FD 以及监听的事件:
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {aeApiState *state = eventLoop->apidata;struct epoll_event ee = {0}; /* avoid valgrind warning *//* If the fd was already monitored for some event, we need a MOD* operation. Otherwise we need an ADD operation. */int op = eventLoop->events[fd].mask == AE_NONE ?EPOLL_CTL_ADD : EPOLL_CTL_MOD;ee.events = 0;mask |= eventLoop->events[fd].mask; /* Merge old events */if (maskif (maskee.data.fd = fd;if (epoll_ctl(state->epfd,op,fd,return 0;}由于 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 提供给上层使用 。