【Linux】I/O 轮询技术 - select

Posted by 西维蜀黍 on 2021-09-29, Last Modified on 2025-04-23

select

select 是基于 I/O 多路复用模型。select 可以让内核在 "多个 fd 对应的 I/O 操作中任何一个 “就绪"(指数据已经被拷贝到 kernel space)或 "经过指定时间后",唤醒(wake)并通知用户线程(在唤醒之前,用户线程因为被阻塞而处于 sleep 状态)。比如:

  • 当 1、4 或 5 中任何一个 fd 的状态为可读时
  • 或当 4、7 中任何一个 fd 的状态为可写时
  • 或当 6、8 中任何一个 fd 的处理过程中抛出异常时
  • 或经过 10.2 秒后

注意,select 仅仅负责轮询工作:

  • 在调用 select() 之前,需要调用 read() 以发起一个读取 I/O 操作(此后才需要轮询操作);
  • 在调用 select() 之后,需要再次调用 read() 以将数据从内核空间读取到用户空间,并最终将数据返回给用户线程

#include <sys/select.h>

int select (int n,
			fd_set *readfds, 
            fd_set *writefds, 
            fd_set *exceptfds, 
            struct timeval *timeout);

FD_CLR(int fd, fd_set *set); 
FD_ISSET(int fd, fd_set *set); 
FD_SET(int fd, fd_set *set); 
FD_ZERO(fd_set *set);

n 是一个 int 类型,为值最大的 fd 的数值。比如我想监控 1、3、8、10 这四个 fd ,则 n 为 10。

总结

  • 它是在 read 的基础上改进的一种方案,通过对 fd 上的事件状态来进行判断;
  • 以同步的方式实现了 I/O 多路复用;
  • 调用 select() 时,需要指定三组期望被观察的 fd 集合(readfdswritefdsexceptfds)。

解释

  • 期望被观察的 fd 分为三组,对于 readfds,被包含在 readfds 集合中 fd 会被内核观察,当任何一个 fd 的状态变化为数据可读时,select() 函数被返回;类似地,对于 writefds,当这个集合中任何一个 fd 的状态变化为数据可写时,select() 函数被返回;对于 exceptfds,当其中的任何一个 fd 的处理抛出异常时;
  • select() 函数被返回时,三组 fd 集合会被修改,即 ** 只包含那些对应数据已经准备完成的 fd **。比如,readfds fd 集合中包含 7 和 9 两个 fd ,当 select() 函数被返回时,只有 fd 7 包含在新的 readfds fd 集合中(因为,此时只有 fd 7 对应的 I/O 操作完成了,即数据可用)。
  • select() 会返回在三组期望被观察的 fd 集合(readfdswritefdsexceptfds)中,已经准备就绪的 fd 的数量的总和。如果发生错误,则返回 - 1。
  • select() 中,如果仅仅检测一个值为 900 的 fd 时,内核需要从 0 开始扫描各个 fd ,直到第 900 个(在调用 select() 时,需要传入值最大的 fd 的数字)

更多细节请查询《Linux System Programming Talking Directly to the Kernel and C Library》P53。

Select 的缺点

  1. 每次调用 select,都需要把待监听的 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会非常大
  2. 每次调用 select 时 kernel 都需要线性扫描整个 fd_set,所以随着监控的描述符 fd 数量增长,其 I/O 性能会线性下降

poll 的实现和 select 非常相似,只是描述 fd 集合的方式不同,poll 使用 pollfd 结构而不是 select 的 fd_set 结构,poll 解决了最大文件描述符数量限制的问题,但是同样需要从用户态拷贝所有的 fd 到内核态,也需要线性遍历所有的 fd 集合,所以它和 select 只是实现细节上的区分,并没有本质上的区别。

Reference