第 15 期 - IO 多路复用技术全解析(select/poll/epoll)
logoFRONTALK AI/11月7日 16:31/阅读原文

摘要

文章先介绍传统 BIO 在处理大量客户端请求时的性能问题,引出 IO 多路复用技术,然后分别阐述 NIO、select、poll、epoll 的执行过程、优缺点,还对同步、异步、阻塞、非阻塞概念进行解释,最后探讨已有 epoll 为何还有系统用 select 的原因。

一、BIO 的原理与问题

BIO 是传统的同步阻塞 I/O 模型。当进行输入输出操作时,如果读写操作未完成,执行 IO 的线程会被阻塞住,直到数据读写完成。在 BIO 模型中,有一个独立的 Acceptor 线程负责接收客户端连接请求,每接收到一个连接请求,就会为其创建一个新的线程处理,处理完后销毁线程。这是典型的一请求一应答模型,适用于低并发场景。例如在其流程中,服务端发起read调用后进行上下文切换,如果文件标识符处于读未就绪状态,当前线程就会一直挂起,直到一系列的数据拷贝操作完成才能解除阻塞进行业务处理,而且read函数在阻塞过程中无法接受其他客户端请求。这种模型在大量客户端请求时,服务性能会急剧下降,可能导致资源不足无法继续提供服务。

二、NIO 的原理与特点

NIO 可以支持多个客户端请求复用一个线程,是一种多路复用 I/O,相比 BIO 提供了非阻塞的通信方式,能处理更多客户端链接。其整体流程为服务端发起read调用时,即使没有数据也不会阻塞而是返回 -1;当客户端传输数据到服务端网卡设备,数据经一系列拷贝操作后,在数据到达内核缓冲区之前是非阻塞的,后续从内核缓冲区拷贝到用户缓冲区之后才会解除阻塞进行业务处理。NIO 内部有一个Selector类,通过底层selectpollepollkqueue等来实现多路复用,整体过程包括创建Selector对象、为通道注册感兴趣事件、执行Selectorselect方法(阻塞直到有事件发生),一旦有事件发生,select方法会返回发生事件的通道数量。

三、select 的原理与缺点

select 是 NIO 中多路复用的具体实现,能够同时监视多个文件描述符是否就绪,只有监听的文件描述符就绪或者超时了相应进程/线程才会被唤醒。其方法int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)各参数有特定意义,返回值为准备好的描述符个数。在执行流程方面,会将所有描述符从用户态拷贝到内核态等待,内核检测fd_set中的fd是否就绪/超时,有数据时标识置为 1,文件描述符集合返回给用户态后唤醒等待队列中的进程/线程,用户态再循环遍历查看就绪的fd。但是 select 存在一些问题,例如在 32 位操作系统bitmap大小一般为 1024,64 位操作系统为 2048;fd_set不可重复使用每次需新建;用户态和内核态需要反复拷贝fd_set;返回值是就绪描述符个数,需再次遍历,时间复杂度为O(n)

四、poll 的原理与局限

poll 模型在一定程度上解决了 select 的部分问题。int poll (struct pollfd *fdarray, unsigned long nfds, int timeout),其中pollfd结构体包含fd(文件描述符)、events(需要关注的事件)、revents(对events的回馈)。其工作原理和 select 类似,主要优化在fd上。执行流程是将进程/线程全部描述符封装成pollfds一次性从用户态拷贝到内核态,内核遍历查看是否有就绪的fd,有就绪fd时更改revents标识并返回就绪fd数量给用户态,用户态循环遍历fd进行处理。poll 解决了 select 中bitmap大小限制和fd_set不可复用的问题,但仍然存在频繁拷贝pollfds、每次返回值需重新遍历查看具体哪个fd就绪、用户态和内核态需要反复拷贝等问题,且时间复杂度为O(n)

五、epoll 的原理与优点

epoll 是select/poll的增强版本。在使用时,会先创建epoll,当客户端请求过来时创建event并放进epoll中,然后等待唤醒。其流程包括调用epoll_create创建eventpoll;调用epoll_ctl创建epitem并赋值到eventpoll;调用epoll_wait,先检查就绪列表中是否有就绪的fd,没有则阻塞等待并把当前进程/线程放到等待队列中。当有数据到达时,会触发epoll_event_callback函数处理相关操作。epoll 有很多优点,例如通过红黑树和就绪链表高效管理文件描述符和触发事件;能够有效处理大量并发连接,不会随文件描述符数量增加而降低效率;采用事件驱动的 I/O 模型,仅对活跃描述符操作,减少资源浪费;除了水平触发还提供边缘触发,用户空间程序可缓存 IO 状态减少epoll_wait调用提高效率。

六、同步、异步、阻塞、非阻塞概念

  1. 同步阻塞:例如视频中心提供一个接口,每次调用需等待视频中心处理完成并返回处理后的视频,再存储起来。
  2. 同步非阻塞:视频中心提供两个接口,提交任务后可做别的事,但要不断轮询查询视频中心获取视频并存储。
  3. 异步非阻塞:视频中心提供一个接口,调用后直接返回,视频中心会把处理完的视频放到给定地址。
  4. 异步阻塞:视频中心提供一个接口,调用后视频中心把处理完的视频放到给定地址,直到存储完再返回成功。 总结来说,阻塞/非阻塞指当前线程/进程是否需要等待;同步/异步指结果是否需要当前线程/进程参与。

七、关于 IO 多路复用相关问题的解答

  1. 为什么要有 IO 多路复用:传统 BIO 每来一个请求就分配一个进程(线程),随着用户增长,为维护海量用户需要海量服务器,成本巨大,所以出现了 IO 多路复用技术。
  2. 不同 IO 多路复用解决的问题
    • select:解决每个客户端请求都创建一个链接造成的资源浪费问题。
    • poll:解决了 select 中fd_set无法复用、最大链接有限制的问题。
    • epoll:解决了需要频繁拷贝fd,以及在用户态需要遍历全部fd查询哪个已经就绪(时间复杂度为O(1))的问题。
  3. 为什么已有 epoll 还有系统用 select
    • 时间精度:select 函数中的超时时间是微秒,而 poll、epoll 是毫秒,select 时间更精确。
    • 平台支持:select 绝大部分平台都支持,而 epoll 是 linux2.6 推出的,像 mac os 不支持而是采用kqueue
    • 复杂度与适用场景:epoll 的常数时间复杂度高,需要维护红黑树、双向链表等;如果是很少量线程更适合使用 select,因为线程很少时N几乎是常量级,并且来回复制的fdset也不大。

八、零拷贝相关

IO 相关除了 IO 多路复用,零拷贝也是面试常考察的重点,提供了零拷贝文章链接:juejin.cn/post/740403 …

 
Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有