Linux epoll 深度实战:从 select/poll 到百万级并发的事件驱动模型

一、为什么需要 epoll?—— select 与 poll 的历史局限

在网络编程的演进历程中,I/O 多路复用技术始终是构建高性能服务器的基石。在 epoll 出现之前,select 和 poll 是主流的事件通知机制,但它们的设计局限性严重制约了系统的并发能力。

1.1 select 的三大瓶颈

select 函数诞生于 1983 年(BSD 4.2),其接口定义如下:


int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

select 存在三个核心问题:

第一,fd_set 的大小受限。fd_set 本质是一个由 FD_SETSIZE(通常 1024)位组成的位图,这意味着 select 能监控的文件描述符数量被硬编码限制在 1024。在 C10K 问题面前,这是致命缺陷。

第二,每次调用必须重置 fd_set。select 会修改传入的 fd_set 指针内容,因此每次调用前都需要重新设置关心的事件集合,这对于高频循环调用是额外的开销。

第三,O(n) 的扫描复杂度。select 返回后,调用方必须遍历整个 fd_set 来确定哪些 fd 就绪,当 fd 数量很大但有事件的比例很低时,这种轮询扫描极其低效。

1.2 poll 的改进与不足

poll 用动态数组代替位图,解决了 fd 数量限制:


int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
    int   fd;      /* 文件描述符 */
    short events;  /* 请求事件 */
    short revents; /* 返回事件 */
};

但 poll 仍然继承了 select 的 O(n) 扫描问题——内核和用户态都需要遍历整个 fd 数组来查找就绪的 fd。当并发连接达到数万级别时,这个线性扫描的开销不可接受。

1.3 C10K 问题与 epoll 的诞生

1999年,Dan Kegel 提出了著名的"C10K 问题":如何让单台服务器同时处理 10,000 个并发连接?在 epoll 之前,解决方案要么是使用多进程/多线程模型(Apache prefork),要么是用户态异步框架(这些都存在各自的局限)。

Linux 2.5.44(2002年)引入 epoll,在内核层面用红黑树管理 fd 集合,用就绪链表直接返回就绪事件,实现了 O(1) 的事件通知效率(相对于监控的 fd 总数而言),一举解决了选择问题。

二、epoll 核心 API 与数据结构

2.1 三个系统调用

epoll 的 API 极其精简,只有三个系统调用:


// 1. 创建 epoll 实例,返回 epfd
int epoll_create(int size);        // 旧版(Linux 2.6.8后忽略size)
int epoll_create1(int flags);      // 新版(支持 EPOLL_CLOEXEC)

// 2. 注册/修改/删除监控事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

// 3. 等待就绪事件
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);

2.2 epoll_event 结构体与事件类型

struct epoll_event 是 epoll 的核心数据结构:


typedef union epoll_data {
    void     *ptr;
    int       fd;
    uint32_t  u32;
    uint64_t  u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;   /* epoll 事件位掩码 */
    epoll_data_t data;     /* 用户数据传递 */
};

常用事件类型说明:

事件宏触发条件典型用途
EPOLLIN对端有数据可读读取 TCP 数据、UDP 数据报
EPOLLOUT对端可写(发送缓冲区未满)发送数据、写回调
EPOLLRDHUP对端关闭连接(半关闭)优雅关闭连接
EPOLLET边缘触发模式(Edge Triggered)高吞吐场景,减少事件触发次数
EPOLLONESHOT事件触发后自动禁用 fd多线程下避免多个线程同时处理同一 fd
EPOLLERR错误发生(总是监控)连接异常处理
EPOLLHUP挂起(总是监控)对端 RST 或异常断开

epoll_ctl 操作码详解


// 注册新 fd 到 epoll 实例
epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &event);

// 修改已注册 fd 的关注事件
epoll_ctl(epfd, EPOLL_CTL_MOD, modify_fd, &event);

// 从 epoll 实例移除 fd(关闭 fd 自动移除)
epoll_ctl(epfd, EPOLL_CTL_DEL, remove_fd, NULL);

三、底层实现原理:红黑树 + 就绪链表

3.1 内核数据结构总览

