土法炼钢兴趣小组的算法知识备份

【网络工程】epoll 深度剖析:ET/LT 模式、源码分析与性能特征

文章导航

分类入口
network
标签入口
#epoll#edge-trigger#level-trigger#linux-kernel#high-performance

目录

上一篇介绍了从阻塞 I/O 到 epoll 的演进。本文深入 epoll 的内部——内核如何实现 O(1) 事件通知、ET 和 LT 模式的本质区别、以及在生产环境中使用 epoll 的工程细节。

一、epoll 内核实现

1.1 数据结构

epoll 在内核中维护两个核心数据结构:

// 内核源码简化版(linux/fs/eventpoll.c)

struct eventpoll {
    spinlock_t lock;
    
    // 红黑树:存储所有被监听的 fd
    // 用于 epoll_ctl 的增删改查,O(log n)
    struct rb_root_cached rbr;
    
    // 就绪链表:存储已就绪的 fd
    // epoll_wait 直接从这里取,O(1)
    struct list_head rdllist;
    
    // 等待队列:存储调用 epoll_wait 阻塞的进程
    wait_queue_head_t wq;
    
    // ... 其他字段
};

// 红黑树中每个节点
struct epitem {
    struct rb_node rbn;         // 红黑树节点
    struct list_head rdllink;   // 就绪链表链接
    struct epoll_filefd ffd;    // 对应的 fd
    struct eventpoll *ep;       // 所属的 epoll 实例
    struct epoll_event event;   // 用户设置的事件和数据
};

工作流程:

epoll_ctl(EPOLL_CTL_ADD, fd, events):
  1. 创建 epitem 节点
  2. 插入红黑树(O(log n))
  3. 在 fd 对应的设备等待队列上注册回调函数
     → 当 fd 就绪时,回调函数将 epitem 加入就绪链表

fd 就绪时(如网卡收到数据):
  1. 内核中断处理程序处理收到的数据
  2. 数据复制到 socket 接收缓冲区
  3. 检查 socket 的等待队列,找到 epoll 注册的回调
  4. 回调函数:将 epitem 加入就绪链表 + 唤醒 epoll_wait

epoll_wait():
  1. 检查就绪链表
  2. 如果非空 → 将就绪事件拷贝到用户空间,返回
  3. 如果为空 → 进程阻塞在等待队列上
  4. 被唤醒后 → 重复步骤 1

1.2 为什么是 O(1)

select/poll vs epoll 的根本区别:

select/poll:
  每次调用时:
  1. 把用户空间的 fd 集合拷贝到内核       O(n)
  2. 内核遍历所有 fd,检查每个是否就绪     O(n)
  3. 把结果拷贝回用户空间                  O(n)
  
  10000 个 fd 中 10 个就绪 → 仍需检查 10000 个

epoll:
  epoll_ctl 时(一次性):
  1. 在红黑树中注册 fd                    O(log n)
  2. 在 fd 的设备等待队列注册回调           O(1)
  
  epoll_wait 时(每次调用):
  1. 检查就绪链表                          O(1)
  2. 拷贝就绪事件到用户空间                 O(k),k = 就绪数量
  
  10000 个 fd 中 10 个就绪 → 只处理 10 个
  
  核心优势:事件到达时主动通知(回调),而非被动遍历

二、ET vs LT 模式

epoll 支持两种触发模式:Level Triggered(LT,水平触发)和 Edge Triggered(ET,边缘触发)。

2.1 行为差异

Level Triggered(LT,默认模式):
  只要 fd 满足条件(如可读),每次 epoll_wait 都会通知

  时间线:
  T1: 收到 10KB 数据 → epoll_wait 返回 EPOLLIN
  T2: 应用读了 5KB    → 缓冲区还剩 5KB
  T3: epoll_wait      → 再次返回 EPOLLIN(因为缓冲区仍有数据)
  T4: 应用读了 5KB    → 缓冲区为空
  T5: epoll_wait      → 不返回 EPOLLIN(缓冲区空了)

