C语言epoll编程完全指南

系统编程epoll

epoll概述与优势

epoll 是 Linux 内核提供的高性能 I/O 多路复用机制,从 Linux 2.5.44 开始引入,专为处理大规模并发连接而设计。与传统的 select 和 poll 不同,epoll 采用事件驱动模型,仅在就绪的文件描述符上触发通知,避免了遍历全部描述符的开销。

为什么需要epoll

  • 描述符数量限制:select 默认最多监控 1024 个描述符(FD_SETSIZE)
  • 线性扫描:select/poll 每次调用需遍历全部描述符,O(n) 复杂度
  • 数据拷贝:select/poll 每次调用需将 fd_set 从用户态拷贝到内核态

epoll 的核心优势:无数量限制(仅受系统内存约束)、事件通知(O(1) 获取就绪事件)、共享红黑树与就绪链表避免重复拷贝。

epoll API接口

epoll_create - 创建epoll实例

#include <sys/epoll.h>

/* 创建epoll实例,size仅作提示,内核动态调整 */
int epfd = epoll_create(1024);
if (epfd == -1) { perror("epoll_create"); return -1; }

/* epoll_create1 支持更多标志 */
int epfd2 = epoll_create1(EPOLL_CLOEXEC); /* exec时自动关闭 */

epoll_ctl - 控制事件注册

struct epoll_event ev;
ev.events = EPOLLIN;       /* 关注可读事件 */
ev.data.fd = sockfd;

/* 添加 / 修改 / 删除 */
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);

常用事件类型:EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLRDHUP(对端半关闭)、EPOLLERR(错误,始终监控)、EPOLLHUP(挂起,始终监控)、EPOLLET(启用ET模式)、EPOLLONESHOT(只通知一次)。

epoll_event 结构体

struct epoll_event {
    uint32_t     events;   /* epoll事件掩码 */
    epoll_data_t data;      /* 用户数据 */
};
typedef union epoll_data {
    void     *ptr;
    int       fd;
    uint32_t  u32;
    uint64_t  u64;
} epoll_data_t;

epoll_wait - 等待事件就绪

#define MAX_EVENTS 64
struct epoll_event events[MAX_EVENTS];

int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
    if (errno == EINTR) goto retry;
    perror("epoll_wait");
}

for (int i = 0; i < nfds; i++) {
    if (events[i].data.fd == listen_fd)
        /* 处理新连接 */;
    else
        /* 处理数据 */;
}

timeout 参数:-1 阻塞等待、0 非阻塞立即返回、>0 等待指定毫秒。

LT水平触发模式

LT(Level Triggered)是 epoll 的默认模式。只要描述符上有数据可读,epoll_wait 就会持续通知,直到所有数据被处理完毕。

  • 持续通知:条件满足时每次 epoll_wait 都返回该事件
  • 编程简单:可分批处理数据,不必一次读完
  • 兼容性好:与 select/poll 行为一致
/* LT模式:可以只读一部分数据 */
void handle_read_lt(int fd) {
    char buf[128];
    ssize_t n = read(fd, buf, sizeof(buf));
    if (n <= 0) { close(fd); return; }
    /* 只读了128字节,若缓冲区还有数据,
       下次 epoll_wait 仍会通知 EPOLLIN */
    write(fd, buf, n);
}

ET边沿触发模式

ET(Edge Triggered)模式只在描述符状态变化时通知一次。如果一次没有读完所有数据,epoll 不会再次通知,直到新的数据到达。

  • 仅通知一次:状态变化时触发,之后不再重复
  • 必须读完:需循环读取直到 EAGAIN
  • 非阻塞必须:描述符必须设为非阻塞,否则读空后会阻塞
  • 性能更优:减少内核到用户的通知次数
#include <fcntl.h>

int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

/* ET模式:必须循环读取直到 EAGAIN */
void handle_read_et(int fd) {
    char buf[512];
    while (1) {
        ssize_t n = read(fd, buf, sizeof(buf));
        if (n > 0) {
            write(fd, buf, n);
        } else if (n == 0) {
            close(fd); return;
        } else {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                break;  /* 数据已全部读完 */
            close(fd); return;
        }
    }
}

