epoll 是 Linux 下多路复用的 “终极方案”,专为高并发(万级连接)设计,解决了 select/poll 轮询效率低、fd 数量受限、非线程安全等核心痛点!本文从原理、核心接口、边缘触发(ET)/ 水平触发(LT)对比到实战代码全解析,零基础也能掌握 epoll 的核心用法。

一、核心背景:为什么需要 epoll?

select/poll 在高并发场景下的致命缺陷,epoll 都做了颠覆性优化:

痛点 select/poll epoll
效率问题 轮询所有 fd,fd 越多效率越低 事件驱动,仅处理有状态变化的 fd,效率与 fd 数量无关
fd 数量限制 select 硬限制 1024,poll 受系统资源但仍轮询 无硬限制(支持万级以上 fd)
线程安全 非线程安全,多线程使用易出问题 线程安全,可多线程同时操作
内核 / 用户态拷贝 每次调用拷贝全部 fd 集合 仅拷贝一次,后续复用
触发模式 仅支持水平触发(LT) 支持 LT(默认)+ 边缘触发(ET),灵活适配场景

 epoll 的核心定位:

  • 中小并发(千级):select/poll 够用;
  • 高并发(万级):epoll 是唯一选择(如 Nginx、Redis 等中间件均基于 epoll)。

二、epoll 核心接口详解

epoll 的核心是 3 个接口,全部定义在 <sys/epoll.h> 头文件中,使用前需包含该头文件。

1. epoll_create:创建 epoll 实例

#include <sys/epoll.h>

int epoll_create(int size);
功能:

创建一个 epoll 实例,返回该实例的文件描述符(后续操作均基于此 fd)。

参数:
  • size:早期内核用于提示 epoll 实例可监听的 fd 最大数,现代内核已忽略该值(仅需传大于 0 的整数即可)。
返回值:
  • 成功:返回 epoll 实例的 fd(非负整数);
  • 失败:返回 - 1,错误码存于errno

2. epoll_ctl:管理 epoll 事件(增 / 删 / 改)

int epoll_ctl(
    int epfd,          // epoll实例的fd(epoll_create返回值)
    int op,            // 操作类型:增/删/改
    int fd,            // 要监控的文件描述符(套接字/文件)
    struct epoll_event *event // 监控的事件类型及关联数据
);
关键参数说明:
参数值 含义
op EPOLL_CTL_ADD:添加 fd 及事件到 epoll 实例• EPOLL_CTL_DEL:从 epoll 实例删除 fd 及事件• EPOLL_CTL_MOD:修改 fd 的监控事件
struct epoll_event epoll 的核心事件结构体,定义如下:c<br>typedef union epoll_data {<br> void *ptr; // 自定义指针(如指向结构体)<br> int fd; // 关联的文件描述符(最常用)<br> uint32_t u32;<br> uint64_t u64;<br>} epoll_data_t;<br><br>struct epoll_event {<br> uint32_t events; // 监控/触发的事件类型(如EPOLLIN)<br> epoll_data_t data;// 关联数据(通常存fd)<br>};<br>
常用事件常量:
事件 含义
EPOLLIN 可读事件(套接字有数据 / 新连接)
EPOLLOUT 可写事件(套接字缓冲区空闲)
EPOLLERR 错误事件
EPOLLHUP 挂起事件(对端关闭连接)
EPOLLET 边缘触发模式(需配合 EPOLLIN/EPOLLOUT 使用)
EPOLLONESHOT 单次触发(事件处理后需重新注册)
返回值:
  • 成功:返回 0;
  • 失败:返回 - 1,错误码存于errno

3. epoll_wait:等待事件触发

int epoll_wait(
    int epfd,                  // epoll实例的fd
    struct epoll_event *events,// 输出参数:保存已触发的事件
    int maxevents,             // events数组的最大容量(需大于0)
    int timeout                // 超时时间(ms)
);
参数说明:
  • timeout
    • -1:永久阻塞,直到有事件触发;
    • 0:非阻塞,立即返回;
    • >0:超时毫秒数,超时返回 0。
返回值:
  • 成功:返回已触发的事件数;
  • 超时:返回 0;
  • 失败:返回 - 1,错误码存于errno

核心优势:epoll_wait 仅返回有状态变化的 fd 事件,无需遍历所有 fd,这是 epoll 效率远超 select/poll 的关键!

三、核心概念:水平触发(LT)vs 边缘触发(ET)

epoll 支持两种触发模式,这是 epoll 的核心特性,也是面试高频考点。

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

原理:

当 fd 有可读写事件时,epoll_wait持续触发通知,直到数据被全部读写完。

特点:
  • 容错性高:无需一次性读写完数据,适合新手;
  • 效率略低:未处理完的数据会重复触发通知;
  • 无需设置非阻塞 IO(可阻塞,也可非阻塞)。

2. 边缘触发(ET)

原理:

当 fd 的状态从无事件→有事件时,epoll_wait 仅触发一次通知,即使数据未读写完,后续也不会再触发,直到 fd 状态再次变化。

特点:
  • 效率高:仅触发一次,减少冗余通知;
  • 要求严格:必须一次性读写完所有数据;
  • 必须设置 fd 为非阻塞 IO(避免读写时阻塞)。
非阻塞 IO 设置代码:
// 将fd设置为非阻塞模式(ET模式必加)
int flag = fcntl(fd, F_GETFL); // 读取现有标记
fcntl(fd, F_SETFL, flag | O_NONBLOCK); // 添加非阻塞标记

3. LT vs ET 核心对比

特性 水平触发(LT) 边缘触发(ET)
触发时机 fd 有数据就持续触发 fd 状态变化时仅触发一次
数据处理 可分多次读写 必须一次性读写完
IO 模式 可阻塞 / 非阻塞 必须非阻塞
效率 低(冗余通知) 高(无冗余通知)
容错性 高(新手友好) 低(易丢数据)
适用场景 中小并发、数据量小 高并发、大数据量

四、实战代码:epoll 基础用法

示例 1:epoll 监听标准输入(基础版)

#include <stdio.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#define EPOLL_SIZE 10000 // 仅为兼容旧内核,现代内核忽略

int main(void) {
    // 1. 创建epoll实例
    int epfd = epoll_create(EPOLL_SIZE);
    if (epfd == -1) {
        perror("epoll_create failed");
        exit(EXIT_FAILURE);
    }

    // 2. 注册标准输入(fd=0)的可读事件(LT模式)
    struct epoll_event ev;
    ev.events = EPOLLIN; // 默认LT模式,去掉EPOLLET
    ev.data.fd = 0;      // 关联fd=0(标准输入)

    if (epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ev) == -1) {
        perror("epoll_ctl add failed");
        close(epfd);
        exit(EXIT_FAILURE);
    }