Edge Triggered(ET):
  只在 fd 状态变化时通知一次

  时间线:
  T1: 收到 10KB 数据 → epoll_wait 返回 EPOLLIN
  T2: 应用读了 5KB    → 缓冲区还剩 5KB
  T3: epoll_wait      → 不返回 EPOLLIN(状态没有变化)
  T4: 又收到 3KB 数据 → epoll_wait 返回 EPOLLIN(新的边缘事件)
  
  ⚠ T2-T3 之间的 5KB 数据如果不主动读完,可能永远读不到

2.2 ET 模式的编程范式

ET 模式要求:每次收到事件通知时,必须循环处理直到 EAGAIN

// ET 模式的正确读取方式
void handle_read_et(int fd) {
    char buf[4096];
    
    // 必须循环读取直到 EAGAIN
    while (1) {
        ssize_t n = read(fd, buf, sizeof(buf));
        
        if (n < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 数据已读完,可以安全返回等待下一个事件
                break;
            }
            // 真正的错误
            perror("read error");
            close(fd);
            return;
        }
        
        if (n == 0) {
            // 对端关闭连接
            close(fd);
            return;
        }
        
        // 处理 n 字节数据
        process_data(buf, n);
    }
}

// ET 模式的正确写入方式
void handle_write_et(int fd, const char *data, size_t len) {
    size_t sent = 0;
    
    while (sent < len) {
        ssize_t n = write(fd, data + sent, len - sent);
        
        if (n < 0) {
            if (errno == EAGAIN) {
                // 发送缓冲区满,需要等待 EPOLLOUT 事件
                // 保存未发送的数据,注册 EPOLLOUT
                struct epoll_event ev;
                ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
                ev.data.fd = fd;
                epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
                break;
            }
            perror("write error");
            close(fd);
            return;
        }
        
        sent += n;
    }
}

2.3 LT 模式的编程范式

LT 模式更宽容——即使没读完数据,下次 epoll_wait 还会通知:

// LT 模式的读取方式(简单直观)
void handle_read_lt(int fd) {
    char buf[4096];
    
    // LT 模式下不需要循环到 EAGAIN
    // 下次 epoll_wait 还会通知
    ssize_t n = read(fd, buf, sizeof(buf));
    
    if (n < 0) {
        if (errno == EAGAIN) return;  // 不应该发生(LT 模式下)
        perror("read error");
        close(fd);
        return;
    }
    
    if (n == 0) {
        close(fd);
        return;
    }
    
    process_data(buf, n);
}

2.4 ET vs LT 性能对比

ET vs LT 性能差异:

场景 1: 大量连接,少量活跃
  ET:  epoll_wait 返回少量就绪事件 → 高效
  LT:  如果有数据未读完,持续触发 → 多余的 epoll_wait 调用
  → ET 略优

场景 2: 少量连接,高吞吐
  ET:  每次必须循环读到 EAGAIN → 读取逻辑稍复杂
  LT:  每次读一部分即可 → 更灵活的读取策略
  → 差异不大

场景 3: 大消息体(如文件传输)
  ET:  一次通知后必须持续读/写 → 可能长时间占据一个连接的处理
  LT:  可以每次读一部分,公平地处理其他连接
  → LT 更适合公平调度

实际性能测试(echo server,10000 并发):
  ET 模式:  ~105,000 req/s
  LT 模式:  ~98,000 req/s
  差异:     ~7%(ET 略快,但不是数量级差异)

结论:
  ET 的优势不在于单次操作更快,而在于减少了 epoll_wait 的调用次数。
  对于大多数应用,性能差异在 5-15% 之内。
  
  真正该关注的是编程正确性:
  - ET 模式出 bug 更难排查(数据"丢失"其实是没读完)
  - LT 模式更安全,更不容易出错

2.5 主流框架的选择

框架 / 软件            │ 触发模式    │ 原因
───────────────────────┼────────────┼────────────────────────
Nginx                  │ ET         │ 性能优先,代码质量高
Redis                  │ LT         │ 简单可靠,单线程模型
libuv (Node.js)        │ LT         │ 跨平台兼容(kqueue 仅 LT)
Go runtime             │ LT (ET-like)│ 自定义调度,混合模式
Java NIO (Netty)       │ LT         │ 默认 LT,可配置 ET
libev                  │ LT         │ 简单性优先
libevent               │ LT         │ 兼容性优先

