news 2026/5/8 16:10:30

深入理解select:I/O多路复用基础

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解select:I/O多路复用基础

引言

在传统的多进程/多线程服务器模型中,每一个客户端连接都需要一个独立的进程或线程来处理。当客户端数量较少时,这种方式工作良好。但当并发连接数达到成千上万时,进程/线程的创建、销毁和切换开销会急剧增加,导致服务器性能严重下降。

I/O 多路复用就是为了解决这个问题而生的。它的核心思想是:用一个线程同时监控多个文件描述符,只处理那些已经就绪的(有数据可读、可写等)描述符

I/O 多路复用允许单线程同时处理多个 I/O 操作,避免了为每个连接创建独立进程/线程的开销。Linux 系统提供了三种 I/O 多路复用机制:selectpollepoll。今天,我们将从最基础的select开始讲解。


第一部分:select 函数基础

一、函数原型

#include <sys/select.h> #include <sys/time.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

二、参数详解

参数类型作用
nfdsint监控的最大文件描述符值 + 1
readfdsfd_set*监控读事件(数据可读、新连接到达)
writefdsfd_set*监控写事件(缓冲区空闲、可发送数据)
exceptfdsfd_set*监控异常事件(通常设为 NULL)
timeouttimeval*超时时间(NULL表示永久阻塞)

三、返回值

返回值含义
>0就绪的文件描述符数量
=0超时(没有就绪的描述符)
=-1调用失败(可通过 errno 获取错误码)

四、timeout 结构体