    char buff[BUFSIZ] = {0};
    struct epoll_event events[1]; // 仅监听1个fd,数组大小设为1

    while (1) {
        // 3. 等待事件触发(超时5秒)
        int epoll_events_count = epoll_wait(epfd, events, 1, 5000);
        if (epoll_events_count == -1) {
            perror("epoll_wait failed");
            close(epfd);
            exit(EXIT_FAILURE);
        } else if (epoll_events_count == 0) {
            printf("超时...\n");
            continue;
        }

        // 4. 处理触发的事件(仅1个事件)
        for (int i = 0; i < epoll_events_count; i++) {
            if (events[i].data.fd == 0) { // 标准输入事件
                int nread;
                // 获取可读取的字节数
                ioctl(0, FIONREAD, &nread);
                if (nread == 0) { // Ctrl+D结束输入
                    printf("keyboard done\n");
                    close(epfd);
                    exit(0);
                }

                // LT模式:分多次读也会持续触发
                nread = read(0, buff, 2); // 每次只读2字节
                buff[nread] = '\0';
                printf("read %d bytes: %s\n", nread, buff);
                memset(buff, 0, sizeof(buff)); // 清空缓冲区
            }
        }
    }

    close(epfd);
    return 0;
}

示例 2:边缘触发(ET)模式改造

仅需修改事件注册部分,并添加非阻塞设置(标准输入默认阻塞,需改为非阻塞):

#include <stdio.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> // fcntl头文件

#define EPOLL_SIZE 10000

