Redis IO多路复用技术及epoll实现原理

10、Redis IO多路复用技术以及epoll实现原理

       Redis是一个单线程的但性能是非常好的内存数据库,主要用来作为缓存系统。Redis采用网络IO多路复用技术来保证在多连接的时候,系统吞吐量高。

10.1 为什么Redis要使用IO多路复用

       首先,Redis是跑在单线程中的,所有的操作都是顺序线性执行的,但是由于读写操作等待用户输入或者输出都是阻塞的,所以I/O操作往往不能直接返回,这会导致某一文件的I/O阻塞导致整个进程无法为客户服务,而I/O多路复用模型就是为了解决这个问题而出现的。

 

       select、poll、epoll都是IO多路复用的模型。I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个描述符就绪,能够通知应用程序进行相应操作。

       Redis的I/O模型使用的就是epoll,不过它也提供了select和kqueue的实现,默认采用epoll。

       那么epoll到底什么东西?我们一起来看看。

 

10.2 epoll实现机制

10.2.1 场景举例

       设想一个如下场景:

       有100W个客户端同时与一个服务器保持着TCP连接。而每一时刻只有几百上千个TCP连接是活跃着的(事实上大部分场景都是这样的情况)。如何实现这样的高并发?

       在select/poll时代,服务器进程每次都要把100W个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已经发生的网络事件,这一过程资源消耗较大,因此select/poll一般只能处理几千的并发连接。

       如果没有I/O事件发生,我们的程序就会阻塞在select处。但是依然存在一个问题,我们从select那里仅仅知道了有I/O事件发生,但却并不知道是哪几个流(可能一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出或者写入数据的流,对它们进行操作。

       但是,使用select,我们有O(n)无差别轮询复杂度,同时处理的流越多,每一次无差别的轮询时间就越长。

 

10.2.2 select/poll缺点

       总结:select/poll缺点如下:

  1. 每次调用select/poll都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
  2. 同时每次调用select/poll都需要在内核遍历传进来的所有fd,这个开销在fd很多时会很大;
  3. select支持的文件描述符数量太小,默认1024;
  4. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  5. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知给进程;

 

        相比于select模型,poll模型使用链表保存文件描述符,因此没有了监视文件数量的限制,但是其他缺点依然存在。

 

10.2.3 epoll实现机制

       epoll的设计和实现与select完全不同。epoll是poll的一种优化,返回后不需要对所有的fd进行遍历,它在内核中维护了fd列表。select和poll是将这个内核列表维持在用户态,然后复制到内核态。与select/poll不同,epoll不在是一个单独的调度系统,而是由epoll_create / epolll_ctl / epoll_wait三个系统组成,后面将会看到这样做的好处。epoll在2.6以后的内核才支持。

       epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构?B+树)。把原先的select/poll调用分成三个部分。

  1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源);
  2. 调用epoll_ctl向epoll对象中添加这100W个连接的套接字;
  3. 调用epoll_wait收集在这上面发生的事件连接。

        如此一来,要实现上面所说的的场景,只要在进程启动的时候创建一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除socket连接。同时,epoll_wait的效率也是非常高的,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100W个连接的句柄数据,内核也不需要去遍历全部的连接。

 

10.2.4 epoll优点

1)epoll没有最大并发连接限制,上限是最大可以打开的文件的数目,这个数字远大于“2048”,**一般来说,这个数目跟系统内存关系很大**,具体数目可以cat /proc/sys/fs/file-max查看。

 

2)效率提升,epoll最大的优点就在于它只管你活跃的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率会远远高于select/poll。

 

3)无内存拷贝,epoll在这点上使用了“共享内存”,这个内存拷贝也就省略了。

 

10.3 Redis epoll底层实现

        当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。

eventpoll结构体如下所示:
	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中,每个事件都会建立一个epitem结构体,如下所示:
struct epitem{
	// 红黑树节点
	struct rb_node rbn;

	// 双向链表节点
	struct list_head rdlist;

	// 事件句柄信息
	struct epoll_filefd ffd;

	// 指向其所属的eventpoll对象
	struct eventpoll *ep;

	// 期待发生的事件类型
	struct epoll_event event;

}

        当调用epoll_wait方法检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双向链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件的数量返回给用户。

优势:

  1. 不用重复传递。我们调用epoll_wait时候就相当于以前调用select/poll,但这时却不用传递socket文件句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的文件句柄列表。
  2. 在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统中创建一个file结点。当然这个file不是普通的文件,它只服务于epoll。
  3. 极其高效的原因。这是由于我们在调用epoll_create时候,内核除了帮我们在epoll文件系统中创建了file结点,在内核cache里创建了个红黑树用于储存以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时候,仅仅观察这个list链表有没有数据即可。如果有数据就立即返回,没有数据就sleep,等到timeout时候,即使list没有数据也返回。所以epoll_wait非常高效。 

        epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置我们每一个想要监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在此之上建立slab层,简单的说,就是物理上分配好你想要的size内存对象,每次使用都是使用空闲的已经分配好的对象。

 

这个准备就绪的list链表是怎么维护的呢?

        当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪的list链表里。所以,当一个socket上有数据到了,内核再把网卡中的数据copy到内核中后,就把socket插入到准备就绪的链表里了。(备注:好好理解这句话)

        从上面可以看出,epoll基础就是回调。

        如此,一颗红黑树,一张准备就绪的句柄链表,少量的内核cache,就帮我们解决了高并发下的socket处理问题。

        执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查红黑树中是否存在,存在就立即返回,不存在则添加进红黑树,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时,立刻返回准备就绪链表里的数据即可。

       最后看看epoll独有的两种模式LT和ET。无论是LE还是ET都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时此次返回这个句柄,而ET模式仅在第一次返回。

       关于LT和ET,有一段描述,LT和ET都是电子里面的术语,ET是边缘触发,LT是水平触发,一个表示只有在变化的边际触发,一个表示在某个阶段都会触发。

       LT,ET这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪的list链表,最后,epoll_wait干了这件事情,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回这个句柄。(从上面这段,可以看出,LT还有一个回放过程,低效了。)

 

觉着老铁我写的还不错滴请继续关注下一章:终章篇咯!

 

  • 7
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值