LT与ET核心差异:LT重复通知直到数据读完,ET仅通知一次;LT可分批处理,ET必须一次读完;ET必须配合非阻塞I/O;ET减少通知次数,高并发下更高效。

epoll与select/poll对比

  • 最大描述符数:select 1024(可改需重编译);poll/epoll 无限制
  • 时间复杂度:select/poll O(n);epoll O(1)
  • 内存拷贝:select/poll 每次全量拷贝;epoll 仅首次注册时拷贝
  • 触发模式:select/poll 仅 LT;epoll 支持 LT 和 ET
  • 跨平台:select 跨平台;poll 大多数平台;epoll 仅 Linux

在 10000 个并发连接、仅 10 个活跃的场景下,select/poll 需遍历全部描述符,epoll 仅处理 10 个就绪事件,性能优势明显。select 仍适合跨平台、低并发、高精度超时场景。

实战:高性能回显服务器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT        8888
#define MAX_EVENTS  1024
#define BUF_SIZE    4096
static volatile int running = 1;
static void sig_handler(int sig) { running = 0; }

static int set_nonblocking(int fd) {
    int fl = fcntl(fd, F_GETFL, 0);
    return (fl < 0) ? -1 : fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

static void handle_accept(int epfd, int lfd) {
    while (1) {
        struct sockaddr_in cli;
        socklen_t len = sizeof(cli);
        int cfd = accept(lfd, (struct sockaddr *)&cli, &len);
        if (cfd < 0) {
            if (errno == EAGAIN) break;
            break;
        }
        set_nonblocking(cfd);
        struct epoll_event ev = {
            .events = EPOLLIN | EPOLLET,
            .data.fd = cfd
        };
        epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    }
}

static void handle_client(int epfd, int fd) {
    char buf[BUF_SIZE];
    while (1) {
        ssize_t n = read(fd, buf, sizeof(buf));
        if (n > 0) {
            write(fd, buf, n);  /* 回显 */
        } else if (n == 0) {
            epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
            close(fd); return;
        } else {
            if (errno == EAGAIN) break;
            epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
            close(fd); return;
        }
    }
}

int main(void) {
    signal(SIGPIPE, SIG_IGN);
    signal(SIGINT, sig_handler);

    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port   = htons(PORT),
        .sin_addr.s_addr = htonl(INADDR_ANY)
    };
    bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    listen(lfd, 128);
    set_nonblocking(lfd);
    printf("Echo server on port %d (epoll ET)\n", PORT);

    int epfd = epoll_create1(0);
    struct epoll_event ev = { .events = EPOLLIN | EPOLLET, .data.fd = lfd };
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event events[MAX_EVENTS];
    while (running) {
        int nfds = epoll_wait(epfd, events, MAX_EVENTS, 1000);
        if (nfds < 0) { if (errno == EINTR) continue; break; }
        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == lfd)
                handle_accept(epfd, lfd);
            else if (events[i].events & (EPOLLERR | EPOLLHUP))
                close(events[i].data.fd);
            else if (events[i].events & EPOLLIN)
                handle_client(epfd, events[i].data.fd);
        }
    }
    close(lfd); close(epfd);
    return 0;
}
# 编译与测试
gcc -o echo_server echo_server.c
./echo_server
nc 127.0.0.1 8888

总结

  • epoll_create/epoll_ctl/epoll_wait - 三大核心接口,创建实例、注册事件、等待就绪
  • LT模式 - 默认模式,持续通知直到条件不满足,编程简单安全
  • ET模式 - 高性能模式,仅通知一次状态变化,必须配合非阻塞I/O和循环读取
  • 性能优势 - O(1) 事件获取、无描述符数量限制、零拷贝设计
  • 实战要点 - ET模式下 accept/read 都需循环处理直到 EAGAIN,正确处理 EPOLLERR/EPOLLHUP

epoll 是 Linux 网络编程的基石技术,Nginx、Redis 等高性能服务器均基于 epoll 构建。掌握 LT/ET 模式的差异和非阻塞I/O的配合使用,是编写高性能网络程序的关键。建议从 LT 模式入手,理解 epoll 机制后再切换到 ET 模式以获得更优性能。