文章目录
一、Select 概述
Select 是 Linux 操作系统中最早出现的 I/O 多路复用技术之一。它允许一个进程同时监控多个文件描述符(File Descriptor, 简称 fd)的 I/O 状态变化,例如:
- 某个 socket 上是否有数据可读;
- 某个文件是否可写;
- 是否发生了异常事件。
核心思想:通过一个系统调用
select()就能同时等待多个文件描述符的 I/O 事件。
设计哲学
Select 的设计遵循“简单优先”的哲学——通过位图(fd_set)表示被监听的文件描述符集合,内核每次遍历并检测是否有事件发生,最终再把结果返回。
二、Select 整体调用流程
下图展示了 Select 在用户态与内核态之间的完整工作过程:
三、kern_select 参数验证与初始化
kern_select 是 Select 的内核入口函数,负责参数校验、内核数据结构分配与初始化。
🧩 核心结构体
poll_wqueues:用于管理 poll/select 的等待队列。select_table:连接用户态位图和内核 poll 机制的数据中枢。
四、do_select() 核心实现流程
do_select() 是整个 Select 的核心逻辑。其任务:遍历 fd_set 中所有被设置的位,对每个 fd 调用对应文件的 poll 方法以检测状态。
五、fd_set 位图结构详解
Select 使用固定大小的位图来表达所有需要监听的文件描述符集合。
在 64 位系统上:1024 / 64 = 16 个 unsigned long。
每一个 bit 表示一个文件描述符是否被加入监听。
六、文件描述符处理与 poll 调用链
内核会调用每个文件描述符对应的 poll 方法。不同资源类型对应不同的 poll 函数:
这些 poll 函数都会返回一个事件掩码,如:
EPOLLIN:可读;EPOLLOUT:可写;EPOLLERR:异常。
七、等待机制实现
当所有监控的 fd 暂无事件发生时,Select 会将当前进程挂起至等待队列,直到事件触发、超时或信号到来。
八、用户态事件处理示例
用户程序通过 select() 获取已就绪的文件描述符,并进行对应操作:
int maxfd = 0;
fd_set read_fds, write_fds, except_fds;
struct timeval timeout = {5, 0}; // 5秒超时
FD_ZERO(&read_fds);
FD_SET(sockfd, &read_fds);
maxfd = sockfd + 1;
int ready = select(maxfd, &read_fds, &write_fds, &except_fds, &timeout);
if (ready > 0) {
for (int i = 0; i < maxfd; i++) {
if (FD_ISSET(i, &read_fds)) read_data(i);
if (FD_ISSET(i, &write_fds)) write_data(i);
if (FD_ISSET(i, &except_fds)) handle_error(i);
}
} else if (ready == 0) {
printf("Select超时,无事件发生。\n");
} else {
perror("select失败");
}
九、性能瓶颈分析
🔍 瓶颈总结:
- 每次调用都要重新构建
fd_set; - 最多只能同时监听 1024 个文件描述符;
- 任何时间复杂度为
O(n)的遍历在高并发下都会成为性能瓶颈。 - select 从调用到返回,用户态与内核态之间的内存拷贝共发生2次:
- 调用时的拷贝:用户态将待监听的 fd_set 拷贝到内核态,内核基于此集合监听文件描述符的就绪事件;
- 返回时的拷贝:内核将更新后的 fd_set(仅保留就绪的文件描述符)拷贝回用户态,供用户程序遍历判断。
十、与 Poll、Epoll 的对比
| 特性 | Select | Poll | Epoll |
|---|---|---|---|
| 时间复杂度 | O(n) | O(n) | O(1) |
| 事件通知方式 | 轮询 | 轮询 | 回调驱动 |
| 文件描述符限制 | 1024 | 理论无限 | 理论无限 |
| 拷贝开销 | 每次调用 | 每次调用 | 仅修改时 |
| 适用场景 | 低并发、简单程序 | 中等并发 | 高并发、网络服务器 |
6395

被折叠的 条评论
为什么被折叠?



