select/poll/epoll 核心区别

一、select/poll/epoll总结回顾

1.select/poll vs epoll 核心区别(面试必背)

维度 select/poll epoll
描述符拷贝次数 每次循环都要把所有描述符 + 事件拷贝到内核 每个描述符仅拷贝一次(添加到内核红黑树后无需重复传)
内核事件检测 轮询所有描述符,时间复杂度 O (n) 注册回调函数,有事件时主动触发,时间复杂度 O (1)
用户事件处理 需遍历所有描述符找就绪的,O (n) 直接返回就绪描述符列表,O (1)
描述符数量限制 select 默认上限 1024,poll 无硬限但效率低 无实际限制(仅受系统资源约束)

2.epoll 的 3 个核心函数(面试必答)

  1. epoll_create :在内核中创建红黑树结构(用于存储所有监听的描述符),返回 epoll 句柄;
  2. epoll_ctl :对内核红黑树进行添加 / 删除 / 修改描述符的操作(这一步完成描述符的 "一次拷贝");
  3. epoll_wait :阻塞等待事件,直接返回就绪的描述符列表(无需遍历)。

3.epoll 的两种触发模式(面试高频考点)

  1. LT(水平触发,默认)

    • 只要描述符有未处理的就绪数据,epoll_wait就会重复通知;
    • 优点:编程简单,不易丢数据;
    • 缺点:可能重复触发,效率略低。
  2. ET(边缘触发)

    • 仅在描述符 "从无数据→有数据" 的状态变化瞬间通知一次;
    • 优点:触发次数少,效率更高;
    • 缺点:需一次性读完所有数据(否则会丢数据),编程复杂度高。
4.面试总结话术(精简版)

select/poll 是早期 IO 复用方案,核心问题是 "重复拷贝 + 遍历低效",只适合低并发;epoll 通过 "内核红黑树存描述符 + 回调函数检测事件 + 直接返回就绪列表",实现了 "一次拷贝 + O (1) 事件处理",是高并发场景的首选,同时支持 LT/ET 两种触发模式,可根据需求选择。

二、epoll-LT(水平触发)模式核心知识点

1.epoll LT 水平触发(默认模式) 核心定义 + 特性【面试必背】

  1. LT(Level Trigger)水平触发是 epoll 的默认触发模式 ,无需额外配置(ev.events = EPOLLIN 就是 LT)。
  2. 核心核心规则:只要文件描述符的内核接收缓冲区中有数据可以读取,epoll 就会报告读事件发生;数据就绪,如果没有处理完就继续提醒,直到把缓冲区的数据彻底读完为止
  3. 触发本质:检测的是「缓冲区的数据状态」,只要是有数据的就绪状态,就持续触发,不会停止。

2.LT 模式下 epoll_wait 返回次数 & 原因

✅ 1. 场景 1:代码中 recv(c, buff, 1, 0) (一次只读 1 字节),客户端发hello(5 字节)

  • epoll_wait 返回次数:5 次
  • 原因:缓冲区有 5 个字节未读,每次只读走 1 个,缓冲区始终有剩余数据,LT 持续触发读事件,每读 1 个字节就触发 1 次 epoll_wait,直到 5 个字节全部读完,共返回 5 次 → 对应打印 5 次 recv+5 次 ok。

3. 通用结论(LT 模式)

epoll_wait的返回次数 = 内核缓冲区中「未读完数据的次数」有多少批次未读数据、就触发多少次,直到缓冲区读空

4.LT 模式 优缺点(面试对比题必背)

✅ 优点:

  1. 编程简单,无数据丢失风险,新手友好;
  2. 是 epoll 默认模式,兼容性最好;
  3. 不用处理非阻塞 IO,代码逻辑简洁。

❌ 缺点:

  1. 触发次数多,epoll_wait 频繁返回,CPU 占用略高;
  2. 易出现「重复响应」的现象(多打印 OK),但属于特性不是 bug。

三、ET模式(边缘触发)

1.ET模式的开启

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>  // fcntl函数头文件
#include <errno.h>  // 错误码头文件

