Redis是一个单线程但性能非常好的内存数据库,主要用作缓存系统。Redis使用网络IO多路复用技术,在多连接时保证系统的高吞吐量。1、Redis为什么要使用IO多路复用?首先,Redis是单线程运行的,所有的操作都是顺序线性执行的。但是,由于读写操作阻塞等待用户输入或输出,I/O操作往往不能直接返回,这会造成某个文件的I/O阻塞,导致整个流程无法为客户服务。I/O多路复用模型似乎解决了这个问题。select、poll、epoll都是IO多路复用模型。I/O多路复用是一种机制,通过它可以监视多个文件描述符,一旦一个描述符就绪,就可以通知应用程序执行相应的操作。Redis的I/O模型使用epoll,但也提供了select和kqueue的实现,默认使用epoll。2.epoll的实现机制①场景示例设想如下场景:100W个客户端同时与服务器保持一个TCP连接。并且每个时刻只有数百或数千个TCP连接处于活动状态(事实上,大多数场景都是这种情况)。如何实现这么高的并发。在select/poll时代,server进程每次要告诉操作系统100W个连接(从用户态拷贝handle数据结构到内核态),让操作系统内核检查是否有事件发生这些套接字,轮询完成后,将句柄数据复制到用户态,让服务器应用程序轮询处理已经发生的网络事件。这个过程消耗了大量的资源,所以select/poll一般只能处理几千个并发连接。如果没有I/O事件发生,我们的程序将在select处阻塞。但是还有一个问题,我们只知道select有一个I/O事件,但是不知道是哪些流(可能是一个,多个,甚至是全部),只能胡乱轮询所有的流,找到可以读取或写入数据并对其进行操作的流。但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每次无差别轮询的时间就会越长。select/poll的缺点1.每次调用select/poll时,都需要将fd集合从用户态复制到内核态。当fd很多的时候这个开销会很大;对于所有进来的fd,当fd很多的时候这个开销会很大;3、select支持的文件描述符数量太少,默认为1024个;4、select返回一个包含整个句柄的数组,应用程序需要遍历整个数组寻找哪些句柄有事件;5、select的触发方式为水平触发。如果应用程序没有完成对一个就绪文件描述符的IO操作,那么后续的每次select调用仍然会通知这些文件描述符的进程;与select模型相比,poll模型使用链表来保存文件描述符,因此对监控的文件数量没有限制,但其他缺点仍然存在。epoll的实现机制epoll的设计和实现与select完全不同。epoll是poll的优化。返回后不需要遍历所有的fd。它维护内核中的fd列表。选择和轮询在用户态维护这个内核列表,然后将其复制到内核态。与select/poll不同的是,epoll不再是一个单独的调度系统,而是由三个系统组成:epoll_create/epoll_ctl/epoll_wait。稍后将看到这样做的好处。epoll仅在2.6之后的内核中受支持。epoll在Linux内核中申请了一个简单的文件系统(文件系统一般使用什么数据结构?B+树)。将原来的select/poll调用分成三部分。1、调用epoll_create()创建一个epoll对象(在epoll文件系统中为这个handle对象分配资源);2、调用epoll_ctl将这100W个连接的socket加入到epoll对象中;3.调用epoll_wait收集其上发生的事件连接。这样,要实现上面提到的场景,只需要在进程启动时创建一个epoll对象,然后在需要的时候向epoll对象添加或删除socket连接即可。同时epoll_wait的效率也非常高,因为在调用epoll_wait的时候,这100W个连接的句柄数据并没有拷贝到操作系统中,内核不需要遍历所有的连接。epoll的优点1.epoll没有最大并发连接限制。上限是可以打开的最大文件数。这个数字比“2048”大得多。一般来说,这个数字与系统内存有很大关系。具体数量可以在cat/proc/sys/fs/file-max查看。2、效率提升。epoll最大的优点就是它只关心你的活跃连接数,和连接总数无关。所以在实际的网络环境中,epoll的效率会比select/poll高很多。3.没有内存拷贝,此时epoll使用的是“共享内存”,这个内存拷贝也省略了。3、Redisepoll的底层实现当进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体。该结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构如下:structeventpoll{.../*红黑树的根节点,存放所有加入epoll时需要监听的事件*/structrb_rootrbr;/*双链表存放满足条件的Event,通过epoll_wait返回给用户*/structlist_headrdlist;...};每个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法添加到epoll对象中的事件。这些事件会挂在红黑树中,这样重复添加的事件可以被红黑树高效识别(红黑树中插入事件的效率为lgn,其中n为树的高度).并且所有添加到epoll的事件都会和设备(网卡)驱动建立回调关系,即当相应的事件发生时,会调用这个回调方法。这个回调方法在内核中叫做ep_poll_callback,它会将发生的事件添加到rdlist双向链表中。在epoll中,每个事件都会创建一个epitem结构,如下:structepitem{//红黑树节点structrb_noderbn;//双向链表节点structlist_headrdlist;//事件句柄信息structepoll_filefdffd;//指向它所属的eventpoll对象structeventpoll*ep;//期望的事件类型structepoll_eventevent;调用epoll_wait方法检查事件是否发生时,只需要检查eventpoll对象中的rdlist双向链表中是否有epitem元素即可。如果rdlist不为空,将发生的事件复制到用户态,并将事件数返回给用户。优点:1.无需重复传输。当我们调用epoll_wait时,相当于之前调用了select/poll,但是此时不需要将socket文件句柄传递给内核,因为内核已经在epoll_ctl中获取了要监听的文件句柄列表。2.在内核中,一切皆文件。因此,epoll向内核注册了一个文件系统,用于存放上述被监控的socket。当你调用epoll_create时,会在这个虚拟的epoll文件系统中创建一个文件节点。当然这个文件不是普通文件,它只是为epoll服务的。3、效率极高的原因。这是因为当我们调用epoll_create时,内核不仅帮我们在epoll文件系统中创建了一个文件节点,还在内核缓存中创建了一颗红黑树来存放epoll_ctl发送过来的socket,同时还创建了另一个链表链表用于存储就绪事件。调用epoll_wait时,观察list链表中是否有数据即可。有数据则立即返回,无数据则休眠,超时后即使列表没有数据也返回。所以epoll_wait是非常高效的。epoll在被内核初始化的时候(操作系统启动),会开辟epoll自己的内核高速缓存区来放置我们要监控的每一个socket。这些套接字会以红黑树的形式存储在内核缓存中,以支持快速查找、插入和删除。这个内核高速缓存区就是创建连续的物理内存页,然后在上面构建一个slab层。简单的说,就是在物理上分配你想要大小的内存对象,每次使用的时候,都是使用空闲分配的内存。好对象。这个就绪列表是如何维护的?当我们执行epoll_ctl时,除了将socket放到epoll文件系统中文件对象对应的红黑树上,我们还会为内核中断处理程序注册一个回调函数,告诉内核如果这个中断句柄到达后,它被放入就绪列表列表中。因此,当数据到达一个socket时,内核将网卡中的数据复制给内核,然后将socket插入到就绪链表中。(备注:好好理解这句话)从上面可以看出,epoll的基础是回调。这样一棵红黑树,一个就绪句柄链表,少量的内核缓存帮助我们解决了高并发下的socket处理问题。执行epoll_create时,会创建一个红黑树和一个就绪链表。执行epoll_ctl时,如果添加了sockethandle,检查红黑树中是否存在,存在则立即返回,不存在则添加到红黑树,然后向内核注册回调该函数用于在中断事件到来时向就绪链表中插入数据。执行epoll_wait时,立即返回就绪链表中的数据即可。最后再看看epoll的两个特有模式LT和ET。LE和ET都适用于上述过程。不同的是,在LT模式下,只要一个句柄上的事件没有被处理一次,后面调用epoll_wait的时候就会返回本次句柄,而ET模式只是第一次返回。关于LT和ET,有说明。LT和ET都是电子学术语。ET是边沿触发,LT是电平触发,一种是只在变化沿触发,一种是在某个阶段触发。LT和ET是怎么做到的?当某个socket句柄上有事件发生时,内核会将句柄插入到上述就绪链表中。最后epoll_wait做了这件事情,就是检查这些sockets。如果不是ET模式(是LT模式的句柄),当这些socket上确实有未处理的事件时,将句柄放回刚刚清空的就绪列表。所以,对于非ET句柄,只要上面有事件,epoll_wait每次都会返回这个句柄。(从上段可以看出,LT还是有回放的过程,效率很低。)
