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(边缘触发):只喊你一次吃饭,吃不吃完都不喊了(事件就绪只提醒一次)。
相关推荐
AnalogElectronic17 小时前
linux 测试网络和端口是否连通的命令详解
linux·网络·php
Edward1111111118 小时前
4月28日防火墙问题
linux·运维·服务器
子琦啊19 小时前
【算法复习】字符串 | 两个底层直觉,吃透高频题
linux·运维·算法
AOwhisky20 小时前
Kubernetes 学习笔记:集群管理、命名空间与 Pod 基础
linux·运维·笔记·学习·云原生·kubernetes
小龙在慢慢变强..20 小时前
目录结构(FHS 标准)
linux·运维·服务器
2035去旅行20 小时前
嵌入式开发,如何选择C标准库
linux·arm开发
刘延林.20 小时前
win11系统下通过 WSL2 安装Ubuntu 24.04 使用RTX 5080 GPU
linux·运维·ubuntu
CodeOfCC1 天前
Linux 嵌入式arm64安装openclaw
linux·运维·服务器
宵时待雨1 天前
linux笔记归纳3:linux开发工具
linux·运维·笔记
magrich1 天前
安装NoMachine并解决无外接显示器桌面黑屏
linux·运维·服务器