跳转到正文
zeno's blog
返回

Linux I/O(一):epoll 高性能的本质与使用要点

专题: Linux I/O

Table of contents

Open Table of contents

TL;DR

epoll 把 select/poll 的 O(n) 轮询重构为「注册一次 + 设备就绪时回调插入链表」的事件驱动模型;正确使用的核心是 ET 必须配非阻塞 + 循环到 EAGAINEPOLLERR/EPOLLHUP 无需请求但必须处理close 前必须显式 EPOLL_CTL_DEL(防 dup 场景的悬垂监听)。

常见误解澄清:epoll 性能好不是因为 mmap 共享内存。epoll_wait 使用 copy_to_user 把就绪事件拷贝给用户态,和普通 syscall 一致。


1. Why:select/poll 为什么不够

select(nfds, fd_set*, ...) 的签名注定它无法扩展:

  1. 每次调用都传全量 fd 集合 — 用户态到内核态 O(n) 拷贝
  2. 内核内部轮询所有 fdfor (每个 fd) { f_op->poll(fd) },即便只有 1 个就绪也要扫完
  3. 返回后用户态还要 O(n) 扫 fd_setFD_ISSET 查每个 fd
  4. FD_SETSIZE = 1024 硬上限(glibc 默认)

C10K 的本质:n 很大但稀疏就绪(10000 连接,每秒 100 个活动)时,O(n) 扫描浪费了 99% 的 CPU。

epoll 的重构不是把 O(n) 改成 O(log n),而是从轮询模型转向回调驱动

借 Jonathan Corbet 在 LWN 的话:“epoll is not a faster poll; it’s a different beast.”


2. 内核实现原理

2.1 struct eventpoll 的核心字段

每个 epoll 实例对应内核 fs/eventpoll.c 中的 struct eventpoll

字段作用
rbr(红黑树)所有被注册的 fd → epitem 节点
rdllist(就绪链表)当前处于就绪状态的 epitem
wq(等待队列)调用 epoll_wait 阻塞的进程
ovflist扫描 rdllist 时新到事件的临时溢出链

每个被监听的 fd 对应一个 epitem,同时挂在 rbr 和(就绪时)rdllist 上。

2.2 为什么 epoll_wait 是 O(1)

关键:epoll_wait 只扫 rdllist,不扫 rbr

对比 select/poll:必须扫描全部注册的 fd(相当于 epoll 的 rbr)。

2.3 wakeup callback 机制

注册阶段(ep_insertep_ptable_queue_proc):

  1. epoll 调用目标 fd 的 f_op->poll(file, &pt)
  2. 这个 poll 方法内部 poll_wait(file, &fd 的等待队列, pt)
  3. epoll 提供的 pt->_qproc 回调把一个 eppoll_entry(回调函数 = ep_poll_callback)挂到目标 fd 的等待队列

触发阶段(数据到达):

  1. 网卡中断 → 协议栈 → TCP 收到新数据 → 唤醒 socket 等待队列
  2. 内核遍历队列里的 entry,调用它们的 wakeup 函数
  3. epoll 的 entry 调用 ep_poll_callback:把 epitem 挂进 rdllist、唤醒 wq 中阻塞的进程

没有任何轮询,纯粹由 I/O 事件驱动。这就是 epoll 相对 select/poll 的真正价值。

2.4 mmap 是常见误解

网上大量中文资料说”epoll 快是因为 epoll_wait 用 mmap 共享内存”——错的。查 fs/eventpoll.cep_send_events:使用 __put_user / copy_to_user 把 events 从内核拷到用户提供的数组,用户数组是普通内存。

epoll 快的真正原因只有两个:O(1) 就绪链表 + 回调驱动

2.5 epoll 实例也是 fd(嵌套)

epoll_create1() 返回的 fd 有自己的 f_op->pollep_eventpoll_poll),所以可以被另一个 epoll 监听。内核限制嵌套深度 EP_MAX_NESTS = 4epoll_ctl 时通过 ep_loop_check 检测环路。


3. 核心 API

3.1 创建 / 销毁

#include <sys/epoll.h>
int epoll_create1(int flags);         // Linux 2.6.27+
// 老接口(size 自 2.6.8 起被忽略):
int epoll_create(int size);

必须用 epoll_create1(EPOLL_CLOEXEC),在启动新程序的子进程场景防 fd 泄漏。