趋势:大多数框架选择 LT 模式,除了追求极致性能的 Nginx

三、EPOLLONESHOT

EPOLLONESHOT 确保一个 fd 只被一个线程处理——事件触发一次后自动禁用,直到重新用 epoll_ctl(MOD) 重新激活:

// 多线程 epoll 服务器中使用 EPOLLONESHOT
void worker_thread(void *arg) {
    int epfd = *(int *)arg;
    struct epoll_event events[64];
    
    while (1) {
        int n = epoll_wait(epfd, events, 64, -1);
        
        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            
            // 安全:EPOLLONESHOT 保证同一 fd 不会被另一个线程同时处理
            handle_request(fd);
            
            // 处理完毕后重新激活
            struct epoll_event ev;
            ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
            ev.data.fd = fd;
            epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
        }
    }
}

// 没有 EPOLLONESHOT 的问题:
// 线程A 正在处理 fd=5 的数据
// 此时 fd=5 又有新数据到达
// 线程B 也拿到 fd=5 的事件
// → 两个线程同时操作同一个 fd → 数据竞争

四、惊群问题

4.1 accept 惊群

多个进程/线程同时 epoll_wait 在同一个 server socket 上,当新连接到达时,所有进程都被唤醒,但只有一个能成功 accept:

惊群问题(Thundering Herd):

Worker 1: epoll_wait(server_fd) → 阻塞
Worker 2: epoll_wait(server_fd) → 阻塞
Worker 3: epoll_wait(server_fd) → 阻塞
Worker 4: epoll_wait(server_fd) → 阻塞

新连接到达 → 唤醒所有 4 个 Worker

Worker 1: accept() → 成功 ✓
Worker 2: accept() → EAGAIN ✗(连接已被 Worker 1 拿走)
Worker 3: accept() → EAGAIN ✗
Worker 4: accept() → EAGAIN ✗

3 个 Worker 被无意义地唤醒 → CPU 浪费

4.2 解决方案

方案 1: EPOLLEXCLUSIVE(Linux 4.5+)
  只唤醒一个等待中的进程

  ev.events = EPOLLIN | EPOLLEXCLUSIVE;
  epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);

方案 2: SO_REUSEPORT(Linux 3.9+)
  每个 Worker 绑定自己的 server socket,内核做负载均衡

  int opt = 1;
  setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
  
  优势:
  - 内核层面的连接分发,无锁
  - 每个 Worker 有独立的 accept 队列
  - CPU 亲和,同一连接始终由同一 Worker 处理

方案 3: Nginx 的 accept_mutex
  进程间互斥锁,同一时刻只有一个 Worker 监听 server_fd

  events {
      accept_mutex on;
      accept_mutex_delay 500ms;
  }
  
  注意:Nginx 1.11.3+ 默认关闭 accept_mutex,改用 EPOLLEXCLUSIVE

SO_REUSEPORT 的实际效果:

// reuseport_server.c — 多进程 SO_REUSEPORT 服务器
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>

#define PORT 8080
#define NUM_WORKERS 4

void worker(int worker_id) {
    // 每个 Worker 创建自己的 socket
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
    
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons(PORT),
    };
    
    bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_fd, 1024);
    
    // 每个 Worker 独立的 epoll
    int epfd = epoll_create1(0);
    struct epoll_event ev = { .events = EPOLLIN, .data.fd = server_fd };
    epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);
    
    printf("Worker %d started (pid=%d)\n", worker_id, getpid());
    
    struct epoll_event events[64];
    while (1) {
        int n = epoll_wait(epfd, events, 64, -1);
        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == server_fd) {
                int client_fd = accept(server_fd, NULL, NULL);
                if (client_fd >= 0) {
                    // 处理连接...
                    close(client_fd);
                }
            }
        }
    }
}

int main() {
    for (int i = 0; i < NUM_WORKERS; i++) {
        if (fork() == 0) {
            worker(i);
            exit(0);
        }
    }
    
    // 等待子进程
    for (int i = 0; i < NUM_WORKERS; i++) {
        wait(NULL);
    }
    
    return 0;
}

