【Linux | IO多路复用】epoll的底层原理详解
epoll 是一种高效的 I/O 多路复用机制,广泛用于 Linux 系统中,用于处理大量并发的文件描述符。它比传统的 select 和 poll 方法具有更好的性能,特别是在处理大量并发连接时。
1.epoll的设计思路
epoll是在select 出现 N 多年后才被发明的,是select 和 poll(poll 和 select 基本一样,有少量改进)的增强版本。epoll通过以下一些措施来改进效率:
-
措施一:功能分离
-
select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。

如上图所示,每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket相对固定,并不需要每次都修改。
epoll将这两个操作分开,先用epoll_ctl 维护等待队列,再调用epoll_wait 阻塞进程。显而易见,效率就能得到提升。
为方便理解后续的内容,我们先了解一下epoll的用法。如下的代码中,先用epoll_create 创建一个epoll对象 epfd,再通过epoll_ctl 将需要监视的socket添加到 epfd 中,最后调用epoll_wait 等待数据:
int s =socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...) int epfd =epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中 while(1){ int n =epoll_wait(...) for(接收到数据的socket){ //处理 }
}
功能分离,使得epoll有了优化的可能。
措施二:就绪列表
select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的socket,就能避免遍历。

如上图所示,计算机共有三个socket,收到数据的sock2和sock3 被就绪列表rdlist 所引用。当进程被唤醒后,只要获取rdlist 的内容,就能够知道哪些socket收到数据。
2.epoll底层使用的数据结构
2.1索引的数据结构
既然epoll将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的socket,至少要方便地添加和移除,还要便于搜索,以避免重复添加。
epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是0(logn)。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构,所以select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。

2.2就绪列表的数据结构
就绪列表引用着就绪的socket,所以它应能够快速的插入数据。程序可能随时调用epoll_ctl 添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被移除。所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,epoll使用双向链表来实现就绪队列(对应上图的rdlist)。
第二点, epoll使用事件驱动的机制,内核里维护了一个双向链表来记录就绪事件,当某个socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait()函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个socket 集合,大大提高了检测的效率。

epoll 的方式即使监听的 Socket 数量越多的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常的多了,上限就为系统定义的进程打开的最大文件描述符个数。因而,epoll 被称为解决 C10K 问题的利器。插个题外话,网上文章不少说, epoll_wait 返回时,对于就绪的事件,epoll 使用的是共享内存的方式,即用户态和内核态都指向了就绪链表,所以就避免了内存拷贝消耗。
这是错的!看过 epoll 内核源码的都知道,压根就没有使用共享内存这个玩意。你可以从下面这份代码看到,epoll_wait 实现的内核代码中调用了put_user 函数,这个函数就是将数据从内核拷贝到用户空间。
3.epoll的工作流程
3.1.创建epoll对象
如下图所示,当某个进程调用epoll_create 方法时,内核会创建一个 eventpoll 对象(也就是程序中 epfd 所代表的对象)。

eventpoll 对象也是文件系统中的一员,和socket一样,它也会有等待队列。创建一个代表该epoll的 eventpoll 对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为 eventpoll 的成员。
3.2.维护监视列表
创建epoll对象后,可以用epoll_ctl 添加或删除所要监听的socket。以添加socket为例。

如上图,如果通过epoll_ctl 添加sock1、sock2 和sock3 的监视,内核会将 eventpoll 添加到这三个socket的等待队列中。当socket收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程。
3.3.接收数据
当socket收到数据后,中断程序会给 eventpoll 的“就绪列表”添加socket引用。

如上图展示的是sock2 和sock3 收到数据后,中断程序让rdlist 引用这两个socket。
eventpoll 对象相当于socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。
当程序执行到epoll_wait 时,如果rdlist 已经引用了socket,那么epoll_wait 直接返回,如果 rdlist 为空,阻塞进程。
3.4.阻塞和唤醒进程
假设计算机中正在运行进程 A 和进程 B,在某时刻进程 A 运行到了epoll_wait 语句。

如上图所示,内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。
当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态(如下图)。

也因为rdlist 的存在,进程 A 可以知道哪些socket发生了变化。
4.实例代码
下面是一个使用 epoll 的示例代码,演示了如何创建 epoll 实例、注册文件描述符、等待事件和处理事件。此示例是一个简单的 TCP 服务器,能够接受客户端连接并处理数据。
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>// 设置文件描述符为非阻塞
void set_nonblocking(int fd) {int flags = fcntl(fd, F_GETFL, 0);if (flags == -1) {perror("fcntl F_GETFL 错误");exit(1);}if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {perror("fcntl F_SETFL 错误");exit(1);}
}// 服务器主函数
int main(int argc, const char* argv[])
{// 创建监听套接字int lfd = socket(AF_INET, SOCK_STREAM, 0);if(lfd == -1){perror("socket 错误");exit(1);}// 设置监听套接字为非阻塞set_nonblocking(lfd);// 绑定服务器地址和端口struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(9999); // 监听端口9999serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网络接口的IP地址// 设置端口复用int opt = 1;setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 将套接字绑定到指定地址int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));if(ret == -1){perror("绑定错误");exit(1);}// 开始监听连接请求ret = listen(lfd, 64);if(ret == -1){perror("监听错误");exit(1);}// 创建一个 epoll 实例int epfd = epoll_create(100);if(epfd == -1){perror("epoll_create 错误");exit(1);}// 将监听套接字 lfd 加入 epoll 实例,监听读事件,使用ET模式struct epoll_event ev;ev.events = EPOLLIN | EPOLLET; // 监听读事件,ET模式ev.data.fd = lfd; // 数据是监听套接字 lfdret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);if(ret == -1){perror("epoll_ctl 错误");exit(1);}// 用于存放触发事件的数组struct epoll_event evs[1024];int size = sizeof(evs) / sizeof(struct epoll_event);// 进入事件处理循环while(1){// 等待事件触发int num = epoll_wait(epfd, evs, size, -1);if(num == -1){perror("epoll_wait 错误");exit(1);}// 处理所有触发的事件for(int i = 0; i < num; ++i){int curfd = evs[i].data.fd; // 获取当前事件对应的文件描述符// 如果是监听套接字 lfd 有事件发生,表示有新连接if(curfd == lfd){// 接受所有新连接while (1) {int cfd = accept(lfd, NULL, NULL);if(cfd == -1){if (errno == EAGAIN || errno == EWOULDBLOCK) {// 所有连接都已处理break;} else {perror("accept 错误");continue;}}// 设置新连接为非阻塞set_nonblocking(cfd);// 将新连接 cfd 添加到 epoll 实例中监听其读事件,使用ET模式ev.events = EPOLLIN | EPOLLET;ev.data.fd = cfd;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);if(ret == -1){perror("epoll_ctl-accept 错误");exit(1);}printf("新连接 %d 加入\n", cfd);}}else{// 处理已连接套接字的数据收发char buf[1024];int len;// 使用循环确保将缓冲区中所有数据读取完毕while ((len = recv(curfd, buf, sizeof(buf), 0)) > 0) {printf("客户端 %d 说: %s", curfd, buf);send(curfd, buf, len, 0);memset(buf, 0, sizeof(buf));}if(len == -1 && (errno != EAGAIN && errno != EWOULDBLOCK)){perror("recv 错误");// 出错时关闭连接,并从 epoll 实例中删除epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);close(curfd);}else if(len == 0){// 客户端断开连接printf("客户端 %d 已断开连接\n", curfd);epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);close(curfd);}}}}close(lfd);return 0;
}
相关文章:
【Linux | IO多路复用】epoll的底层原理详解
epoll 是一种高效的 I/O 多路复用机制,广泛用于 Linux 系统中,用于处理大量并发的文件描述符。它比传统的 select 和 poll 方法具有更好的性能,特别是在处理大量并发连接时。 1.epoll的设计思路 epoll是在select 出现 N 多年后才被发明的&a…...
npm run serve 提示异常Cannot read property ‘upgrade‘ of undefined
npm run serve 提示Cannot read property ‘upgrade’ of undefined 一般是proxy的target代理域名问题导致的,如下: 解决方案: proxy: { “/remoteDealerReportApi”: { target: ‘http://demo-.com.cn’, //此域名有问题,会导致…...
Muggle OCR 是一个高效的本地OCR(光学字符识别)模块
Muggle OCR 是一个高效的本地OCR(光学字符识别)模块,专为“麻瓜”设计,用于简化文本识别的过程。这个模块特别适用于处理印刷文本和解析验证码1。 以下是一些关于 Muggle OCR 的主要特点和使用方法: 特点:…...
【SpringBoot】万字源码解析——启动流程
Spring Boot启动流程 Spring Boot 的入口类: SpringBootApplication public class IntelGradingApplication {public static void main(String[] args) {SpringApplication.run(IntelGradingApplication.class, args);} }Spring Boot 的启动过程可以分为两方面&am…...
Nginx 配置初步 下
Nginx 配置初步(下) 一行代表一个指令; 每个指令有其上下文环境,比如 listen 指令只能在 http 指令块中出现,不能单独出现。1. Http 服务配置初步 1.1 常用指令 Nginx 的所有模块,打开模块我们就能看到模块中支持的指令。最常用…...
可视化ETL平台-Kettle的安装及简单使用
本章知识简介 主线A: 自连接查询; 主线B: 安装JDK与Kettle; 主线C: 使用Kettle工具. 本章目标: 1: 知道使用一张表可以实现自连接查询; [了解]注意: 左表、右表都是同一张表 2: 了解Kettle环境的安装流程; [了解]a.安装JDKb.安装Kettle 3: 熟悉使用kettle将txt数…...
java8 动态加载jar包至系统的classpath
1. io.test包 创建MyMain.java类,创建addJarToClasspath方法将jar包动态加载进系统的classpath中 package io.test;import java.io.File; import java.net.URL; import java.net.URLClassLoader; import java.lang.reflect.Method;public class MyMain {public st…...
C++二级题 计算好数:1数大于0数(二进制的位运算)
1、题目 若将一个正整数化为二进制数,在此二进制数中,我们将数字1的个数多于数字0的个数的这类二进制数称为好数。 例如: (13)10 (1101)2,其中1的个数为3,0的个数为1,则此数是好数; (10)10 (1…...
数字孪生城市:智慧城市的未来蓝图
在当今数字化时代,智能技术的广泛应用正在改变人们的生活和工作方式。数字孪生城市作为未来新型智慧城市演进的重要方向,数字孪生城市是一种将城市物理世界的各个方面转化为数字形式的技术,通过网络空间与物理世界之间的实时数据交换和仿真分…...
Java篇图书管理系统
目录 前言 一. 图书管理系统的核心 二. 图书管理系统基本框架 2.1 book包 2.1.1 Book(书籍类) 2.1.2 Booklist (书架类) 2.2 user包 2.2.1 User类 2.2.2 Administrator(管理员类) 2.2.3 Visitor(用户类) 2.…...
BUUCTF之web篇
第一题 [极客大挑战 2019]EasySQL 打开靶机后可以看到这是一个登陆的页面 我们可以尝试两种方式登录 弱口令爆破(burpsuite) 通过SQL注入里的万能密码来跳过账户和密码验证的过程 这里就需要万能密码aor true # 在这里单引号的作用是结束用户名或者密码…...
010——二叉树(2)线索化
引入: 问题1: n个节点的二叉树,用二叉链表存储,问在这个二叉链表中一共有 __个指针域? 其中,有 __个指针域不为NULL,__个指针域为NULL? 答:2n n-1 n1 在二叉链表中…...
鸿蒙拍照小助手02
项目文件目录 为了确保项目文件目录清晰,以下是完整的项目文件目录结构: code 拍照小助手/ │ ├── entry/ │ ├── src/ │ │ ├── main/ │ │ │ ├── js/ │ │ │ │ └── 默认/ │ │ │ │ ├── 页面/ │ │ │ │ │ ├── 主页/ │ │ │ │ │ │ ├…...
lua while循环
软考鸭微信小程序 过软考,来软考鸭! 提供软考免费软考讲解视频、题库、软考试题、软考模考、软考查分、软考咨询等服务 Lua作为一种小巧精致的语言,特别适用于嵌入其他程序提供脚本支持。在编程中,循环结构是不可或缺的一部分,而while循环则是…...
JAVA篇之类和对象
目录 一. 面向对象 1.1 面向对象和面向过程 二. 类的定义和使用 2.1 什么是类 2.2 类的定义格式 三. 类的实例化 四. this引用 4.1 this引用的作用 五. 构造方法 5.1 构造方法重载 5.2 通过this调用其他构造方法 5.3 默认初始化 结语 一. 面向对象 Java 是一门面向对…...
IO流详解_CoderLix
主要内容 File类IO流字节流字符流异常处理Properties缓冲流转换流序列化流打印流 File类 1.1 概述 java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。 1.2 构造方法 public File(String pathname) :通过…...
241023-RHEL非管理员安装Docker并开放指定宿主机端口部署Gitlab
A. RHEL非管理员安装Docker 要在没有管理员权限的情况下离线安装 Docker 和 Docker Compose,虽然受到一定限制,仍有一些可行的步骤可以帮助你在有限权限下完成这项任务。需要注意的是,这种方式适用于本地用户环境下的 Docker 安装࿰…...
python ubuntu安装加速
ubuntu升级python到python3.11(可能是全网最靠谱的方法,亲测有效)_ubuntu python3.11-CSDN博客 python-release安装包下载_开源镜像站-阿里云...
100种算法【Python版】第12篇——快速幂算法
本文目录 1 基本原理2 基本步骤3 数学示例4 python代码1 基本原理 快速幂算法(Fast Exponentiation)是一种高效计算整数幂的方法,尤其适用于计算大数的幂。其主要思想是利用分治法和二进制表示来减少乘法运算的次数,从而加快计算速度。 计算 x n x^n x...
Java多线程详解②(全程干货!!!)Thread Runnable
这里是Themberfue 上节主要讲完了多线程的一些基础知识,这节通过代码进一步理解多线程🫡 多线程 Java标准库中提供了Thread类,以程序员们编写多线程代码,我们可以查看官方文档进一步了解Thread的特性以及提供的接口。 类似于Sy…...
手把手调试UDS:用CANoe/CANalyzer实战$22、$2E、$27和$31服务
手把手调试UDS:用CANoe/CANalyzer实战$22、$2E、$27和$31服务 在汽车电子控制单元(ECU)的开发与维护中,UDS(Unified Diagnostic Services)协议扮演着至关重要的角色。对于从事ECU刷写、参数标定和故障诊断的…...
告别画面撕裂!用DRM的drmModePageFlip和drmHandleEvent实现流畅翻页(附Linux应用层完整代码)
彻底解决Linux图形显示撕裂:DRM事件驱动编程实战指南 当你在嵌入式Linux设备上开发图形界面时,是否遇到过画面撕裂的困扰?那种图像上半部分显示新帧而下半部分还停留在旧帧的视觉割裂感,不仅影响用户体验,更暴露了底层…...
1-5 线程池:Thread+阻塞队列+循环
今天我们要带大家探究Java线程池的运行机制,但在这之前,需要先复习一下线程。 Thread的设计理念 假设现在是1990年代,而你是Sun公司的核心开发,是Java这门语言的设计者之一。在这次发版时,你们希望Java能够支持多线程…...
开源AI对话聚合器GPTFree:聚合免费API,搭建私有AI助手
1. 项目概述:一个开源AI对话聚合器的诞生 最近在GitHub上闲逛,发现了一个挺有意思的项目,叫“GPTFree”。光看名字,你可能会以为这是个“免费使用GPT”的噱头,但点进去仔细研究后,我发现它的内核远比名字要…...
解锁纯净动漫世界:Hanime1Plugin如何让你的Android观影体验焕然一新
解锁纯净动漫世界:Hanime1Plugin如何让你的Android观影体验焕然一新 【免费下载链接】Hanime1Plugin Android插件(https://hanime1.me) (NSFW) 项目地址: https://gitcode.com/gh_mirrors/ha/Hanime1Plugin 你是否厌倦了观看动漫时被各种广告和弹窗打扰&…...
如何用MaxBot抢票机器人轻松买到演唱会门票:2025年完整使用指南
如何用MaxBot抢票机器人轻松买到演唱会门票:2025年完整使用指南 【免费下载链接】tix_bot Max搶票機器人(maxbot) help you quickly buy your tickets 项目地址: https://gitcode.com/gh_mirrors/ti/tix_bot 还在为抢不到热门演唱会门票而苦恼吗?…...
球面水蛭量化技术:高效处理球形视觉数据的创新方法
1. 项目概述:当视觉数据遇上球面量化在计算机视觉领域,我们常常需要处理海量的高维数据。传统量化方法在处理球形分布数据时,往往会遇到信息损失严重、计算效率低下的问题。Spherical Leech Quantization(球面水蛭量化)…...
STM32 HAL库驱动28BYJ-48步进电机:从CubeMX配置到精准角度控制的避坑指南
STM32 HAL库驱动28BYJ-48步进电机:从CubeMX配置到精准角度控制的避坑指南 在小型自动化设备开发中,28BYJ-48步进电机因其低成本、易驱动的特点成为许多项目的首选。但当需要实现精确角度控制时——比如将电机用于云台稳定系统、3D打印机进料机构或替代模…...
AI辅助编程的边界——Cursor实战与工程判断力
前言 在蚂蚁集团AI Coding笔试中,我用Cursor在1小时内完成了一个大模型推理网关的完整实现。坦白说,如果没有AI辅助,这个速度我做不到。但面试官可能会追问一句:“既然AI这么厉害,那程序员的价值在哪?” 这…...
SHAMISA:自监督无参考图像质量评估方法解析
1. 项目概述SHAMISA是一种创新的自监督无参考图像质量评估方法,它突破了传统质量评估对参考图像的依赖。我在计算机视觉领域工作多年,发现现有NR-IQA方法往往需要大量标注数据或复杂的特征工程。而SHAMISA通过自监督学习框架,仅需单张图像就能…...