3.2 epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event {
    uint32_t events;
    epoll_data_t data;   // union { void *ptr; int fd; uint32_t u32; uint64_t u64; }
};

为什么 struct epoll_event 在 Linux 下是 __attribute__((packed)):在 x86_64 下如不 packed,uint32_t events 后的 union 会有 4 字节对齐填充让 struct 大小为 16 字节;而 i386 下只有 12 字节。packed 让二者一致为 12 字节,保证 x86_64 内核处理 32-bit 程序调用时 ABI 兼容。

重要 errnoEPERM — 目标 fd 不支持 epoll。最典型是 regular file(见第 8 节 pitfall)。

3.3 epoll_wait / epoll_pwait / epoll_pwait2

int epoll_wait  (int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait (int epfd, struct epoll_event *events, int maxevents, int timeout,
                 const sigset_t *sigmask);
int epoll_pwait2(int epfd, struct epoll_event *events, int maxevents,
                 const struct timespec *timeout, const sigset_t *sigmask);  // 5.11+

timeout 语义

epoll_pwait2(5.11+) 使用 timespec 提供纳秒精度,解决 int 毫秒的精度问题。

返回值> 0 就绪数,0 超时,-1 错误(EINTR 必须循环重试)。


4. 事件标志(events flags)

标志方向需显式请求最低内核关键点
EPOLLIN输入2.6可读
EPOLLOUT输出2.6可写
EPOLLRDHUP输入2.6.17对端 close 或 shutdown(SHUT_WR)
EPOLLPRI输入2.6TCP OOB
EPOLLERR否,始终上报2.6fd 错误
EPOLLHUP否,始终上报2.6fd 双向挂起
EPOLLET修饰2.6边缘触发
EPOLLONESHOT修饰2.6.2触发一次后禁用,需 MOD 重武装
EPOLLEXCLUSIVE修饰4.5+多 waiter 惊群:只唤一个;仅 ADD
EPOLLWAKEUP修饰3.5+阻止系统 suspend,需 CAP_BLOCK_SUSPEND

EPOLLRDHUP 的精确语义

man epoll(7):“Stream socket peer closed connection, or shut down writing half of connection.”

关键差异EPOLLHUP = 双向都挂;EPOLLRDHUP = 对端不再写。

EPOLLERR / EPOLLHUP 的致命陷阱

这两个事件无需请求也会返回。事件循环里如果只判 events & EPOLLIN 不兜底,错误 fd 会每次 epoll_wait 都返回就绪,CPU 原地爆炸、fd 永远泄漏。

所有生产代码事件循环第一步必须先判 EPOLLERR | EPOLLHUP


5. LT vs ET(核心章节)

5.1 本质定义

模式触发条件
LT (Level Triggered)只要 fd 处于就绪状态,每次 epoll_wait 都返回它
ET (Edge Triggered)只在 fd 从「未就绪」变为「就绪」的那一刻通知一次

ET 是”状态变化”,不是”新数据到达”——这是最常见的误解。

5.2 为什么 ET 必须配非阻塞 I/O

阻塞 fd + ET:

  1. epoll_wait 返回 EPOLLIN
  2. read(fd, buf, N) 读到 N 字节
  3. 缓冲区里还有数据,但你以为读完了,调 epoll_wait
  4. 没有”未就绪 → 就绪”的状态变化,ET 不再通知
  5. 下次主动 read,缓冲区空 → 阻塞 fd 永久卡死线程

LT 没这个问题,因为只要缓冲区有数据它就一直通知。

5.3 ET 的”状态不变”陷阱(最精微)

T1: 缓冲区 100B,fd 变为就绪 → ET 通知
T2: 应用读 50B,缓冲区还剩 50B
T3: 对端又发 100B,缓冲区变 150B

T3 时 fd 的就绪状态没变(一直”有数据可读”),ET 不会再次通知。应用如果 T2 停手等 epoll,就永远卡住。

所以 ET 下 read 必须循环到 EAGAIN——把缓冲区读空让状态变为”未就绪”,之后新数据才会触发”未就绪 → 就绪”。

5.4 ET 下的正确写法

accept 循环

while (true) {
    int client = accept4(listen_fd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC);
    if (client == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) break;
        if (errno == EINTR) continue;
        perror("accept4"); break;
    }
    register_client(client);
}

read 循环

