IO Multiplexing

IO Multiplexing

Tip

该笔记主要关注网络 I/O,关于 5 种 IO 模型的介绍,参考IO模型

Select

select 是一种 I/O 多路复用 机制,用于同时监听多个文件描述符上的 I/O 事件就绪情况,如可读、可写或异常状态。

1
2
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

判断条件可参考 UNIX 网络编程卷一 6.3.1 章节,下图汇总了相关条件:

select返回就绪条件

timeout

timeout 参数用于设置等待时间,有三种方式:

  • 不等待:轮询文件描述符后立即返回,timeval 中指定时间为 0
  • 等待直到有文件描述符就绪:timeval 中指定等待时间,select 会阻塞直到有文件描述符就绪或超时
  • 无限等待:timeval 设置为 NULL,select 会一直阻塞直到有文件描述符就绪
struct timeval

1
2
3
4
5
 struct timeval {
     long tv_sec;  // seconds, 秒
     long tv_usec; // microseconds, 微秒
 };

Fd Set

fd_set 通常是一个位图(bitmap),在内部实现为一个长整型数组。数组中的每一个比特位(bit)对应一个文件描述符

这个位图的大小是固定的,由常量 FD_SETSIZE 决定(在 Linux 上通常是 1024),这个在Linux内核中是硬编码的,修改改 fd_set 大小需要改源码、重新编译 glibc 和内核

在编写 C 代码时,可以使用下面的宏来操作 fd_set

1
2
3
4
FD_ZERO(fd_set *fdset);          // clear all bits in fdset
FD_SET(int fd, fd_set *fdset);   // turn on the bit for fd in fdset
FD_CLR(int fd, fd_set *fdset);   // turn off the bit for fd
FD_ISSET(int fd, fd_set *fdset); // is the bit for fd on?
Example

例如,我们希望监听文件描述符 3、5 和 8 的可读事件,可以这样设置 fd_set

1
2
3
4
5
6
7
8
9
 fd_set read_fds;

 FD_ZERO(&read_fds);
 FD_SET(3, &read_fds); // 监听 fd 3
 FD_SET(5, &read_fds); // 监听 fd 5
 FD_SET(8, &read_fds); // 监听 fd 8

 select(9, &read_fds, NULL, NULL, &timeout);

当 select 执行完成后,会返回就绪文件描述符的总个数,并相应的修改 fd_set,将就绪的文件描述符的位设置为 1。为此,每次重新调用 select 前,通常需要再次设置 fd_set

select工作流程

nfds

nfds(number of file descriptors)通常设置为传入的文件描述符集合中最大的文件描述符加 1,即:

$$
\text{nfds} = \max(\text{MaxReadFd}, \text{MaxWriteFd}, \text{MaxExceptFd}) + 1
$$

Unix 网络编程卷一指出,“存在这个参数以及计算其值的额外负担纯粹是为了效率原因”。理解该参数的意义,首先假设没有 nfds 参数:

select 函数会将整个 1024-bit 位图(fd_set)复制到内核空间,在函数执行结束后再将位图复制到用户空间,这部分开销是略大的。

而一个普通进程要检查的文件描述符通常远小于 1024 个。仍以上面的代码为例,我们只关心 3, 5, 8 这三个文件描述符,后面的 1015 个 bit 并不关心,这部分数据的传输是不必要的。

通过 nfds 参数,select 可以不必拷贝完整的 1024-bit 位图,而是仅根据 nfds 决定拷贝到哪个字节为止,以节省性能和内存带宽。

Poll

poll 提供了与 select 相似的功能,内核中也是线性扫描数组,时间复杂度仍为 $O(n)$。

区别一方面是传入的 fds 大小可以自行更新(仍受系统资源限制);另外就是 API 简洁一些。

Warning

在 Linux 系统中,每个进程能打开的文件描述符数量是受限的,默认值通常为 1024,具体值可通过 ulimit 命令查看和修改。

select 的文件描述符上限还受限于实现层面的硬编码(通常为 FD_SETSIZE,默认 1024),即使系统允许更多 fd,select 依然无法支持。

1
2
3
4
5
6
 # 查看当前进程可打开的最大文件描述符数量
 ulimit -n

 # 查看系统级别允许的最大文件总数(所有进程合计)
 cat /proc/sys/fs/file-max
1
2
3
4
5
6
7
int poll(struct pollfd *fds, unsigned long nfds, int timeout);

struct pollfd {
    int fd;         // 文件描述符
    short events;   // events of interest on fd
    short revents;  // events that occurred on fd
};

events 每个 bit 代表一种事件,和 select 类似可以划分为读、写、异常三个类别。shell 中运行 man poll 查看更具体的事件类型划分。

revents 在调用 poll 后会被内核填充,表示实际发生的事件。

Epoll

epoll 是 Linux 特有的 I/O 多路复用机制,专为处理大量文件描述符而设计。它比 selectpoll 更高效,尤其在处理大量并发连接时。对 epoll 的详解可以参考图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的!

特性selectpollepoll
接口标准POSIXPOSIXLinux 特有
fd 数量限制受限(一般 1024)无限制无限制
fd 表达方式位图数组内核结构
是否复制 fd 集合每次都要每次都要注册一次
扫描复杂度O(n)O(n)O(1)~O(活跃 fd 数)

Ref

作者

GnixAij

发布于

2025-07-19

更新于

2025-08-11

许可协议

评论