📌 核心结论(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 服务器
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很像,但逻辑完全变了。
cd ~/linux_study/network_test实战:极简 epoll 服务器
vim epoll_server.c(输入以下代码)
#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
gcc epoll_server.c -o epoll_server ./epoll_server👉核心区别:你看最后的for循环,只有nfds次!epoll_wait直接告诉你“有几个连接活了”,并把它们塞进events数组里。你再也不用写个for循环去挨个检查“这个连接活了吗?那个连接活了吗?”。这就是降维打击!