Linux内核机制之epoll详解

目录

简介:

一、IO 多路复用介绍

1、select,poll,epoll 引入

2、select,poll,epoll 区别分析

3、epoll 原理

3.1 epoll 相关函数介绍

1)epoll_create

2)epoll_ctl

3)epoll_wait

3.2 epoll 高效的原理

1)epoll 高效原因一

2)epoll 高效原因二

3.3 epoll 的水平触发(Level Triggered, LT)和边缘触发(Edge Triggered, ET)

1)水平触发(LT)

2)边缘触发(ET)

3.4 哪些 fd 可以用 epoll 来管理?


简介:


在 Linux 系统之中有一个核心武器:epoll 池,在高并发的,高吞吐的 IO 系统中常常见到 epoll 的身影。

一、IO 多路复用介绍


        1 个程序就可以负责管理多个 fd 句柄,负责应对所有的业务方的 IO 请求。这种一对多的 IO 模式我们就叫做 IO 多路复用。

多路是指?多个业务方(句柄)并发下来的 IO 。

复用是指?复用一个处理程序。

1、select,poll,epoll 引入


        写个 for 循环,每次都尝试 IO 一下,读/写到了就处理,读/写不到就 sleep 下。这样我们不就实现了 1 对多的 IO 多路复用嘛。

问题:for 循环每次要定期 sleep 1s,这个会导致吞吐能力极差,因为很可能在刚好要 sleep 的时候,所有的 fd 都准备好 IO 数据,而这个时候却要硬生生的等待 1s,可想而知。。。不sleep又会导致CPU占用过高。

       我们再梳理下 IO 多路复用的需求和原理。IO 多路复用就是 1 个线程处理 多个 fd 的模式。我们的要求是:这个 “1” 就要尽可能的快,避免一切无效工作,要把所有的时间都用在处理句柄的 IO 上,不能有任何空转,sleep 的时间浪费。

         Linux内核提供 select,poll,epoll 工具实现IO多路复用。

2、select,poll,epoll 区别分析


        Linux  内核提供了 3 种工具 select,poll,epoll 实现IO多路复用

为什么有 3 种?

历史不断改进,矬 -> 较矬 -> 卧槽、高效 的演变而已。

这 3 种都能够管理 fd 的可读可写事件,在所有 fd 不可读不可写无所事事的时候,可以阻塞线程,切走 cpu 。fd 有情况的时候,都要线程能够要能被唤醒。

而这三种方式以 epoll 池的效率最高。为什么效率最高?其实无非就是 epoll 做的无用功最少,select 和 poll 或多或少都要多余的拷贝,需遍历fd ,所以效率自然就低了。

举个例子,以 select 和 epoll 来对比举例:池子里管理了 1024 个句柄,loop 线程被唤醒的时候,select 都是蒙的,都不知道这 1024 个 fd 里谁 IO 准备好了。这种情况怎么办?只能遍历这 1024 个 fd ,一个个测试。假如只有一个句柄准备好了,那相当于做了 1 千多倍的无效功。

epoll 则不同,从 epoll_wait 醒来的时候就能精确的拿到就绪的 fd 数组,不需要任何测试,拿到的就是要处理的。

3、epoll 原理


下面我们看一下 epoll 的使用和原理。

3.1 epoll 相关函数介绍


epoll 的使用非常简单,只有下面 3 个系统调用。

  • epoll_create:负责创建一个池子,一个监控和管理句柄 fd 的池子;
  • epoll_ctl:负责管理这个池子里的 fd 增、删、改;
  • epoll_wait:就是负责打盹的,让出 CPU 调度,但是只要有“事”,立马会从这里唤醒;

1)epoll_create
int epoll_create(int size);

功能:创建一个epoll句柄

参数:

        size:监听个数

返回值:epoll 句柄,如果为-1 的话表示创建失败
2)epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:负责管理epoll里的 fd 增、删、改;
参数:
  • epfd: 要操作的 epoll 句柄,也就是使用 epoll_create 函数创建的 epoll 句柄
  • op: 表示要对 epfd(epoll 句柄)进行的操作,可以设置为:

EPOLL_CTL_ADD

