当前位置: 首页 > 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.dtcms.com/a/365571.html

相关文章:

  • SystemServer 启动流程
  • EDVAC:现代计算机体系的奠基之作
  • 影像服务免费方案:GIS Server让GeoTIFF数据在山海鲸中直观呈现
  • PyTorch实战——ResNet与DenseNet详解
  • 关于嵌入式学习——嵌入式硬件2
  • Mac电脑Tomcat+Java项目中 代码更新但8080端口内容没有更新
  • 打破信息洪流:微算法科技(NASDAQ:MLGO)推出一种移动互联网环境下数字媒体热点挖掘算法
  • 01-Redis 发展简史与核心定位解析:从诞生到三大产品矩阵
  • 微信小程序预览和分享文件
  • 从检索的角度聊聊数据结构的演进​
  • Vue 评论组件设计 V1.0
  • 关于linux软件编程15——数据库编程sqlite3
  • Spring Boot配置error日志发送至企业微信
  • EI会议:第七届人工智能与先进制造国际会议(AIAM 2025)
  • 多智能体协作系统(CrewAI)
  • 素材合集!直播间带货音乐BGM合集,抖音直播间常用热门音乐合集,根据中文分类,方便查找
  • AI重塑SaaS:从被动工具到智能角色的技术演进路径
  • Shell 三剑客之 awk 命令详解(理论+实战)
  • Datawhale AI夏令营复盘[特殊字符]:我如何用一个Prompt,在Coze Space上“画”出一个商业级网页?
  • SDK介绍
  • MCP模型库哪个好?2025年收录12万+服务的AI智能体工具集成平台推荐
  • 面试复习题-kotlin
  • Springboot 练手项目(删除部门-接口开发)
  • Get the pikachu靶场SSRF漏洞 (windows环境)
  • AR技术赋能电力巡检:开启智能安全新时代
  • 前端-安装VueCLI
  • Ubuntu环境下的 RabbitMQ 安装与配置详细教程
  • 【开题答辩全过程】以 基于大数据的地震数据分析系统的设计与实现为例,包含答辩的问题和答案
  • 理解用户需求的方法
  • JDBC的功能和使用