☰
Linux 学习笔记(五十八):IO 多路复用封神——epoll 底层原理揭秘
2026/11/6 1:46:46 网站建设 项目流程

📌 核心结论(TL;DR):

  1. epoll 三大 API:epoll_create(建红黑树)、epoll_ctl(注册 fd)、epoll_wait(收就绪 fd)。

  2. 红黑树:存储所有被监听的 fd,每次ctl只拷贝一次,彻底解决 select/poll 的“全量拷贝”问题。

  3. 就绪队列:基于网卡中断的回调机制,有数据自动放入就绪队列,彻底解决“全量遍历”问题。

  4. 结果:epoll的复杂度是 O(1),十万个连接里只有一个活跃,也能瞬间找到。

一、 为什么 epoll 是降维打击?

对于海量连接,select/poll每次调用都要经历:

  1. 全量拷贝:把几十万个 fd 拷贝到内核。

  2. 全量遍历:内核挨个查这几十万个 fd,看谁有数据。
    如果只有少数活跃连接,这简直是灾难。epoll通过引入红黑树和就绪队列,完美解决了这两个痛点。

二、 核心底层机制

  1. 红黑树 (rbr):调用epoll_ctl将 fd 注册到红黑树中。增删改查性能是 O(logN),而且只在注册时拷贝一次。

  2. 回调机制:当 fd 注册到内核时,内核会绑定一个回调函数。当网卡接收数据触发中断时,回调函数立刻执行,把这个 fd 放入就绪队列 (rdlist)。

  3. 就绪队列: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循环去挨个检查“这个连接活了吗?那个连接活了吗?”。这就是降维打击!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询