当前位置: 首页 > news >正文

多路复用 I/O 函数——`select`函数解析

好的,我们以 Linux 中经典的多路复用 I/O 函数——select 为例,进行一次完整、深入且包含全部代码的解析。

<摘要>

select 是 Unix/Linux 系统中传统的多路复用 I/O 系统调用。它允许一个程序同时监视多个文件描述符(通常是套接字),阻塞等待直到一个或多个描述符就绪(如变得可读、可写或发生异常),或者等待超时。它是构建能够处理多个客户端连接的服务器(如早期的 Web 服务器、聊天室)的基础方法。虽然性能上不如 epoll,但其跨平台特性(POSIX 标准)使其仍有广泛应用价值。


<解析>

select 函数是处理并发 I/O 的“老将”。它的核心思想是:“告诉我一组你关心的文件描述符,我来帮你盯着,一旦其中有任何一个有动静(可读、可写、出错),或者等到你指定的时间,我就醒来通知你。” 这样,单个线程就可以管理多个连接。

1) 函数的概念与用途
  • 功能:同步地监视多组(可读、可写、异常)文件描述符的状态变化。它会使进程阻塞,直到有描述符就绪或超时。
  • 场景
    • 管理多个网络客户端连接的服务器。
    • 需要同时监听标准输入和网络套接字的客户端(如聊天程序)。
    • 需要设置精确超时的 I/O 操作。
    • 跨平台程序(Windows 也支持 select)。
2) 函数声明与出处

select 定义在 <sys/select.h> 头文件中。

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
3) 返回值含义与取值范围
  • 成功:返回就绪的文件描述符的总数。如果超时则返回 0
  • 失败:返回 -1,并设置相应的错误码 errno
    • EBADF:在某一个集合中传入了无效的文件描述符。
    • EINTR:这个调用在阻塞期间被信号中断。通常需要重新调用 select
    • EINVAL:参数 nfds 为负数或超时时间值无效。
4) 参数的含义与取值范围
  1. int nfds

    • 作用:指定所有被监控的文件描述符集合中最大值加 1。内核通过这个值来线性扫描哪些描述符就绪,从而提高效率。
    • 取值范围:通常是 max_fd + 1max_fd 是所有监听描述符中最大的那个)。
  2. fd_set *readfds

    • 作用:指向一个 fd_set 类型的对象,该对象中包含了我们关心是否可读的文件描述符集合。传入时是“我们关心的”,返回时是“就绪的”。
    • 取值范围NULL 表示不关心可读事件。
  3. fd_set *writefds

    • 作用:指向一个 fd_set 类型的对象,该对象中包含了我们关心是否可写的文件描述符集合。
    • 取值范围NULL 表示不关心可写事件。
  4. fd_set *exceptfds

    • 作用:指向一个 fd_set 类型的对象,该对象中包含了我们关心是否发生异常的文件描述符集合。异常通常指带外数据(OOB data)到达。
    • 取值范围NULL 表示不关心异常事件。
  5. struct timeval *timeout

    • 作用:指定 select 等待的超时时间。这是一个结构体指针,可以精确到微秒。
    • 结构体定义
      struct timeval {long tv_sec;  /* seconds (秒)*/long tv_usec; /* microseconds (微秒)*/
      };
      
    • 取值范围
      • NULL无限阻塞。直到有描述符就绪。
      • {0, 0}非阻塞轮询。立即返回,检查描述符状态。
      • {n, m}等待最多 n 秒 m 微秒

fd_set 相关操作宏(非常重要)

void FD_ZERO(fd_set *set);      // 清空一个 fd_set
void FD_SET(int fd, fd_set *set); // 将一个 fd 加入 set
void FD_CLR(int fd, fd_set *set); // 将一个 fd 从 set 中移除
int FD_ISSET(int fd, fd_set *set); // 检查一个 fd 是否在 set 中(就绪)
5) 函数使用案例

