IO Multiplexing | IO多路复用
IO多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select,poll,epoll都是IO多路复用的机制。值得一提的是,epoll仅对于Pipe或者Socket这样的读写阻塞型IO起作用,正常的文件描述符则是会立刻返回文件的内容,因此epoll等函数对普通的文件读写并无作用。
首先来看下可读事件与可写事件:当如下任一情况发生时,会产生套接字的可读事件:
- 该套接字的接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的大小;
- 该套接字的读半部关闭(也就是收到了FIN),对这样的套接字的读操作将返回0(也就是返回EOF);
- 该套接字是一个监听套接字且已完成的连接数不为0;
- 该套接字有错误待处理,对这样的套接字的读操作将返回-1。
当如下任一情况发生时,会产生套接字的可写事件:
- 该套接字的发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的大小;
- 该套接字的写半部关闭,继续写会产生SIGPIPE信号;
- 非阻塞模式下,connect返回之后,该套接字连接成功或失败;
- 该套接字有错误待处理,对这样的套接字的写操作将返回-1。
select,poll,epoll本质上都是同步IO,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步IO则无需自己负责进行读写,异步IO的实现会负责把数据从内核拷贝到用户空间。select本身是轮询式、无状态的,每次调用都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。epoll则是触发式处理连接,维护的描述符数目不受到限制,而且性能不会随着描述符数目的增加而下降。
方法 |
数量限制 |
连接处理 |
内存操作 |
select |
描述符个数由内核中的FD_SETSIZE限制,仅为1024;重新编译内核改变FD_SETSIZE的值,但是无法优化性能 |
每次调用select都会线性扫描所有描述符的状态,在select结束后,用户也要线性扫描fd_set数组才知道哪些描述符准备就绪(O(n)) |
每次调用select都要在用户空间和内核空间里进行内存复制fd描述符等信息 |
poll |
使用pollfd结构来存储fd,突破了select中描述符数目的限制 |
类似于select扫描方式 |
需要将pollfd数组拷贝到内核空间,之后依次扫描fd的状态,整体复杂度依然是O(n)的,在并发量大的情况下服务器性能会快速下降 |
epoll |
该模式下的Socket对应的fd列表由一个数组来保存,大小不限制(默认4k) |
基于内核提供的反射模式,有活跃Socket时,内核访问该Socket的callback,不需要遍历轮询 |
epoll在传递内核与用户空间的消息时使用了内存共享,而不是内存拷贝,这也使得epoll的效率比poll和select更高 |