文章目录
- 1、简述
- 2、多路复用的三个函数
- 3、创建epoll实例
- 4、绑定端口、监听端口
- 5、向epoll实例注册连接事件
- 6、从epoll实例中获取就绪的事件
1、简述
众所周知,redis是一款抗高并发的利器,据官方压测,单机可达10万qps。但背后实际处理命令的线程只有一条,这听上去其实挺匪夷所思的,因为在我们的日常开发中,说到高并发,多线程是一个非常常用的解决方案。那redis凭什么靠一条线程,就能支持高并发呢?最主要的原因,就是标题所说的IO多路复用,IO多路复用是怎么做的呢?这是老八股了,IO多路复用,背后依赖的是多路复用的函数,有select、poll、epoll,linux默认使用的是epoll函数,redis把客户端连接通过epoll函数给到内核,内核监听到连接有可读写的事件,就将该事件返回redis进行处理。那具体的实现细节呢?redis怎么给的内核,内核又怎么返回的?
2、多路复用的三个函数
epoll函数由3个函数组合来完成多路复用这件事。分别是:
epoll_create、epoll_ctl、epoll_wait
1)、epoll_create:创建epoll实例
2)、epoll_ctl:将连接对应的socket描述符注册到epoll实例中
3)、epoll_wait:获取epoll实例中可读写的描述符
画一个简单的流程图串一下这三个函数的作用
从图中可以看出,redis在启动的时候,先是通过epoll_create函数创建epoll实例,然后绑定端口、监听端口,然后通过epoll_ctl函数注册连接事件,最后会搞一个死循环,通过epoll_wait函数获取可读写的事件(每一个事件对应的都是一个可读写的客户端连接)
铺垫完上面的流程,我们看一下源码。redis的启动源码在server.c文件的main方法中,main方法是redis启动的入口,其中有很多流程,但是我们不要全部都看,就看图中流程涉及到的逻辑
3、创建epoll实例
首先是通过内核提供的epoll_create函数创建epoll实例,这个流程入口在initServer方法中.
void initServer(void) {......//创建epoll实例server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);if (server.el == NULL) {serverLog(LL_WARNING,"Failed creating the event loop. Error message: '%s'",strerror(errno));exit(1);}......
}
aeCreateEventLoop是创建epoll实例的入口,我们进入这个方法。
aeEventLoop *aeCreateEventLoop(int setsize) {......if (aeApiCreate(eventLoop) == -1) goto err;......
}
其中又调用了一个aeApiCreate方法,这个方法是对epoll_create函数做了一层封装,我们继续进入aeApiCreate方法。
static int aeApiCreate(aeEventLoop *eventLoop) {......//创建epoll实例//这里的1024并不是说epoll函数只能监听1024个描述符.因为在2.6.8内核之后,内核维护的是一个动态的队列,理论上我们可以一直添加描述符state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */......
}
这里就看到了我们想找的epoll_create函数。这种只看主流程的源码阅读方法,很容易能得到一些结论,也很容易坚持下去。
4、绑定端口、监听端口
创建完epoll实例后,接下来就是绑定端口、监听端口。
这部分的代码也是在initServer方法中,就在创建epoll实例的下方
void initServer(void) {......//创建epoll实例server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);if (server.el == NULL) {serverLog(LL_WARNING,"Failed creating the event loop. Error message: '%s'",strerror(errno));exit(1);}server.db = zmalloc(sizeof(redisDb)*server.dbnum);......//绑定、监听端口if (server.port != 0 &&listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)exit(1);......
}
绑定、监听端口的逻辑在listenToPort方法中,该方法的入参有3个值,第一个就是要绑定、监听的端口,默认是6379。第二个值是描述符,第三个是描述符的数量。这个时候,后面这两个参数还没值,需要到listenToPort方法中赋值。
int listenToPort(int port, int *fds, int *count) {......//绑定IPV6fds[*count] = anetTcp6Server(server.neterr,port,NULL,server.tcp_backlog);......//绑定IPV4fds[*count] = anetTcpServer(server.neterr,port,NULL,server.tcp_backlog);......(*count)++;
}
所以最终fds数组一共赋值2个值。count赋值2
5、向epoll实例注册连接事件
这个逻辑还是在initServer方法中。server.ipfd_count的值就是上面的那个count值,是2。所以这个循环会执行2次,注册2个连接事件,一个IPV4、一个IPV6
aeCreateFileEvent,是一个非常重要的方法,是用来创建事件的。该方法有5个入参。
第一个是redis对应epoll实例的结构体,第二个是需要注册的描述符,第三个是需要注册的事件类型,第四个是事件触发后的回调函数,最后一个是客户端数据。我们是注册连接事件,所以不会有客户端数据,此时客户端还没有连接redis
void initServer(void) {......//注册连接事件for (j = 0; j < server.ipfd_count; j++) {......if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,acceptTcpHandler,NULL) == AE_ERR)......}......
}
我们进入aeCreateFileEvent方法,
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)
{......//aeApiAddEvent函数内部调用epoll_ctl函数if (aeApiAddEvent(eventLoop, fd, mask) == -1)return AE_ERR;......//将acceptTcpHandler回调函数挂到当前连接事件上if (mask & AE_READABLE) fe->rfileProc = proc;if (mask & AE_WRITABLE) fe->wfileProc = proc;......
}
aeCreateFileEvent主要就是做两件事,注册连接事件、给事件挂回调函数,回调函数就是acceptTcpHandler。aeApiAddEvent是对epoll_ctl函数的封装。我们进入其中看一下
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {......//调用epoll的epoll_ctl函数注册事件,一共4个参数。//1、epoll实例//2、要执行的操作类型,添加事件还是修改修改事件。第一次肯定是添加事件//3、要监听的文件描述符//4、epoll_event类型变量if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;......
}
这里,我们就看到了epoll_ctl函数。
6、从epoll实例中获取就绪的事件
这个获取就绪事件的动作,是在main方法的aeMain函数中。
int main(int argc, char **argv) {......//执行aeMain函数开启事件循环处理框架aeMain(server.el);......
}
我们进入aeMain函数。
void aeMain(aeEventLoop *eventLoop) {//只要redis实例没有停止,while循环就会一直执行eventLoop->stop = 0;//redis服务是否停止的标志,如果stop值变为1,说明redis服务停止了while (!eventLoop->stop) {......aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);}
}
我们看到获取就绪的事件函数是aeProcessEvents,我们进入其中看一下
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{......//调用多路复用APInumevents = aeApiPoll(eventLoop, tvp);......
}
可以看到一个aeApiPoll函数,该函数是对epoll_wait函数的封装,我们继续进入aeApiPoll函数。
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;//EPOLLIN代表epoll模型的读事件,这一行代码的意思是将epoll的读事件映射到redis事件驱动框架的读事件if (e->events & EPOLLIN) mask |= AE_READABLE;//EPOLLOUT代表epoll模型的写事件,这一行代码的意思是将epoll的写事件映射到redis事件驱动框架的写事件if (e->events & EPOLLOUT) mask |= AE_WRITABLE;//EPOLLERR:错误事件,表示文件描述符对应套接字出错if (e->events & EPOLLERR) mask |= AE_WRITABLE;if (e->events & EPOLLHUP) mask |= AE_WRITABLE;//将epoll模型中已就绪的描述符映射到redis事件循环框架的就绪事件数组中eventLoop->fired[j].fd = e->data.fd;//给已就绪的事件设置事件类型eventLoop->fired[j].mask = mask;}}return numevents;
}
在其中,我们看到了epoll_wait函数,epoll_wait一共四个入参。
第一个是:要监听的描述符集合,第二个是要监听的事件类型,第三个是要监听的描述符数量,第四个是等待结果返回的超时时间
返回了结果后,下面的逻辑就是处理这个就绪的事件,这个方法是redis IO多路复用的关键所在,redis不停的接收客户端请求,这个方法是主要逻辑,我给每一行代码都加了注释,可以细看一下。
redis的这部分多路复用逻辑写的很清晰,可以认真梳理一下,对多路复用的原理会有更清晰的认识。
文章参考了极客时间的redis源码课程《redis源码剖析与实战》,文章写的挺好,有兴趣的小伙伴可以去看看。