epoll 的内核实现基于三个核心数据结构:


// 每个监控的 fd 对应一个 epitem(红黑树节点)
struct epitem {
    struct rb_node  rbn;           // 红黑树节点(以 fd 为键)
    struct list_head rdllink;      // 就绪链表节点
    struct epoll_filefd ffd;       // fd 和 file 指针
    struct eventpoll *ep;          // 所属 epoll 实例
    struct epoll_event event;      // 注册的事件掩码
};

// epoll 实例(每个 epoll_create 创建一个)
struct eventpoll {
    struct mutex mtx;              // 操作锁
    wait_queue_head_t wq;          // 进程等待队列(epoll_wait 阻塞用)
    struct list_head rdllist;      // 就绪链表
    struct rb_root rbr;            // 红黑树根节点
    struct epoll_filefd *ovflist;  // 溢出链表(优化路径)
};

3.2 事件注册路径

当调用 epoll_ctl(EPOLL_CTL_ADD) 时内核执行以下步骤:

步骤一:分配 epitem 对象,初始化红黑树节点和就绪链表节点。

步骤二:调用 ep_install(),将 epitem 插入到 eventpoll 的红黑树中(O(log n) 插入)。

步骤三:通过 poll_table 调用底层 file_operations 的 poll 方法,注册回调函数 ep_poll_callback()注册到目标 fd 设备的等待队列中。

关键的是,此时不会阻塞,只是建立了 epoll 实例与 fd 之间的回调关系。

3.3 事件触发与就绪链表

当 fd 就绪(例如网卡收到数据触发中断)时,中断处理程序会:

步骤一:唤醒该 fd 等待队列上的所有回调函数,执行 ep_poll_callback()。

步骤二:ep_poll_callback() 将对应的 epitem 加入 eventpoll 的 rdllist 就绪链表。

步骤三:检查 eventpoll 的 wq 等待队列是否有进程在阻塞等待(即有进程在调用 epoll_wait),如果有则唤醒它。

这一过程完全由内核驱动,不需要主动扫描所有 fd。这意味着不论监控 100 个 fd 还是 10 万个 fd,事件触发路径的开销都是 O(1)。

3.4 就绪事件返回(epoll_wait)

当 epoll_wait 被调用时:

步骤一:检查 rdllist 是否为空,若为空则将当前进程加入 wq 等待队列并设置状态为 TASK_INTERRUPTIBLE,然后调度出去(阻塞等待)。

步骤二:当被唤醒(有事件到达或超时),从 rdllist 取出就绪的 epitem,将其 event 数组拷贝回用户空间。

步骤三:关键优化——对于水平触发(LT)模式但就绪 fd 不支持 WQ_FLAG_EXCLUSIVE 的,epitem 会重新挂回 rdllist 以支持下次通知。ET 模式的则直接从链表中删除。

另外,内核还有一个 ovflist 溢出链表优化:如果在 epoll_wait 拷贝就绪事件到用户空间期间有新事件到来,为避免丢失,新事件暂存到 ovflist,处理完后再转移回 rdllist。

四、水平触发 vs 边缘触发

4.1 水平触发 Level Triggered(LT,默认模式)

LT 是 epoll 的默认工作模式。当 fd 处于就绪状态时,epoll_wait 每次调用都会通知用户。这类似于 select/poll 的行为。

LT 的特点:

编程模型简单,不用担心事件遗漏。只要缓冲区还有数据没读完,下次 epoll_wait 还会通知你。但每次通知都需要读写操作,在高并发场景下意味着更多的系统调用。

LT 模式的典型服务端代码模式:


// LT 模式:epoll_wait 返回后读取直到 EAGAIN
while (1) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < n; i++) {
        if (events[i].events & EPOLLIN) {
            // 只需一次 read 调用,没读完 LT 会再次通知
            ssize_t count = read(events[i].data.fd, buf, BUF_SIZE);
            if (count == -1 && errno == EAGAIN) {
                // 缓冲区已空,退出
                break;
            }
            // 处理 buf 中的数据...
        }
    }
}