while (true) {
    ssize_t n = read(fd, buf, sizeof(buf));
    if (n > 0)       { process(buf, n); continue; }
    if (n == 0)      { close_conn(fd); return; }   // 对端关
    if (errno == EAGAIN) break;                    // 读空
    if (errno == EINTR)  continue;
    close_conn(fd); return;                         // 真错误
}

write 循环 + EPOLLOUT 动态注册(关键):

// 首次 write
ssize_t n = write(fd, buf, len);
if (n < (ssize_t)len) {
    save_pending(fd, buf + n, len - n);
    epoll_mod(fd, EPOLLIN | EPOLLOUT | EPOLLET);  // 挂 EPOLLOUT
}

// EPOLLOUT 触发时消费 pending
while (has_pending(fd)) {
    ssize_t n = write(fd, pending.data(), pending.size());
    if (n < 0 && errno == EAGAIN) return;
    consume_pending(fd, n);
}
// 写完必须取消 EPOLLOUT,否则 LT 下 CPU 100%
epoll_mod(fd, EPOLLIN | EPOLLET);

5.5 EPOLLONESHOT vs ET 在多线程的取舍

ETEPOLLONESHOT
通知次数状态变化时一次触发一次后禁用
多线程安全性多个线程 epoll_wait 同一 epoll 时仍可能重复收到同一事件内核保证同一 fd 不会被多个 waiter 同时拿到
重武装不需要必须 EPOLL_CTL_MOD 重武装

多线程 worker pool 标准模式:主线程 epoll_wait → dispatch 给 worker → worker 处理完 EPOLL_CTL_MOD(EPOLLIN | EPOLLONESHOT) 重武装。保证同一 fd 任意时刻只被一个 worker 持有,比 ET + 手动加锁更安全。


6. 惊群问题(Thundering Herd)

两种惊群

三方案对比

方案版本机制负载均衡限制
EPOLLEXCLUSIVE4.5+内核:fd 就绪只唤一个 waiter一般(按唤醒顺序)仅 ADD,不能 MOD;不能嵌套 epoll
SO_REUSEPORT3.9+每个 worker 独立 listen fd,内核按四元组 hash 分发优秀worker 数固定
accept_mutex(Nginx 老做法)任意应用层 mutex较差(饥饿风险)纯用户态,延迟高

实战推荐:Linux 4.5+ 优先 SO_REUSEPORT——每 worker 独立 listen fd 和独立 epoll,无锁、无惊群、CPU 亲和性好。Nginx 从 1.9.1 起默认支持 reuseport。


7. C++17 可运行代码示例

7.1 FdGuard(所有示例共享的 RAII)

// fd_guard.hpp
#pragma once
#include <unistd.h>
#include <utility>

class FdGuard {
    int fd_{-1};
public:
    FdGuard() = default;
    explicit FdGuard(int fd) noexcept : fd_(fd) {}
    ~FdGuard() { if (fd_ >= 0) ::close(fd_); }
    FdGuard(const FdGuard&) = delete;
    FdGuard& operator=(const FdGuard&) = delete;
    FdGuard(FdGuard&& o) noexcept : fd_(std::exchange(o.fd_, -1)) {}
    FdGuard& operator=(FdGuard&& o) noexcept {
        if (this != &o) { if (fd_ >= 0) ::close(fd_); fd_ = std::exchange(o.fd_, -1); }
        return *this;
    }
    int get() const noexcept { return fd_; }
    int release() noexcept { return std::exchange(fd_, -1); }
    explicit operator bool() const noexcept { return fd_ >= 0; }
};

7.2 LT 模式 echo server(最小可用)

编译:g++ -std=c++17 -Wall -Wextra -O2 echo_lt.cpp -o echo_lt

#include "fd_guard.hpp"
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>

constexpr int PORT = 9001;
constexpr int MAX_EVENTS = 64;

static void die(const char* m) { perror(m); std::exit(1); }