向epfd添加文件参数fd表示的描述符
EPOLL_CTL_MOD修改参数fd的event事件
EPOLL_CTL_DEL从epfd删除fd描述符
  • fd:要监视的文件描述符
  • event: 要监视的事件类型,为 epoll_event 结构体类型指针, epoll_event 结构体类型如下所示:
struct epoll_event {uint32_t events; /* epoll 事件 */epoll_data_t data; /* 用户数据 */
};

        结构体 epoll_event 的 events 成员变量表示要监视的事件,这些事件可以进行“或”操作,也就是说可以设置监视多个事件:

EPOLLIN

有数据可以读取

EPOLLOUT

可以写数据
EPOLLPRI

有紧急的数据需要读取

EPOLLERR

指定的文件描述符发生错

EPOLLHUP

指定的文件描述符挂起
EPOLLET

设置 epoll 为边沿触发,默认触发模式为水平触发

EPOLLONESHOT一次性的监视,当监视完成以后还需要再次监视某个 fd,那么就需要将fd 重新添加到 epoll 里面
返回值:0,成功; -1,失败,并且设置 errno 的值为相应的错误码
3)epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

功能:等待事件的发生

参数:
  • epfd: 要等待的 epoll
  • events: 指向 epoll_event 结构体的数组,当有事件发生的时候 Linux 内核会填写 events,调用者可以根据 events 判断发生了哪些事件
  • maxevents: events 数组大小,必须大于 0
  • timeout: 超时时间,单位为 ms
