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

深入剖析 I/O 复用之 select 机制

深入剖析 I/O 复用之 select 机制

在网络编程中,I/O 复用是一项关键技术,它允许程序同时监控多个文件描述符的状态变化,从而高效地处理多个 I/O 操作。select 作为 I/O 复用的经典实现方式,在众多网络应用中扮演着重要角色。本文将深入探讨 select 的原理、使用方法、相关数据结构以及实际应用示例。

一、I/O 复用概述

I/O 复用使得程序能够同时监听多个文件描述符,适用于多种场景:

  • 客户端程序需要同时处理多个套接字。
  • 客户端要兼顾用户输入和网络连接处理。
  • TCP 服务器需同时管理监听套接字和已连接套接字。
  • 服务器要同时处理 TCP 请求和 UDP 请求。
  • 服务器需要监听多个端口。

二、select 原理

select 系统调用通过维护三个文件描述符集合(读集合、写集合和异常集合)来监视不同类型的事件。它会阻塞当前进程,直到有一个或多个文件描述符就绪(有数据可读、可写或发生异常),或者达到指定的超时时间。

三、select 使用方法

函数原型

#include <sys/select.h>  
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);  
  • nfds:需要监视的最大文件描述符编号加 1。
  • readfds:读文件描述符集合,用于监视文件描述符是否有数据可读。
  • writefds:写文件描述符集合,用于监视文件描述符是否可写。
  • exceptfds:异常文件描述符集合,用于监视文件描述符是否发生异常。
  • timeout:超时时间,指定 select 函数的最大阻塞时间。如果为 NULL,则会一直阻塞直到有文件描述符就绪。

fd_set 数据结构

fd_set 用于存储文件描述符集合,本质上是一个位图(bitmask)。每个文件描述符对应位图中的一位,若该位置为 1,则表示该文件描述符在集合内;若为 0,则表示不在集合内。

在这里插入图片描述

操作 fd_set 的宏函数

  • FD_ZERO(fd_set *set):将 fd_set 集合初始化为空,即把集合中所有位都置为 0。
  • FD_SET(int fd, fd_set *set):将指定的文件描述符 fd 添加到 fd_set 集合中,把对应位设置为 1。
  • FD_CLR(int fd, fd_set *set):从 fd_set 集合中移除指定的文件描述符 fd,将对应位设置为 0。
  • FD_ISSET(int fd, fd_set *set):检查指定的文件描述符 fd 是否在 fd_set 集合中。若在集合中则返回非零值,否则返回 0。

timeout 结构体

struct timeval {  long tv_sec; // 秒数  long tv_usec; // 微秒数  
};  

用于指定 select 函数的超时时间。

四、使用 select 实现 TCP 服务器示例代码解析

代码功能

此代码创建了一个 TCP 服务器,借助 select 函数实现 I/O 复用,能够同时处理多个客户端的连接与数据收发。服务器监听本地地址 127.0.0.16000 端口,当有新的客户端连接时会接受连接,接收客户端发送的数据,并向客户端回复 "ok"

代码逐段解析

头文件与常量定义
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/select.h>  
#include <sys/socket.h>  
#include <arpa/inet.h>  
#include <netinet/in.h>  #define MAXARR 10  

引入所需头文件,并定义常量 MAXARR 表示存储文件描述符数组的最大长度。

函数声明与实现
  • socket_init 函数
    int socket_init() {  int sockfd = socket(AF_INET, SOCK_STREAM, 0);  if (sockfd == -1) {  perror("socket err");  return -1;  }  struct sockaddr_in saddr;  memset(&saddr, 0, sizeof(saddr));  saddr.sin_family = AF_INET;  saddr.sin_port = htons(6000);  saddr.sin_addr.s_addr = inet_addr("127.0.0.1");  int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));  if (res == -1) {  perror("bind err");  return -1;  }  if ((res = listen(sockfd, 5) < 0)) {  perror("listen err");  return -1;  }  return sockfd;  
    }  
    
    该函数用于初始化服务器套接字,包括创建套接字、绑定地址和端口、开始监听连接。若出现错误则返回 -1
  • arr_init 函数
    void arr_init(int arr[]) {  for (int i = 0; i < MAXARR; i++) {  arr[i] = -1;  }  
    }  
    
    把存储文件描述符的数组初始化为 -1,表示数组中没有有效的文件描述符。
  • arr_add 函数
    void arr_add(int arr[], int fd) {  for (int i = 0; i < MAXARR; i++) {  if (arr[i] == -1) {  arr[i] = fd;  break;  }  }  
    }  
    
    把新的文件描述符添加到数组中,找到第一个值为 -1 的位置,将新的文件描述符存入该位置。
  • arr_del 函数
    void arr_del(int arr[], int fd) {  for (int i = 0; i < MAXARR; i++) {  if (arr[i] == fd) {  arr[i] = -1;  break;  }  }  
    }  
    
    从数组中删除指定的文件描述符,找到该文件描述符所在的位置,将其值置为 -1
  • accept_cli 函数
    void accept_cli(int sockfd, int arr[]) {  int c = accept(sockfd, NULL, NULL);  if (c == -1) {  perror("accept err");  return;  }  printf("cli(%d) accept\n", c);  arr_add(arr, c);  
    }  
    
    接受新的客户端连接,若接受成功则将客户端的套接字文件描述符添加到数组中。
  • recv_cli 函数
    void recv_cli(int fd, int arr[]) {  char buff[128] = {0};  int n = recv(fd, buff, 127, 0);  if (n < 0) {  perror("recv err");  printf("cli(%d) close\n", fd);  close(fd);  arr_del(arr, fd);  return;  }  if (n == 0) {  printf("cli(%d) close\n", fd);  close(fd);  arr_del(arr, fd);  return;  }  printf("buff(c=%d):%s\n", fd, buff);  send(fd, "ok", 2, 0);  
    }  
    
    接收客户端发送的数据,若接收出错或者客户端关闭连接,则关闭对应的套接字并从数组中删除该文件描述符;若接收到数据,则打印数据并向客户端回复 "ok"