int main() {
    FdGuard listen_fd(::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0));
    if (!listen_fd) die("socket");

    int yes = 1;
    ::setsockopt(listen_fd.get(), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);
    if (::bind(listen_fd.get(), (sockaddr*)&addr, sizeof(addr)) < 0) die("bind");
    if (::listen(listen_fd.get(), SOMAXCONN) < 0) die("listen");

    FdGuard epfd(::epoll_create1(EPOLL_CLOEXEC));
    if (!epfd) die("epoll_create1");

    epoll_event ev{};
    ev.events = EPOLLIN;                  // LT 模式
    ev.data.fd = listen_fd.get();
    if (::epoll_ctl(epfd.get(), EPOLL_CTL_ADD, listen_fd.get(), &ev) < 0) die("epoll_ctl");

    epoll_event events[MAX_EVENTS];
    char buf[4096];

    for (;;) {
        int n = ::epoll_wait(epfd.get(), events, MAX_EVENTS, -1);
        if (n < 0) { if (errno == EINTR) continue; die("epoll_wait"); }

        for (int i = 0; i < n; ++i) {
            int fd = events[i].data.fd;
            uint32_t e = events[i].events;

            // 必须先处理错误
            if (e & (EPOLLERR | EPOLLHUP)) {
                ::epoll_ctl(epfd.get(), EPOLL_CTL_DEL, fd, nullptr);
                ::close(fd);
                continue;
            }

            if (fd == listen_fd.get()) {
                // 即便 LT 也建议循环 accept 清空 backlog
                for (;;) {
                    int c = ::accept4(listen_fd.get(), nullptr, nullptr,
                                      SOCK_NONBLOCK | SOCK_CLOEXEC);
                    if (c < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) break;
                        if (errno == EINTR) continue;
                        perror("accept4"); break;
                    }
                    epoll_event cev{};
                    cev.events = EPOLLIN | EPOLLRDHUP;
                    cev.data.fd = c;
                    if (::epoll_ctl(epfd.get(), EPOLL_CTL_ADD, c, &cev) < 0) {
                        perror("epoll_ctl"); ::close(c);
                    }
                }
            } else if (e & (EPOLLIN | EPOLLRDHUP)) {
                ssize_t r = ::read(fd, buf, sizeof(buf));
                if (r > 0) {
                    ssize_t off = 0;
                    while (off < r) {
                        ssize_t w = ::write(fd, buf + off, r - off);
                        if (w < 0) { if (errno == EINTR) continue; break; }
                        off += w;
                    }
                } else {
                    ::epoll_ctl(epfd.get(), EPOLL_CTL_DEL, fd, nullptr);
                    ::close(fd);
                }
            }
        }
    }
}

7.3 ET 模式的正确实现(带写缓冲管理)

编译:g++ -std=c++17 -Wall -Wextra -O2 echo_et.cpp -o echo_et

#include "fd_guard.hpp"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <unordered_map>
#include <vector>

struct Conn {
    int fd;
    std::deque<std::vector<char>> out;    // 待发送
    bool writable = true;
};

static std::unordered_map<int, Conn> g_conns;
static int g_epfd = -1;

static void mod_events(int fd, uint32_t ev) {
    epoll_event e{};
    e.events = ev;
    e.data.fd = fd;
    ::epoll_ctl(g_epfd, EPOLL_CTL_MOD, fd, &e);
}

static void close_conn(int fd) {
    ::epoll_ctl(g_epfd, EPOLL_CTL_DEL, fd, nullptr);   // 务必先 DEL 再 close
    ::close(fd);
    g_conns.erase(fd);
}

static void try_write(Conn& c) {
    while (!c.out.empty()) {
        auto& front = c.out.front();
        ssize_t n = ::write(c.fd, front.data(), front.size());
        if (n < 0) {
            if (errno == EAGAIN) { c.writable = false; return; }
            if (errno == EINTR)  continue;
            close_conn(c.fd); return;
        }
        if ((size_t)n < front.size()) {
            front.erase(front.begin(), front.begin() + n);
            c.writable = false;
            return;
        }
        c.out.pop_front();
    }
    mod_events(c.fd, EPOLLIN | EPOLLRDHUP | EPOLLET);   // 写完取消 EPOLLOUT
}

static void handle_read(Conn& c) {
    char buf[4096];
    for (;;) {
        ssize_t r = ::read(c.fd, buf, sizeof(buf));
        if (r > 0) { c.out.emplace_back(buf, buf + r); continue; }
        if (r == 0) { close_conn(c.fd); return; }
        if (errno == EAGAIN) break;
        if (errno == EINTR)  continue;
        close_conn(c.fd); return;
    }
    if (!c.out.empty()) {
        if (c.writable) try_write(c);
        if (!c.out.empty()) mod_events(c.fd, EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET);
    }
}