五、epoll 的工程陷阱

5.1 fd 泄漏

// 常见错误:关闭 fd 后忘记从 epoll 中删除
close(client_fd);
// 缺少: epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);

// 实际上在 Linux 中,close(fd) 会自动将 fd 从 epoll 中移除
// 但有一个特例:dup / dup2

int fd1 = accept(server_fd, NULL, NULL);
int fd2 = dup(fd1);

epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev);

close(fd1);
// fd1 被关闭了,但底层的 file description 还在(因为 fd2 引用了它)
// epoll 监听的是 file description,不是 fd 编号
// → fd1 的事件仍然会被触发!

// 正确做法:
epoll_ctl(epfd, EPOLL_CTL_DEL, fd1, NULL);
close(fd1);
close(fd2);

5.2 EPOLLHUP 和 EPOLLERR

// EPOLLHUP 和 EPOLLERR 始终会被监听,即使你没有注册
// 不需要在 events 中设置它们

ev.events = EPOLLIN;  // 不需要 | EPOLLHUP | EPOLLERR

// 但你必须在事件处理时检查它们
if (events[i].events & EPOLLHUP) {
    // 对端关闭连接(TCP FIN)
    close(fd);
}

if (events[i].events & EPOLLERR) {
    // socket 错误(如 RST)
    int error = 0;
    socklen_t len = sizeof(error);
    getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len);
    fprintf(stderr, "Socket error on fd %d: %s\n", fd, strerror(error));
    close(fd);
}

5.3 ET 模式的写入陷阱

// ET 模式下的写入问题

// 场景:需要发送大量数据
// 问题:发送缓冲区满时,write 返回 EAGAIN
//        ET 模式下,必须等到 EPOLLOUT 事件才能继续写

struct connection {
    int fd;
    char *write_buf;
    size_t write_len;
    size_t write_pos;
};

void try_write(struct connection *conn) {
    while (conn->write_pos < conn->write_len) {
        ssize_t n = write(conn->fd,
                         conn->write_buf + conn->write_pos,
                         conn->write_len - conn->write_pos);
        
        if (n < 0) {
            if (errno == EAGAIN) {
                // 缓冲区满,注册 EPOLLOUT 等待可写
                struct epoll_event ev;
                ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
                ev.data.ptr = conn;
                epoll_ctl(epfd, EPOLL_CTL_MOD, conn->fd, &ev);
                return;
            }
            // 错误
            close(conn->fd);
            return;
        }
        
        conn->write_pos += n;
    }
    
    // 数据全部写完,取消 EPOLLOUT 监听
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;
    ev.data.ptr = conn;
    epoll_ctl(epfd, EPOLL_CTL_MOD, conn->fd, &ev);
    
    // 释放写缓冲区
    free(conn->write_buf);
    conn->write_buf = NULL;
}

六、epoll 与 timerfd/signalfd 集成

epoll 的一个设计哲学是”一切皆文件描述符”——定时器、信号也可以转化为 fd,统一用 epoll 管理:

6.1 timerfd:定时器作为文件描述符

// 使用 timerfd 在 epoll 事件循环中实现定时任务
#include <sys/timerfd.h>

// 创建定时器 fd
int timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);

// 设置 5 秒后触发,之后每 5 秒重复
struct itimerspec ts = {
    .it_interval = { .tv_sec = 5, .tv_nsec = 0 },  // 重复间隔
    .it_value    = { .tv_sec = 5, .tv_nsec = 0 },   // 首次触发
};
timerfd_settime(timer_fd, 0, &ts, NULL);

// 注册到 epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = timer_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, timer_fd, &ev);

// 在事件循环中处理定时器
// epoll_wait 返回后:
if (events[i].data.fd == timer_fd) {
    uint64_t expirations;
    read(timer_fd, &expirations, sizeof(expirations));
    // expirations = 自上次读取以来触发的次数
    
    // 执行定时任务:清理空闲连接、统计指标等
    cleanup_idle_connections();
    report_metrics();
}

6.2 signalfd:信号作为文件描述符

// 使用 signalfd 在事件循环中处理信号
#include <sys/signalfd.h>
#include <signal.h>