在 LT 模式下,如果只读了一部分数据就停止,下次 epoll_wait 会继续通知事件就绪,所以不需要一次性读完。

4.2 边缘触发 Edge Triggered(ET)

ET 模式通过 EPOLLET 标志位设置,它只在 fd 状态变化时通知一次(边沿触发)。这种模式要求用户必须一次性处理完所有可用数据,否则会丢失事件。

ET 的特点:

事件触发次数大幅减少,吞吐能力显著提升。但编程复杂度更高,必须使用非阻塞 IO,且必须循环读写直到返回 EAGAIN 错误。

ET 模式要求 fd 必须设置为非阻塞模式:


// 设置非阻塞模式
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

// 注册时添加 EPOLLET 标志
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);

ET 模式的典型服务端读处理:


// ET 模式:必须循环 read 直到 EAGAIN
if (events[i].events & EPOLLIN) {
    while (1) {
        ssize_t count = read(fd, buf, BUF_SIZE);
        if (count == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 数据全部读完,退出循环
                break;
            }
            // 真正的错误
            close(fd);
            break;
        } else if (count == 0) {
            // 对端关闭连接
            close(fd);
            break;
        }
        // 处理 buf 中的 count 字节数据
        process_data(buf, count);
    }
}

4.3 LT vs ET 性能对比

在典型的 HTTP 服务器场景下:

LT 模式:每次 epoll_wait 返回后只读一次,有大量数据时可能触发多次 epoll_wait 调用,系统调用开销增加。

ET 模式:每次触达时一次性读完,减少了 epoll_wait 调用次数,在 100K+ 并发场景下吞吐量可提升 20%-30%。

但 ET 模式需要注意不能阻塞——如果 ET 模式下使用阻塞 IO 且数据量大于一次 read 能读的量,会导致后续数据永远丢失(因为 ET 只通知一次)。

五、完整 epoll HTTP 服务器实战

5.1 架构设计

下面我们将构建一个基于 epoll + 线程池的 HTTP 服务器,架构如下:

主线程:创建 epoll 实例,accept 新连接,注册到 epoll;epoll_wait 等待事件。

工作线程:从主线程获取就绪的连接句柄,解析 HTTP 请求,生成响应,写回客户端。

5.2 事件驱动核心循环


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

#define MAX_EVENTS 65536
#define BUF_SIZE 8192
#define PORT 8080

static void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

static int create_server(uint16_t port) {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(port),
        .sin_addr.s_addr = INADDR_ANY
    };

    if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // 重要:listen 的 backlog 应足够大
    if (listen(listen_fd, 4096) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    set_nonblocking(listen_fd);
    return listen_fd;
}

// 客户端连接上下文
struct client {
    int fd;
    char read_buf[BUF_SIZE];
    int read_offset;
    char write_buf[BUF_SIZE];
    int write_len;
    int write_offset;
    int keep_alive;
};

static struct client *new_client(int fd) {
    struct client *c = calloc(1, sizeof(struct client));
    c->fd = fd;
    c->keep_alive = 1;
    return c;
}

static void close_client(int epfd, struct client *c) {
    epoll_ctl(epfd, EPOLL_CTL_DEL, c->fd, NULL);
    close(c->fd);
    free(c);
}

// HTTP 请求解析与响应生成
static int handle_request(struct client *c) {
    // 简单解析 HTTP 请求行
    char *method = c->read_buf;
    char *path = strchr(method, ' ');
    if (!path) return -1;
    *path++ = 0;

    char *version = strchr(path, ' ');
    if (!version) return -1;
    *version++ = 0;

    // 检查 Connection: keep-alive
    c->keep_alive = (strstr(version, "Connection: keep-alive") != NULL);

    // 构造 HTTP/1.1 200 响应
    const char *body = "Hello from epoll server!";
    c->write_len = snprintf(c->write_buf, BUF_SIZE,
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/plain\r\n"
        "Content-Length: %zu\r\n"
        "Connection: %s\r\n"
        "\r\n"
        "%s",
        strlen(body),
        c->keep_alive ? "keep-alive" : "close",
        body);
    c->write_offset = 0;
    return 0;
}

