I/O复用(select、poll、epoll) :阻塞在select、poll、epoll上,等待多个描述符就绪,返回其中一个
信号(SIGIO) :首先开启套接字的信号驱动式I/O功能(并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
异步I/O:上述I/O都是同步I/O。异步I/O工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到用户空间缓冲区)完成后通知。这种模型与信号驱动模型的主要区别在于:
- 信号驱动式I/O是由内核通知何时可以启动一个I/O操作
- 而异步I/O模型是由内核通知I/O操作何时完成
同步I/O和异步I/O对比
- 同步I/O操作(synchronous I/O opetation)导致请求进程阻塞,直到I/O操作完成;
- 异步I/O操作(asynchronous I/O opetation)不导致请求进程阻塞。
select/poll
select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合:
- 然后调用 select 函数将文件描述符集合拷贝 到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写,。
- 接着再把整个文件描述符集合拷贝 回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合 ,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
select 和 poll 的区别
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024
,只能监听 0~1023 的文件描述符。
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。
epoll
epoll 通过两个方面,很好解决了 select/poll 的问题。
- epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字 ,把需要监控的 socket 通过
epoll_ctl()
函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是O(logn)
,通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
- epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件 ,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用
epoll_wait()
函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET) 和水平触发(level-triggered,LT) 。
- 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
- 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。
多路复用 API 返回的事件并不一定可读写的,如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况。
描述符就绪条件
-
读就绪
- 套接字接收缓冲区有数据,对这样的套接字执行读操作不会阻塞并将返回一个大于0的值(读入的字节数)。
- 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)。
- 该套接字是一个监听套接字且已完成的连接数不为0,accept不会阻塞。
- 其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把
errno
设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
-
写就绪
- 套接字发送缓冲区有数据,写操作不阻塞且返回一个大于0的值(写入的字节数)
- 该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号
- 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终
- 其上有一个套接字错误待处理。
select处理对端TCP
-
如果对端TCP发送数据,那么该套接字变为可读,并且read返回一个大于0的值(即读入数据的字节数)。
-
如果对端TCP发送一个FIN(对端进程终止),那么该套接字变为可读,并且read返回0 (EOF)。
-
如果对端TCP发送一个RST(对端主机崩溃并重新启动),那么该套接字变为可读,并且read返回-1,而errno中含有确切的错误码。