IO多路复用---select详解及示例

本文详细介绍了Linux系统中IO多路复用的概念,特别是select系统调用的使用,包括其API参数说明、工作原理及存在的局限性。此外,还提供了一个客户端与服务器交互的select应用实例,涉及网络字节序和主机字节序的转换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、IO多路复用
I/O复用使得程序能同时监听多个文件描述符,这使得程序的性能得到了很大程度的提高。
Linux下实现I/O复用的系统调用主要有select、poll、epoll。(这里注意:epoll是Linux系统所特有的),但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

二、select系统调用
select系统调用的用途:在一段时间内,监听文件描述符上的可读、可写、和异常时间的发生。本文主要介绍select系统调用API。

三、select API及主要函数说明

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

参数说明:

  • nfds参数指定被监听的文件描述符的总量。通常被设置为select监听的所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的。
  • readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。
    这三个参数都是fd_set结构指针类型,fd_set结构体实际上仅包含一个整数数组,该数组的每一个元素的每一位标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE来指定,这就限制了select能同时处理的文件名描述符的总量。
//主要使用以下函数来访问fd_set结构体的位
#include<sys/select.h>
FD_ZERO(fd_set *fdset);    //清除fdset的所有位
FD_SET(int fd,fd_set *fdset);      //设置feset的位fd也就是将新建立连接的fd加入到fdset里
FD_CLR(int fd,fd_set *fdset);     //清除fdset的位fd即就是删除fdset里面对应的fd
int FD_ISSET(int fd,fd_set *fdset);   //检测fdset的位fd是否被设置即就是判断指定描述符是否在集合中
  • timeout参数是用来设置select函数的超时时间,是一个timeval结构类型的指针,采用指针是因为内核将修改它以告诉应用程序select等待了多久。
struct timeval
{
   
   
     long tv.sec;  //秒数
     long tv.usec; //毫秒
}

由此可见,如果给timeout变量的tv.sec和tv.usec都传递0,则select将立即返回。如果给timeval传NULL,则select将一直阻塞,直到某个文件描述符就绪。

  1、select成功时返回就绪的文件描述符的总数;
  2、如果在超时时间内没有任何文件描述符就绪,select将返回0;
  3、select失败时返回-1并设置error。如果在select等待时期,程序接收到信号,则立刻返回-1,并设置error为EINTR。

四、select系统调用原理

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

  • 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

    一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

  • 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

    当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
    
  • 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

五、select示例:Client向Server发送消息,Server接收消息并原样发送给Client,Client再把消息输出到终端。

  • 网络中的地址结构
struct sockaddr_in
{
   
   
      int sin_family;//指代协议族,在socket编程中只能是AF_INET
      short sin_port://端口号
      struct in_addr sin_addr;//IP地址
}
struct in_addr
{
   
   
        u_int32_t  s_addr;
}
  • 主机字节序和网络字节序
    主机:有大端模式也有小端模式
    网络:大端模式
#include <winsock.h>
   unsigned long int   htonl( unsigned long int hostlong);//将主机的无符号长整形数转换成网络字节顺序。
   unsigned long short htons(unsigned long short hostlong) //把unsigned short类型从主机序转换到网络序
   unsigned long short ntohs(unsigned long short hostlong) //把unsigned short类型从网络序转换到主机序
   unsigned long int   ntohl(unsigned long int hostlong) //把unsigned long类型从网络序转换到主机序

server:

//myserver
#include<netinet/in.h>  //sockaddr_in
#include<sys/types.h>   //socket
#include<sys/socket.h>  //socket
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/ioctl.h>
#include<sys/time.h>
#include<iostream>
#include<vector>
#include<string>
#include<cstdlib>
#include<cstdio>
#include<cstring>
using namespace std;
#define BUFFER_SIZE 1024   //select需要通过宏定义来确定最大可以监听的文件描述符数量

struct PACKRT_HEAD
{
   
   
    /* data */
    int length;
};

