Linux网络(三十三):告别O(n)噩梦:从select/poll的挣扎到epoll红黑树+回调的降维打击,一文打通IO多路复用


◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️网络系列个人专栏: 【主题曲】计算机网络
⭐️此方的GitHub: github_此方
⭐️ 我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录

  • 概要&序論
  • [一、epoll 的诞生背景与核心定位](#一、epoll 的诞生背景与核心定位)
    • [1.1 epoll 的核心定位](#1.1 epoll 的核心定位)
    • [1.2 为什么我们需要 epoll?](#1.2 为什么我们需要 epoll?)
    • [1.3 epoll 初识与历史地位](#1.3 epoll 初识与历史地位)
    • [1.4 架构上的全面飞跃](#1.4 架构上的全面飞跃)
  • [二、快速认识 epoll 的核心接口](#二、快速认识 epoll 的核心接口)
    • [2.1 epoll_create:创建一个 epoll 模型](#2.1 epoll_create:创建一个 epoll 模型)
    • [2.2 epoll_ctl:管理红黑树上的节点](#2.2 epoll_ctl:管理红黑树上的节点)
    • [2.3 struct epoll_event:事件数据结构](#2.3 struct epoll_event:事件数据结构)
    • [2.4 epoll_wait:等待就绪事件](#2.4 epoll_wait:等待就绪事件)
  • [三、深入 epoll 底层原理:红黑树与就绪队列的完美配合](#三、深入 epoll 底层原理:红黑树与就绪队列的完美配合)
    • [3.1 核心组件:从 select 的位图到红黑树](#3.1 核心组件:从 select 的位图到红黑树)
    • [3.2 核心机制:回调与激活](#3.2 核心机制:回调与激活)
    • [3.3 epoll_wait:只关心就绪队列](#3.3 epoll_wait:只关心就绪队列)
    • [3.4 几个绕不开的核心疑问](#3.4 几个绕不开的核心疑问)
      • [3.4.1 我们该如何看待就绪队列?](#3.4.1 我们该如何看待就绪队列?)
      • [3.4.2 如果用户缓冲区大小不够怎么办?](#3.4.2 如果用户缓冲区大小不够怎么办?)
      • [3.4.3 处理就绪事件需要遍历所有 fd 吗?](#3.4.3 处理就绪事件需要遍历所有 fd 吗?)

概要&序論

  Hello大家好,我是此方。前两篇文章我们讲解了select和poll,本文来带大家了解epoll,它相比前两者效率更高,但是复杂程度迭代了好几个级别。

一、epoll 的诞生背景与核心定位

  在之前的文章中,我们先后剖析了 select 和 poll。虽然它们实现了 IO 多路复用,但随着并发连接数的飙升,它们的致命缺陷也暴露无遗:每次调用都要进行大量的参数拷贝,且内核需要 O(n) 遍历所有 fd。为了解决这个性能瓶颈,我们今天的主角------epoll 终于登场了。

1.1 epoll 的核心定位

  首先明确一点,epoll 的核心定位:基于对多个 fd 等待的就绪事件通知机制。它的本质依然是"等"加"通知",目的和 select、poll 是一样的。

  但是,它在底层实现上迎来了翻天覆地的变化。

1.2 为什么我们需要 epoll?

  回顾一下我们上一节讲的 poll 的痛点:poll 随着用户 fd 的增多,效率会发生一定程度的降低。

  当几万个连接同时存在,但只有少量连接活跃时,select 和 poll 依然会把这几万个 fd 拷贝到内核,再轮询扫描一遍,这极大地浪费了 CPU 资源。高并发场景迫切需要一个不需要重复拷贝,且只返回就绪 fd 的机制。

1.3 epoll 初识与历史地位

  按照 man 手册的说法,epoll 是为处理大批量句柄而作了改进的 poll。

  它是在 2.5.44 内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)。它几乎具备了之前所说的一切优点,被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法。

1.4 架构上的全面飞跃

  需要特别提醒的是,epoll 的实现原理、接口使用,和 select/poll 差别非常大!

  它不再使用一个巨大的数组去轮询,而是采用了事件驱动(Event-Driven)的红黑树加就绪链表结构。接下来,我们就来揭开它底层架构的秘密。

先讲接口吧。原理先缓一缓。

二、快速认识 epoll 的核心接口

  epoll 的接口设计与 select、poll 截然不同。它不再是用一个函数包揽所有操作,而是把创建、控制、等待拆分成了三个独立的系统调用。我们逐一拆解。

2.1 epoll_create:创建一个 epoll 模型

  要使用 epoll,第一步必须先创建一个 epoll 句柄。

c 复制代码
int epoll_create(int size);

  这个接口的作用是创建一个 epoll 模型。size 这个参数在过去(老版本内核)是用来告诉内核你需要监听的文件描述符上限,但是现在已经被忽略了(the size argument is ignored, but must be greater than zero)。你随便传一个大于 0 的数字即可。

  它成功时返回一个文件描述符,代表着这个 epoll 实例;失败则返回 -1。

2.2 epoll_ctl:管理红黑树上的节点

  创建完模型后,我们需要往里面增删改要监听的文件描述符,这时就要用到 epoll_ctl。

c 复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

  这个接口的本质是:用户告诉内核,你要帮我关心哪个 fd,上面的什么 event 事件。它的参数含义如下:

  • epfd:就是刚才 epoll_create 返回的句柄。
  • op:操作类型,比如 EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)。
  • fd:你要监听的目标文件描述符。
  • event:告诉内核你要监听什么事件。

  实际上,我们已经把想要去等待的文件描述符通过 epoll_ctl 交给内核了。底层实现上,epoll_ctl 内部会创建一个红黑树的结点,把 fd 放进去,这样内核就能高效地管理海量连接了。成功返回 0,失败返回 -1。

2.3 struct epoll_event:事件数据结构

  在 epoll_ctl 中,我们需要传入一个指向 struct epoll_event 结构体的指针。这个结构体非常关键,它包含了事件类型和用户数据。

c 复制代码
struct epoll_event {
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};

  events 成员表示事件类型,和 poll 基本一致。目前够用的核心事件有:

  • EPOLLIN:表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭)。
  • EPOLLOUT:表示对应的文件描述符可以写。

  data 成员是用户数据,这个字段非常巧妙。内核不会去修改它,你把里面写成什么都可以,操作系统不管这个。它的存在,纯粹是方便用户去得知"在 epoll_wait 返回后,我返回的这几个文件描述符就绪了!"至于用户到底想在这个 data 里存 fd 还是指针,这就是用户自己的代码设计的事情了。

2.4 epoll_wait:等待就绪事件

  最后,就是最核心的等待接口 epoll_wait。

c 复制代码
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

  这个接口的本质是:内核通知用户,你让我关心的那些 fd 上面的哪些事件已经就绪。参数拆解如下:

  • epfd:依然是 epoll 句柄。
  • events:这是一个由用户提供的内核已经就绪的文件描述符的数组,内核会把就绪的事件信息填入这个数组中。
  • maxevents:告诉内核这个数组的长度。
  • timeout:超时时间,单位是毫秒,同 poll 一致。

  它成功时返回就绪的 fd 个数,失败返回 -1。

  通过这三个拆开的接口,epoll 完美实现了对海量连接的高效管理。下一节,我们来看它如何用红黑树和就绪链表消灭 O(n) 遍历。

三、深入 epoll 底层原理:红黑树与就绪队列的完美配合

  在前面,我们拆解了 epoll 的三个核心接口。但你一定会好奇,为什么拆成三个接口就能实现 O(1) 的高效?这就要深入内核,看看 epoll 到底维护了什么样的底层数据结构。

3.1 核心组件:从 select 的位图到红黑树

  回顾一下之前 select/poll 的痛苦,它们每次都要把庞大的集合拷贝进内核,然后让内核从头到尾遍历。epoll 的设计者觉得这太蠢了,于是采用了一种全新的架构。

  当我们调用 epoll_create 时,内核会创建并初始化一个 epoll 模型,这里面包含了一个红黑树 、一个就绪队列以及相关的内核数据结构。并且,会分配给这个 epoll 模型一个文件描述符(epfd),这就是为什么 epoll 返回的是一个 fd,因为我们要把 epoll 和文件系统做关联。

  这个红黑树的功能,类似于以前 poll/select 中我们自己维护的那张辅助数组。只不过,epoll 的设计者把这东西放进了内核里,同时为了极高的查询和修改效率,使用了红黑树。

  既然是一棵树,那红黑树的 key 应该是什么呢?答案就是 fd!

3.2 核心机制:回调与激活

  讲到这里,很多人的第一个疑惑是:内核怎么知道某个 fd 就绪了?总不能一直死循环遍历红黑树吧?

  这就要归功于网络协议栈底层的回调机制。

  当我们调用 epoll_ctl(..., EPOLL_CTL_ADD, ...) 时,不仅仅是把 fd 插入到了红黑树中,更重要的是:向底层注册回调方法! 此时,我们为这个特定的 fd 建立起了针对具体 fd 的回调关联。

  当底层网卡驱动收到数据,产生硬件中断,交付给网络协议栈时,发现这个 fd 上有数据就绪,就会自动进行回调。回调操作会激活红黑树中的这个节点,并把它激活到就绪队列(就绪链表)中。

  在内核中,这个 struct rb_node 结点不仅仅包含左右子树指针,还包含一个 list_head link,用来挂载到就绪队列上,以及就绪事件 revents。

3.3 epoll_wait:只关心就绪队列

  理解了上面的机制,epoll_wait 的工作原理就一目了然了。

  epoll_wait 的本质是内核告诉用户,你关心的那些 fd 上面的哪些事件已经就绪。它检测是否有 fd 就绪的复杂度是 O(1) 的。为什么?因为内核根本不需要去遍历所有监控的 fd(红黑树上的所有节点),它只需要直接检查就绪链表即可!

  当检测到有事件就绪后,内核需要将这些就绪的 fd 事件从内核空间拷贝到用户传入的 events 数组中。由于只拷贝就绪的,不拷贝未就绪的,这使得 epoll 在处理海量连接时,性能极其强悍。

3.4 几个绕不开的核心疑问

  在理解了整体架构后,我们来看几个极高频的细节问题。

3.4.1 我们该如何看待就绪队列?

  就绪队列本质上就是一个基于事件就绪的生产者消费者模型 。底层网卡中断产生数据是生产者,把节点丢进就绪队列;应用层调用 epoll_wait 是消费者,从队列里拿走就绪的事件。epoll 的接口是线程安全的。

3.4.2 如果用户缓冲区大小不够怎么办?

  epoll_wait 接收一个 maxevents 参数,也就是用户传入的 events 数组的最大长度。如果在一次返回中,就绪的事件数量大于了 maxevents,这并不会影响系统运行。没拿完的,默认给你保留着,下次拿即可。内核会严格按照下标开始(0 开始),依次拷贝保存就绪事件和 fd。

3.4.3 处理就绪事件需要遍历所有 fd 吗?

  不需要。这也是 epoll 与 select/poll 的最大区别。当我们拿到 epoll_wait 的返回值 n 后,直接使用 for (int i = 0; i < n; i++) 遍历就绪的 events 数组即可。应用层处理就绪事件的时候,处理的全都是就绪的,基本不需要非法检测。

最难的部分来了,我们要看看epoll的底层构造。

  整张图片非常大,大家可以去我的仓库看。Epoll内核结构图


好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!

相关推荐
2301_777998341 小时前
Linux 网络:数据链路层
linux·服务器·网络
迷途之人不知返1 小时前
【文件系统】-1-磁盘与扇区的定位方法
linux
无足鸟ICT1 小时前
【RHCA+】yum源配置(软件仓库)
linux·运维
国科安芯1 小时前
空间锂离子电池组管理系统CANFD通信网络的辐射可靠性设计
网络·信息与通信·canfd·电池·低轨卫星·抗辐射
2401_868534781 小时前
网规物流企业网络问题
网络
0+1111 小时前
Linux --IP分片和组装
linux·网络·tcp/ip
Kira Skyler1 小时前
CVE-2026-99999 deferred-metadata trust inversion 原理分析.md
linux
zxanz12 小时前
SSL 证书该怎么申请?操作复杂吗?
网络·网络协议·ssl
AR-26710-2 小时前
Linux Day24——TCP/IP分层与封装
linux·运维