上一篇介绍了从阻塞 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 的核心设计:
- 红黑树存储注册的 fd →
epoll_ctl增删改 O(log n) - 回调机制将就绪 fd 加入就绪链表 → 无需遍历所有 fd
- 就绪链表让
epoll_wait直接获取就绪事件 → O(k) 只和就绪数相关
ET vs LT 选择:
- LT(默认):更安全,不会丢事件,适合大多数应用(Redis、Node.js 的选择)
- ET:性能略优 5-15%,但编程复杂度高,必须循环处理到 EAGAIN(Nginx 的选择)
生产环境要点:
- 使用
SO_REUSEPORT做多进程负载均衡,避免惊群 - 多线程共享 epoll 时使用
EPOLLONESHOT保证线程安全 - 注意
dup()/fork()场景下 fd 与 file description 的区别
上一篇: Socket 编程模型演进:从阻塞到多路复用 下一篇: 零拷贝网络:sendfile、splice 与 MSG_ZEROCOPY
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】Socket 编程模型演进:从阻塞到多路复用
网络编程模型的选择决定了服务的并发能力上限。本文从阻塞 I/O 到非阻塞、select、poll、epoll,逐步解剖每种模型的系统调用开销、性能边界与适用场景,用 C 代码实测从 C10K 到 C1M 的演进。
【网络工程】零拷贝网络:sendfile、splice 与 MSG_ZEROCOPY
数据从磁盘到网卡的传统路径涉及 4 次拷贝和多次上下文切换。本文系统剖析 sendfile、splice、vmsplice、MSG_ZEROCOPY 四种零拷贝技术的内核实现、适用场景与性能差异,并以 Kafka 和 Nginx 为案例分析零拷贝在生产系统中的工程实践。
【网络工程】DPDK 与用户态网络栈:内核旁路的工程实践
当内核网络栈的上下文切换和拷贝开销成为瓶颈时,DPDK 提供了内核旁路方案。本文从 PMD 轮询模型、Hugepage 内存管理、NUMA 亲和到 F-Stack 用户态协议栈,系统讲解 DPDK 的工程原理与生产实践。
【网络工程】XDP 与 AF_XDP:eBPF 驱动的早期包处理
XDP 在内核网络栈最早期处理数据包,兼顾 DPDK 级性能与内核生态兼容性。本文从 XDP 的三种执行模式、程序编写实战、AF_XDP 的零拷贝路径到 Facebook Katran L4 负载均衡器的 XDP 实现,系统讲解 eBPF 驱动的高性能包处理。