Redis 源码分析-Redis 中的事件驱动
之前写过 Socket网络通信及IO模型演进,现在看起来还是有些浅显了,恰好最近在读 redis 的源码,又对其有了一些新的理解。
网络编程模型及对比
select、poll、epoll 都是用来实现 IO 多路复用的,而多路复用发展的背景就是 socket 网络通信下提升系统的并发处理能力,单纯依靠多进程或多线程来实现对硬件要求很高,而且上限比较低,采用 IO 多路复用,可以很轻易的处理上千的并发。
虽然多路复用的机制有多种,但每一种都有一些通用的我们需要关注的问题:
- 该机制监听套接字上的什么事件
- 该机制能监听多少套接字
- 当有套接字就绪时,该机制是如何找到就绪套接字的
Linux 针对每一个套接字都会有一个文件描述符(fd)
,也就是一个非负整数,用来唯一标识该套接字。所以,在多路复用机制的函数中,Linux 通常会用文件描述符作为参数。有了文件描述符,函数也就能找到对应的套接字,进而进行监听、读写等操作。
select
select 机制的核心函数就是 select 函数
c
/*
@Param __nfds 监听的套接字个数
@Param __readfds 监听读事件的套接字数组
@Param __writefds 监听写事件的套接字数组
@Param __exceptfds 监听异常事件的套接字数组
@Param __timeout 本次调用的阻塞时间
*/
int select (int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)
调用后 select 函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。当 select 函数返回后,可以 通过遍历 fdset,来找到就绪的描述符。
fd_set 的结构体定义是这样的:
c
# define __FD_SETSIZE 1024
# define __NFDBITS 32
# define (long int) __fd_mask
typedef struct {
...
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
...
} fd_set
所以 __fds_bits 字段其实就是 long int 的一个数组,数组中每个元素 32 位,共有 (1024/32 = 32) 个元素,每一位用来标识一个 fd,因此最多标识 1024 个 fd
。该字段虽然可以
使用时,系统需要首先创建好需要监听的读事件、写事件、异常事件的描述符集合,接着调用 select 函数,阻塞等待
返回就绪的描述符数目。

以下是示例代码:
c
int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd) //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字
fd_set rset; //被监听的描述符集合,关注描述符上的读事件
int max_fd = sock_fd
//初始化rset数组,使用FD_ZERO宏设置每个元素为0
FD_ZERO(&rset);
//使用FD_SET宏设置rset数组中位置为sock_fd的文件描述符为1,表示需要监听该文件描述符
FD_SET(sock_fd,&rset);
//设置超时时间
struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;
while(1) {
//调用select函数,检测rset数组保存的文件描述符是否已有读事件就绪,返回就绪的文件描述符个数
n = select(max_fd+1, &rset, NULL, NULL, &timeout);
//调用FD_ISSET宏,在rset数组中检测sock_fd对应的文件描述符是否就绪
if (FD_ISSET(sock_fd, &rset)) {
//如果sock_fd已经就绪,表明已有客户端连接;调用accept函数建立连接
conn_fd = accept();
//设置rset数组中位置为conn_fd的文件描述符为1,表示需要监听该文件描述符
FD_SET(conn_fd, &rset);
}
//依次检查已连接套接字的文件描述符
for (i = 0; i < maxfd; i++) {
//调用FD_ISSET宏,在rset数组中检测文件描述符是否就绪
if (FD_ISSET(i, &rset)) {
//有数据可读,进行读数据处理
}
}
}
虽然 select 函数可以一次性监听多个 socket,但是当有 socket 就绪时,需要遍历来查找就绪套接字。
poll
Poll 机制的核心函数是 poll
c
/*
@Param __fds 包括套接字fd的一个结构体数组
@Param __nfds __fds 数组的元素个数
@Param __timeout 阻塞等待的超时时间
*/
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
pollfd 的定义是这样的:
c
struct pollfd {
int fd; //进行监听的文件描述符
short int events; //要监听的事件类型
short int revents; //实际发生的事件类型
};
Poll 机制的主要流程如下:
- 创建 pollfd 数组和监听套接字,并进行绑定
- 将监听套接字加入 pollfd 数组,并设置其监听读事件,也就是客户端的连接请求
- 循环调用 poll 函数,检测 pollfd 数组中是否有就绪的文件描述符。
- 如果是连接套接字就绪,这表明是有客户端连接,我们可以调用 accept 接受连接,并创建已连接套接字,并将其加入 pollfd 数组,并监听读事件;
- 如果是已连接套接字就绪,这表明客户端有读写请求,我们可以调用 recv/send 函数处理读写请求。
流程图:

实例代码:
c
int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd) //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字
//poll函数可以监听的文件描述符数量,可以大于1024
#define MAX_OPEN = 2048
//pollfd结构体数组,对应文件描述符
struct pollfd client[MAX_OPEN];
//将创建的监听套接字加入pollfd数组,并监听其可读事件
client[0].fd = sock_fd;
client[0].events = POLLRDNORM;
maxfd = 0;
//初始化client数组其他元素为-1
for (i = 1; i < MAX_OPEN; i++)
client[i].fd = -1;
while(1) {
//调用poll函数,检测client数组里的文件描述符是否有就绪的,返回就绪的文件描述符个数
n = poll(client, maxfd+1, &timeout);
//如果监听套件字的文件描述符有可读事件,则进行处理
if (client[0].revents & POLLRDNORM) {
//有客户端连接;调用accept函数建立连接
conn_fd = accept();
//保存已建立连接套接字
for (i = 1; i < MAX_OPEN; i++){
if (client[i].fd < 0) {
client[i].fd = conn_fd; //将已建立连接的文件描述符保存到client数组
client[i].events = POLLRDNORM; //设置该文件描述符监听可读事件
break;
}
}
maxfd = i;
}
//依次检查已连接套接字的文件描述符
for (i = 1; i < MAX_OPEN; i++) {
if (client[i].revents & (POLLRDNORM | POLLERR)) {
//有数据可读或发生错误,进行读数据处理或错误处理
}
}
}
和 select 函数相比,poll 函数的改进之处主要就在于,它允许一次监听超过 1024 个文件描述符。但是当调用了 poll 函数后,我们仍然需要遍历每个文件描述符,检测该描述符是否就绪,然后再进行处理。
epoll
epoll 机制就稍微复杂些了
c
typedef union epoll_data
{
...
int fd; //记录文件描述符
...
} epoll_data_t;
struct epoll_event
{
uint32_t events; // epoll 监听的事件类型
epoll_data_t data; // 应用程序数据
};
对于 epoll 机制来说,我们则需要先调用 epoll_create 函数,创建一个 epoll 实例。这个 epoll 实例内部维护了两个结构,分别是记录要监听的文件描述符 和已经就绪的文件描述符 ,而对于已经就绪的文件描述符来说,它们会被返回给用户程序进行处理,而不需要用户遍历查找
。
epoll 机制中有以下几个重要函数:
c
// 创建一个 epoll 的句柄(通过它操作 epoll 实例对象),size 参数已经没有实际意义了
int epoll_create(int size);
/*
对指定描述符 fd 执行 op 操作
@Param epfd epoll_create(int size)函数返回的 epoll 实例句柄
@Param op 操作标识(添加、删除、修改)
@Param fd 需要监听的那个描述符
@Param event 需要监听的描述符对应的事件
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
等待 epfd 上的 io 事件,最多返回 maxevents 个事件。
@Param epfd epoll_create(int size)函数返回的 epoll 实例句柄
@Param events 存放返回的就绪事件
@Param maxevents 最多返回的事件数,也是标识 events 的容量
@Param timeout 超时时间(毫秒,正整数时间,0是非阻塞,-1 永久阻塞直到事件发生)
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll 作为大多数场景下性能最好的 IO 模型,其主要函数的底层的实现逻辑都值得学习
epoll_create
c
1. int epoll_create(int size);
创建一个 epoll 的句柄,size 用来告诉内核这个监听的数目一共有多大,在2.6.8之后这个参数就没有实际价值了,因为内核维护一个动态的队列了。
当创建好 epoll 句柄后,它就会占用一个 fd 值,在 linux下 如果查看/proc/进程id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。当某一进程调用 epoll_create 方法时,Linux内核会创建一个 eventpoll 结构体,这个结构体中有两个成员与 epoll 的使用方式密切相关。
struct eventpoll {
// 红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控的事件
struct rb_root rbr;
// 双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
struct list_head rdlist;
...
}
- 在使用完 epoll 后,必须调用close()关闭,否则可能导致 fd 被耗尽。
epoll_ctl
c
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上
struct eventpoll {
// 红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控的事件
struct rb_root rbr;
// 双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
struct list_head rdlist;
...
}
每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系
,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫 **ep_poll_callback**,它会将发生的事件添加到 rdlist 双链表中
。
epoll_wait
c
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待 epfd 上的 io 事件,最多返回 maxevents 个事件。
通过回调函数内核会将 I/O 准备好的描述符添加到 rdlist 双链表管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,参数 timeout 是超时时间(毫秒,正整数时间,0 是非阻塞,-1 永久阻塞直到事件发生)。该函数返回需要处理的事件数目,如返回 0 表示已超时。
当然epoll对文件描述符的操作有两种模式:LT (level trigger)(默认)和ET (edge trigger)。LT模式是默认模式。
示例代码
c
int sock_fd,conn_fd; //监听套接字和已连接套接字的变量
sock_fd = socket() //创建套接字
bind(sock_fd) //绑定套接字
listen(sock_fd) //在套接字上进行监听,将套接字转为监听套接字
epfd = epoll_create(EPOLL_SIZE); //创建epoll实例,
//创建epoll_event结构体数组,保存套接字对应文件描述符和监听事件类型
ep_events = (epoll_event*)malloc(sizeof(epoll_event) * EPOLL_SIZE);
//创建epoll_event变量
struct epoll_event ee
//监听读事件
ee.events = EPOLLIN;
//监听的文件描述符是刚创建的监听套接字
ee.data.fd = sock_fd;
//将监听套接字加入到监听列表中
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ee);
while (1) {
//等待返回已经就绪的描述符
n = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
//遍历所有就绪的描述符
for (int i = 0; i < n; i++) {
//如果是监听套接字描述符就绪,表明有一个新客户端连接到来
if (ep_events[i].data.fd == sock_fd) {
conn_fd = accept(sock_fd); //调用accept()建立连接
ee.events = EPOLLIN;
ee.data.fd = conn_fd;
//添加对新创建的已连接套接字描述符的监听,监听后续在已连接套接字上的读事件
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ee);
} else { //如果是已连接套接字描述符就绪,则可以读数据
...//读取数据并处理
}
}
}
对比
select
-
优点
- select 目前几乎在所有的平台(POSIX)上支持,具有良好的跨平台支持。
-
缺点
- 单个进程能够监视的文件描述符的数量存在最大限制,它由 FD_SETSIZE 设置,默认值是1024。虽然能够修改宏定义或重新编译内核等进行修改,但可能造成效率的降低。
- fd 集合在内核被置位过,与传入的 fd 集合不同,不可重用。(内核在发现对应的 fd 有事件后,可能会直接修改 fd 的一些标志,导致 fd 与传入的时候不同,再次调用 select 前需要恢复)。
- 调用时 fd 集合需要从用户态拷贝到内核态,当 fd 很多时开销会很大。
- 需要遍历寻找发生事件的 fd 集合,当 fd 很多时开销会很大。
poll
- 优点
- 利用 pollfd 数组可以监视的文件描述符无上限,解决了 select 的缺点1。
- 每次在扫描完成后,只需要重置 pollfd 结构体中的 revents 即可复原,解决了 select 的缺点2。
- 缺点
- 调用时 pollfd 数组需要从用户态拷贝到内核态,当 fd 很多时开销会很大。
- 需要遍历 pollfd 数组寻找发生事件的 fd 集合,当 fd 很多时开销会很大。
epoll
- 优点
- 灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。
- 无需遍历,当调用 epoll_wait 检查是否有事件发生时,只需要检查 eventpoll 对象中的 rdlist 双链表中是否有 epitem 元素即可。如果 rdlist 不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户 。
- 缺点
- 每次只遍历活跃的 fd (如果是 LT,也会遍历先前活跃的 fd),在活跃 fd 较少的情况下就会很有优势,如果大部分 fd 都是活跃的,epoll 的效率可能还不如 select/poll。

Redis 中的 select 和 epoll
Redis 针对不同操作系统,会选择不同的 IO 多路复用机制来封装事件驱动框架
c
// ae.c
#ifdef HAVE_EVPORT
#include "ae_evport.c" // Solaris
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c" // Linux
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c" // MacOS
#else
#include "ae_select.c" // Windows
#endif
#endif
#endif
按照这个判断逻辑,在 linux 会使用 epoll 而在 windows 会使用 select
在 ae.h 头文件中
c
/* File event structure */
/* IO 事件结构体 */
typedef struct aeFileEvent {
int mask; // 事件类型掩码 /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc; // 读事件处理函数
aeFileProc *wfileProc; // 写事件处理函数
void *clientData; // 客户端私有数据指针
} aeFileEvent;
/* Time event structure */
/* 时间事件结构体 */
typedef struct aeTimeEvent {
long long id; /* time event identifier. */
long when_sec; /* seconds */
long when_ms; /* milliseconds */
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *prev;
struct aeTimeEvent *next;
} aeTimeEvent;
/* A fired event */
/* 已经发生的事件 */
typedef struct aeFiredEvent {
int fd; // 文件描述符
int mask; // 事件标志
} aeFiredEvent;
/* State of an event based program */
typedef struct aeEventLoop {
int maxfd; /* highest file descriptor currently registered */
int setsize; /* max number of file descriptors tracked */
long long timeEventNextId;
time_t lastTime; /* Used to detect system clock skew */
aeFileEvent *events; /* Registered events */ // I/O 事件,会涉及到文件描述符
aeFiredEvent *fired; /* Fired events */ // 已触发事件
aeTimeEvent *timeEventHead; // 时间事件
int stop;
void *apidata; /* This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
} aeEventLoop;
select(ae_select.c)
c
#include <sys/select.h>
#include <string.h>
/*
前面提到 select 传入的 fd 集合可能在内核态被修改,导致无法复用
redis 设置了 _rfds, _wfds 备份在调用前的 fd 集合, 用于后续的遍历
*/
typedef struct aeApiState {
fd_set rfds, wfds;
/* We need to have a copy of the fd sets as it's not safe to reuse
* FD sets after select(). */
fd_set _rfds, _wfds;
} aeApiState;
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
if (!state) return -1;
FD_ZERO(&state->rfds);
FD_ZERO(&state->wfds);
// 把当前 state 关联进 eventLoop 中
eventLoop->apidata = state;
return 0;
}
static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
/* Just ensure we have enough room in the fd_set type. */
if (setsize >= FD_SETSIZE) return -1;
return 0;
}
static void aeApiFree(aeEventLoop *eventLoop) {
zfree(eventLoop->apidata);
}
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;
}
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
if (mask & AE_READABLE) FD_CLR(fd, &state->rfds);
if (mask & AE_WRITABLE) FD_CLR(fd, &state->wfds);
}
// 捕获事件
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, j, numevents = 0;
// 在 select() 系统调用中,fd_set 集合是由内核维护的,并且它在每次调用 select() 时会被内核修改和置位
// 因此 aeApiState 中使用 _rfds _wfds 备份了原来的 fd,在每次 select 调用前进行备份
// 保存当前的 rfds 和 wfds 状态,以便后续操作(如 select() 调用后判断文件描述符的状态)时能够使用这些副本而不影响原始的文件描述符集合
memcpy(&state->_rfds, &state->rfds, sizeof(fd_set));
memcpy(&state->_wfds, &state->wfds, sizeof(fd_set));
// select 调用
// int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
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;
}
static char *aeApiName(void) {
return "select";
}
epoll(ae_epoll.c)
c
#include <sys/epoll.h>
typedef struct aeApiState {
int epfd; // epoll 实例描述符
struct epoll_event *events; // 结构体数组,记录监听事件
} aeApiState;
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
if (!state) return -1;
// 创建 events 数组
state->events = zmalloc(sizeof(struct epoll_event) * eventLoop->setsize);
if (!state->events) {
zfree(state);
return -1;
}
// 创建 epoll 实例, 参数中的 size 在新版本内核中已无实际意义
state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
if (state->epfd == -1) {
zfree(state->events);
zfree(state);
return -1;
}
// 将 epoll 信息存入 eventLoop
eventLoop->apidata = state;
return 0;
}
static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
aeApiState *state = eventLoop->apidata;
state->events = zrealloc(state->events, sizeof(struct epoll_event) * setsize);
return 0;
}
static void aeApiFree(aeEventLoop *eventLoop) {
aeApiState *state = eventLoop->apidata;
close(state->epfd);
zfree(state->events);
zfree(state);
}
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;
// 通过 epoll_ctl 注册希望监听的事件
if (epoll_ctl(state->epfd, op, fd, &ee) == -1) return -1;
return 0;
}
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0}; /* avoid valgrind warning */
int mask = eventLoop->events[fd].mask & (~delmask);
ee.events = 0;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
if (mask != AE_NONE) {
epoll_ctl(state->epfd, EPOLL_CTL_MOD, fd, &ee);
} else {
/* Note, Kernel < 2.6.9 requires a non null event pointer even for
* EPOLL_CTL_DEL. */
epoll_ctl(state->epfd, EPOLL_CTL_DEL, fd, &ee);
}
}
// 捕获事件
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
// epoll_wait 调用,返回监听到的事件数量
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;
}
static char *aeApiName(void) {
return "epoll";
}