I/O复用

I/O复用的概念

I/O复用是一种允许单个线程或进程同时监控多个文件描述符(如套接字、管道等)的技术,当其中任何一个文件描述符就绪(可读、可写或异常)时,程序可以立即处理。这种技术避免了为每个I/O操作创建单独的线程或进程,提高了资源利用率。

I/O复用技术的核心优势

I/O复用技术通过单线程或少量线程监控多个I/O流的状态变化,解决了传统阻塞I/O模型中"一连接一线程"的资源浪费问题。典型实现包括select、poll、epoll(Linux)和kqueue(BSD)等系统调用。

举个栗子:

假设你是一个快递站管理员,传统方式是你雇100个快递员,每人盯一个快递柜(一个客户),哪怕柜子空着,快递员也得傻等着。结果人太多,工资(内存)发不起,管理(CPU调度)也乱套。

但是I/O复用可以一个线程监控多个文件描述符大幅提高了资源的利用率

提升系统资源利用率

传统阻塞I/O需要为每个连接创建独立线程/进程,当并发连接数增长时,线程切换和内存消耗呈线性增长。I/O复用使单个线程能处理成千上万的连接,显著降低CPU和内存开销。例如在C10K问题中,复用技术可将线程数从10,000降至个位数。

应对高并发场景

现代网络应用常需处理大量并发连接(如即时通讯、实时交易系统)。I/O复用技术通过事件驱动机制,在连接有实际数据到达时才触发处理逻辑,避免空转等待。Nginx、Redis等高性能服务器均采用此模型实现百万级并发。

实现非阻塞操作

复用技术与非阻塞I/O结合时,程序能在等待I/O就绪期间执行其他任务。例如Web服务器在等待磁盘I/O时仍可处理新请求,这种能力在单线程Node.js架构中体现尤为明显。

精确控制超时机制

通过复用API的超时参数(如select的timeval),程序可统一管理所有连接的超时逻辑,避免为每个连接单独设置定时器。这在需要严格响应时间的金融系统中至关重要。

跨平台事件通知

不同操作系统提供各自的I/O复用接口(epoll/kqueue/IOCP),抽象层(如libevent)基于这些原语实现跨平台事件循环。开发者无需针对每个平台重写事件处理逻辑。

典型应用场景

  • 需要维持大量空闲连接(如长轮询服务)
  • 延迟敏感型应用(如在线游戏服务器)
  • 资源受限环境(嵌入式系统)
  • 需要混合处理网络和本地I/O(GUI事件循环)

I/O复用的函数

Select

select的系统调用的用途是:在一段时间内,监听用户感兴趣的文件描述符的可读,可写,和异常等事件,

select的函数调用如下

cpp 复制代码
#include<sys/select.h>
int select(int maxfd,fd_set *readfds,fd_set *writefds,
fd_set* exceptfds,struct timeval *timeout);

select成功时返回就绪(可读,可写,异常)的文件描述符的总数,如果在超时时间内没有任何文件描述符的返回,select将返回0,select失败返回-1,如果select在等待时间接收到信号,则select立即返回-1;

函数参数

maxfd指定被监听的文件描述符的总数,通常被设定为select要监听的所有文件描述符中的最大值+1,就是相当于快递柜的总数

readfds,writefds,exceptfds分别指向可读可写,异常事件对应的文件描述符的合集,应用程序调用select可以通过这三个参数传入自己感兴趣的文件描述符对待,select返回时内核将修改他们来通知应用程序的哪些文件描述符已经就位

fd_set的定义如下

通过下列宏可以访问fd_set中的位,

cpp 复制代码
FD_ZERO(fd_set *fdset);//清楚fd_set中的所有位
FD_SET(int fd, fd_set *fdset)//设置fd_set的位
FD_CLR(int fd,fd_set *fdset)//清楚fd_set的位fd
int FD_ISSET(int fd,fd_set *fdset)//测试fd_set里面的位fd是否被设置

timeout参数用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。timeval 结构的定义如下:

cpp 复制代码
struct timeval

{

 long tv_sec; //秒数

 long tv_usec; // 微秒数

};

如果给timeout的两个成员都是0,则select将立即返回。如果timeout传递 NULL,则select将一直阻塞,直到某个文件描述符就绪.

