Linux——I/O复用

目录

一、I/O复用技术

[1.1 I/O复用定义](#1.1 I/O复用定义)

[1.2 I/O复用本质](#1.2 I/O复用本质)

[1.3 实现原理](#1.3 实现原理)

[1.4 应用场景](#1.4 应用场景)

[1.5 常见的I/O复用技术](#1.5 常见的I/O复用技术)

二、select()

[2.1 工作原理](#2.1 工作原理)

[2.2 select实现TCP服务器端](#2.2 select实现TCP服务器端)

[2.2.1 C语言通过select()实现TCP服务器端](#2.2.1 C语言通过select()实现TCP服务器端)

[2.2.2 select的缺点](#2.2.2 select的缺点)

三、poll()

[3.1 工作原理](#3.1 工作原理)

[3.2 poll实现TCP服务端](#3.2 poll实现TCP服务端)

[3.3 poll优缺点](#3.3 poll优缺点)

四、epoll()

[4.1 工作原理](#4.1 工作原理)

epoll的核心机制

水平触发与边缘触发

epoll的性能优势

epoll的适用场景

[4.2 epoll实现TCP服务端](#4.2 epoll实现TCP服务端)

[4.3 ET模式和LT模式](#4.3 ET模式和LT模式)

ET模式与LT模式的概念

ET模式(边缘触发)

LT模式(水平触发)

关键区别对比

代码示例

[4.4 epoll()的优缺点](#4.4 epoll()的优缺点)


一、I/O复用技术

1.1 I/O复用定义

I/O复用(Input/Output Multiplexing)是一种高效处理多个I/O操作的机制,允许单个进程或线程同时监控多个文件描述符(如套接字、管道等),并在这些描述符中的任意一个准备好进行读写操作时通知程序。其核心目标是减少系统资源消耗,避免为每个I/O操作创建独立的线程或进程,从而提升并发性能。

1.2 I/O复用本质

I/O复用使得程序能够同时监听多个文件描述符,但本身也是阻塞的。如果多个描述符同时就绪,则只能按顺序处理其中的每一个文件描述符。

1.3 实现原理

通过系统调用(如selectpollepollkqueue)将多个文件描述符注册到内核中,内核监控这些描述符的状态变化(如可读、可写或异常)。当某个描述符就绪时,内核通知应用程序,应用程序再处理对应的I/O事件。

1.4 应用场景

  • 网络服务器(如Web服务器、聊天程序)需同时处理多个客户端连接。
  • 需要非阻塞I/O操作的场景,避免线程阻塞导致资源浪费。
  • 高并发系统中优化性能,减少线程/进程切换的开销。

1.5 常见的I/O复用技术

(1)select

通过检测多个文件描述符的状态(可读、可写、异常)来进行I/O操作。

但在处理的文件描述符较多时效率较低。

(2)poll

类似于select,也是在指定时间内轮询一定数量的文件描述符,测试是否有就绪者。

但没有描述符数量的限制,支持较大规模的文件描述符集,更加灵活。

(3)epoll(Linux特有)

使用一组函数来完成任务,而不是单个函数。

比select和poll更高效,采用事件驱动机制,适合大量连接的服务器应用。

二、select()

2.1 工作原理

select() 是一种 I/O 多路复用机制,用于监视多个文件描述符的状态变化(如可读、可写或异常)。它允许程序在单个线程中同时处理多个 I/O 操作,避免阻塞等待单个描述符。

基本流程:

  1. 初始化文件描述符集合 :通过 fd_set 结构体设置需要监视的文件描述符(如 sockets、pipes 等)。

  2. 调用 select():传入待监视的描述符集合及超时时间。内核会检查这些描述符的状态。

  3. 内核检查状态:内核遍历所有描述符,判断是否有满足条件的事件(如数据可读、缓冲区可写等)。

  4. 返回结果 :select() 返回就绪的描述符数量,并修改 fd_set 集合,仅保留就绪的描述符。

  5. 处理就绪事件 :程序遍历 fd_set,处理已就绪的 I/O 操作。

关键特点

  • 同步阻塞:select() 本身是阻塞调用,直到有描述符就绪或超时。

  • 数量限制 :受限于 FD_SETSIZE(通常 1024),不适合高并发场景。

  • 效率问题:每次调用需重新传递描述符集合,且内核需线性扫描所有描述符。

性能对比

  • 优点:跨平台兼容性好,适合少量连接。

  • 缺点:与 epoll 或 kqueue 相比,高并发时性能较差。

2.2 select实现TCP服务器端

2.2.1 C语言通过select()实现TCP服务器端

①首先,创建一个数组,该数组用来存放程序中的文件描述符。

②然后,创建一个集合,并调用FD_ZERO 方法,将集合中的每一个位清空。

③接着,使用FD_SET方法,将数组中的每个文件描述符传入到fd_set的集合中。

④调用select()方法,返回就绪文件描述符的总数。

⑤最后,使用FD_ISSET方法,检测哪些位被设置。

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
//TCP服务器端使用select,同时处理监听套接字和连接套接字
 
#define MAXFD 10  //定义最大的文件描述符
int socket_init();//初始化套接字
 
void fds_init(int fds[]) //清空数组,每个元素是-1
{
    for(int i=0;i<MAXFD;i++)
    {
        fds[i]=-1;
    }
}
 
void fds_add(int fds[],int fd)//添加一个描述符
{
    for(int i=0;i<MAXFD;i++)
    {
        if(fds[i]==-1)
        {
            fds[i]=fd;
            break;
        }
    }
}
 
void fds_del(int fds[],int fd)//删除一个描述符
{
    for(int i=0;i<MAXFD;i++)
    {
        if(fds[i]==fd)
        {
            fds[i]=-1;
        }
    }
}
 
void accept_client(int sockfd,int fds[]) //接受连接
{
    int c=accept(sockfd,NULL,NULL);//得到连接套接字
    if(c<0)
    {
        return;
    }
    printf("accept c=%d\n",c);
    fds_add(fds,c);//将连接套接子加入数组
}
 
void recv_data(int c,int fds[]) //接受数据
{
    char buff[128]={0};
    int n=recv(c,buff,127,0);//只要接收缓冲区有数据,select会继续执行(未读完继续读)
    if(n<=0)   //如果对方关闭或者接受失败,则删除数组中的套接字
    {
        close(c);
        fds_del(fds,c);
        printf("client close\n");
        return;
    }
    printf("bufff=%s\n",buff);
    send(c,"ok",2,0);
}
 
int main()
{
    //创建套接字
    int sockfd=socket_init(); 
    if(sockfd==-1)
    {
        exit(1);
    }
    //定义一个数组,收集描述符
    //原因:需要数组来保存所有的文件描述符,将数组中的元素再存放到集合中,执行select后,select会修改集合中的文件描述符
    int fds[MAXFD];
    fds_init(fds);//初始化数组
    fds_add(fds,sockfd);//向数组中添加监听套接字
    fd_set fdset;//定义一个集合
    while(1)
    {
        FD_ZERO(&fdset);//清除fdset的所有位
        int maxfd=-1;//定义文件描述符最大值
        for(int i=0;i<MAXFD;i++) //循环遍历数组,将数组中的套接字放入到集合,并置为1
        {
            if(fds[i]==-1)
            {
                continue;
            }
            FD_SET(fds[i],&fdset);//将数组中的元素添加到集合中,将对应的文件标识符位置置为1
            if(maxfd<fds[i]) //找出最大的文件描述符
            {
                maxfd=fds[i];
            }
        }
        //定义时间
        struct timeval tv={5,0};
        //使用select方法,会修改fdset集合
        int n=select(maxfd+1,&fdset,NULL,NULL,&tv);
        //失败
        if(n==-1)
        {
            printf("select err\n");
        }
        else if(n==0) //超时
        {
            printf("time out\n");
        }
        else{
            for(int i=0;i<MAXFD;i++)
            {
                if(fds[i]==-1)
                {
                    continue;
                }
                if(FD_ISSET(fds[i],&fdset))//测试发现该描述符fds[i]有事件
                {
                    if(fds[i]==sockfd)//accept
                    {
                        accept_client(sockfd,fds);
                    }
                    else//recv
                    {
                         recv_data(fds[i],fds);
                    }
                }
            }
        }
    }
 
 
}

int socket_init()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
    if(sockfd==-1)
    {
        return -1;
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET; //使用ipv4协议
    saddr.sin_port=htons(6000);//端口号
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//ip地址
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定端口和ip地址
    if(res==-1)
    {
        printf("bind err\n");
        return -1;
    }
    res=listen(sockfd,5); //创建监听队列
    if(res==-1)
    {
        return -1;
    }
    return sockfd;
}

需要注意的是:使用数组,而不直接集合是因为select方法会对集合进行修改,不能保证下一次调用的正确性。

2.2.2 select的缺点

(1)文件描述符数量有限(受限于FD_SETSIZE)

(2)每次调用时都需要重新设置文件描述符集合,效率较低

(3)在大量连接时性能下降(因为要线性扫描所有描述符)

三、poll()

3.1 工作原理

poll() 是 Unix/Linux 系统中的系统调用,用于监视多个文件描述符的状态变化,如是否可读、可写或出现异常。它是 select() 的改进版本,解决了 select() 的一些限制,如文件描述符数量受限和性能问题。

poll() 的函数原型

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

参数说明

  • fds :指向 struct pollfd 数组的指针,每个结构体描述一个需要监视的文件描述符。

  • nfds :数组 fds 中元素的数量。

  • timeout:超时时间(毫秒)。

    • 0:立即返回,不阻塞。

    • -1:无限阻塞,直到事件发生。

    • >0:等待指定毫秒数后超时返回。

pollfd 结构体

c 复制代码
struct pollfd {
    int fd;         // 文件描述符
    short events;   // 监视的事件(输入)
    short revents;  // 实际发生的事件(输出)
};

常用事件标志

  • POLLIN:数据可读。

  • POLLOUT:数据可写。

  • POLLERR:错误发生。

  • POLLHUP:连接挂起(如对端关闭)。

  • POLLNVAL:文件描述符未打开。

工作流程

  1. 初始化 pollfd 数组,设置需要监视的文件描述符和事件(events)。

  2. 调用 poll(),系统会阻塞直到事件发生或超时。

  3. poll() 返回后,检查每个 pollfdrevents 字段,判断哪些文件描述符发生了事件。

  4. 根据 revents 处理对应的文件描述符(如读写数据)。

返回值

  • >0:发生事件的文件描述符数量。

  • 0:超时且无事件发生。

  • -1 :出错,可通过 errno 获取错误原因。

与 select() 的对比

  • 文件描述符数量poll() 使用数组,无固定限制;select()FD_SETSIZE 限制。

  • 性能poll() 在文件描述符较多时效率更高。

  • 事件类型poll() 提供更丰富的事件标志。

poll() 适合需要监视大量文件描述符的场景,但在高并发场景下,epollkqueue 可能更高效。

3.2 poll实现TCP服务端

①首先,创建一个pollfd数组,该数组用来存放程序中的文件描述符。

②为该数组的数据成员(fd,events,revents)赋值

③使用&检测事件

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
#include<poll.h>
//TCP服务器端使用poll
//poll传递的参数是结构体数组
 
#define MAXFD 10  //定义最大的文件描述符
int socket_init();//初始化套接字
 
void fds_init(struct pollfd fds[])//初始化结构体数组
{
    for(int i=0;i<MAXFD;i++)
    {
        fds[i].fd=-1;
        fds[i].events=0;
        fds[i].revents=0;
    }
}
 
void fds_add(struct pollfd fds[],int fd) //向结构体数组中添加描述符
{
    for(int i=0;i<MAXFD;i++)
    {
        if(fds[i].fd==-1)
        {
            fds[i].fd=fd;
            fds[i].events=POLLIN;//设置事件为可读
            fds[i].revents=0;
            break;
        }
    }
}
 
void fds_del(struct pollfd fds[],int fd) //向结构体数组中删除描述符
{
    for(int i=0;i<MAXFD;i++)
    {
        if(fds[i].fd==fd)
        {
            fds[i].fd=-1;
            fds[i].events=0;
            fds[i].revents=0;
            break;
        }
    }
}
 
void accept_client(int sockfd,struct pollfd fds[])//接受客户端的连接
{
    int c=accept(sockfd,NULL,NULL);
    if(c<0)
    {
        return ;
    }
    printf("accept c=%d\n",c);
    fds_add(fds,c);//向结构体数组中添加文件描述符
}
 
void recv_data(int c,struct pollfd fds[]) //接受客户端的数据
{
    char buff[128]={0};
    int num=recv(c,buff,127,0);//只要接收缓冲区有数据,poll会继续执行(未读完继续读)
    if(num<=0) //对方关闭缓冲区或者缓冲区五无数据则关闭连接套接字
    {
        close(c);
        fds_del(fds,c);//从结构体数组中删除描述符
        printf("client close\n");
        return;
    }
    printf("buff=%s\n",buff);
    send(c,"ok",2,0);
 
}
int main()
{
    int sockfd=socket_init(); //创建套接字
    if(sockfd==-1)
    {
        exit(1);
    }
    struct pollfd fds[MAXFD]; //定义结构体数组
    fds_init(fds);//初始化结构体数组
    fds_add(fds,sockfd);//向结构体数组中添加数据
    while(1)
    {
        int n=poll(fds,MAXFD,5000);//执行poll
        if(n==-1) //失败
        {
            printf("poll err\n");
        }
        else if(n==0)//超时
        {
            printf("time out \n");
        }
        else
        {
            for(int i=0;i<MAXFD;i++)
            {
                if(fds[i].fd==-1)//无效
                {
                    continue;
                }
                if(fds[i].revents&POLLIN) //检测是否有读数据的描述符
                {
                    if(fds[i].fd==sockfd) //如果是监听套接字,则建立连接
                    {
                        accept_client(sockfd,fds);
                    }
                    else  //接受客户端连接
                    {
                        recv_data(fds[i].fd,fds);
                    }
                }
            }
        }
    }
 
}
 
 
int socket_init()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
    if(sockfd==-1)
    {
        return -1;
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET; //使用ipv4协议
    saddr.sin_port=htons(6000);//端口号
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//ip地址
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定端口和ip地址
    if(res==-1)
    {
        printf("bind err\n");
        return -1;
    }
    res=listen(sockfd,5); //创建监听队列
    if(res==-1)
    {
        return -1;
    }
    return sockfd;
}

3.3 poll优缺点

优点:

**实时性:**poll(投票/轮询)能够快速获取用户反馈或数据变化,适用于需要频繁更新的场景,例如实时监控或动态数据展示。

**简单易用:**实现轮询的逻辑通常较为简单,代码复杂度低,适合基础场景或快速原型开发。

**兼容性强:**几乎所有设备和浏览器都支持轮询,无需依赖特定技术(如WebSocket或Server-Sent Events)。
缺点:

**资源浪费:**频繁的轮询请求可能导致服务器和客户端资源浪费,尤其是在无数据变化时仍会发送请求。

**延迟问题:**轮询间隔决定了数据更新的实时性。较长的间隔会导致延迟,较短的间隔会增加服务器负载。

**可扩展性差:**高并发场景下,大量客户端频繁轮询可能导致服务器性能瓶颈。替代方案(如长轮询或WebSocket)更适合大规模应用。

四、epoll()

4.1 工作原理

epoll是Linux内核提供的一种高效I/O多路复用机制,主要用于处理大量文件描述符的I/O事件。相比select和poll,epoll在性能和扩展性上具有显著优势。

epoll的核心机制

epoll通过三个系统调用实现:epoll_createepoll_ctlepoll_waitepoll_create创建一个epoll实例并返回对应的文件描述符。epoll_ctl用于向epoll实例注册、修改或删除需要监控的文件描述符及其事件。epoll_wait等待事件的发生并返回就绪的事件列表。

epoll采用红黑树和就绪列表的双数据结构设计。红黑树存储所有注册的文件描述符,保证插入、删除和查找操作的高效性。就绪列表存储已就绪的事件,避免遍历所有文件描述符。

水平触发与边缘触发

epoll支持两种事件触发模式:水平触发(LT)和边缘触发(ET)。水平触发模式下,只要文件描述符处于就绪状态,epoll会不断通知应用程序。边缘触发模式下,epoll仅在文件描述符状态发生变化时通知一次,应用程序需处理所有可用数据。

水平触发模式更简单,但可能效率较低。边缘触发模式更高效,但需要应用程序更精确地处理事件,避免遗漏数据。

epoll的性能优势

epoll避免了select和poll的线性扫描问题,仅关注活跃的文件描述符。这使得epoll在高并发场景下性能更优,尤其适合处理大量连接但活跃度不高的网络应用。

内核通过回调机制将就绪的文件描述符加入就绪列表,无需遍历所有描述符。epoll_wait直接返回就绪列表,时间复杂度为O(1)。

epoll的适用场景

epoll特别适合高并发的网络服务器,如Web服务器、即时通讯服务器等。在这些场景中,epoll能够高效处理成千上万的并发连接,同时保持较低的CPU和内存开销。

epoll不适用于文件I/O或低并发的场景,因为文件I/O通常不适合非阻塞模式,而低并发场景中select或poll可能已经足够。

4.2 epoll实现TCP服务端

①首先,使用epoll_create()创建epoll对象 。

②然后,使用epoll_ctl()操作内核事件表。

③接着,使用epoll_wait()等待事件发生,获取就绪队列。

事件发生后,立即获得就绪的描述符集合(无须线性扫描全部描述符)

cpp 复制代码
#include<sys/epoll.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
 
 
#define MAXFD 10
//创建套接字
int socket_init() 
{
   int sockfd=socket(AF_INET,SOCK_STREAM,0);
   if(sockfd==-1)
    {
        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)
    {
        printf("bind err\n");
        return -1;
    }
    res=listen(sockfd,5);
    if(res==-1)
    {
        return -1;
    }
    return sockfd;
 
}
 
//添加数据到内核事件表
void epoll_add(int epfd,int fd)
{
    struct epoll_event ev; //定义数组结构体
    ev.data.fd=fd;  //将数据添加到表中
    ev.events=EPOLLIN;//设置事件为读事件
    if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
    {
        printf("epoll_ctl err\n");
    }                                                                                          
}
//从内核表中删除数据
void epoll_del(int epfd,int fd)
{
    if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
    {
        printf("epoll ctl del err\n") ;
    }
}
//接受客户端的连接
void accept_client(int sockfd,int epfd)
{
    int c=accept(sockfd,NULL,NULL);
    if(c<=0)
    {
        printf("accept err\n");
        return ;
    }
    printf("accept c=%d\n",c);
    epoll_add(epfd,c);//将c添加到内核事件表
} 
//接受客户端的数据
void recv_data(int c,int epfd)
{
    char buff[128]={0};
    int n=recv(c,buff,1,0);
    if(n<=0)
    {
        epoll_del(epfd,c); //将c从内核事件表删除
        close(c);
        printf("client close\n");
        return;
    }
    printf("buff=%s\n",buff);
    send(c,"ok",2,0);
}
 
int main()
{
    int sockfd=socket_init();//创建套接字
    if(sockfd==-1)
    {
        exit(1);
    }
    //创建内核事件表(红黑树) 就绪队列(链表)
    int epfd=epoll_create(MAXFD);
 
    //将监听套接字添加到内核事件表
    epoll_add(epfd,sockfd);
 
    struct epoll_event evs[MAXFD];//用户数组,收集就绪描述符
    while(1)
    {
        //获取就绪描述符,会阻塞
        int n=epoll_wait(epfd,evs,MAXFD,5000);//从内核事件表中获取就绪描述符存放到evs中
        if(n==-1)  //失败
        {
            printf("epoll wait err\n");
            exit(1);
        }
        else if(n==0) //超时
        {
            printf("time out\n");
        }
        else
        {
            for(int i=0;i<n;i++)//就绪描述符的个数=n
            {
                int fd=evs[i].data.fd;//从就绪数组中取出描述符
                if(fd==-1)
                {
                    continue; //无效
                }
                if(evs[i].events&EPOLLIN) //判断读事件是否发生               
                {
                    if(fd==sockfd) //accept
                    {
                        accept_client(fd,epfd); 
                    }
                    else   
                    {
                        recv_data(fd,epfd);
                    }
                 
                }    
                
 
            }
        }
 
    }
}

4.3 ET模式和LT模式

ET模式与LT模式的概念

ET(Edge Triggered)模式和LT(Level Triggered)模式是I/O多路复用技术中的两种事件触发机制,常见于epollselectpoll等系统调用中。两者的核心区别在于事件通知的方式和对用户态程序的要求。

ET模式(边缘触发)

ET模式仅在状态变化时触发事件。例如,当文件描述符(fd)从不可读变为可读(如新数据到达)时,仅通知一次。若未完全处理数据,后续不会重复触发。

特点:

  • 高效:减少重复事件通知的次数。
  • 需非阻塞IO:必须一次性处理完所有数据,否则可能丢失事件。
  • 需手动维护:需循环读取数据直到EAGAINEWOULDBLOCK错误。

适用场景:

  • 高并发场景,如高性能服务器。
  • 开发者能确保事件被及时处理。

LT模式(水平触发)

LT模式在状态满足条件时持续触发事件。例如,只要fd可读,每次调用epoll_wait都会通知,直到数据被完全读取。

特点:

  • 简单易用:无需担心事件丢失,未处理的数据会重复触发。
  • 可能效率较低:频繁的事件通知可能增加开销。
  • 阻塞/非阻塞IO均可:未强制要求一次性处理完数据。

适用场景:

  • 对代码健壮性要求较高的场景。
  • 开发者希望简化事件处理逻辑。

关键区别对比

特性 ET模式 LT模式
触发条件 状态变化时(边缘) 状态持续满足时(水平)
通知次数 仅一次 多次直至条件解除
IO要求 必须非阻塞 阻塞或非阻塞均可
数据未处理 可能丢失事件 持续触发事件
性能 更高 相对较低

代码示例

ET模式下的处理(需非阻塞IO)

c 复制代码
while (true) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, timeout);
    for (int i = 0; i < n; i++) {
        if (events[i].events & EPOLLIN) {
            while (1) {
                ssize_t cnt = read(fd, buf, sizeof(buf));
                if (cnt == -1) {
                    if (errno == EAGAIN) break; // 数据已读完
                    else handle_error();
                } else if (cnt == 0) {
                    close(fd); // 连接关闭
                }
                // 处理数据
            }
        }
    }
}

LT模式下的处理

c 复制代码
while (true) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, timeout);
    for (int i = 0; i < n; i++) {
        if (events[i].events & EPOLLIN) {
            ssize_t cnt = read(fd, buf, sizeof(buf));
            if (cnt <= 0) close(fd); // 连接关闭或错误
            // 处理数据(未读完的数据下次会再次触发)
        }
    }
}

选择建议

  • ET模式:追求极致性能,且能确保及时处理事件。
  • LT模式:优先代码简洁性和容错性,或对性能要求不高。

在实际开发中,epoll默认使用LT模式,需通过EPOLLET标志显式启用ET模式。

4.4 epoll()的优缺点

epoll()的优点:

**高效的事件通知机制:**epoll采用基于事件驱动的回调机制,仅通知活跃的文件描述符(FD),避免了遍历所有FD的开销。与select/poll相比,性能随FD数量增加而线性下降的问题得到显著改善。

**支持大并发连接:**epoll使用红黑树管理FD,内核通过mmap共享用户空间和内核空间的数据,减少了复制开销。理论上支持的FD数量仅受系统内存限制,适合高并发场景(如Web服务器)。

**边缘触发(ET)与水平触发(LT)模式:**ET模式仅在FD状态变化时通知一次,减少重复事件触发;LT模式兼容传统poll行为,简化编程。开发者可根据场景选择模式。

**零拷贝优化:**通过共享内存机制(mmap)避免FD集合在用户和内核空间之间的频繁拷贝,降低CPU和内存开销。
epoll()的缺点:

**仅限Linux平台:**epoll是Linux特有的API,不具备跨平台兼容性。其他系统需使用类似机制(如kqueue、IOCP)。

**编程复杂度较高:**ET模式需非阻塞IO并处理EAGAIN错误,否则可能遗漏事件。相比select/poll,代码逻辑更复杂。

**不适合短连接场景:**频繁增删FD(如短连接的HTTP服务)会导致红黑树调整开销,可能弱化性能优势。

**内核版本依赖:**旧内核(早于2.5.44)不支持epoll,且部分特性(如EPOLLEXCLUSIVE)需较新版本。

相关推荐
chuanauc26 分钟前
记录一次在 centos 虚拟机 中 安装 Java环境
java·linux·centos
企鹅侠客1 小时前
Bash与Zsh与Fish:在Linux中你应该使用哪个Shell
linux·开发语言·bash·zsh·fish
脑袋大大的2 小时前
钉钉企业应用开发技巧:在单聊会话中实现互动卡片功能
服务器·microsoft·钉钉·企业应用开发
海星船长丶2 小时前
基于docker进行渗透测试环境的快速搭建(在ubantu中docker设置代理)
运维·docker·容器
qinyia2 小时前
利用Wisdom SSH高效搭建CI/CD工作流
运维·ci/cd·ssh
是阿建吖!2 小时前
【Linux | 网络】socket编程 - 使用TCP实现服务端向客户端提供简单的服务
linux·网络·tcp/ip
渡我白衣3 小时前
Linux操作系统之进程间通信:管道概念
linux
Amelio_Ming3 小时前
C++开源项目—2048.cpp
linux·开发语言·c++
科智咨询3 小时前
双轮驱动:政策激励与外部制约下的国产服务器市场演进
运维·服务器·gpu算力
行而不知3 小时前
家庭网络中的服务器怎么对外提供服务?
运维·服务器·内网穿透·ddns