// 阻塞信号(防止默认处理)
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGUSR1);
sigprocmask(SIG_BLOCK, &mask, NULL);

// 创建信号 fd
int sig_fd = signalfd(-1, &mask, SFD_NONBLOCK);

// 注册到 epoll
ev.events = EPOLLIN;
ev.data.fd = sig_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sig_fd, &ev);

// 在事件循环中处理信号
if (events[i].data.fd == sig_fd) {
    struct signalfd_siginfo info;
    read(sig_fd, &info, sizeof(info));
    
    switch (info.ssi_signo) {
        case SIGINT:
        case SIGTERM:
            printf("Received shutdown signal, graceful exit\n");
            running = 0;
            break;
        case SIGUSR1:
            printf("Received SIGUSR1, reloading config\n");
            reload_config();
            break;
    }
}

6.3 eventfd:进程/线程间通知

// 使用 eventfd 实现线程间的事件通知
#include <sys/eventfd.h>

// 创建 eventfd(用于通知 epoll 循环有新任务)
int notify_fd = eventfd(0, EFD_NONBLOCK);

// 注册到 epoll
ev.events = EPOLLIN;
ev.data.fd = notify_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, notify_fd, &ev);

// 其他线程通知事件循环
void submit_task(Task *task) {
    enqueue(task_queue, task);
    uint64_t val = 1;
    write(notify_fd, &val, sizeof(val));  // 唤醒 epoll_wait
}

// 事件循环中处理通知
if (events[i].data.fd == notify_fd) {
    uint64_t val;
    read(notify_fd, &val, sizeof(val));
    
    // 处理队列中的任务
    while ((task = dequeue(task_queue)) != NULL) {
        execute_task(task);
    }
}

这种”一切皆 fd”的设计让 epoll 事件循环可以统一管理网络 I/O、定时器、信号和线程间通信,不需要 poll/signal handler/条件变量等多种机制混合使用。

七、epoll 与 kqueue 对比

                    epoll (Linux)           kqueue (BSD/macOS)
──────────────────────────────────────────────────────────────
API              三个系统调用              两个系统调用
                 (create/ctl/wait)        (kqueue/kevent)
批量修改         不支持(每次一个 fd)      支持(changelist)
事件类型         EPOLLIN/EPOLLOUT/...     EVFILT_READ/WRITE/...
触发模式         LT / ET                  LT(默认)/ EV_CLEAR(类ET)
定时器           需要 timerfd             EVFILT_TIMER(内置)
信号             需要 signalfd            EVFILT_SIGNAL(内置)
文件系统监听     需要 inotify             EVFILT_VNODE(内置)
进程监听         不支持                   EVFILT_PROC(内置)

kqueue 的优势:
  - 功能更丰富(定时器、信号、文件系统、进程监控一体化)
  - 支持批量修改(一次 kevent 调用修改多个事件)
  - API 设计更优雅

epoll 的优势:
  - Linux 上性能极致优化
  - 生态更广(大多数服务器是 Linux)

七、实战:epoll 性能基准测试

// epoll_bench.c — 测量 epoll 在不同连接数下的性能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>

#define MAX_EVENTS 1024

long now_ns() {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000000000L + ts.tv_nsec;
}

