Redis使用IO多路复用的原因
redis是运行在单线程中,所有的操作都是顺序执行的,但是由于读写惭怍等待用户输入或者输出都是阻塞的,因此很多IO操作在一般情况下都不能直接返回,而某个IO阻塞会导致整个进行无法对其他客户端提供服务,因此redis使用IO多路复用来解决这个问题。
什么是IO多路复用
IO多路复用是建立在内核提供的多路分离函数select基础之上,使用select函数可以避免同步非阻塞IO模型中的轮询等待问题。
如上图所示,用户首先将要进行IO操作的socket添加到select中,然后阻塞等待select返回。当数据到达的时候,select返回结果,用户正式发起读请求,继续执行。从上面这个流程看,该模型似乎和同步阻塞IO看起来没有太大的区别,还多了监听和select函数的调用。但是,使用select之后,最大的优势是用户可以在一个线程内同时处理多个socket和IO,用户可以注册多个socket,然后不断的调用select读取被激活的socket,达到一个线程同时处理多个IO请求的目的。而同步阻塞模型需要多线程才能完成。
IO多路复用的实现机制
select,poll,epoll都是IO多路复用的实现机制。在redis中的IO多路复用主要是通过epoll实现的,不过redis也提供了select和kqueue的实现,默认是使用epoll。
三种实现机制的区别
首先,我们先设想一下场景:有100万个客户端同时与一个服务器保持着连接,但是每一个时候,通常只有几百个连接是活跃的,这种场景下如何实现高并发?
select和poll的处理方式
使用select/poll,服务器进程每次都会把100万个连接告诉操作系统(从用户空间复制socket到内核空间),让操作系统去查询这些连接是否有时间发生,轮询完毕后,如果有,再将socket复制到用户态,让服务器轮询已发生的网络事件,这个过程需要消耗的资源较大,因此select和poll通常只能处理几千个并发连接。如果没有IO发生,那么线程就会阻塞在select出。而且,select是是通知服务器,有事件发生了,但是具体是哪几个事件,服务端并不知道,只能无差别的轮询,找到发生事件的socket。所以,使用这两种机制会导致有O(0)的轮询复杂度,同时处理的请求越多,那么所需要的时间就越长,无法处理大量的请求。
这两种模式的缺点
- 每次调用select/poll就需要把socket从用户态拷贝到内核态,当socket多的时候,开销就会很大
- 每次调用select/poll都需要在内核中轮询所有的socket,当连接数多的时候也会消耗大量的资源
- select支持的文件描述法太小了,只有1024,无法支持大量的连接
- select返回的是所有的句柄,需要遍历所有的句柄才能知道哪些是发生了事件的
- select是水平触发方式,如果程序没有对一个已经就绪的socket进行处理,那么下次还会将这些socket返回给应用程序
- 相比select模型,poll只是没有限制监听文件数量的限制
epoll的实现机制
epoll是poll的优化,返回之后不需要再遍历句柄列表,返回的是发生了事件的句柄。epoll把句柄列表维护在了内核中,而select/poll把列表维护在用户态,然后传递到内核。epoll不再是一个独立的系统调用,而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成,后面会详细说明。epoll通过向内核申请一个文件系统,把select/poll调用分成了三部分:
- epoll_create:建立一个epoll对象,在申请的文件系统中为这个对象分配资源
- epoll_ctl:向epoll对象添加这100万个socket
- epoll_wait:收集发生事件的连接
通过这三个部分,上面说的场景,我们只需要在进程启动的时候创建一个epoll对象,然后在这个对象中添加或者删除连接。而且,epoll_wait的效率非常高,因为不需要再向操作系统复制100万个socket,而内核也不需要去遍历全部连接。
epoll的有点
- epoll没有最大并发量的限制,上限是可以打开的文件数量,正常与内存有关,可以通过cat /proc/sys/fs/file-max 查询
- 效率提升,epoll最大的优势就是只管活跃的连接,与总连接数无关,因此在实际网络环境中的效率要源高于select和poll
- 降低资源开销,需要再进行内存拷贝
Redis epoll底层实现
当某个进程调用epoll_create方法的时候,内核会创建一个eventpoll的结构体,这个结构体有两个成员
struct eventpoll{
//红黑树的根节点,这个红黑树存储着所有需要被监听的事件
struct rb_root rbr;
//这个是双向链表,链表中存储着通过epoll_wait返回给用户的满足条件的事件
struct list_head rdlist;
}
每个epoll对象都有一个eventpoll结构,存储着通过epoll_ctl向epoll添加的事件,这些事件会被挂载在红黑树中。而所有添加到epoll中的事件都会与设备驱动(网卡)建立回调关系,也就是当事件发生的时候,会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到双向链表rdlist中。每个事件都会创建一个叫epitem的结构体
struct epitem{
struct rb_node rbn; //红黑树节点
struct list_head rdllink; //双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向所属的eventpoll对象
struct epoll_event event; // 期待发生的事件
}
当调用epoll_wait的时候,只需要检查eventpoll对象的rdlist链表是否有epitem元素即可,如果不为空则说明有事件发生,将发生的事件复制到用户态,同时讲事件数量返回给用户
优势
- 不需要重复传递。当我们调用epoll_wait的时候相当于调用之前的selct/poll,但是我们需要要将socket传递到内核,因为在epoll_ctl的时候内核就已经获取到需要监控的socket
- epoll申请了一个文件系统用于存储需要监控的socket,当调用epoll_create的时候会在epoll文件系统中创建一个file节点。epoll在初始化的时候,会开辟自己的内核高速cache区,用于存放需要监控的socket,这些socket以红黑树的形式保存在内核cache中,以支持快色的查找,插入和删除。
- 高效的原因:epoll建立了一个双向链表,用于存储发生事件的socket,当调用epoll_wait的时候,只需要判断链表中是否为空,如果不为空就返回,为空就继续等待。所以,epoll_wait也很高效。
准备就绪的list链表的维护
当调用epoll_ctl,除了把事件添加到红黑树中之外,还会注册一个回调函数,告诉内核,如果句柄中断到了就把它放到这个list中。因此,当socket上有数据的时候,内核把网卡上的数据复制到内核后就会把这个socket插入到链表中。
epoll整个执行过程
- 调用epoll_create创建红黑树和就绪链表
- 当执行epoll_ctl时,检查红黑树是否有该节点,有就返回,没有就添加节点,并且注册回调函数
- 当调用epoll_wait时,返回就绪链表即可
epoll的两种模式
epoll有LT和ET两种模式,这两种模式都支持上面所说的流程。区别是:
- LT水平触发模式:只要句柄上的事件没有处理完,那么下次调用epoll_wait的时候还会返回这个句柄
- ET边缘触发模式:句柄只在第一次的时候返回,不管该事件有没有被处理,后面都不会再返回。
这两种模式的实现原理:当我们调用epoll_wait的时候,会把就绪链表拷贝到用户态,然后就会清空链表。最后epoll_wait会检查这些返回的socket,如果是LT模式,如果socket还有未被处理的数据,那么会把这个socket继续添加到就绪列表,而ET则没有这个过程。因此LT模式有个回放的过程,效率会比ET低些。