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

  • 使用 FD_SET 将 fd 加入 RFDS 。
  • 调用 Select 方法监控 RFDS 中的 FD 是否可读 。
  • 当 Select 返回时,检查 FD 的状态并完成对应的操作 。
  • int fd = /* file descriptor */fd_set rfds; FD_ZERO(&rfds); FD_SET(fd, &rfds)for ( ; ; ) {select(fd+1, &rfds, NULL, NULL, NULL);if (FD_ISSET(fd, &rfds)) {/* file descriptor `fd` becomes readable */} } 而在 Redis 的 ae_select 文件中代码的组织顺序也是差不多的,首先在 aeApiCreate 函数中初始化 rfds 和 wfds:
    static int aeApiCreate(aeEventLoop *eventLoop) {aeApiState *state = zmalloc(sizeof(aeApiState));if (!state) return -1;FD_ZERO(&state->rfds);FD_ZERO(&state->wfds);eventLoop->apidata = https://www.isolves.com/it/sjk/Redis/2019-05-14/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 (mask & AE_READABLE) FD_SET(fd,&state->rfds);if (mask & AE_WRITABLE) FD_SET(fd,&state->wfds);return 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(&state->_rfds,&state->rfds,sizeof(fd_set));memcpy(&state->_wfds,&state->wfds,sizeof(fd_set));retval = select(eventLoop->maxfd+1,&state->_rfds,&state->_wfds,NULL,tvp);if (retval > 0) {for (j = 0; j <= eventLoop->maxfd; j++) {int mask = 0;aeFileEvent *fe = &eventLoop->events[j];if (fe->mask == AE_NONE) continue;if (fe->mask & AE_READABLE && FD_ISSET(j,&state->_rfds))mask |= AE_READABLE;if (fe->mask & AE_WRITABLE && FD_ISSET(j,&state->_wfds))mask |= AE_WRITABLE;eventLoop->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 = https://www.isolves.com/it/sjk/Redis/2019-05-14/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 (mask & AE_READABLE) ee.events |= EPOLLIN;if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;ee.data.fd = fd;if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;return 0; } 由于 epoll 相比 select 机制略有不同,在 epoll_wait 函数返回时并不需要遍历所有的 FD 查看读写情况 。
    在 epoll_wait 函数返回时会提供一个 epoll_event 数组:
    typedef union epoll_data {void *ptr;int fd; /* 文件描述符 */uint32_t u32;uint64_t u64; } epoll_data_t;struct epoll_event {uint32_t events; /* 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->events & EPOLLIN) mask |= AE_READABLE;if (e->events & EPOLLOUT) mask |= AE_WRITABLE;if (e->events & EPOLLERR) mask |= AE_WRITABLE;if (e->events & EPOLLHUP) mask |= AE_WRITABLE;eventLoop->fired[j].fd = e->data.fd;eventLoop->fired[j].mask = mask;}}return numevents; } 子模块的选择
    因为 Redis 需要在多个平台上运行,同时为了最大化执行的效率与性能,所以会根据编译平台的不同选择不同的 I/O 多路复用函数作为子模块,提供给上层统一的接口 。


    推荐阅读