#define MAXFD 10  // 最大监听文件描述符数量


/**************************************************************************
函数名:socket_init
功能:TCP服务器初始化(创建套接字+绑定+监听)
返回值:成功返回监听套接字描述符,失败返回-1
**************************************************************************/
int socket_init()
{
    // 创建TCP套接字(IPv4+字节流)
    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;         // IPv4协议
    saddr.sin_port = htons(6000);       // 绑定端口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");
        close(sockfd);
        return -1;
    }

    // 开启监听(半连接队列长度5)
    if (listen(sockfd, 5) == -1)
    {
        perror("listen err");
        close(sockfd);
        return -1;
    }

    return sockfd;
}


/**************************************************************************
函数名:setnonblock
功能:将文件描述符设置为非阻塞模式
参数:fd - 要设置的文件描述符
**************************************************************************/
void setnonblock(int fd)
{
    // 获取fd当前的状态标志
    int oldfl = fcntl(fd, F_GETFL);
    // 添加非阻塞标志(O_NONBLOCK)
    int newfl = oldfl | O_NONBLOCK;
    // 设置新的状态标志
    if (fcntl(fd, F_SETFL, newfl) == -1)
    {
        printf("fcntl err\n");
    }
}


/**************************************************************************
函数名:epoll_add
功能:将文件描述符添加到epoll内核事件表(红黑树),并开启ET+非阻塞
参数:epfd - epoll句柄;fd - 要添加的描述符
**************************************************************************/
void epoll_add(int epfd, int fd)
{
    struct epoll_event ev;
    ev.data.fd = fd;                  // 关联要监听的描述符
    ev.events = EPOLLIN | EPOLLET;    // 监听读事件 + 开启ET边缘触发
    setnonblock(fd);                  // 将fd设置为非阻塞(ET模式必须)
    
    // 调用epoll_ctl添加fd到epoll内核事件表
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
    {
        printf("epoll ctl add err\n");
    }
}


/**************************************************************************
函数名:epoll_del
功能:从epoll内核事件表中删除指定描述符
参数:epfd - epoll句柄;fd - 要删除的描述符
**************************************************************************/
void epoll_del(int epfd, int fd)
{
    // 调用epoll_ctl删除指定fd
    if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1)
    {
        printf("epoll ctl del err\n");
    }
}


/**************************************************************************
函数名:accept_client
功能:处理新客户端连接(accept+添加到epoll)
参数:sockfd - 监听套接字;epfd - epoll句柄
**************************************************************************/
void accept_client(int sockfd, int epfd)
{
    // 接收新连接(忽略客户端地址)
    int c = accept(sockfd, NULL, NULL);
    if (c < 0)
    {
        return;
    }
    printf("accept c=%d\n", c);
    epoll_add(epfd, c);  // 将新客户端fd添加到epoll(自动开启ET+非阻塞)
}


/**************************************************************************
函数名:recv_data
功能:处理客户端数据(ET模式+非阻塞IO,读空缓冲区)
参数:c - 客户端描述符;epfd - epoll句柄
**************************************************************************/
void recv_data(int c, int epfd)
{
    // while循环读空缓冲区(ET模式必须)
    while (1)
    {
        char buff[128] = {0};
        // 非阻塞读数据(一次读1字节)
        int n = recv(c, buff, 1, 0);

        // 处理recv返回值
        if (n == -1)
        {
            // 情况1:缓冲区无数据(EAGAIN/EWOULDBLOCK是"暂时无数据"的错误码)
            if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                send(c, "ok", 2, 0);  // 数据读空后回复ok
            }
            // 情况2:真的读失败
            else
            {
                printf("recv err\n");
            }
            break;  // 跳出循环(ET模式仅触发一次,读完即止)
        }
        // 客户端断开连接
        else if (n == 0)
        {
            epoll_del(epfd, c);  // 从epoll中删除
            close(c);            // 关闭连接
            printf("close...\n");
            break;
        }
        // 成功读取数据
        else
        {
            printf("%s\n", buff);  // 打印读取到的数据
        }
    }
}


