I/O多路复用之select、poll、epoll
I/O多路复用可以在单线程的情况下,同时监听多个描述符fd,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
但是select、poll、epoll本质上都是同步I/O,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,相对于异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
select
select是POSIX规定,基本上所有平台均可支持。
select函数监视的文件描述符分为三类,分别是writefds、readfds、exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、except)或者超时(timeout指定等待时间,如果立即返回设为null)返回。当select函数返回后,可以通过遍历fdset来找到就绪的描述符。(因此时间复杂度是O(n))
存在缺点:
- 单个进程能够监视的文件描述符的数量存在最大限制,Linux上FD_SETSIZE为1024
- 对socket扫描是轮询方式、效率低
- 需要维护一个用来存放大量fd的数据结构,用户空间和内核空间在传递该结构时复制开销大(从用户空间拷贝到内核空间的问题)
poll
相对比select,poll没有最大连接数的限制,原因是采用链表方式来存储。同样存在如下缺点:
- 大量的fd数组被整体复制于用户态内核态空间之间
- poll有LT(水平触发)的特点,如果报告了fd后,未被处理那么下次poll时还会再次报告该fd
- 线性遍历fd集合,复杂度为O(n)、效率低下
epoll
epoll为Linux特有的,其它平台有类似库的实现,比如BSD中的kqueue、Windows中的iocp。
epoll基于事件驱动方式,通过epoll_ctl注册fd事件(EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)并且注册callback回调函数,一旦某fd就绪通过激活回调函数,epoll_wait则可只返回发生的事件避免了轮询的过程,时间复杂度为O(1)。
epoll优点如下:
- 没有最大连接数fd限制
- 通过事件驱动方式,不需要通过轮询遍历的方式,只管”活跃“的连接,才会调用callback函数
- 内存拷贝,通过内核空间和用户空间共享同一块mmap()内存实现
- 支持LT(水平触发、默认的模式):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,下次调用epoll_wait时,会再次响应应用程序并通知此事件;
- 支持ET(边缘触发):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件,由于它只会通知一次,直到下次再有数据流入前都不会再通知,所以在ET模式下,read一个fd时应该把它buffer读完 或 一直读到read的返回值小于请求值 或 遇到EAGAIN错误信息。
对fds集合进行增删改操作时,在Linux2.6.8内核之前,epoll采用hash来组织fds集合,在epoll_create(int size)内核根据size来分配hash的大小。在Linux2.6.8之后的内核中,epoll采用红黑树来组织监控fds集合,则size实际没有意义。