目录
啥是IO复用
- 在传统阻塞 IO 模式下,每个线程会阻塞等待一个 fd(文件描述符或套接字描述符等)的就绪状态,导致需要大量的线程,资源消耗高。
- IO复用,就是一个线程可以监视多个 fd,一旦某个 fd 准备就绪(可读、可写或异常),就可以对其进行对应的 IO 操作;就不用每个 fd 都开个线程去等着,做到高效的处理更多IO请求。
有了IO复用的需求,接下来我们看看在Linux下如何实现
select
- select 是把所有的文件描述符列表交给内核,让内核帮忙检查哪些文件有事件发生
- 它就像是个小本子,每次让系统内核帮你看着的时候,都要把小本子的内容从你的地方(用户态)抄一遍给系统(内核态)
- 过程:
- 用户程序将需要监听的文件描述符集合(fd_set)传给内核。
- 内核会轮询所有文件描述符,检查它们是否有事件发生。
- 如果有事件发生,select 返回,用户程序需要手动遍历 fd_set 找出触发事件的文件。
- 每次调用 select 都需要重新设置 fd_set,因为返回后 fd_set 的内容会被内核修改。
- select 还有文件描述符数量限制,一般是最大只能监听 1024 个(可以通过修改 FD_SETSIZE 来增加)
poll
- 跟 select 类似,也需要把所有文件交给内核;不过它的机制更灵活(用一个结构体数组)
- poll 有点像升级版的 select,不过没有 select 的 1024 个文件描述符的限制
- 过程:
- 用户程序将文件描述符和对应事件的数组传给内核。
- 内核轮询数组中的所有文件描述符,检查是否有事件发生。
- 如果有事件发生,poll 返回,用户程序需要遍历数组,找出哪些描述符触发了事件。
- 与 select 不同,poll 的数组内容不会被内核清空,可以直接重复使用。
epoll
- epoll 就聪明多了,你把要关注的文件告诉 epoll,它就在自己的地方(内核里)记下来;当有数据来了,它会自动把消息放到一个队列里,你只需要去队列里拿消息就行。
- 过程:
- 使用 epoll_create 创建一个 epoll 实例,内核为其分配一个数据结构来管理文件描述符。
- 通过 epoll_ctl 将要监听的文件描述符添加到 epoll 实例中。
- 内核维护一个内部的 事件队列,当有事件发生时,内核会将该文件描述符的事件加入队列中。
- 程序调用 epoll_wait 来获取队列中已经准备好的文件描述符和它们的事件
为啥 select 和 poll 需要遍历
select
- Select 使用 fd_set 数据结构来管理文件集合,fd_set 本质上是一个位图,当调用select函数后,内核会检查这些文件描述符的状态,将就绪的文件描述符在位图中标记出来。
- Select 函数返回后,不会直接告诉你是哪些描述符就绪,所以应用程序需要遍历之前设置的 fd_set 集合,通过 FD_ISSET 宏来逐个检查每个文件描述符是否在就绪集合中。
poll
- poll 使用 pollfd 结构体数组来管理文件描述符。每个pollfd结构体包含文件描述符、要监听的事件和实际发生的事件等信息
- 当poll函数返回后,内核会在每个 pollfd 的 revents 字段中标记出实际发生的事件,但是 poll 不会直接告诉你具体是哪个文件描述符的哪个事件就绪了,所以应用程序需要遍历整个 pollfd 数组,检查每个 pollfd 的 revents
epoll
- epoll 不需要遍历
- 因为,内核在文件描述符就绪时,会主动将其放入一个就绪队列中,用户程序调用 epoll_wait 时,直接从这个就绪队列取,所以,无需遍历其它文件描述符。
为什么用 select 和 poll 时·内核·需要去轮询
历史原因
- select 是一种非常早期的设计,当时的需求和场景相对简单,轮询的的成本在当时是可以接受的。
- poll 是对 select 的改进,支持更大的文件描述符集合,但仍需轮询。
- 随着系统并发量的增加和对性能要求的提高,epoll 顺时而生。
调度机制
- 核心问题:
- 硬件中断可以告诉内核“某个设备有新数据到达了”,但内核还需要知道这个设备是对应哪个文件描述符(多个文件描述符可能指向相同类型的设备)
- 而 select 和 poll 不保存与文件描述符的长期关联关系
- 内核在硬件中断或事件发生时,无法主动通知是哪些文件描述符发生了变化
- 所以它们必须从用户传递过来的文件描述符集合中一个一个去检查。
epoll 如何知道是哪个文件描述符有变化?
- 在调用 epoll_ctl 注册文件描述符时,内核会将文件描述符和监听事件关联起来
- 内核会为每个文件描述符绑定协议栈或驱动中的回调函数
- 当设备状态变化(如网卡接收数据)时,硬件中断触发回调函数,通知协议栈解析,定位到具体的文件文件描述符
- 如果文件描述符的状态满足监听条件,内核会将其加入 epoll 的事件队列
所以,主要原因就是在用 select 和 poll 时,内核并没有利用协议栈和驱动的回调机制,这意味着,内核在收到硬件中断时,无法定位到具体的文件描述符,必须轮询来确认。
总结
- select 和 poll:需要用户态和内核态频繁交互,且依赖内核轮询,效率较低
- epoll:借助回调机制和就绪队列,避免了内核轮询和用户态遍历,大幅提升性能