int main(void) {
    int listen_fd = create_server(PORT);
    int epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    // 注册 listen_fd 到 epoll
    struct epoll_event ev = {
        .events = EPOLLIN,
        .data.ptr = new_client(listen_fd)  // 用 ptr 区分客户端和监听 fd
    };
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

    struct epoll_event events[MAX_EVENTS];

    printf("epoll HTTP server running on port %d\n", PORT);

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

        for (int i = 0; i < nfds; i++) {
            struct client *c = events[i].data.ptr;

            // 监听 fd:接受新连接
            if (c->fd == listen_fd) {
                while (1) {
                    struct sockaddr_in client_addr;
                    socklen_t addr_len = sizeof(client_addr);
                    int conn_fd = accept4(listen_fd,
                        (struct sockaddr*)&client_addr, &addr_len,
                        SOCK_NONBLOCK | SOCK_CLOEXEC);
                    if (conn_fd == -1) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;  // 没有更多连接了
                        }
                        perror("accept4");
                        break;
                    }

                    struct client *new_c = new_client(conn_fd);
                    struct epoll_event cev = {
                        .events = EPOLLIN | EPOLLRDHUP | EPOLLERR,
                        .data.ptr = new_c
                    };
                    epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &cev);
                }
                continue;
            }

            // 错误处理
            if (events[i].events & (EPOLLERR | EPOLLHUP)) {
                close_client(epfd, c);
                continue;
            }

            // 可读事件
            if (events[i].events & EPOLLIN) {
                ssize_t n = read(c->fd,
                    c->read_buf + c->read_offset,
                    BUF_SIZE - c->read_offset - 1);
                if (n == -1) {
                    if (errno != EAGAIN && errno != EWOULDBLOCK) {
                        close_client(epfd, c);
                    }
                    continue;
                } else if (n == 0) {
                    close_client(epfd, c);
                    continue;
                }

                c->read_offset += n;
                c->read_buf[c->read_offset] = 0;

                // 检查请求是否完整(简单判断 \r\n\r\n)
                if (strstr(c->read_buf, "\r\n\r\n")) {
                    if (handle_request(c) == 0) {
                        // 切换到写关注
                        struct epoll_event wev = {
                            .events = EPOLLOUT | EPOLLRDHUP | EPOLLERR | EPOLLET,
                            .data.ptr = c
                        };
                        epoll_ctl(epfd, EPOLL_CTL_MOD, c->fd, &wev);
                    } else {
                        close_client(epfd, c);
                    }
                }
                continue;
            }

            // 可写事件
            if (events[i].events & EPOLLOUT) {
                while (c->write_offset < c->write_len) {
                    ssize_t n = write(c->fd,
                        c->write_buf + c->write_offset,
                        c->write_len - c->write_offset);
                    if (n == -1) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;  // 缓冲区满,下次再写
                        }
                        close_client(epfd, c);
                        goto next_event;
                    }
                    c->write_offset += n;
                }

                if (c->write_offset == c->write_len) {
                    if (c->keep_alive) {
                        // 重置状态,等待下一个请求
                        c->read_offset = 0;
                        c->write_offset = 0;
                        c->write_len = 0;
                        struct epoll_event rev = {
                            .events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLET,
                            .data.ptr = c
                        };
                        epoll_ctl(epfd, EPOLL_CTL_MOD, c->fd, &rev);
                    } else {
                        close_client(epfd, c);
                    }
                }
            }
            next_event:;
        }
    }

    close(epfd);
    close(listen_fd);
    return 0;
}

编译运行:


gcc -O2 -Wall -o epoll_server epoll_server.c
./epoll_server
# 测试
curl http://localhost:8080/

六、epoll 的陷阱与最佳实践

6.1 文件描述符耗尽问题

Linux 对进程可打开的 fd 数量有三层限制:

系统级:fs.file-max(全局 fd 总数上限,通过 /proc/sys/fs/file-max 查看和修改)。

用户级:ulimit -n(单进程最大 fd 数)。在 systemd 服务中通过 LimitNOFILE= 配置。

调度级:fs.nr_open(单个进程硬限制)。使用 epoll 做 C10K/C100K 服务器时,必须将这些限制调高:


# 临时修改
ulimit -n 100000

# 永久修改 /etc/security/limits.conf
*  soft  nofile  100000
*  hard  nofile  500000

# 系统级调整
sysctl -w fs.file-max=2000000
sysctl -w fs.nr_open=2000000

6.2 惊群效应(Thundering Herd)

多线程 epoll 场景下的经典问题:多个线程阻塞在同一个 epoll 实例的 epoll_wait 上,当一个事件到来时,所有线程被唤醒,但实际上只有一个线程能处理该事件。大量不必要的唤醒会导致 CPU 浪费和缓存抖动。

解决方案一:EPOLLEXCLUSIVE 标志(Linux 4.5+)。这个标志使内核在唤醒时只唤醒一个线程,相当于给 epoll 添加了单播唤醒语义:


struct epoll_event ev;
ev.events = EPOLLIN | EPOLLEXCLUSIVE;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);

解决方案二:SO_REUSEPORT 多监听套接字。多个线程分别创建独立 socket,绑定同一端口(需要开启 SO_REUSEPORT),每个线程独立 accept,内核负责均衡分发新连接。这是 Nginx 等高性能服务器采用的方案。

6.3 连接丢失问题(ET 模式必读)

ET 模式下最容易犯的错误不是"忘记循环 read 到 EAGAIN",而是忽略了 accept 循环。在 ET 模式下,如果监听 fd 上有多个新连接到来,内核可能只通知一次。因此 accept 时必须在循环中调用,直到返回 EAGAIN:


// 错误:只 accept 一次
int conn_fd = accept(listen_fd, ...);  // 第一个连接
// 第二个连接永远不会被处理!

// 正确:循环 accept 直到 EAGAIN
while (1) {
    int conn_fd = accept4(listen_fd, ..., SOCK_NONBLOCK);
    if (conn_fd == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            break;  // 没有更多连接
        }
        perror("accept4");
        break;
    }
    // 处理新连接...
}

6.4 EPOLLONESHOT 与多线程竞态

在多线程 epoll 服务器中,如果一个 fd 的就绪事件被添加到就绪链表后,多个工作线程可能同时被唤醒并处理同一个 fd,造成数据混淆或崩溃。EPOLLONESHOT(Linux 2.6.2+)解决这个问题:事件触发后自动禁用该 fd,处理完成后需要手动 EPOLL_CTL_MOD 重新启用。


// 注册时添加 EPOLLONESHOT
ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET;

// 工作线程处理完毕后必须重新 arm
struct epoll_event re_ev;
re_ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET;
re_evdata.ptr = c;
epoll_ctl(epfd, EPOLL_CTL_MOD, c->fd, &re_ev);

七、epoll 与 io_uring 的对比

Linux 5.1 引入的 io_uring 是新一代异步 I/O 接口,与 epoll 在设计哲学上有本质区别。

epoll 本质上是一个异步事件通知机制,它告诉你哪个 fd 可读了、可写了,但真正的 I/O 操作(read/write)仍然由用户线程执行。

io_uring 则是一个真正的异步 I/O 执行机制,read/write 等 I/O 操作被提交到共享队列后即返回,由内核异步完成,用户只需检查完成状态。

关键区别在于:

系统调用开销:epoll 每次 I/O 仍需用户态发起 read/write 系统调用;io_uring 在 SQPOLL 模式下可做到零系统调用(内核轮询提交队列自动执行)。

数据拷贝路径:epoll + read/write 需要内核态与用户态之间拷贝;io_uring 支持预注册缓冲区,支持真正零拷贝。

适用场景:epoll 更适合事件驱动的网络服务器(频繁的连接管理,数据量相对小);io_uring 更适合存储密集型工作负载(大文件 I/O、大量随机读写、NVMe 设备)。

实践建议:两者并非替代关系,生产中可以结合使用——用 epoll 管理连接生命周期,用 io_uring 处理大块数据读写。Cloudflare、Nginx(实验分支)都有此类实践。

八、性能调优与监控

8.1 epoll 相关的内核参数