/**************************************************************************
主函数:基于epoll-ET模式的高并发TCP服务器
**************************************************************************/
int main()
{
    // 初始化服务器,获取监听套接字
    int sockfd = socket_init();
    if (sockfd == -1)
    {
        exit(1);
    }

    // 创建epoll内核事件表(红黑树)
    int epfd = epoll_create(MAXFD);
    if (epfd == -1)
    {
        perror("epoll_create err");
        close(sockfd);
        exit(1);
    }

    // 将监听套接字添加到epoll(开启ET+非阻塞)
    epoll_add(epfd, sockfd);

    // 定义epoll_wait的就绪事件数组
    struct epoll_event evs[MAXFD];

    while (1)
    {
        // 调用epoll_wait等待事件,超时时间5000ms(5秒)
        int n = epoll_wait(epfd, evs, MAXFD, 5000);
        if (n == -1)  // epoll_wait调用失败
        {
            printf("epoll err\n");
            continue;
        }
        else if (n == 0)  // 超时(5秒内无事件)
        {
            printf("time out\n");
            continue;
        }

        // 遍历所有就绪事件
        for (int i = 0; i < n; i++)
        {
            // 判断事件类型为读事件
            if (evs[i].events & EPOLLIN)
            {
                // 监听套接字就绪 → 处理新连接
                if (evs[i].data.fd == sockfd)
                {
                    accept_client(sockfd, epfd);
                }
                // 客户端套接字就绪 → 处理数据
                else
                {
                    recv_data(evs[i].data.fd, epfd);
                }
            }
        }
    }

    // 释放资源(实际while(1)不会执行到这里)
    close(epfd);
    close(sockfd);
    exit(0);
}

这是epoll-ET 模式 + 非阻塞 IO 的高并发服务器标准实现,解决了 ET 模式下 "数据丢失" 的问题,核心设计是面试高频考点:

1. 关键改造点(针对 ET 模式的必做操作)
  • 开启 ET 触发ev.events = EPOLLIN | EPOLLET(ET 模式是高并发核心);
  • 设置非阻塞 IOsetnonblock(fd)(ET 模式必须,避免 while 循环阻塞);
  • while 循环读空缓冲区recv_data中用while(1)读数据,直到缓冲区为空(ET 模式仅触发一次,必须读空)。
2. 核心函数解析(面试必答)
(1)setnonblock函数
  • 作用:将文件描述符从阻塞模式 改为非阻塞模式
  • 原理:通过fcntl函数获取 / 设置 fd 的状态标志,添加O_NONBLOCK标志;
  • 必要性:ET 模式下,recv若无数据会返回-1,若为阻塞模式会一直卡着,非阻塞模式则会返回错误码EAGAIN/EWOULDBLOCK(表示 "暂时无数据")。
(2)epoll_add函数
  • 同时完成 3 件事:
    1. 关联描述符到 epoll 事件结构体;
    2. 开启 ET 边缘触发;
    3. 设置 fd 为非阻塞;
  • 这是 ET 模式服务器的 "标准配置",三者缺一不可。
(3)recv_data函数(ET 模式核心)
  • while(1)循环:强制读空内核缓冲区,避免数据积压 / 丢失;
  • recv返回-1的处理:
    • errno == EAGAIN/EWOULDBLOCK:缓冲区已空,跳出循环并回复 "ok";
    • 其他错误:真的读失败,打印错误;
  • recv返回0:客户端断开,删除 fd 并关闭连接;
  • recv返回>0:打印读取到的数据。

✅ 面试复习笔记(ET 模式核心考点)

1. ET 模式的 "三个必须"(面试必背)
  • 必须开启EPOLLET标志;
  • 必须将 fd 设置为非阻塞;
  • 必须用while循环读空缓冲区。
2. ET 模式的优势(面试对比题)
  • 触发次数极少:客户端发一次数据,epoll_wait仅返回 1 次;
  • CPU 占用极低:避免 LT 模式的重复触发,适合万级以上高并发场景(如 Nginx)。
