Linux 学习笔记(五十八):IO 多路复用封神——epoll 底层原理揭秘

📌 核心结论(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 服务器

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 循环去挨个检查"这个连接活了吗?那个连接活了吗?"。这就是降维打击!

相关推荐
叼烟扛炮20 分钟前
Linux 学习笔记(五十一):数据链路层与 ARP 协议——局域网寻址与黑客骗局
linux·网络·网络协议·php·tcpdump
星恒随风1 小时前
Linux网络编程:彻底理解IO多路复用select!从fd_set位图、读写就绪到手写TCP多客户端字典服务器
linux·服务器·网络
陈布衣2 小时前
MySQL(1)_MySQL 5.7.21 Linux 安装笔记
linux·笔记·mysql
xyzzklk3 小时前
在Open-WebUI中提升向量化速度
网络·人工智能·爬虫·ai·语言模型·自然语言处理·安卓
分布式存储与RustFS3 小时前
给 RustFS 桶加一层内存缓存:用 Alluxio 让热数据少跑一遍网络
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
Oiiouui3 小时前
linux服务器部署Logto后端与对应前端的实现
linux·服务器·前端·后端
ITKEY_4 小时前
用 Emacs Dired 实现 em:退出文件管理器后,让终端自动切换目录
linux·服务器·emacs
wt95274 小时前
VMware 虚拟机磁盘扩容实战文档(Ubuntu 22.04 + LVM)
linux·后端
就像楞次定律4 小时前
【Linux 学习记之二】C++ 构建体系与底层链接机制的深度透视(2)
linux·后端