void benchmark_epoll_wait(int num_fds, int num_ready) {
    int epfd = epoll_create1(0);
    int *pipes = malloc(sizeof(int) * num_fds * 2);
    
    // 创建 num_fds 个 pipe 对
    for (int i = 0; i < num_fds; i++) {
        pipe(&pipes[i * 2]);
        
        int flags = fcntl(pipes[i * 2], F_GETFL, 0);
        fcntl(pipes[i * 2], F_SETFL, flags | O_NONBLOCK);
        
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = pipes[i * 2];
        epoll_ctl(epfd, EPOLL_CTL_ADD, pipes[i * 2], &ev);
    }
    
    // 让 num_ready 个 pipe 就绪
    for (int i = 0; i < num_ready; i++) {
        write(pipes[i * 2 + 1], "x", 1);
    }
    
    // 测量 epoll_wait 耗时
    struct epoll_event events[MAX_EVENTS];
    
    long total_ns = 0;
    int iterations = 10000;
    
    for (int iter = 0; iter < iterations; iter++) {
        long start = now_ns();
        int n = epoll_wait(epfd, events, MAX_EVENTS, 0);
        long elapsed = now_ns() - start;
        total_ns += elapsed;
        
        // 读取数据并重新写入以保持就绪状态
        if (iter < iterations - 1) {
            for (int i = 0; i < n; i++) {
                char buf;
                read(events[i].data.fd, &buf, 1);
            }
            for (int i = 0; i < num_ready; i++) {
                write(pipes[i * 2 + 1], "x", 1);
            }
        }
    }
    
    printf("fds=%-8d ready=%-6d avg_latency=%.1fμs\n",
           num_fds, num_ready, (double)total_ns / iterations / 1000.0);
    
    // 清理
    for (int i = 0; i < num_fds; i++) {
        close(pipes[i * 2]);
        close(pipes[i * 2 + 1]);
    }
    close(epfd);
    free(pipes);
}

int main() {
    printf("=== epoll_wait Latency Benchmark ===\n");
    printf("%-12s %-10s %s\n", "Total FDs", "Ready FDs", "Avg Latency");
    
    int fd_counts[] = {100, 1000, 10000, 50000};
    int ready_counts[] = {1, 10, 100};
    
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 3; j++) {
            if (ready_counts[j] <= fd_counts[i]) {
                benchmark_epoll_wait(fd_counts[i], ready_counts[j]);
            }
        }
        printf("\n");
    }
    
    return 0;
}

编译并运行:

$ gcc -O2 -o epoll_bench epoll_bench.c
$ ./epoll_bench

=== epoll_wait Latency Benchmark ===
Total FDs    Ready FDs  Avg Latency
fds=100      ready=1     avg_latency=1.2μs
fds=100      ready=10    avg_latency=1.5μs
fds=100      ready=100   avg_latency=3.8μs

fds=1000     ready=1     avg_latency=1.3μs
fds=1000     ready=10    avg_latency=1.6μs
fds=1000     ready=100   avg_latency=3.9μs

fds=10000    ready=1     avg_latency=1.3μs
fds=10000    ready=10    avg_latency=1.7μs
fds=10000    ready=100   avg_latency=4.1μs

fds=50000    ready=1     avg_latency=1.4μs
fds=50000    ready=10    avg_latency=1.8μs
fds=50000    ready=100   avg_latency=4.2μs

关键观察:
  - 总 fd 数从 100 增到 50000,epoll_wait 延迟几乎不变
  - 延迟主要取决于就绪 fd 数量(拷贝到用户空间的开销)
  - 证实了 epoll 的 O(k) 特性(k = 就绪数)

八、总结

epoll 的核心设计:

  1. 红黑树存储注册的 fd → epoll_ctl 增删改 O(log n)
  2. 回调机制将就绪 fd 加入就绪链表 → 无需遍历所有 fd
  3. 就绪链表epoll_wait 直接获取就绪事件 → O(k) 只和就绪数相关

ET vs LT 选择:

生产环境要点:


上一篇: Socket 编程模型演进:从阻塞到多路复用 下一篇: 零拷贝网络:sendfile、splice 与 MSG_ZEROCOPY

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-07-23 · network

【网络工程】零拷贝网络:sendfile、splice 与 MSG_ZEROCOPY

数据从磁盘到网卡的传统路径涉及 4 次拷贝和多次上下文切换。本文系统剖析 sendfile、splice、vmsplice、MSG_ZEROCOPY 四种零拷贝技术的内核实现、适用场景与性能差异,并以 Kafka 和 Nginx 为案例分析零拷贝在生产系统中的工程实践。

2025-07-25 · network

【网络工程】XDP 与 AF_XDP:eBPF 驱动的早期包处理

XDP 在内核网络栈最早期处理数据包,兼顾 DPDK 级性能与内核生态兼容性。本文从 XDP 的三种执行模式、程序编写实战、AF_XDP 的零拷贝路径到 Facebook Katran L4 负载均衡器的 XDP 实现,系统讲解 eBPF 驱动的高性能包处理。


By .