main 函数
int main() {  int sockfd = socket_init();  if (sockfd == -1) {  exit(1);  }  int arr[MAXARR];  arr_init(arr);  arr_add(arr, sockfd);  fd_set fdset;  while (1) {  FD_ZERO(&fdset);  int maxfd = -1;  for (int i = 0; i < MAXARR; i++) {  if (arr[i] == -1) {  continue;  }  FD_SET(arr[i], &fdset);  if (arr[i] > maxfd) {  maxfd = arr[i];  }  }  struct timeval tv = {5, 0};  int n = select(maxfd + 1, &fdset, NULL, NULL, &tv);  if (n == -1) {  perror("select err");  continue;  } else if (n == 0) {  printf("TIME OUT\n");  continue;  } else {  for (int i = 0; i < MAXARR; i++) {  if (arr[i] == -1) {  continue;  } else {  if (FD_ISSET(arr[i], &fdset)) {  if (arr[i] == sockfd) {  accept_cli(arr[i], arr);  } else {  recv_cli(arr[i], arr);  }  }  }  }  }  }  
}  
  • 初始化服务器套接字,若失败则退出程序。
  • 初始化存储文件描述符的数组,并将服务器套接字文件描述符添加到数组中。
  • 进入无限循环:
    • 每次循环开始时,清空 fd_set 集合。
    • 遍历数组,将有效的文件描述符添加到 fd_set 集合中,并找出最大的文件描述符。
    • 设置 select 函数的超时时间为 5 秒。
    • 调用 select 函数进行监听,根据返回值判断情况:若返回 -1 表示出错,打印错误信息并继续循环;若返回 0 表示超时,打印超时信息并继续循环;若返回大于 0 的值,表示有文件描述符就绪。
    • 再次遍历数组,检查哪些文件描述符就绪。若为服务器套接字,则调用 accept_cli 函数接受新的连接;若为客户端套接字,则调用 recv_cli 函数接收数据。

五、select 的优缺点

优点

  • 跨平台支持select 是一种标准的系统调用,几乎所有的 Unix/Linux 系统和 Windows 系统都支持,具有良好的跨平台性。
  • 简单易用select 的接口相对简单,使用起来比较方便,对于小规模的应用场景非常适用。

缺点

  • 文件描述符数量限制select 有最大文件描述符数量的限制,一般为 1024。如果需要处理大量的文件描述符,可能会受到限制。
  • 性能问题select 需要遍历所有的文件描述符来检查其状态,时间复杂度为 O(n),当文件描述符数量较多时,性能会受到影响。
  • 内核和用户空间数据拷贝:每次调用 select 时,都需要将文件描述符集合从用户空间拷贝到内核空间,在文件描述符数量较多时,会带来一定的开销。

六、适用场景

由于 select 存在一些局限性,它适用于文件描述符数量较少、对性能要求不是特别高的场景,例如一些简单的网络服务器、嵌入式系统等。在实际应用中,若需要处理大量文件描述符或对性能有更高要求,可以考虑使用 pollepoll 等更高级的 I/O 复用机制。

通过深入理解 select 的原理、使用方法和优缺点,我们能够在网络编程中更好地运用这一技术,构建高效稳定的网络应用。

http://www.xdnf.cn/news/4804.html

相关文章:

  • debian12 安装docker
  • ImportError: cannot import name ‘Optional‘ from ‘pydantic‘
  • C++错误: multiple definition of `Q‘
  • 超详细讲解注意力机制、自注意力机制、多头注意力机制、通道注意力机制、空间注意力机制
  • CCF编程能力等级认证 一级 第一次课
  • 【PhysUnits】3 量纲接口实现解析(dimensional.rs)
  • 如何处理oracle 12c DG归档日志缺失(gap)导致备库同步中断问题
  • 创建型模式:抽象工厂(Abstract Factory)模式
  • Matlab 数控车床进给系统的建模与仿真
  • ubuntu24.04安装cuda
  • 微程序控制器的详细工作过程
  • 基于精准光域控制的光触发RFID标签
  • 【万字长文】深入浅出 LlamaIndex 和 LangChain:从RAG到智能体,轻松驾驭LLM应用开发
  • python调用国税乐企直连接口开数电票之API调用工具类
  • AI领域的MCP(Model-Centric Paradigm)
  • 无线远传压力传感装置:城市管网的 “压力小卫士”
  • 【盟通杂谈】为什么现代工业需要工业协议?
  • CDGP|数据全生命周期的“收数、治数、用数、保数”体系详解
  • 操作系统 : 线程概念与控制
  • Nacos源码—Nacos集群高可用分析(三)
  • 无需标记点,精准面部捕捉!无线面捕头盔赋予自由表演空间
  • 力扣94. 二叉树的中序遍历
  • 简单网络交换、路由
  • Java 异常处理
  • 【DBMS学习系列】一、DBMS(数据库管理系统)的存储模型
  • 学成在线之课程管理
  • 100个常用的DeepSeek指令
  • Vue事件总线(EventBus)使用指南:详细解析与实战应用 父子组件传值
  • Chain of Thought (思维链) 技术详解
  • 按摩椅上的气囊系统 是现代按摩椅中非常关键的组成部分,它与机芯系统相辅相成,为用户提供全方位、更接近真人按摩的体验