我们之前采用的多进程方式实现的服务器端,一次创建多个工作子进程来给客户端提供服务。其实这种方式是存在问题的。

可以打个比方:如果我们先前创建的几个进程承载不了目前快速发展的业务的话,是不是还得增加进程数?我们都知道系统创建进程是需要消耗大量资源的,所以这样就会导致系统资源不足的情况。

那么有没有一种方式可以让一个进程同时为多个客户端端提供服务?

接下来要讲的IO复用技术就是对于上述问题的最好解答。

对于IO复用,我们可以通过一个例子来很好的理解它。

某教室有10名学生和1名老师,这些学生上课会不停的提问,所以一个老师处理不了这么多的问题。那么学校为每个学生都配一名老师,也就是这个教室目前有10名老师。此后,只要有新的转校生,那么就会为这个学生专门分配一个老师,因为转校生也喜欢提问题。如果把以上例子中的学生比作客户端,那么老师就是负责进行数据交换的服务端。则该例子可以比作是多进程的方式。后来有一天,来了一位具有超能力的老师,这位老师回答问题非常迅速,并且可以应对所有的问题。而这位老师采用的方式是学生提问前必须先举手,确认举手学生后在回答问题。则现在的情况就是IO复用。

目前的常用的IO复用模型有三种: select,poll,epoll

 

IO复用

目前的常用的 IO 复用模型有三种:select、poll、epoll。

I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select、poll、epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。

jdk1.4 是使用的 select/poll 模型,jdk1.5 以后把 select/poll 改为了 epoll 模型。

 

select模型

说的通俗一点就是各个客户端连接的文件描述符也就是套接字,都被放到了一个集合中,调用select函数之后会一直监视这些文件描述符中有哪些可读,如果有可读的描述符那么我们的工作进程就去读取资源。PHP 中有内置的函数来完成 select 系统调用。

我们在 select 函数中告诉内核需要监听的不同状态的文件描述符以及能接受的超时时间,函数会返回所有状态下就绪的描述符的个数,并且可以通过遍历 fdset,来找到就绪的文件描述符。

存在的问题:

  • 每次调用 select,都需要把待监控的 fd 集合从用户态拷贝到内核态,当 fd 很大时,开销很大。
  • 每次调用 select,都需要轮询一遍所有的 fd,查看就绪状态。这个开销在 fd 很多时也很大。
  • select 支持的最大文件描述符数量有限,默认是 1024。

 

poll模型

poll 和 select 的实现非常类似,本质上的区别就是存放 fd 集合的数据结构不一样。 select 在一个进程内可以维持最多 1024 个连接,poll 在此基础上做了加强,可以维持任意数量的连接。

但 select 和 poll 方式有一个很大的问题就是,我们不难看出来 select 是通过轮训的方式来查找是否可读或者可写,打个比方,如果同时有100万个连接都没有断开,而只有一个客户端发送了数据,所以这里它还是需要循环这么多次,造成资源浪费。

所以后来出现了 epoll 系统调用。

 

epoll模型

epoll 在 Linux2.6 内核正式提出,是基于事件驱动的 I/O 方式。,相对于 select 来说,epoll 没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次(可以理解为一块公共内存,该内存既不属于用户态也不属于内核态)。

epoll 是 select 和 poll 的增强版,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入 Ready 队列的描述符集合就行了。

epoll是基于内核的反射机制,在有活跃的 socket 时,系统会调用我们提前设置的回调函数。而 poll 和 select 都是遍历。

epoll 的好处:

  • 避免内存级拷贝;
  • 事件驱动(不是轮询);

但是也并不是所有情况下 epoll 都比 select/poll 好,比如在如下场景:

在大多数客户端都很活跃的情况下,系统会把所有的回调函数都唤醒,所以会导致负载较高。既然要处理这么多的连接,那倒不如 select 遍历简单有效。

 

select,poll,epoll的区别

select

poll

epoll(基于事件驱动)

操作方式

遍历

遍历

回调

底层实现

数组

链表

红黑树

IO效率

每次调用都进行线性遍历,时间复杂度为O(n)

每次调用都进行线性遍历,时间复杂度为O(n)

事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)

最大连接数

1024(x86)或2048(x64)

无上限

无上限

fd拷贝

每次调用select,都需要把fd集合从用户态拷贝到内核态

每次调用poll,都需要把fd集合从用户态拷贝到内核态

调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝

epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。

表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

 

select机制的问题

  • 每次调用select,都需要把​​fd_set​​​集合从用户态拷贝到内核态,如果​​fd_set​​集合很大时,那这个开销也很大
  • 同时每次调用select都需要在内核遍历传递进来的所有​​fd_set​​​,如果​​fd_set​​集合很大时,那这个开销也很大
  • 为了减少数据拷贝带来的性能损坏,内核对被监控的​​fd_set​​集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)

 

References