3. 常见面试题
  • Q:ET 模式为什么要设置非阻塞? A:ET 模式下,recv若无数据会返回-1,若为阻塞模式会一直阻塞在recv;非阻塞模式下会返回EAGAIN/EWOULDBLOCK,可以正常跳出循环。

  • **Q:ET 模式为什么要用 while 循环读数据?**A:ET 模式仅触发一次读事件,若不读空缓冲区,剩余数据会积压在缓冲区,直到新数据到来才会被读取,导致数据丢失。

4. 运行效果

运行结果解析

从日志能看到:

  1. 新连接处理 :客户端连接后,服务器打印accept c=5(成功接收连接);
  2. 数据读取(ET 模式特性)
    • 客户端输入hello,服务器一次性读空数据,打印h/e/l/l/orecv一次读1个字符的效果);
    • 客户端后续输入abc/aaa/bb/cc,服务器每次都读空数据并回复ok
  3. 客户端断开 :输入end后,服务器打印close...(成功处理断开);
  4. 超时逻辑 :无数据时,每 5 秒打印time outepoll_wait超时正常)。

为什么这是 "正确的 ET 模式效果"

  1. 仅触发一次epoll_wait :客户端每次发数据,服务器只触发 1 次epoll_wait,所有读操作都在这次触发的while循环里完成(比如hello的 5 个字符,在一次epoll_wait里读了 5 次);
  2. 无重复触发 :对比之前 LT 模式的 "多次epoll_wait+ 多次ok",现在 ET 模式每次数据仅回复 1 次ok,效率极高;
  3. 无数据丢失while循环+非阻塞IO保证了每次数据都被读空,客户端发的hello/abc等数据都完整打印。

四、总结

一、select/poll vs epoll 核心区别(用 "快递员" 类比)

维度 select/poll(低效快递员) epoll(高效快递员)
描述符拷贝 每次送快递(循环),都要把所有包裹(描述符)重新搬上车 包裹(描述符)只搬上车一次,之后直接从车上取
内核找包裹 挨个翻车厢找要送的包裹(轮询,O (n)) 包裹到了主动喊你(回调,O (1))
用户找包裹 自己把所有包裹翻一遍,找要送的(遍历,O (n)) 直接给你 "待送包裹清单"(就绪描述符,O (1))

二、epoll 的 3 个核心函数(用 "快递站" 类比)

  • epoll_create:开一个快递站(内核事件表,用红黑树存包裹);
  • epoll_ctl:往快递站里加 / 删 / 改包裹(描述符);
  • epoll_wait:找快递站要 "待送包裹清单"(就绪描述符)。

三、epoll 的 2 种触发模式(用 "提醒吃饭" 类比)

  • LT(水平触发):喊你吃饭,你没吃完就一直喊,直到吃完(事件就绪没处理完,持续提醒);
  • ET(边缘触发):只喊你一次吃饭,吃不吃完都不喊了(事件就绪只提醒一次)。
相关推荐
weixin_462446231 小时前
ubuntu真机安装tljh jupyterhub支持跨域iframe
linux·运维·ubuntu
Ghost Face...2 小时前
深入解析网卡驱动开发与移植
linux·驱动开发
a41324472 小时前
在CentOS系统上挂载硬盘到ESXi虚拟机
linux·运维·centos
MMME~2 小时前
Linux下的软件管理
linux·运维·服务器
迷途之人不知返2 小时前
Linux操作系统的基本指令
linux·服务器
松涛和鸣2 小时前
DAY49 DS18B20 Single-Wire Digital Temperature Acquisition
linux·服务器·网络·数据库·html
BIBI20492 小时前
通过 Studio 3T 远程连接 CentOS 7 上的 MongoDB
linux·mongodb·centos·nosql·配置·问题解决·环境搭建
小小ken2 小时前
ubuntu添加新网卡时,无法自动获取IP原因及解决办法
linux·网络·tcp/ip·ubuntu·dhcp
Xの哲學3 小时前
Linux 软中断深度剖析: 从设计思想到实战调试
linux·网络·算法·架构·边缘计算