回顾
上次博客结尾的时候简单提到了多路复用技术。在I/O编程过程中,如果需要多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术是通过把多个I/O的阻塞复用到同一个select的阻塞上,这样程序在单线程情况下可以同时处理多个客户端请求。这与多线程实现相比较,I/O多路复用的最大优势是系统开销小,系统不需要创建多余线程或者线程池,减少了系统维护的工作量。那么I/O多路复用模型的应用场景有哪些呢?
服务器需要同时处理多个处于监听状态或者多个连接状态的套接字
服务器需要同时处理多种网络协议的套接字
在目前支持I/O多路复用的系统调用有select、pselect、poll、epoll,在Linux网络编程中,在很长的时间内都是使用了select做为轮询和网络事件通知,但是在前面的分享中我们提到了一个概念,利用select在最大文件打开数上有一定的限制,所以在新的Linux中找到了一个epoll作为替代,当然epoll和select原理是比较相似的,但是为了克服select的缺点,epoll做了很大的优化。
1.支持一个进程打开的socket描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)
我们知道select的最大的缺点就是单个线程打开的FD是有限的,它由FD_SETSIZE来设置,默认是1024,对于有千万级的TCP连接来说这个数量有点少。当然可以重新编译内核来实现扩展,但是这样做的就会导致网络效率的下降。这就出现了之前提到的多线程多进程解决方案。在Linux上创建进程的消耗比较小,但是如果数量较大的话也是不可被忽视的。进程间的数据交换是一个复杂的过程,对于Java来说没有共享内存,需要通过Socket通信或者其他的方式进行数据同步,这就会导致额外的性能消耗,同时增加了程序的复杂度,所以是一种不完美的解决方案。在新的Linux中epoll并没有这个限制,它所能支持的FD上限就是操作系统所能支持的最大文件句柄数。当然这个数字远大于1024.这个文件句柄数可以通过 cat /proc/sys/fs/file-max 来查看。通常这个值与操作系统内存有关。
2.I/O效率不会随着FD的数目增加而线性下降
在传统的select/poll中另外的一个硬伤就是,当select集合较大的时候,由于网路延迟或者出现空的链路,在执行过程任意时刻只有少数的一部分select是活跃状态,但是select/poll每次调用都会扫描整个集合。整个就导致效率处于一个线性下降的状态。而epoll则不会出现整个问题,它只会对于活跃的socket进行操作,这个原因是内核实现中epoll是根据每个fd上的callback函数实现操作,这就是说只有它处于活跃状态socket才会去调用callback函数,其他的处于idle状态的socket则不会被调用callback。这里这个epoll的操作实际上是一个为AIO。针对epoll和select的性能对比,如果所有的socket都处于活跃状态,例如一个高速的LAN环境,epoll并不比select/poll效率高,相反,如果过多的使用epoll_ctl,效率相比还有稍微的下降,但是如果使用idle connections模拟WAN环境,epll的效率就远在与select/poll之上。
3.使用mmap加速内核与用户空间的消息传递
当然使用select/poll还是epoll都是需要内核把FD消息通知给用户空间,那么如何避免不必要的内存复制就是个问题,在epoll中通过内核和用户空间mmap同一块内存实现。
4.epoll的API相对简单
epoll的API使用包括创建epoll描述符、添加监听事件、阻塞等待所监听的事件发生,关闭epoll描述符等等。
值得一提的是,用来克服select/poll 的方式不止只有epoll,epoll只是一种Linux的实现方案。在freeBSD下有个kqueue等,但是这个实现的逻辑比较复杂也是一个比较老的实现方式所以这里就不再过多的说明。