一文详解IO多路复用!

I/O多路复用通过一种机制,可以监视多个描述符。

一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

select,poll,epoll都是IO多路复用的机制。

  • 但select,poll,epoll本质上都是同步I/O。

因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select:

它仅仅知道了,有 I/O 事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部)。

只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。

所以select 具有 O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

它是基于数组来存储的,它有最大连接数的限制。

poll:

poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态。

但是它没有最大连接数的限制,原因是它是基于链表来存储的

epoll:

epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述符。

把需要监控的 socket 通过 epoll_ct()函数加入到内核中的红黑树里。

  • 红黑树是个高效的数据结构,增删查一般时间复杂度都是 O(logn)

通过对这颗红黑树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合。

  • 只需要传入一个待检测的socket,减少了内核和用户空间大量的数据拷贝和内存分配。

epoll 使用事件驱动的机制,内核里面维护了一个链表来记录就绪事件。

  • 当某个 socket 有事件发生时,通过回调函数内核会将其将入到这个就绪事件列表中。

  • 当用户调用 epoll_wait() 时,只会返回有事件发生的文件描述符的个数。

    • 不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

epoll 相关接口的作用如下:

  • epoll_create()epoll_ctl()epoll_wait()系统调用。

image-20240130145523446

epoll 的方式监听的 Socket 数量越多的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常多。

  • 上限就为系统定义的进程打开的最大文件描述符个数。

epoll 支持两种事件触发模式,分别是边缘触发和水平触发。

边缘触发

使用边缘触发时,当被监控的 Socket 描述符有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次。

  • 即时进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此程序要保证一次性将内核缓冲区的数据读取完。

水平触发

使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务端不断地从 epoll_wait 中苏醒。

  • 知道内核缓冲区数据被 read 函数读完才结束,目的是告诉有数据需要读取。

select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。