int main() {
    FdGuard lfd(::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0));
    int yes = 1;
    ::setsockopt(lfd.get(), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
    sockaddr_in a{};
    a.sin_family = AF_INET;
    a.sin_addr.s_addr = htonl(INADDR_ANY);
    a.sin_port = htons(9002);
    ::bind(lfd.get(), (sockaddr*)&a, sizeof(a));
    ::listen(lfd.get(), SOMAXCONN);

    FdGuard ep(::epoll_create1(EPOLL_CLOEXEC));
    g_epfd = ep.get();

    epoll_event ev{};
    ev.events = EPOLLIN | EPOLLET;         // listen fd 也用 ET
    ev.data.fd = lfd.get();
    ::epoll_ctl(g_epfd, EPOLL_CTL_ADD, lfd.get(), &ev);

    epoll_event events[128];
    for (;;) {
        int n = ::epoll_wait(g_epfd, events, 128, -1);
        if (n < 0) { if (errno == EINTR) continue; perror("epoll_wait"); break; }
        for (int i = 0; i < n; ++i) {
            int fd = events[i].data.fd;
            uint32_t e = events[i].events;

            if (fd == lfd.get()) {
                // ET 下必须循环 accept
                for (;;) {
                    int c = ::accept4(lfd.get(), nullptr, nullptr,
                                      SOCK_NONBLOCK | SOCK_CLOEXEC);
                    if (c < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) break;
                        if (errno == EINTR) continue;
                        perror("accept4"); break;
                    }
                    g_conns.emplace(c, Conn{c});
                    epoll_event cev{};
                    cev.events = EPOLLIN | EPOLLRDHUP | EPOLLET;
                    cev.data.fd = c;
                    ::epoll_ctl(g_epfd, EPOLL_CTL_ADD, c, &cev);
                }
                continue;
            }

            auto it = g_conns.find(fd);
            if (it == g_conns.end()) continue;
            Conn& c = it->second;

            if (e & (EPOLLERR | EPOLLHUP)) { close_conn(fd); continue; }
            if (e & EPOLLOUT) { c.writable = true; try_write(c); }
            if (e & (EPOLLIN | EPOLLRDHUP)) handle_read(c);
        }
    }
}

7.4 EpollReactor:用 data.ptr 做类型安全分发

// epoll_reactor.hpp
#pragma once
#include "fd_guard.hpp"
#include <sys/epoll.h>
#include <cerrno>
#include <functional>
#include <memory>
#include <system_error>
#include <unordered_map>
#include <vector>

class EpollReactor {
public:
    using Handler = std::function<void(uint32_t events)>;

    EpollReactor() : ep_(::epoll_create1(EPOLL_CLOEXEC)) {
        if (!ep_) throw std::system_error(errno, std::system_category(), "epoll_create1");
    }

    void add(int fd, uint32_t events, Handler h) {
        auto hp = std::make_shared<Handler>(std::move(h));
        epoll_event ev{};
        ev.events = events;
        ev.data.ptr = hp.get();
        if (::epoll_ctl(ep_.get(), EPOLL_CTL_ADD, fd, &ev) < 0)
            throw std::system_error(errno, std::system_category(), "EPOLL_CTL_ADD");
        handlers_[fd] = std::move(hp);
    }

    void mod(int fd, uint32_t events) {
        auto it = handlers_.find(fd);
        if (it == handlers_.end()) throw std::runtime_error("mod: fd not registered");
        epoll_event ev{};
        ev.events = events;
        ev.data.ptr = it->second.get();
        if (::epoll_ctl(ep_.get(), EPOLL_CTL_MOD, fd, &ev) < 0)
            throw std::system_error(errno, std::system_category(), "EPOLL_CTL_MOD");
    }

    void del(int fd) {
        ::epoll_ctl(ep_.get(), EPOLL_CTL_DEL, fd, nullptr);   // 必须 DEL 再 close
        handlers_.erase(fd);
    }

    int run_once(int timeout_ms, int maxevents = 64) {
        std::vector<epoll_event> evs(maxevents);
        int n = ::epoll_wait(ep_.get(), evs.data(), maxevents, timeout_ms);
        if (n < 0) {
            if (errno == EINTR) return 0;
            throw std::system_error(errno, std::system_category(), "epoll_wait");
        }
        for (int i = 0; i < n; ++i) {
            auto* h = static_cast<Handler*>(evs[i].data.ptr);
            if (h && *h) (*h)(evs[i].events);
        }
        return n;
    }

private:
    FdGuard ep_;
    std::unordered_map<int, std::shared_ptr<Handler>> handlers_;
};