示例 1:基础用法 - 监听标准输入(阻塞等待)
此示例演示如何使用 select 监听标准输入(STDIN_FILENO),实现一个带超时等待的输入提示符。

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <unistd.h>int main() {fd_set read_fds;struct timeval timeout;int retval;char buf[256];printf("You have 5 seconds to type something...\n");while(1) {// 1. 设置超时时间(每次循环都需要重新设置,因为select调用后会修改timeout)timeout.tv_sec = 5;timeout.tv_usec = 0;// 2. 清空并设置要监视的描述符集合(每次循环都需要重新设置,因为select调用后会修改read_fds)FD_ZERO(&read_fds);FD_SET(STDIN_FILENO, &read_fds); // STDIN_FILENO is 0// 3. 调用select,nfds是最大fd+1retval = select(STDIN_FILENO + 1, &read_fds, NULL, NULL, &timeout);if (retval == -1) {perror("select()");exit(EXIT_FAILURE);} else if (retval == 0) {printf("\nTimeout! No data within 5 seconds.\n");printf("Waiting again...\n");} else {// 检查我们关心的描述符是否真的就绪if (FD_ISSET(STDIN_FILENO, &read_fds)) {// 从标准输入读取数据ssize_t count = read(STDIN_FILENO, buf, sizeof(buf) - 1);if (count > 0) {buf[count] = '\0'; // Null-terminate the stringprintf("You typed: %s", buf);} else {perror("read");break;}}}}return 0;
}

示例 2:监听多个套接字(服务器端模型)
此示例展示一个简易的单线程回显服务器,可以同时处理监听新连接和已连接客户端的读事件。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>#define PORT 8080
#define MAX_CLIENTS 10
#define BUF_SIZE 1024int main() {int server_fd, new_socket, client_sockets[MAX_CLIENTS];fd_set read_fds;int max_sd, sd, activity, i, valread;struct sockaddr_in address;int addrlen = sizeof(address);char buffer[BUF_SIZE];// 初始化客户端套接字数组for (i = 0; i < MAX_CLIENTS; i++) {client_sockets[i] = 0;}// 创建服务器套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 绑定套接字到端口if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 开始监听if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}printf("Server listening on port %d\n", PORT);while(1) {// 清空描述符集FD_ZERO(&read_fds);// 添加服务器监听套接字FD_SET(server_fd, &read_fds);max_sd = server_fd;// 添加所有有效的客户端套接字for (i = 0; i < MAX_CLIENTS; i++) {sd = client_sockets[i];if (sd > 0) {FD_SET(sd, &read_fds);}if (sd > max_sd) {max_sd = sd;}}// 等待活动,无限超时activity = select(max_sd + 1, &read_fds, NULL, NULL, NULL);if ((activity < 0) && (errno != EINTR)) {perror("select error");}// 1. 检查是否有新的连接到来(监听套接字是否可读)if (FD_ISSET(server_fd, &read_fds)) {if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}printf("New connection, socket fd is %d, IP: %s, Port: %d\n",new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));// 将新套接字添加到客户端数组for (i = 0; i < MAX_CLIENTS; i++) {if (client_sockets[i] == 0) {client_sockets[i] = new_socket;printf("Adding to list of sockets as %d\n", i);break;}}if (i == MAX_CLIENTS) {printf("Too many clients. Rejected.\n");close(new_socket);}}// 2. 检查是哪个客户端套接字有数据可读for (i = 0; i < MAX_CLIENTS; i++) {sd = client_sockets[i];if (FD_ISSET(sd, &read_fds)) {// 读取数据if ((valread = read(sd, buffer, BUF_SIZE)) == 0) {// 对方关闭了连接getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);printf("Host disconnected, IP %s, port %d\n",inet_ntoa(address.sin_addr), ntohs(address.sin_port));close(sd);client_sockets[i] = 0; // 从数组中清除} else {// 回显数据buffer[valread] = '\0';printf("Received from client %d: %s", sd, buffer);send(sd, buffer, valread, 0); // Echo back}}}}return 0;
}

使用 telnet 127.0.0.1 8080 命令可以测试此服务器。

示例 3:非阻塞检查可写性
此示例演示如何用 select 检查一个套接字是否可写,这在连接建立后首次发送数据或处理阻塞写时有用。

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);fd_set write_fds;struct timeval timeout;int retval;// 这里我们尝试连接一个可能不响应SYN的地址来演示struct sockaddr_in serv_addr;serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(80); // HTTP port// Let's assume we have an address that might block (e.g., a slow server)// inet_pton(AF_INET, "93.184.216.34", &serv_addr.sin_addr); // example.com// 设置为非阻塞模式 (对于这个演示很重要)int flags = fcntl(sockfd, F_GETFL, 0);fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);// 发起非阻塞连接connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));FD_ZERO(&write_fds);FD_SET(sockfd, &write_fds);timeout.tv_sec = 3; // 设置3秒连接超时timeout.tv_usec = 0;printf("Waiting for socket to become writable (connected)...\n");retval = select(sockfd + 1, NULL, &write_fds, NULL, &timeout);if (retval == -1) {perror("select()");} else if (retval == 0) {printf("Timeout! Socket connection timed out after 3 seconds.\n");} else {if (FD_ISSET(sockfd, &write_fds)) {int error_code;socklen_t error_len = sizeof(error_code);// 检查套接字上是否有错误getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error_code, &error_len);if (error_code == 0) {printf("Socket is writable! Connection established successfully.\n");// Now you can send data// send(sockfd, "GET / HTTP/1.0\r\n\r\n", 18, 0);} else {printf("Connection failed with error: %s\n", strerror(error_code));}}}close(sockfd);return 0;
}
6) 编译方式与注意事项