例子

实现select监控标准输入

eg.代码如下:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/select.h>

int main(){

  int fd=0;//文件描述符标准输入
  fd_set fdset;
struct timeval time={5,0};
  while(1){
FD_ZERO(&fdset);//清空文件描述符集合
FD_SET(fd,&fdset);//将标准输入流加入到集合中
int n= select(2,&fdset,NULL,NULL,&time);//select监控标准输入
if(-1==n){
  printf("ERROR");
  continue;
}
else if(0==n){
  printf("TIME OUT");
  continue;
}
else{
  if(FD_ISSET(fd,&fdset))//验证文件描述符是否为标准输入
{
char buf[128] ={0};
read (fd,buf,127);
printf("BUF : %s/n",buf); 

}
}
sleep(1);

  }
}

程序结果如图所示

利用selec实现TCP链接
服务器端代码
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include<assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/select.h>
int Init_Socked()//初始化服务器端套接字
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);//使用IPV4的TCP链接
    if (-1 == sockfd)
    {
        exit(1);
    }
    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;//地址族为IPV4
    saddr.sin_port = htons(9999);//端口号
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int n = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));//绑定套接字到指定的地址
    if (-1 == n)
    {
        printf("bind arror");
        exit(1);
    }
    n = listen(sockfd, 5);//监听套接字最大监听队列为5
    if (-1 == n)
        return -1;
    return sockfd;
}

void InitFds(int fds[], int n)//初始化记录服务器记录服务器套接字的数组
{
    int i = 0;
    for (; i < n; i++)
    {
        fds[i] = -1;
    }
}

void AddFdToFds(int fds[], int fd, int n)//将套接字描述符加入到数组中
{
    int i = 0;
    for (; i < n; i++)
    {
        if (fds[i] == -1)
        {
            fds[i] = fd;
            break;
        }
    }
}
void DelFdFromFds(int fds[], int fd, int n)//删除数组中的套接字描述符
{
    int i = 0;
    for (; i < n; i++)
    {
        if (fds[i] == fd)
        {
            fds[i] =-1;
            break;
        }
    }
}
int SetFdToset(fd_set *fd_set, int fds[], int n)
//将数组中的套接字描述符设置到fd_set变量中,并返回最大的文件描述符值
{
    FD_ZERO(fd_set);
    int i = 0, maxfd = fds[0];
    for (; i < n; i++)
    {
        if (fds[i] != -1)
        {
            FD_SET(fds[i], fd_set);
            if (fds[i] > maxfd)
            {
                maxfd = fds[i];
            }
        }
    }
    return maxfd;
}
//链接客户端和服务器
void GetClientLink(int sockfd, int fds[], int n)
{
    int c = accept(sockfd, NULL, NULL);
    if (c < 0)
    {
        return;
    }
    printf("SUCCESS TO LINK");
    AddFdToFds(fds, c, n);
}
//处理客户端的数据
void DealClientData(int fds[], int n, int clifd)
{
    char data[1024] = {0};
    int num = recv(clifd, data, 1024 - 1, 0);
    if (num <= 0)
    {
        DelFdFromFds(fds, clifd, n);
        close(clifd);
        printf("A Client disconnected");
    }
    else
    {
        printf("%d : %s \n", clifd, data);
        send(clifd, "OK", 2, 0);
    }
}
//处理数据函数
void DealReadyEvent(int fds[], int n, fd_set *fdset, int sockfd)
{
    int i = 0;
    for (; i < n; ++i)
    {
        if (fds[i] != -1 && FD_ISSET(fds[i], fdset))//当文件描述符存在并且在fdset集合中
        {

            if (fds[i] == sockfd)
            {
                GetClientLink(sockfd, fds, n);//就绪的是服务器端的套接字说明有新的客户端链接
            }
            else
            {
                DealClientData(fds, n, fds[i]);
            }
        }
    }
}
    int main()
    {
        int sockfd = Init_Socked();
        assert(sockfd!= -1);
        fd_set readfds;
        int fds[128];
        InitFds(fds,128);
        AddFdToFds(fds,sockfd,128);//将服务器端套接字加入到数组监听数组中
        while(1){
            int maxfd = SetFdToset(&readfds,fds,128);
            struct timeval timeout;
            timeout.tv_sec= 2;
            timeout.tv_usec=0;
            int n =select(maxfd+1,&readfds,NULL,NULL,&timeout);
            if(n<0){
                printf("select error");
                break;
            }
            else if(n==0)
            {
  printf("time out");
  continue;

           
}
DealReadyEvent(fds,128,&readfds,sockfd);
        }
        exit (0);
    }