8. Pitfalls(深度版)

8.1 ET 下忘记循环 read 到 EAGAIN(数据卡住)

只读一次会留残余数据。状态一直是”就绪”,ET 不再通知;新数据到达时状态仍然没”未就绪→就绪”,也不触发。

8.2 ET 下用阻塞 fd(死锁线程)

见 5.2:read 读空后第二次 read 阻塞整个事件循环。

8.3 EPOLLOUT 忘了取消注册(CPU 100%)

socket 写缓冲通常空闲可写。LT 模式下注册 EPOLLOUT 但没数据要发、又不 MOD 掉,epoll_wait 立刻返回,循环变忙转。有数据才注册 EPOLLOUT,发完立刻 MOD 取消

8.4 只监听 EPOLLIN 不监听 EPOLLRDHUP(关闭检测延迟)

对端 close 时仅 EPOLLIN 只能靠下次 read 返回 0 检测,中间可能已发送无用响应。加上 EPOLLRDHUP 能立即判定。

8.5 EPOLLERR / EPOLLHUP 不处理(fd 泄漏 + CPU 爆炸)

这俩无需请求即始终上报。事件循环不处理 → 错误 fd 每轮都返回 → CPU 100% + fd 永远泄漏。事件循环第一步必须先判这两个

8.6 close 前没 DEL,且 fd 被 dup 过(最精微的坑)

Linux close(fd)自动从所有 epoll 移除 fd——前提是这是最后一个指向该 struct file 的 fd。如果 fd 被 dup / fork 过,close 只减引用计数,底层 file 还活着,epoll 仍持有监听:

铁律:永远在 close 之前显式 EPOLL_CTL_DEL

8.7 多线程竞争 handler 状态(UAF)

epoll_ctlepoll_wait 自身线程安全,但:

解决:shared_ptr 管理(见 7.4),或 EPOLLONESHOT + 单主线程 epoll_wait + worker 只消费任务。

8.8 data.fd 和 data.ptr 混用

epoll_data_t 是 union:设了 data.ptr 再读 data.fd 会把指针低 32 位当 fd 用。一个 epoll 实例里统一用一种,封装到 Reactor 屏蔽。

8.9 LT 下忘了 MOD 取消不再关心的事件

读完 HTTP request 进入”写响应”状态时,若不 MOD 掉 EPOLLIN,对端 pipeline 发来新请求会触发 EPOLLIN,状态机错乱。

8.10 timeout 精度

int 毫秒最大 24.8 天。纳秒精度或超长超时用 epoll_pwait2(5.11+)。

8.11 普通文件不支持 epoll(EPERM)

Regular file 对 poll 永远返回”就绪”(read 永远不阻塞,即便 EOF 也立即返回 0)。内核对此返回 EPERM

能被 epoll 监听的 fd:socket、pipe、FIFO、eventfd、signalfd、timerfd、inotify fd、另一个 epoll fd、tty、部分 character device。

想监听日志文件变化 → 用 inotify_init1() 得到 inotify fd,再把这个 fd 加入 epoll。

8.12 epoll_wait 返回 == maxevents 暗示可能还有更多

内核每次最多返回 maxevents 个。传 1 则最多 1 个。生产设 64 或 128。返回值等于 maxevents 时下一轮 wait(timeout=0) 可立即取剩余。


9. 与 select / poll / kqueue / io_uring 对比

维度selectpollepollkqueueio_uring
系统POSIXPOSIXLinux 2.6+BSD/macOSLinux 5.1+
注册/查询分离
时间复杂度O(n)O(n)O(1) 就绪扫描O(1)O(1)
fd 上限1024 (FD_SETSIZE)无硬上限RLIMIT_NOFILE无硬上限无硬上限
触发模式LTLTLT + ETLT + ETN/A
regular file是(立即就绪)否(EPERM)是(真正异步)
异步 I/O否(仅通知)
典型场景兼容性兼容性Linux C10K+ 主流BSD C10K+磁盘 + 网络真正异步

要点


10. 生产 Checklist


参考

socket / TCP 层行为见 C++ socket 编程手册


分享这篇文章:

上一篇
GORM(三):生产环境最佳实践
下一篇
GORM(二):关联与预加载