IO多路复用
多路指多个文件描述符,复用指使用一个线程。
IO多路复用通俗的理解就是一个线程监视多个文件描述符****,一旦某个文件描述符就绪,就通知应用程序对其进行读写操作。
select
select会将三个fd_set文件描述符集合(bitMap),即读集合(readfds)、写集合(writefds)和异常集合(exceptfds),拷贝进内核,内核会遍历整个bitMap,并标记bitMap中感兴趣的文件描述符(待监听的文件描述符,值为1的),若有对应事件发生标记为1否则标记为0,然后将修改过的bitMap元素从内核拷贝到用户态中,并返回就绪的文件描述符个数,最后用户态程序遍历整个集合找出发生了事件的文件描述符并进行响应处理。
优点
- 跨平台。select由操作系统内核提供实现,各操作系统(如 Linux、Windows、macOS 等)都提供了与其兼容的内核底层实现。
- 简单易用。通过接受三个位图集合来监听文件描述符的状态变化,简单易用。
不足
- 操作系统默认文件描述符表大小位1024,所以这对select能监听的文件描述符数量有所限制。
- 用户态程序需要自己遍历整个文件描述符集合才能获得就绪的文件描述符。
- 发生两次数据拷贝,无法避免大规模数据拷贝问题。
poll
poll是一种基于事件驱动的多路复用机制,通过轮询并缓存就绪文件描述符,实现高并发IO操作。
poll不再采用selcet的位图数据结构,转而使用pollfd数据结构。
pollfd由三部分构成:fd表示待监听的文件描述符,events表示感兴趣的事件(可读、可写、异常),revents表示实际发生的事件类型(由内核填充完成)。
poll执行的具体过程与select基本一致:
- 先将pollfd数组拷贝进内核
- 内核会遍历pollfd并将就绪pollfd的revents置为对应的事件(可读、可写、异常)
- 然后将就绪的pollfd拷贝会内核态,并返回就绪的事件个数
- 最后由用户态程序遍历整个pollfd数组检查revents字段获得并处理就绪的事件
优点
- 无需遍历多个监测集合来查找就绪事件。
- poll从理论上突破了文件描述符个数1024的限制。
不足
- 发生两次数据拷贝,无法避免大规模数据拷贝问题。
- poll 虽然可以处理的文件描述符数量没有上限,但是它仍然会受到系统限制而无法监测所有的文件描述符。
epoll
epoll是基于
epoll的执行过程:
- epoll在内核中维护了一个文件描述符的集合(采用红黑树结构,可以高效的维护文件描述符,增删查一般时间复杂度是 O(logn)),用户态无需每次重新传入,只需要告诉内核修改的部分即可,可以大幅度减少内核和用户空间之间的数据拷贝的资源消耗
- epoll采用的是事件驱动机制,不需要通过轮询来找到对应的那个文件描述符。当某个文件描述符就绪的时候,会通过回调函数,把它放到一个就绪链表中记录起来,内核就不用遍历文件描述符了
- 当epoll_wait触发时,内核只返回就绪的文件描述符集合,也就是上面提到的那个就绪链表,因为返回给用户态的肯定是已经就绪了的,所以用户态可以拿来即用,也无需做多余的遍历
epoll支持两种触发模式,分别是边缘触发(ET)和水平触发(LT)
-
边缘触发ET
被监控的文件描述符上有可读/可写事件发生时,服务端只会从epoll_wait中苏醒一次。即使没有调用read从内核读取数据,也依然苏醒一次,所以要保证一次性将内核数据读完。 -
水平触发LT
被监控的文件描述符上有可读/可写事件发生时,服务端会从epoll_wait中不断苏醒,直到内核缓冲区数据被读完。
边缘触发可以减少epoll_wait的调用次数,所以边缘触发的效率比水平触发效率高。
一般边缘触发搭配非阻塞式IO。因为若搭配阻塞IO, 对于一个处于阻塞状态的文件描述符,如果它已经准备好了读或写,但是在调用 read 或 write 函数时却被阻塞住了,那么下一次该文件描述符是否就绪都不会被通知。这就会导致程序不能及时处理已经准备好的事件。
select和poll都属与水平触发。
优点
- 内核通过红黑树维护一个文件描述符集合,减少了内核和用户态之间的数据拷贝,提高了效率和资源利用率
- 基于事件驱动机制,不需要通过轮询来找到对应的那个文件描述符
- 支持同时监控大量的文件描述符,没有描述符数量的限制,可以随着系统需要而扩展。
不足
不具备跨平台性,只适用与linux系统