如何高效处理多个socket I/O的读写,是提高服务器性能的重点问题。unix-like下面,现有机制有select,poll, epoll,kqueue,/dev/poll两大类。
Select有个缺点,它用fd_set管理所有要监视的I/O句柄,但是fd_set是一个位数组,只能接受句柄号小于FD_SETSIZE(默认1024)的句柄,虽然进程默认句柄号都是小于1024的,但是可以通过ulimit –n来修改,尤其是连接数超过1024时必需这么做(实际可能更少),如果要将大于1024的句柄放入fd_set,就可能发生数组越界程序崩溃的场面。
Poll虽然解决了FD_SETSIZE问题,但是它和select一样,都有性能上的瓶颈。它们都会随着连接数的增加性能直线下降。这主要有两个原因,其一是每次select/poll操作,kernel都会建立一个当前线程关心的事件列表,并让线程阻塞在这个列表上,这是很耗时的操作。其二是每次select/poll返回后,线程都要扫描所有句柄来dispatch已发生的事件,这也是很耗时的。当连接数巨大时,这种消耗积累起来,就很受不了。
为了解决select/poll的性能问题,unix-like系统上开发出了三套新的利器epoll,kqueue,/dev/poll,其中epoll是linux的,kqueue是freebsd的,/dev/poll是Solaris上的,它们是select/poll的替代品。它们的设计就是针对select/poll的性能问题,主要避免 1。每次调用都建立事件等待列表,取而代之建立长期的事件关注列表,这个列表可通过句柄(比如epfd)来增加和删除事件。2。调用返回之后,不再需要遍历所有句柄进行分发,内核会直接返回当前已发生的事件。不用说,性能在select, poll基础上有了大幅提升。
要注意的是,凡是使用readiness notification(LT)或者readiness change notification(ET)机制,都应该配合非阻塞I/O,因为这种事件通知,并不一定表示文件描述符真正就绪,如果收到通知之后去read,很有可能进入阻塞状态,这会严重影响服务器的并发性能,同时对ET模式,不能漏掉任何事件的处理,并且每次都应该读到socket返回EWOULDBLOCK为止,不然这个socket之后会永远保持沉默。