程序结构流程图如下

客户端代码
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/select.h>
int Init_Socked()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        exit(1);
    }
    struct sockaddr_in saddr, caddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
    return -1;
    return sockfd;
}
int main()
{

    int sockfd = Init_Socked();
    if(-1==sockfd){
        return -1;
    }
    while(1)
    {
        printf("please InPut:");
        char buff[128] = {0};
        fgets(buff, 128, stdin);
        if (strncmp(buff, "bye", 3) == 0)
        {
            break;
        }
        int n = send(sockfd, buff, strlen(buff) - 1, 0);
        if (n <= 0)
        {
            printf("send Error");
            break;
        }
        memset(buff, 0, 128);
        n = recv(sockfd, buff, 127, 0);
        if (n <= 0)
        {
            printf("recv error");
            break;
        }
        printf("%s\n", buff);
    }
    close(sockfd);
    exit(0);
}

结果如下

poll

poll函数调用和select类似,也是在指定的时间内轮询一定数量的文件描述符,以测试其中是否有就绪者

函数参数

cpp 复制代码
#include<poll.h>
int poll(struct poll*fds,nfds_t nfds,int timeout);

poll系统调用成功后返回文件描述符的总数,超时返回0,失败返回-1

nds参数指定被监听事件的集合fds的大小

timeout参数指定poll的超时值,单位是毫秒,timeout为-1时,poll将永久阻塞

fds参数是一个struct pollfd结构类型的数组

cpp 复制代码
struct pollfd
{
int fd;
short events;
short revents;

};
//fd为成员的指定文件描述符,events成员告诉poll监听fd上的哪些事件类型.

poll支持的事件类型

POLLIN读事件是接收缓冲区有数据了就有事件发生,POLLOUT写事件是发送缓冲区只要空着,还有空间,就会有事件发生,所以一开始发送缓冲区空着就会有写事件发生。这个写事件是应用于发送大量数据,但发送缓冲区没有呢么大,如果发送缓冲区满着,send就会阻塞住,所以可以用写事件的发生来检测发送缓冲区是否有空间可以写数据。

如何知道该事件发生?

因为struct pollfd结构体的第二个成员和第三个成员是一个短整型,用一位表示相应的事件,所以我们可以用实际发生的事件&用户关心的事件,看结果是否为真,revent&POLLIN。

利用poll实现TCP链接

