📌 核心结论(TL;DR):
epoll 三大 API :
epoll_create(建红黑树)、epoll_ctl(注册 fd)、epoll_wait(收就绪 fd)。红黑树 :存储所有被监听的 fd,每次
ctl只拷贝一次,彻底解决 select/poll 的"全量拷贝"问题。就绪队列 :基于网卡中断的回调机制,有数据自动放入就绪队列,彻底解决"全量遍历"问题。
结果 :
epoll的复杂度是 O(1),十万个连接里只有一个活跃,也能瞬间找到。
一、 为什么 epoll 是降维打击?
对于海量连接,select/poll 每次调用都要经历:
-
全量拷贝:把几十万个 fd 拷贝到内核。
-
全量遍历 :内核挨个查这几十万个 fd,看谁有数据。
如果只有少数活跃连接,这简直是灾难。
epoll通过引入红黑树 和就绪队列,完美解决了这两个痛点。
二、 核心底层机制

-
红黑树 (rbr) :调用
epoll_ctl将 fd 注册到红黑树中。增删改查性能是 O(logN),而且只在注册时拷贝一次。 -
回调机制 :当 fd 注册到内核时,内核会绑定一个回调函数。当网卡接收数据触发中断时,回调函数立刻执行,把这个 fd 放入就绪队列 (rdlist)。
-
就绪队列 :
epoll_wait只需检查这个就绪队列是否为空。如果不为空,就把队列里的 fd 返回给用户。
三、 代码实战:极简 epoll 服务器

cpp
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
while (1) {
// 只返回就绪的 fd,效率极高
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
// 直接处理就绪的 fd,无需遍历所有连接
int fd = events[i].data.fd;
// ...
}
}
与 poll 最大的不同在于,epoll_wait 的返回值 nfds 直接告诉你有几个事件就绪了。我们循环 nfds 次即可,完全不需要处理那些没有数据的连接。
自我实战操作
咱们直接写一段 epoll 服务器代码。你会发现,代码结构和 poll 很像,但逻辑完全变了。
cpp
cd ~/linux_study/network_test
实战:极简 epoll 服务器
bash
vim epoll_server.c
(输入以下代码)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h> // 引入 epoll 头文件
#define PORT 9999
#define MAX_EVENTS 10
int main() {
// 1. 常规创建、绑定、监听
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(listen_fd, 5);
// 2. 创建 epoll 实例(管家中心)
int epfd = epoll_create(1);
if (epfd < 0) { perror("epoll_create"); return 1; }
// 3. 注册监听 socket (listen_fd) 到红黑树
struct epoll_event ev;
ev.events = EPOLLIN; // 关心读事件
ev.data.fd = listen_fd; // 绑定 fd
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
struct epoll_event events[MAX_EVENTS];
printf("epoll 服务器启动,监听端口 %d...\n", PORT);
while (1) {
// 4. 等待事件就绪
// 返回的是已经就绪的 fd 数量,最大是 MAX_EVENTS
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds < 0) { perror("epoll_wait"); break; }
// 5. 只需要循环 nfds 次!不需要遍历十万个 fd 了!
for (int i = 0; i < nfds; i++) {
int fd = events[i].data.fd;
if (fd == listen_fd) {
// 新连接来了
int conn_fd = accept(listen_fd, NULL, NULL);
printf("新连接接入, fd = %d\n", conn_fd);
// 把新连接加入 epoll 监控
ev.events = EPOLLIN;
ev.data.fd = conn_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
} else {
// 普通客户端发来数据
char buf[1024] = {0};
int n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
printf("收到 fd=%d 的消息: %s\n", fd, buf);
write(fd, buf, n); // Echo 回显
} else if (n == 0) {
printf("客户端 fd=%d 断开\n", fd);
// 从 epoll 中移除并关闭
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
}
}
}
close(listen_fd);
close(epfd);
return 0;
}
编译运行:
bash
bash
gcc epoll_server.c -o epoll_server
./epoll_server
👉 核心区别 :你看最后的 for 循环,只有 nfds 次!epoll_wait 直接告诉你"有几个连接活了",并把它们塞进 events 数组里。你再也不用写个 for 循环去挨个检查"这个连接活了吗?那个连接活了吗?"。这就是降维打击!