class Server
{
   
   
private:
    struct sockaddr_in server_addr;   //sockaddr_in 网络中的地址结构
<think>嗯,用户让我详细介绍一下I/O多路复用select的用法。首先,我需要回忆一下select的基本概念和作用。select是一种I/O多路复用机制,允许程序同时监视多个文件描述符,看是否有可读、可写或异常事件发生。这样可以在单线程中处理多个I/O操作,提高效率。 接下来,我应该从select原理开始解释。用户可能需要了解为什么需要select,比如在同时处理多个socket连接时,避免为每个连接创建一个线程,从而减少资源消耗。不过要注意不要讲得太复杂,保持简洁明了。 然后,需要介绍select的函数原型,参数含义。记得提到nfds是最大的文件描述符加1,三个fd_set参数分别对应读、写、异常事件,timeout是超时时间。这部分要详细,但也要用例子帮助理解,比如fd_set的使用步骤,FD_ZERO、FD_SET这些宏的作用。 然后,使用步骤应该分点说明,比如初始化fd_set,添加描述符,调用select,然后检查哪些描述符就绪。每个步骤要具体,可能还要给出示例代码片段,但用户可能需要的是解释而不是代码,所以用文字描述过程更好。 接着,可能需要讨论select的优缺点。优点包括跨平台支持,缺点有最大文件描述符限制、效率随数量增加下降、需要每次重新设置fd_set等。这部分要客观,指出虽然select有局限,但在某些场景下仍然适用。 然后,给出一个简单的应用场景,比如TCP服务器监听多个客户端连接。用这个例子说明select如何检测新连接和客户端数据到达。用户可能更理解实际应用的例子,这样能帮助他们将理论联系实际。 最后,注意事项部分要提醒用户关于fd_set的大小限制,比如在Linux下通常是1024,以及处理异常情况的重要性。可能还要提到select与其他I/O多路复用技术如poll、epoll的区别,但不需要展开,保持重点在select上。 整个过程中要确保使用中文,结构清晰,分点列出,符合用户要求的系统级指令中的格式要求,比如行内公式用$...$,但在这个问题中可能用不上数学公式,不过要注意其他格式要求,比如单独成段的独立公式,如果有的话。但在这个回答中可能不需要数学公式,所以重点放在文字解释上。 需要检查是否有LaTeX语法错误,但这里可能用到的LaTeX不多,不过如果有代码片段,可能需要用等宽字体或者代码块,但用户没有特别要求,所以用自然语言描述即可。同时要保证回答真实可靠,比如准确描述select的参数和步骤,不出现错误信息。 可能用户是刚开始学习网络编程的开发者,或者需要处理高并发连接的工程师,他们需要了解基本的select用法来优化他们的程序。因此,解释时要避免过于高深,保持基础但全面。同时,用户可能想知道在实际项目中如何应用,所以举例很重要。 总结一下,回答的结构应该是:原理简介、函数原型详解、使用步骤、优缺点分析、应用示例、注意事项。每个部分分点说明,使用标题和子标题,保持层次分明。这样用户能够逐步理解select的用法,并能够应用到实际开发中。</think>### I/O多路复用中 `select` 用法详解 #### 一、基本原理 `select` 是一种**同步I/O多路复用**机制,通过监视多个文件描述符(如套接字)的状态(可读、可写、异常),实现在单线程中同时处理多个I/O事件。核心思想是**将多个阻塞操作集中管理**,避免为每个I/O操作单独创建线程。 --- #### 二、函数原型 ```c #include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` - **参数说明**: - `nfds`:监视的文件描述符最大值 + 1(例如监视0~4号描述符,则`nfds=5`)。 - `readfds`:需检测**可读事件**的文件描述符集合。 - `writefds`:需检测**可写事件**的文件描述符集合。 - `exceptfds`:需检测**异常事件**的文件描述符集合。 - `timeout`:超时时间。若为`NULL`则阻塞等待;若为0则立即返回。 - **返回值**: - 成功时返回就绪的文件描述符总数。 - 超时返回0,错误返回-1。 --- #### 三、使用步骤 1. **初始化文件描述符集合** 使用 `FD_ZERO` 清空集合,`FD_SET` 添加需监视的描述符: ```c fd_set read_fds; FD_ZERO(&read_fds); // 清空集合 FD_SET(sockfd, &read_fds); // 添加sockfd到读集合 ``` 2. **调用 `select`** 传入集合并等待事件就绪: ```c int ret = select(max_fd + 1, &read_fds, NULL, NULL, &timeout); ``` 3. **检查就绪事件** 使用 `FD_ISSET` 判断具体描述符是否就绪: ```c if (FD_ISSET(sockfd, &read_fds)) { // 处理sockfd的可读事件 } ``` --- #### 四、优缺点分析 - **优点**: - 跨平台支持(Linux/Windows/macOS)。 - 简单易用,适合少量并发场景。 - **缺点**: - 文件描述符数量受限(默认1024)。 - 每次调用需重新设置集合,效率随数量增长下降。 - 需遍历所有描述符检查状态,时间复杂度$O(n)$。 --- #### 五、典型应用场景(TCP服务器) ```c // 伪代码示例 int main() { int listen_fd = socket(...); bind(listen_fd, ...); listen(listen_fd, ...); fd_set read_fds; while(1) { FD_ZERO(&read_fds); FD_SET(listen_fd, &read_fds); // 监听新连接 // 添加已连接客户端的fd到read_fds select(max_fd + 1, &read_fds, NULL, NULL, NULL); if (FD_ISSET(listen_fd, &read_fds)) { // 接受新连接 int client_fd = accept(...); FD_SET(client_fd, &read_fds); } // 遍历检查所有客户端fd是否可读 for (each client_fd in read_fds) { if (FD_ISSET(client_fd, &read_fds)) { recv(...); // 读取数据 } } } } ``` --- #### 六、注意事项 1. **文件描述符上限** 通过宏 `FD_SETSIZE` 定义(通常1024),需重新编译内核才能修改。 2. **每次调用后集合会被修改** 需在循环中重新设置集合。 3. **正确处理异常事件** 如TCP带外数据(OOB)或连接错误。 4. **性能瓶颈** 高并发场景建议改用 `epoll`(Linux)或 `kqueue`(BSD)。 --- #### 七、与其他技术对比 | 特性 | select | poll | epoll | |-------------|--------------|--------------|--------------| | 最大描述符数量 | 1024 | 无限制 | 无限制 | | 时间复杂度 | $O(n)$ | $O(n)$ | $O(1)$ | | 触发模式 | 水平触发 | 水平触发 | 支持边缘触发 | | 内存操作 | 每次拷贝全量集合 | 每次拷贝全量集合 | 内核维护红黑树 | --- 通过 `select` 可简化多I/O场景的代码逻辑,但其性能限制使其更适用于中小规模并发场景。理解其原理和限制后,可根据实际需求选择更高效的替代方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值