服务器端代码
cpp 复制代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <fcntl.h>
#include <poll.h>
int Init_Socked()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        exit(1);
    }
    struct sockaddr_in saddr, caddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int n = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (-1 == n)
    {
        printf("bind arror");
        exit(1);
    }
    n = listen(sockfd, 5);
    if (-1 == n)
        return -1;
    return sockfd;
}
void InitPollFds(struct pollfd fds[])
{
    int i = 0;
    for (; i < 128; i++)
    {
        fds[i].fd = -1;
    }
}
void InsertFdToPollfds(struct pollfd fds[], int fd, short event)
{
    int i = 0;
    for (; i < 128; i++)
    {
        if (fds[i].fd == -1)
        {
            fds[i].fd = fd;
            fds[i].events = event;
            break;
        }
    }
}
void GetClientLink(struct pollfd fds[], int sockfd)
{
    struct sockaddr_in caddr;
    memset(&caddr, 0, sizeof(caddr));
    socklen_t len = sizeof(caddr);
    int c = accept(sockfd, (struct sockaddr *)&caddr, &len);
    if (c < 0)
    {
        return;
    }
    printf("A client SUCCESSFUL");
    InsertFdToPollfds(fds, c, POLLIN | POLLRDHUP);
}
int DealClientData(int clifd)
{
    char data[1024] = {0};
    int n = recv(clifd, data, 1024 - 1, 0);
    if (n <= 0)
    {
        return -1;
    }
    printf("%d : %s\n", clifd, data);
    send(clifd, "OK", 2, 0);
    return 0;
}
int DealReadyEvent(struct pollfd fds[], int sockfd)
{

    int i = 0;
    for (; i < 128; i++)
    {
        if (fds[i].fd == -1)
        {
            continue;
        }
        else if (fds[i].revents & POLLRDHUP)
        {
            close(fds[i].fd);
            fds[i].fd = -1;
            printf("A Client disconnect");
            continue;
        }
        else if (fds[i].revents & POLLIN)
        {
            if (fds[i].fd == sockfd)
            {
                GetClientLink(fds, sockfd);
            }
            else
            {
                if (-1 == DealClientData(fds[i].fd))
                {
                    close(fds[i].fd);
                    fds[i].fd = -1;
                    printf("A client disconnected");
                }
            }
        }

        else
        {
            printf("ERROR");
        }
    }
}
int main()
{
    int sockfd = Init_Socked();
    assert(sockfd != -1);

    struct pollfd fds[128];
    InitPollFds(fds);
    InsertFdToPollfds(fds, sockfd, POLLIN);
    while (1)
    {
        int n = poll(fds, 128, 2000);
        if (n < 0)
        {
            printf("Poll error\n");
            continue;
        }
        else if (n == 0)
        {
            printf("timeout");
            continue;
        }
        else
        {
            DealReadyEvent(fds, sockfd);
        }
    }
    exit(0);
}

注意:

由于POLLRDHUP不是 POSIX 标准,GCC 编译器默认不暴露该 Linux 特有宏,必须通过定义_GNU_SOURCE来启用 GNU 扩展特性,才能让编译器识别该宏。

结果如下:

epoll

epoll是Linux独有的I/O复用函数,在实现上与select和poll有很大的差异,epoll使用一组函数而不是单个的函数,而且epoll把用户关心的文件描述符上的事件放在内核里面的一个事件表中,无需像select和poll那样每次调用都传入文件描述符或者事件集.

相关函数

epoll_creat()

用于创建内核的事件表

epoll_ctl()用于创建内核事件表

epoll_wait()用于在一段超时间内等待一组文件描述符上的事件

epoll_create(int size);
cpp 复制代码
#include<sys/epoll.h>
int epoll_create(int size);//创建成功会返回内核事件表的文件描述符失败返回-1
epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
cpp 复制代码
#include<sys/epoll.h>
int epoll_ctl(int epfd,int op,struct epoll_event* event);
//成功返回0,失败返回-1
//epfd指定要操作的内核事件表的文件描述符
//fd指定要操作的文件描述符
//op指定操作的类型
//EPOLL_CTL_ADD 在内核事件表上注册fd上的事件
//EPOLL_CTL_MOD  修改fd上的注册事件
//EPOLL_CTL_DEL  删除fd上的注册事件

//epoll_event定义如下
struct epoll_event
{
_uint32_t events;//epoll事件
epoll_data_t data;//用户数据
}
相关推荐
2401_832298102 小时前
云服务器:边缘计算时代的“智能节点”
运维·服务器·边缘计算
梦里小白龙2 小时前
JAVA 策略模式+工厂模式
java·开发语言·策略模式
Coder_Boy_2 小时前
基于SpringAI的智能运维平台(AI驱动)
大数据·运维·人工智能
安_2 小时前
js 数组splice跟slice
开发语言·前端·javascript
程序员葫芦娃3 小时前
【Java毕设项目】基于SSM的旅游资源网站
java·开发语言·数据库·编程·课程设计·旅游·毕设
Pocker_Spades_A3 小时前
飞算Java在线学生成绩综合统计分析系统的设计与实现
java·开发语言·java开发·飞算javaai炫技赛
开压路机3 小时前
Linux的基本指令
linux·服务器
Yuer20253 小时前
用 Rust 做分布式查询引擎之前,我先写了一个最小执行 POC
开发语言·分布式·rust
Francek Chen3 小时前
【飞算JavaAI】智能开发助手赋能Java领域,飞算JavaAI全方位解析
java·开发语言·人工智能·ai编程·飞算