参数路径默认值说明
/proc/sys/fs/epoll/max_user_watches约内存的 80% / 每 fd 占用字节单用户可注册的 epoll watch 总数
/proc/sys/fs/file-max依赖内存系统级 fd 最大数
/proc/sys/net/core/somaxconn4096(新版 65535)listen backlog 上限
/proc/sys/net/ipv4/tcp_max_syn_backlog256TCP 半连接队列上限

max_user_watches 是关键参数。在监控大量 fd 的场景(如监控文件系统的 inotify + epoll):


# 查看当前值
cat /proc/sys/fs/epoll/max_user_watches

# 调整为 500 万
sysctl -w fs.epoll.max_user_watches=5000000

# 永久生效
echo "fs.epoll.max_user_watches = 5000000" >> /etc/sysctl.conf

8.2 epoll 调优清单

listen backlog 调大。默认 somaxconn 可能只有 4096,对于高并发服务器需要调到 65535。

使用 TCP_NODELAY 关闭 Nagle 算法。对延迟敏感的服务(如游戏、实时通信),Nagle 算法会将小包合并延迟发送,严重影响延迟。

开启 SO_REUSEPORT。对于多 worker 进程/线程,启用 REUSEPORT 让内核做连接分配,避免锁竞争。

考虑使用 accept4 而非 accept。accept4 支持直接设置 SOCK_NONBLOCK | SOCK_CLOEXEC,省去额外的 fcntl 调用。

连接预分配与连接池。在高并发场景下频繁 malloc/free client 结构体有开销,可使用内存池预分配。

九、生产案例:epoll 在知名项目中的应用

9.1 Nginx 的事件驱动核心

Nginx 是 epoll 最著名的生产案例。Nginx 的 event 模块将 epoll 封装为 epoll_module,核心设计思路是:主进程创建 epoll 实例 → worker 进程独立 accept → 连接分配到 worker 的 epoll → 边沿触发(ET)模式减少事件风暴 → 非阻塞读写实现单进程数万连接。

关键配置:

events {
    use epoll;
    worker_connections 65535;
    multi_accept on;
}

其中 multi_accept on 表示一次 epoll_wait 返回后尽可能多地 accept 新连接,避免连接在 backlog 中等待被后续轮次处理。

9.2 Redis 的事件驱动模型

Redis 6.0 之前采用单线程 epoll 模型,将所有客户端连接和命令处理放在一个 aeEventLoop 中轮询。ae.c 抽象了 epoll/kqueue/select 三种底层实现。Redis 虽然 6.0 引入了 IO 多线程,但核心的连接管理和命令分发仍依赖 epoll 的单线程循环。

9.3 Netty 的 Linux epoll 传输

Netty 在 Linux 平台提供了 EpollEventLoopGroup 和 EpollSocketChannel,相比 NIO 的 Selector(基于 poll/select),Netty 的 epoll 实现使用了 EPOLLET 边缘触发 + EPOLLONESHOT,在基准测试中可提升约 30% 的吞吐量。

十、总结与学习路径

epoll 作为 Linux 高性能网络编程的基石,理解其原理对系统程序员至关重要。总结全文核心要点:

核心数据结构:红黑树管理事件注册(O(log n) 增删),就绪链表直接返回就绪项(O(1) 获取)。

两种触发模式:LT 简单安全(默认),ET 高性能需要循环读取。

常见陷阱:ET 必须非阻塞 IO + 循环处理;accept 必须循环到 EAGAIN;多线程需要 EPOLLEXCLUSIVE 或 EPOLLONESHOT。

学习路径建议:

理解 select/poll 的局限 → 掌握 epoll API 调用 → 对比 LT/ET 行为差异 → 熟悉内核红黑树 + 就绪链表实现 → 动手写 epoll HTTP Server → 分析 Nginx/Redis 事件驱动代码 → 了解 io_uring 等下一代异步 IO 接口。

推荐资源:《Unix 网络编程》《Linux 高性能服务器编程》(游双著)、《Understanding Linux Network Internals》、man 手册(man 7 epoll、man 2 epoll_ctl、man 2 epoll_wait)、Linux 内核源码(fs/eventpoll.c)。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部