struct timeval { time_t tv_sec; // 秒 suseconds_t tv_usec; // 微秒(1秒 = 1,000,000微秒) }; // 使用示例 struct timeval tv; tv.tv_sec = 5; // 等待5秒 tv.tv_usec = 0; // 0微秒

timeout 参数的特殊值:

含义
NULL永久阻塞,直到有描述符就绪
tv_sec = 0, tv_usec = 0非阻塞轮询,立即返回
tv_sec > 0等待指定时间后超时

五、fd_set 结构体

fd_set是一个位掩码集合,用于存储要监控的文件描述符。

// fd_set 的内部实现(简化理解) typedef struct { long int __fds_bits[__FD_SETSIZE / (8 * sizeof(long int))]; } fd_set; // 常量 #define FD_SETSIZE 1024 // select 能监控的最大描述符数量

相关操作宏:

作用
FD_ZERO(fd_set *set)清空集合
FD_SET(int fd, fd_set *set)将 fd 加入集合
FD_CLR(int fd, fd_set *set)将 fd 从集合中移除
FD_ISSET(int fd, fd_set *set)检测 fd 是否在集合中(就绪)

重要提醒:select返回后会修改传入的fd_set集合,只保留就绪的描述符。因此每次调用select前都需要重新设置监控集合。


第二部分:select 的就绪条件

一、读就绪条件

场景说明
套接字接收缓冲区有数据可读recv不会阻塞
监听套接字有新连接到达accept不会阻塞
对方关闭连接(收到 FIN)recv返回 0
发生异常需要进一步处理

二、写就绪条件

场景说明
套接字发送缓冲区有空间send不会阻塞
连接已建立可以发送数据

第三部分:select 的基本使用

一、监控标准输入

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/select.h> int main() { fd_set readfds; struct timeval tv; char buffer[128]; while (1) { // 1. 清空集合并添加标准输入 FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); // 2. 设置超时时间(5秒) tv.tv_sec = 5; tv.tv_usec = 0; // 3. 监控读事件 int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv); if (ret == -1) { perror("select error"); break; } else if (ret == 0) { printf("5秒内没有输入,超时!\n"); } else { // 检查标准输入是否就绪 if (FD_ISSET(STDIN_FILENO, &readfds)) { int n = read(STDIN_FILENO, buffer, sizeof(buffer) - 1); if (n > 0) { buffer[n] = '\0'; printf("输入内容: %s", buffer); } } } } return 0; }

二、nfds 参数详解

nfds参数的含义是:所有监控描述符中的最大值 + 1

// 示例:监控描述符 3、5、7 // 最大描述符是 7,所以 nfds = 7 + 1 = 8 // 示例:只监控标准输入(描述符 0) // nfds = 0 + 1 = 1

为什么需要这个参数?

内核需要知道要检测哪些描述符。如果nfds设置过小,可能导致部分描述符未被检测;设置过大则浪费性能(内核会检查到nfds-1)。


第四部分:select 实现多客户端服务器

一、数据结构设计

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/select.h> #define PORT 6000 #define MAX_CLIENTS 1024 #define BUFFER_SIZE 128 // 客户端描述符数组(管理所有连接) int client_fds[MAX_CLIENTS]; // 初始化描述符数组 void init_fds() { for (int i = 0; i < MAX_CLIENTS; i++) { client_fds[i] = -1; // -1 表示空闲 } } // 添加描述符 void add_fd(int fd) { for (int i = 0; i < MAX_CLIENTS; i++) { if (client_fds[i] == -1) { client_fds[i] = fd; break; } } } // 移除描述符 void remove_fd(int fd) { for (int i = 0; i < MAX_CLIENTS; i++) { if (client_fds[i] == fd) { client_fds[i] = -1; break; } } }

二、创建监听套接字

int create_listen_socket() { // 1. 创建套接字 int listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd == -1) { perror("socket error"); return -1; } // 2. 设置端口复用 int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 3. 绑定地址和端口 struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { perror("bind error"); close(listen_fd); return -1; } // 4. 监听 if (listen(listen_fd, 5) == -1) { perror("listen error"); close(listen_fd); return -1; } printf("服务器启动成功,端口:%d\n", PORT); return listen_fd; }

三、构建监控集合

// 将客户端描述符数组中的有效描述符加入 fd_set int build_fdset(fd_set* readfds, int* max_fd) { FD_ZERO(readfds); int max = -1; for (int i = 0; i < MAX_CLIENTS; i++) { if (client_fds[i] != -1) { FD_SET(client_fds[i], readfds); if (client_fds[i] > max) { max = client_fds[i]; } } } *max_fd = max; return 0; }

四、处理就绪事件

void handle_events(fd_set* readfds, int listen_fd) { // 1. 检查监听套接字(新连接) if (FD_ISSET(listen_fd, readfds)) { struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len); if (client_fd != -1) { printf("新客户端连接,fd=%d\n", client_fd); add_fd(client_fd); } } // 2. 检查所有客户端连接套接字(数据收发) for (int i = 0; i < MAX_CLIENTS; i++) { int fd = client_fds[i]; if (fd == -1) continue; if (FD_ISSET(fd, readfds)) { char buffer[BUFFER_SIZE]; int n = recv(fd, buffer, BUFFER_SIZE - 1, 0); if (n <= 0) { // 客户端关闭连接 printf("客户端 %d 断开连接\n", fd); close(fd); remove_fd(fd); } else { buffer[n] = '\0'; printf("收到数据 (fd=%d): %s\n", fd, buffer); send(fd, "OK", 2, 0); } } } }

五、主循环

int main() { // 初始化 init_fds(); // 创建监听套接字 int listen_fd = create_listen_socket(); if (listen_fd == -1) { exit(1); } // 将监听套接字加入管理 add_fd(listen_fd); fd_set readfds; struct timeval tv; while (1) { // 1. 构建监控集合 int max_fd; build_fdset(&readfds, &max_fd); // 2. 设置超时(每次都要重新设置) tv.tv_sec = 2; tv.tv_usec = 0; // 3. 调用 select int ret = select(max_fd + 1, &readfds, NULL, NULL, &tv); if (ret == -1) { perror("select error"); break; } else if (ret == 0) { // 超时,可执行一些定时任务 // printf("select timeout\n"); continue; } // 4. 处理就绪事件 handle_events(&readfds, listen_fd); } close(listen_fd); return 0; }

第五部分:select 的注意事项

一、每次调用前需要重置集合

select返回后会修改fd_set,只保留就绪的描述符。因此,每次调用select前都需要重新构建监控集合。

// 正确做法:每次循环都重新构建 while (1) { FD_ZERO(&readfds); FD_SET(listen_fd, &readfds); // ... 添加其他描述符 select(...); } // 错误做法:只初始化一次 FD_ZERO(&readfds); FD_SET(listen_fd, &readfds); while (1) { select(...); // 第一次调用后 readfds 被修改 // 后续循环中 readfds 已经不是原来的集合了 }

二、超时时间需要重置

select返回后,timeout结构体可能会被修改(剩余时间)。因此每次调用前都需要重新设置。

// 正确做法 while (1) { tv.tv_sec = 2; tv.tv_usec = 0; select(..., &tv); }

三、select 的最大描述符限制

select默认最多支持FD_SETSIZE(通常为 1024)个文件描述符。

# 查看当前系统的文件描述符限制 ulimit -n # 临时修改限制 ulimit -n 1000000

如果需要处理超过 1024 个连接,应该选择epoll

四、缓冲区截断问题

// 如果接收缓冲区太小,一次只能读取部分数据 char buffer[1]; // 只能读1个字节 recv(fd, buffer, 1, 0); // 剩余数据还在接收缓冲区中,下次 select 仍会报告该描述符就绪

当客户端发送 "hello"(5个字节),而服务器缓冲区只有1字节时,需要多次select才能读完所有数据。


第六部分:select 的优缺点总结

优点

优点说明
跨平台几乎所有操作系统都支持
简单易懂API 相对简单,适合学习入门
事件类型丰富同时支持读、写、异常事件

缺点

缺点说明
描述符数量限制默认只能监控 1024 个描述符
每次都需要重新构建集合效率较低
不可知具体哪个描述符就绪需要遍历所有描述符检查
缺乏边缘触发模式只有水平触发
描述符集合会被修改需要保存备份

总结

一、select 使用流程

二、核心函数速查表

函数/宏作用
select()监控多个文件描述符的状态
FD_ZERO()清空集合
FD_SET()将描述符加入集合
FD_CLR()将描述符从集合中移除
FD_ISSET()检测描述符是否在集合中

三、使用注意事项

注意点说明
每次调用前重置集合select会修改原始集合
每次调用前重置超时timeout可能被修改
nfds 是最大值+1否则可能漏检或浪费性能
遍历检查所有描述符不能仅依赖返回值

select是最基础的 I/O 多路复用机制,理解它的工作原理对于学习更高级的epoll非常重要。

学习建议:

  1. 动手实现 select 服务器,理解其工作流程

  2. 对比多进程/多线程模型,体会 I/O 多路复用的优势

  3. 理解select的局限性,为学习epoll打好基础

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/8 16:08:53

抖音批量下载神器:5分钟搞定100个视频的终极免费方案

抖音批量下载神器&#xff1a;5分钟搞定100个视频的终极免费方案 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback suppor…

作者头像 李华
网站建设 2026/5/8 16:08:52

2025届最火的六大降重复率方案解析与推荐

Ai论文网站排名&#xff08;开题报告、文献综述、降aigc率、降重综合对比&#xff09; TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 为降低AIGC检测率&#xff0c;关键在于优化文本的自然度与多样性。要规避使用模板化句式&am…

作者头像 李华
网站建设 2026/5/8 16:08:48

复杂表格项目里,我才明白 Model/View 是救命稻草

最开始做一个工业上位机项目时&#xff0c;我直接用 QTableWidget 展示设备状态表格&#xff0c;想着“先跑起来再说”。表格不大&#xff0c;功能不多&#xff0c;写着写着还能接受。结果业务一复杂&#xff0c;需求加了行内编辑、排序、动态刷新、状态色标识……原本简单的 Q…

作者头像 李华
网站建设 2026/5/8 16:08:45

2026届学术党必备的AI科研神器推荐榜单

Ai论文网站排名&#xff08;开题报告、文献综述、降aigc率、降重综合对比&#xff09; TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 一款针对研究生以及科研人员的智能写作辅助系统&#xff0c;是AI开题报告工具。这个工具依据…

作者头像 李华
网站建设 2026/5/8 16:08:42

终极Windows驱动清理神器:5分钟学会DriverStore Explorer释放数GB空间

终极Windows驱动清理神器&#xff1a;5分钟学会DriverStore Explorer释放数GB空间 【免费下载链接】DriverStoreExplorer Driver Store Explorer 项目地址: https://gitcode.com/gh_mirrors/dr/DriverStoreExplorer 你是不是经常发现Windows系统盘空间莫名其妙地变小&am…

作者头像 李华
网站建设 2026/5/8 16:08:20

测试测量工程师实战指南:从核心逻辑到自动化系统构建

1. 一个时代的注脚&#xff1a;从一篇旧闻聊起前几天在整理资料时&#xff0c;偶然翻到一篇2010年的行业旧闻&#xff0c;标题是“T&M gets dedicated designline”&#xff0c;刊登在EE Times上。文章的作者Colin Holland&#xff0c;时任这个新设立的“测试与测量设计线”…

作者头像 李华