编译命令:

# 编译示例1
gcc -o select_stdin select_stdin.c
# 编译示例2 (需要链接网络库)
gcc -o select_server select_server.c
# 编译示例3
gcc -o select_connect select_connect.c

注意事项:

  1. 参数会被修改select 返回后,readfdswritefdsexceptfdstimeout 参数的值都会被内核修改。它们表示的是就绪的描述符集合和剩余时间。因此,每次调用 select 前都必须重新初始化这些参数。
  2. 性能问题select 采用线性扫描的方式,其效率与最大文件描述符的值 nfds 相关。当需要监视大量描述符时,性能会急剧下降。这是它被 epoll 取代的主要原因。
  3. 描述符数量限制fd_set 有大小限制,通常是 FD_SETSIZE(通常是 1024)。这意味着一个进程通过 select 最多只能同时监视 1024 个文件描述符。
  4. 无法得知具体数量select 返回后,你只知道有多少描述符就绪,但不知道是哪几个。你必须通过 FD_ISSET 遍历整个初始集合来找出就绪的描述符,这在集合很大但就绪描述符很少时效率很低。
7) 执行结果说明
  • 示例1:运行后,程序会等待5秒。如果你在5秒内输入文字并回车,它会立即打印你的输入。如果5秒内无输入,它会打印超时信息并继续等待。
  • 示例2:运行后,服务器启动。使用 telnet 127.0.0.1 8080 连接后,你在 telnet 中输入的任何文字都会被服务器回显给你。服务器日志会打印所有连接和接收到的数据活动。
  • 示例3:运行后,程序会尝试连接 example.com 的80端口。如果网络通畅,3秒内会打印连接成功;如果网络不通或目标不响应,3秒后会打印超时。
8) 图文总结:select 工作流程
返回值 > 0
在 readfds 中就绪
在 writefds 中就绪
不在任何集合中
返回值 == 0
返回值 == -1
EINTR (被信号中断)
其他错误
应用程序准备
设置超时时间 timeout
清空并设置 fd_set 集合
计算 nfds (max_fd + 1)
调用 select() 阻塞等待
select() 返回
有描述符就绪
遍历所有被监听的描述符
使用 FD_ISSET 检查?
处理可读事件
accept/read
处理可写事件
write/connect完成
继续下一轮循环
等待超时
执行超时处理
检查 errno
处理错误
http://www.xdnf.cn/news/1438777.html

相关文章:

  • 一次惊心动魄的线上事故:记一次内存泄漏Bug的排查与解决全过程
  • 从一道面试题开始:如何让同时启动的线程按顺序执行?
  • Bug排查日记:从发现到解决的完整记录
  • 在word中使用lateX公式的方法
  • 力扣115:不同的子序列
  • Unity Android 文件的读写
  • Delphi 5 中操作 Word 表格时禁用鼠标交互
  • 更新远程分支 git fetch
  • 揭开PCB隐形杀手:超周期报废的技术真相
  • AI编码生产力翻倍:你必须掌握的沟通、流程、工具与安全心法
  • 一键掌握服务器健康状态与安全风险
  • 同步工具的底层依赖:AQS
  • Kubernetes 中为 ZenTao 的 Apache 服务器添加请求体大小限制
  • 如何开发一款高稳定、低延迟、功能全面的RTSP播放器?
  • 时序数据库选型指南:为何Apache IoTDB成为工业物联网首选
  • JVM分析(OOM、死锁、死循环)(JProfiler、arthas、jvm自带工具)
  • STM32 - Embedded IDE - GCC - 使用 GCC 链接脚本限制 Flash 区域
  • 【Android】从复用到重绘的控件定制化方式
  • HarmonyOS 应用开发深度解析:基于 ArkTS 的声明式 UI 与状态管理艺术
  • HarmonyOS安装以及遇到的问题
  • Jenkins-Ansible部署discuz论坛
  • 38.Ansible判断+实例
  • PINN物理信息神经网络用于求解二阶常微分方程(ODE)的边值问题,Matlab实现
  • 力扣hot100:缺失的第一个正数(哈希思想)(41)
  • Qwen3-30B-A3B 模型解析
  • 【C++】迭代器详解与失效机制
  • # Shell 文本处理三剑客:awk、sed 与常用小工具详解
  • 【前端面试题✨】Vue篇(一)
  • Linux网络序列化与反序列化(6)
  • Linux文本处理——awk