int main(void) {
    int epfd = epoll_create(EPOLL_SIZE);
    if (epfd == -1) {
        perror("epoll_create failed");
        exit(EXIT_FAILURE);
    }

    // 将标准输入(fd=0)设为非阻塞(ET模式必加)
    int flag = fcntl(0, F_GETFL);
    fcntl(0, F_SETFL, flag | O_NONBLOCK);

    // 注册ET模式的可读事件
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET; // 开启边缘触发
    ev.data.fd = 0;

    if (epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ev) == -1) {
        perror("epoll_ctl add failed");
        close(epfd);
        exit(EXIT_FAILURE);
    }

    char buff[BUFSIZ] = {0};
    struct epoll_event events[1];

    while (1) {
        int epoll_events_count = epoll_wait(epfd, events, 1, 5000);
        if (epoll_events_count == -1) {
            perror("epoll_wait failed");
            close(epfd);
            exit(EXIT_FAILURE);
        } else if (epoll_events_count == 0) {
            printf("超时...\n");
            continue;
        }

        for (int i = 0; i < epoll_events_count; i++) {
            if (events[i].data.fd == 0) {
                int nread_total = 0;
                // ET模式:循环读取所有数据(非阻塞)
                while (1) {
                    int nread = read(0, buff + nread_total, sizeof(buff) - nread_total - 1);
                    if (nread > 0) {
                        nread_total += nread;
                    } else if (nread == 0) { // Ctrl+D
                        printf("keyboard done\n");
                        close(epfd);
                        exit(0);
                    } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
                        // 非阻塞读取完毕,退出循环
                        break;
                    } else { // 其他错误
                        perror("read failed");
                        close(epfd);
                        exit(EXIT_FAILURE);
                    }
                }

                if (nread_total > 0) {
                    buff[nread_total] = '\0';
                    printf("read %d bytes (ET模式一次性读完): %s\n", nread_total, buff);
                    memset(buff, 0, sizeof(buff));
                }
            }
        }
    }

    close(epfd);
    return 0;
}

编译运行 & 测试

# 编译LT版本
gcc epoll_lt.c -o epoll_lt
# 编译ET版本
gcc epoll_et.c -o epoll_et

# 运行LT版本(输入"hello",会分3次触发:2+2+1字节)
./epoll_lt
# 运行ET版本(输入"hello",仅触发1次,一次性读完5字节)
./epoll_et

五、epoll vs select/poll 核心对比

特性 select poll epoll
底层原理 轮询所有 fd 轮询所有 fd 事件驱动(仅处理有事件的 fd)
fd 数量限制 硬限制 1024 无硬限制(轮询效率低) 无硬限制(万级以上)
触发模式 仅 LT 仅 LT LT(默认)+ ET
线程安全
内核 / 用户态拷贝 每次调用拷贝全部 fd 每次调用拷贝全部 fd 仅拷贝一次(mmap 共享)
效率 O (n)(n=fd 总数) O(n) O (1)(仅处理触发的 fd)
适用并发量 小并发(<1024) 中小并发(千级) 高并发(万级 +)
典型应用 简单工具 中小服务 Nginx/Redis/ 高并发服务器

六、常见踩坑点

  1. ET 模式未设置非阻塞 IO:读写时会阻塞,导致程序卡死;
  2. ET 模式未一次性读完数据:未读完的数据不会触发新通知,导致数据丢失;
  3. epoll_create 参数传 0:现代内核虽兼容,但建议传大于 0 的数(如 1);
  4. epoll_wait 的 maxevents 设为 0:直接返回 - 1,需设为 events 数组的实际大小;
  5. 忘记删除无效 fd:客户端关闭后未调用epoll_ctl(EPOLL_CTL_DEL),导致 epoll 实例残留无效 fd;
  6. LT 模式重复触发影响效率:未及时处理数据会持续触发,需合理设计数据处理逻辑。

七、epoll 的进阶使用建议

  1. ET 模式 + 非阻塞 IO:高并发场景首选,效率最高;
  2. EPOLLONESHOT:多线程处理事件时,避免同一个 fd 的事件被多个线程重复处理;
  3. mmap 优化:现代 epoll 通过 mmap 共享内核 / 用户态数据,减少拷贝开销(无需手动设置,内核自动处理);
  4. 文件描述符复用:关闭 fd 前先调用epoll_ctl(EPOLL_CTL_DEL),避免 epoll 实例残留无效 fd。

八、总结

关键点回顾

  1. epoll 的核心头文件是 <sys/epoll.h>,核心接口为epoll_create/epoll_ctl/epoll_wait
  2. epoll 的核心优势是事件驱动 + 无 fd 数量限制 + 线程安全,是高并发的首选;
  3. LT 模式(默认):新手友好,持续触发;ET 模式:效率高,需非阻塞 + 一次性读写完数据;
  4. select/poll 适合中小并发,epoll 适合万级以上高并发(如中间件、网关)。

简单记:epoll 是 select/poll 的 “终极升级版”,解决了轮询效率低、fd 受限的核心问题;高并发场景下,ET 模式 + 非阻塞 IO 是 epoll 的最优用法。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