返回值:0,超时; -1,错误;其他值,准备就绪的文件描述符数量

    3.2 epoll 高效的原理


            Linux 下,epoll 的实现几乎没有做任何无效功,因此 epoll 作为高并发 IO 实现的秘密武器。 我们从使用的角度切入来一步步分析下。

    首先,epoll 的第一步是创建 epoll 池。这个使用 epoll_create 来做:

    示例:

    epollfd = epoll_create(1024);
    if (epollfd == -1) {perror("epoll_create");exit(EXIT_FAILURE);
    }

    这个池子对我们来说是黑盒,这个黑盒是用来装 fd 的,我们暂不纠结其中细节。我们拿到了一个 epollfd ,这个 epollfd 就能唯一代表这个 epoll 池。注意,这里又有一个细节:用户可以创建多个 epoll 池。

    然后,我们就要往这个 epoll 池里放 fd 了,这就要用到 epoll_ctl 了

    示例:

    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, 11, &ev) == -1) {perror("epoll_ctl: listen_sock");    exit(EXIT_FAILURE);
    }

    上面,我们就把句柄 11 放到这个池子里了,op(EPOLL_CTL_ADD)表明操作是增加、修改、删除,event 结构体可以指定监听事件类型,可读、可写。

    1)epoll 高效原因一

    添加 fd 进池子也就算了,如果是修改、删除呢?怎么做到快速?

    这里就涉及到你怎么管理 fd 的数据结构了。最常见的思路:用 list 链表,可以吗?功能上可以,但是性能上拉垮。list 的结构来管理元素,时间复杂度都太高 O(n),每次要一次次遍历链表才能找到位置。池子越大,性能会越慢。

    epoll怎么做到快速增、删、改的?

    红黑树。Linux 内核对于 epoll 池的内部实现就是用红黑树的结构体来管理这些注册进程来的句柄 fd。红黑树是一种平衡二叉树,时间复杂度为 O(log n),就算这个池子就算不断的增删改,也能保持非常稳定的查找性能。

    2)epoll 高效原因二

    怎么才能保证数据准备好之后,立马感知呢?

    epoll_ctl 这里会涉及到一点。秘密就是:回调的设置。在 epoll_ctl 的内部实现中,除了把句柄结构用红黑树管理,另一个核心步骤就是设置 poll 回调

    思考来了:poll 回调是什么?怎么设置?

    先说说 file_operations->poll 是什么?

    这个是定制监听事件的机制实现。通过 poll 机制让上层能直接告诉底层,我这个 fd 一旦读写就绪了,请底层硬件(比如网卡)回调的时候自动把这个 fd 相关的结构体放到指定队列中,并且唤醒操作系统。

    举个例子:网卡收发包其实走的异步流程,操作系统把数据丢到一个指定地点,网卡不断的从这个指定地点掏数据处理。请求响应通过中断回调来处理,中断一般拆分成两部分:硬中断和软中断。poll 函数就是把这个软中断回来的路上再加点料,只要读写事件触发的时候,就会立马通知到上层,采用这种事件通知的形式就能把浪费的时间窗就完全消失了。

    划重点:这个 poll 事件回调机制则是 epoll 池高效最核心原理。

    划重点:epoll 池管理的句柄只能是支持了 file_operations->poll 的文件 fd。换句话说,如果一个“文件”所在的文件系统没有实现 poll 接口,那么就用不了 epoll 机制。

    第二个问题:poll 怎么设置?

    在 epoll_ctl 下来的实现中,有一步是调用 vfs_poll 这个里面就会有个判断,如果 fd 所在的文件系统的 file_operations 实现了 poll ,那么就会直接调用,如果没有,那么就会报告响应的错误码。

    static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt){if (unlikely(!file->f_op->poll))return DEFAULT_POLLMASK;return file->f_op->poll(file, pt);
    }
    你肯定好奇 poll 调用里面究竟是实现了什么?
    总结概括来说:挂了个钩子,设置了唤醒的回调路径。epoll 跟底层对接的回调函数是: ep_poll_callback,这个函数其实很简单,做两件事情:
    1. 把事件就绪的 fd 对应的结构体放到一个特定的队列(就绪队列,ready list);
    2. 唤醒 epoll ,活来啦!
    当 fd 满足可读可写的时候就会经过层层回调,最终调用到这个回调函数,把 对应 fd 的结构体放入就绪队列中,从而把 epoll 从  epoll_wait 出唤醒。

    这个对应结构体是什么?

    结构体叫做 struct epitem ,每个注册到 epoll 池的 fd 都会对应一个 epitem

    • 当用户调用epoll_create()时,会创建eventpoll对象(包含一个红黑树和一个双链表);
    • 而用户调用epoll_ctl(ADD)时,会在红黑树上增加节点(epitem对象);

    就绪队列需要用很高级的数据结构吗?

    就绪队列就简单了,因为没有查找的需求了呀,只要是在就绪队列中的 epitem ,都是事件就绪的,必须处理的。所以就绪队列就是一个最简单的双指针链表

    小结下:epoll 之所以做到了高效,最关键的两点:
    1. 内部管理 fd 使用了高效的红黑树结构管理,做到了增删改之后性能的优化和平衡;
    2. epoll 池添加 fd 的时候,调用 file_operations->poll ,把这个 fd 就绪之后的回调路径安排好。通过事件通知的形式,做到最高效的运行;

          3.epoll 池核心的两个数据结构:红黑树和就绪列表。红黑树是为了应对用户的增删改需求,就绪列表是 fd 事件就绪之后放置的特殊地点,epoll 池只需要遍历这个就绪链表,就能给用户返回所有已经就绪的 fd 数组;

    3.3 epoll 的水平触发(Level Triggered, LT)和边缘触发(Edge Triggered, ET)


            epoll 的水平触发(Level Triggered, LT)和边缘触发(Edge Triggered, ET)是两种不同的事件通知机制,它们定义了 epoll 如何向应用程序报告文件描述符上的事件。理解这两种模式的差异对于使用 epoll 处理并发网络连接是很重要的。

    代码设置epoll触发模式:

    int epollfd = epoll_create1(0);
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET; // 边缘触发模式
    ev.data.fd = sockfd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
    
    1)水平触发(LT)

            在水平触发模式下,只要满足条件的事件仍然存在,epoll 就会重复通知这个事件。比如,如果一个文件描述符上有可读数据,那么只要没有读完,epoll_wait 就会不断报告该文件描述符是可读的。这种模式的特点是:

    • 容错性较好,不易丢失事件。
    • 更易于编程和理解。
    • 可以用于多线程程序中,多个线程可以共享同一个 epoll 文件描述符。

    2)边缘触发(ET)

            边缘触发模式下,事件只在状态变化时被通知一次,之后无论是否读完,也不会再次通知,直到状态再次发生变化才会再次触发。例如,只有当新数据到达使得文件描述符从非可读变为可读时,epoll_wait 才会报告可读事件。边缘触发模式的特点是:

    • 效率更高,因为它减少了事件的重复通知。
    • 需要更加小心地处理每次通知,确保处理所有的数据,否则可能会丢失未处理完的数据。
    • 更适合单线程或者每个线程使用独立 epoll 文件描述符的场景。

    若还不明白水平触发和边缘触发的差异,可以看下这篇文章的例子:

    https://zhuanlan.zhihu.com/p/719987328

    3.4 哪些 fd 可以用 epoll 来管理?


            由于并不是所有的 fd 对应的文件系统都实现了 poll 接口,所以自然并不是所有的 fd 都可以放进 epoll 池,那么有哪些文件系统的 file_operations 实现了 poll 接口?

    最常见的就是网络套接字:socket 。网络也是 epoll 池最常见的应用地点。Linux 下万物皆文件,socket 实现了一套 socket_file_operations 的逻辑( net/socket.c ):

    static const struct file_operations socket_file_ops = {.read_iter =    sock_read_iter,.write_iter =   sock_write_iter,    .poll =     sock_poll,    // ...
    };
    我们看到 socket 实现了 poll 调用,所以 socket fd 是天然可以放到 epoll 池管理的。

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/32342.html

    如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

    相关文章

    以 ArcGIS Pro 为笔,绘就水墨地图画卷

    一、引言 水墨画,作为中国传统绘画艺术的瑰宝,以其独特的韵味和表现力,在艺术领域占据着重要地位。它通过水与墨的交融,展现出山水之间的灵动与韵味。 而将这种艺术形式与现代地理信息系统(GIS)技术相结合…

    JAVA:利用 Jsoup 轻松解析和操作 HTML 的技术指南

    1、简述 在现代 Java 开发中,处理 HTML 数据是一项常见需求,无论是抓取网页数据、解析 HTML 文档,还是操作 DOM 树,Jsoup 都是一个强大的工具。它是一个基于 Java 的 HTML 解析库,支持从 URL、文件或字符串中解析 HTM…

    个人记录的一个插件,Unity-RuntimeMonitor

    没有什么干货,仅仅是个人的记录 基于GUI做的一个工具:好处就是Monitor必须,Unity天然支持实时的Monitor;唯一不好处,就是默认字体太小了,layout居中,居右也是要自行设计的。 (下面文字是有一点点写错,但意思和功能就很牛逼了;并不是都按2 x shift,而是一个 shift 添…

    云服务器安装宝塔面板部署

    单机部署(前端vue项目) 服务器安装宝塔面板 连接到服务器 使用 SSH 连接到你的服务器: ssh rootip安装宝塔面板 运行以下命令来安装宝塔面板: yum install -y wget wget -O install.sh http://download.bt.cn/install/install_6.0.sh sh install.sh安…

    Java数据结构第二十期:解构排序算法的艺术与科学(二)

    专栏:Java数据结构秘籍 个人主页:手握风云 目录 一、常见排序算法的实现 1.1. 直接选择排序 1.2. 堆排序 1.3. 冒泡排序 1.4. 快速排序 一、常见排序算法的实现 1.1. 直接选择排序 每⼀次从待排序的数据元素中选出最小的⼀个元素,存放在…

    【MapSet】哈希表

    目录 1. 搜索树 1.1 概念 1.2 操作-查找 1.3 操作-插入 1.4 操作-删除(难点) 1.5 性能分析 1.6 和java类集的关系 2. 搜索 2.1 概念及场景 2.2 模型 3. Map的使用 3.1 关于Map的说明 3.2 关于Map.Entry的说明 3.3 Map的常用方法说明 3.4 …

    手写一个Tomcat

    Tomcat 是一个广泛使用的开源 Java Servlet 容器,用于运行 Java Web 应用程序。虽然 Tomcat 本身功能强大且复杂,但通过手写一个简易版的 Tomcat,我们可以更好地理解其核心工作原理。本文将带你一步步实现一个简易版的 Tomcat,并深…

    git commit messege 模板设置 (规范化管理git)

    配置方法 git config --global core.editor vim (设置 Git 的默认编辑器为 Vim)在用户根目录下(~),创建一个.git_commit_msg文件,然后把下面的内容拷贝到文件中并保存。 [version][模块][类型]{解决xxx问题…

    亚信安全发布第七期《勒索家族和勒索事件监控报告》

    本周态势快速感知 本周全球共监测到勒索事件121起,与上周相比,勒索事件数量大幅下降,仍需注意防范。从整体上看Clop是影响最严重的勒索家族;本周Ransomhub和Akira也是活动频繁的两个恶意家族,需要注意防范。本周&…

    React基础之项目实战

    规范的项目结构 安装scss npm install sass -D 安装Ant Design组件库 内置了一些常用的组件 npm install antd --save 路由基础配置 npm i react-router-dom 路由基本入口 import Layout from "../page/Layout"; import Login from "../page/Login"; impor…

    第44天:WEB攻防-PHP应用SQL盲注布尔回显延时判断报错处理增删改查方式

    时间轴: 44天知识点总结: 1.mysql的增删改查功能 2.根据源码sql语句的三种sql注入:布尔盲注(必须要有回显) 延时判断(都可以) 报错回显(必须要有报错处理机制) 3.两个cms…

    【51单片机】程序实验15.DS18B20温度传感器

    主要参考学习资料:B站【普中官方】51单片机手把手教学视频 开发资料下载链接:http://www.prechin.cn/gongsixinwen/208.html 单片机套装:普中STC51单片机开发板A4标准版套餐7 目录 DS18B20介绍主要特性内部结构控制时序初始化时序写时序读时序…

    Vue3 深度解析:构建现代Web应用的全新范式

    Vue3 深度解析:构建现代Web应用的全新范式 mindmaproot(Vue3核心革新)性能优化Proxy响应式编译优化体积缩减Composition APIsetup语法逻辑复用TypeScript支持新特性TeleportSuspense片段支持工程化Vite集成自定义渲染器服务端渲染一、Vue3 架构革新:从O…

    推理模型对SQL理解能力的评测:DeepSeek r1、GPT-4o、Kimi k1.5和Claude 3.7 Sonnet

    引言 随着大型语言模型(LLMs)在技术领域的应用日益广泛,评估这些模型在特定技术任务上的能力变得越来越重要。本研究聚焦于四款领先的推理模型——DeepSeek r1、GPT-4o、Kimi k1.5和Claude 3.7 Sonnet在SQL理解与分析方面的能力,…

    cesium安装与配置(visual studio版)

    文章目录 一、下载Cesium二、解压Cesium三、VS打开网站四、参考文献 如有错误,请指正!!! 一、下载Cesium 登录官网,下载Cesium。 点击箭头所指,下载Cesium 二、解压Cesium 解压Cesium压缩包得到以下文件…

    Netty基础—3.基础网络协议二

    大纲 1.网络基础的相关问题总结 2.七层模型和四层模型 3.物理层(网线 光缆 01电信号) 4.数据链路层(以太网协议 网卡mac地址) 5.网络层(IP协议 子网划分 路由器) 6.传输层(TCP和UDP协议 Socket 端口) 7.应用层(HTTP协议 SMTP协议) 8.浏览器请求一个域名会发生什…

    Linux:Ubuntu server 24.02 上搭建 ollama + dify

    一、安装Ubuntu 具体的安装过程可以参见此链接:链接:Ubuntu Server 20.04详细安装教程,这里主要记录一下过程中遇到的问题。 安装时subnet如何填写 在Ubuntu中subnet填写255.255.255.0是错误的,其格式为 xx.xx.xx.xx/yy &#…

    算法练习——双指针算法(更新中)

    一、介绍双指针算法 双指针(或称为双索引)算法是一种高效的算法技巧,常用于处理数组或链表等线性数据结构。它通过使用两个指针来遍历数据,从而减少时间复杂度,避免使用嵌套循环。双指针算法在解决诸如查找、排序、去重…

    stm32week6

    stm32学习 三.通信 5.硬件读取I2C 硬件读取I2C的代码(main.c与软件读取相同): #include "stm32f10x.h" // Device header #include "MPU6050_Reg.h"#define MPU6050_ADDRESS 0xD0 //MPU6050的I2C从机地址/*** 函 数&…

    qt+opengl 播放yuv视频

    一、实现效果 二、pro文件 Qt widgets opengl 三、主要代码 #include "glwidget.h"GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent) {connect(&m_timer, &QTimer::timeout, this,[&](){this->update();});m_timer.start(1000/33); }v…