IO多路复用:I/O是指网络I/O,多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程。意思说一个或一组线程处理多个TCP连接。最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程。  IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),blocking IO只调用了recvfrom;select/poll/epoll 核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程 阻塞IO好,多路复用模型中,每一个socket,设置为non-blocking,阻塞是被select这个函数block,而不是被socket阻塞的。

了解IO多路复用前先要了解IO模型,常见IO模型分为以下四种:

  1. 同步阻塞IO(Blocking IO):即传统的IO模型。
  2. 同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。
  3. IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。
  4. 异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。

同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

  1. 同步阻塞模型

多路复用的三种方式特点 一文读懂io多路复用技术(1)

用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。

2.异步非阻塞

多路复用的三种方式特点 一文读懂io多路复用技术(2)

用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。

3.IO多路复用

多路复用的三种方式特点 一文读懂io多路复用技术(3)

先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

4.异步IO

多路复用的三种方式特点 一文读懂io多路复用技术(4)

“真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。

IO多路复用的三种实现方式:

select

根据fd_size的定义,它的大小为32个整数大小(32位机器为32*32,所有共有1024bits可以记录fd),每个fd一个bit,所以最大只能同时处理1024个fd。在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

select 采用轮询方式遍历fd_size

每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

poll

描述fd集合的方式不同,poll使用 pollfd 结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制

poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪

epoll

epoll没有明确的fd数量限制,我们知道每个epoll监听一个fd,所以最大数量与能打开的fd数量有关,1g的内存的机器上,能打开10万个左右

epoll不需要每次都从用户空间将fd集合复制到内核空间,epoll在用epoll_ctl函数进行事件注册的时候,已经将fd复制到内核中,所以不需要每次都重新复制一次

select 和 poll 都是主动轮询机制,需要遍历每个 FD来确认获取的Active Event; epoll是被动触发方式,给fd注册相应事件的时候,我们为每一个fd指定了一个回调函数,当数据准备好之后,就会把就绪的fd加入一个就绪的队列中,epoll_wait的工作方式实际上就是在这个就绪队列中查看有没有就绪的fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

通过比较 select、 poll和 epoll处理 I/O 的过程来剖析其中的原因:

用户态将文件描述符传入内核的方式

select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程可以打开的 fd数量限制,默认是1024。

poll:将传入的 struct pollfd结构体数组拷贝到内核中进行监听。

epoll:执行 epoll_create会在内核的高速 cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的 epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。

内核态检测文件描述符是否可读可写的方式

select:采用轮询方式,遍历所有 fd,最后返回一个描述符读写操作是否就绪的 mask掩码,根据这个掩码给 fd_set赋值。

poll:同样采用轮询方式,查询每个 fd的状态,如果就绪则在等待队列中加入一项并继续遍历。

epoll:采用回调机制。在执行 epoll_ctl的 add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。

如何找到就绪的文件描述符并传递给用户态

select:将之前传入的 fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。

poll:将之前传入的 fd数组拷贝传出用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。

epoll: epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。这里返回的文件描述符是通过 mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。

继续重新监听时如何重复以上步骤

select:将新的监听文件描述符集合拷贝传入内核中,继续以上步骤。

poll:将新的 struct pollfd结构体数组拷贝传入内核中,继续以上步骤。

epoll:无需重新构建红黑树,直接沿用已存在的即可。

通过以上步骤我们可以发现以下几点

select和 poll的动作基本一致,只是 poll采用链表来进行文件描述符的存储,而 select采用 fd标注位来存放,所以 select会受到最大连接数的限制,而 poll不会。

select、 poll、 epoll虽然都会返回就绪的文件描述符数量。但是 select和 poll并不会明确指出是哪些文件描述符就绪,而 epoll会。造成的区别就是,系统调用返回后,调用 select和 poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而 epoll则直接处理就行了。

select、 poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而 epoll创建的有关文件描述符的数据结构本身就存于内核态中,系统调用返回时也采用 mmap共享存储区,需要拷贝的次数大大减少。

select、 poll采用轮询的方式来检查文件描述符是否处于就绪态,而 epoll采用回调机制。造成的结果就是,随着fd的增加, select和 poll的效率会线性降低,而 epoll不会受到太大影响,除非活跃的 socket很多。

最后总结一下, epoll比 select和 poll高效的原因主要有两点:

减少了用户态和内核态之间的文件描述符拷贝

减少了对就绪文件描述符的遍历。

,