目录

IO 多路复用(Reactor)

IO 多路复用(Reactor)

IO 多路复用技术是为实现单线程处理多请求连接,减少系统因频繁的创建线程或进程而产生的资源消耗,这里的复用特指同时使用单一线程

Linux 下的 select、poll、epoll 为 IO 多路复用的具体实现。当客户端与服务端的 socket 连接建立之后,程序将该 socket 文件描述符注册到 epoll,然后返回,最终交由 epoll 去管理。

epoll 可以同时监听多个文件描述符,当某个或某些文件描述符就绪,则通知程序进行相应的读写操作,否则会一直阻塞直到有文件描述符就绪。我们使用epoll编程时,会设置 socket 非阻塞模式。所以,IO多路复用是同步非阻塞 IO。

select、poll、epoll 比较

select

优点

  • select 目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

缺点

  • 每次调用 select 都需要把文件描述符(FD)从用户态拷贝到内核,开销比较大
  • 每次都需要在内核遍历所有传入的文件描述符(FD)
  • select 单个进程能够监视的文件描述符的数量存在最大限制,默认是1024,比较小。当然,也可以通过修改宏定义改掉,但这会造成效率的降低。

poll

poll 即轮训,poll 和 select 本质上是一样的,只是描述 fd 集合的方式不同。poll 使用的是 pollfd 结构,select 使用的是 fd_set 结构。

epoll

epoll 是对 select 和 poll 的改进,而且改正了 select、poll 的三个缺点和不足。相对于 select 和 poll 来说,epoll 更加灵活,没有描述符限制。

epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。

优点

  • 每次注册新事件到 epoll 句柄都会把所有的 fd 拷贝进来,而不是在 epoll_wait 中重复拷贝,这样确保 fd 只会被拷贝一次
  • epoll 不是像 select/poll 那样每次都把 fd 加入等待队列,epoll 把每个 fd 指定一个回调函数,当设备就绪时,唤醒等待队列的等待者就会调用其它的回调函数,这个回调函数会把就绪的 fd 放入一个就绪链表。epoll_wait 就是在这个就绪链表中查看有没有就绪 fd。
  • epoll 没有 fd 数目限制

缺点

  • 如果没有大量的 idle-connection 或者 dead-connection,epoll 的效率并不会比 select/poll 高很多,但是当遇到大量的 idle-connection,就会发现 epoll 的效率大大高于 select/poll。

模式

  • 水平触发(level-triggered):满足状态时触发

当被监控的文件描述符上有可读写事件发生时,epoll_wait() 会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait() 时,它还会通知你在上次没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!

  • 边缘触发(edge-triggered):状态改变时触发

当被监控的文件描述符上有可读写事件发生时,epoll_wait() 会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait() 时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你去读写余下的数据!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!

例如一个 socket 经过长时间等待后接收到一段 100k 的数据,两种触发方式都会向程序发出就绪通知。假设程序从这个socket 中读取了 50k 数据,并再次调用监听函数,水平触发依然会发出就绪通知,而边缘触发会因为 socket “有数据可读”这个状态没有发生变化而不发出通知且陷入长时间的等待。

总结

  • select,poll 实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用 epoll_wait 不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程。虽然都要睡眠和交替,但是 select 和 poll 在“醒着”的时候要遍历整个 fd 集合,而 epoll 在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。这就是回调机制带来的性能提升。

  • select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(在 epoll_wait 的开始,注意这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列)。这也能节省不少的开销。