当前位置: 首页 > news >正文

【Linux | IO多路复用】epoll的底层原理详解

epoll 是一种高效的 I/O 多路复用机制,广泛用于 Linux 系统中,用于处理大量并发的文件描述符。它比传统的 selectpoll 方法具有更好的性能,特别是在处理大量并发连接时。

1.epoll的设计思路

epoll是在select 出现 N 多年后才被发明的,是select 和 poll(poll 和 select 基本一样,有少量改进)的增强版本。epoll通过以下一些措施来改进效率:

  1. 措施一:功能分离

  2. 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 多路复用机制&#xff0c;广泛用于 Linux 系统中&#xff0c;用于处理大量并发的文件描述符。它比传统的 select 和 poll 方法具有更好的性能&#xff0c;特别是在处理大量并发连接时。 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代理域名问题导致的&#xff0c;如下&#xff1a; 解决方案&#xff1a; proxy: { “/remoteDealerReportApi”: { target: ‘http://demo-.com.cn’, //此域名有问题&#xff0c;会导致…...

Muggle OCR 是一个高效的本地OCR(光学字符识别)模块

Muggle OCR 是一个高效的本地OCR&#xff08;光学字符识别&#xff09;模块&#xff0c;专为“麻瓜”设计&#xff0c;用于简化文本识别的过程。这个模块特别适用于处理印刷文本和解析验证码1。 以下是一些关于 Muggle OCR 的主要特点和使用方法&#xff1a; 特点&#xff1a;…...

【SpringBoot】万字源码解析——启动流程

Spring Boot启动流程 Spring Boot 的入口类&#xff1a; SpringBootApplication public class IntelGradingApplication {public static void main(String[] args) {SpringApplication.run(IntelGradingApplication.class, args);} }Spring Boot 的启动过程可以分为两方面&am…...

Nginx 配置初步 下

Nginx 配置初步(下) 一行代表一个指令&#xff1b; 每个指令有其上下文环境&#xff0c;比如 listen 指令只能在 http 指令块中出现&#xff0c;不能单独出现。1. Http 服务配置初步 1.1 常用指令 Nginx 的所有模块&#xff0c;打开模块我们就能看到模块中支持的指令。最常用…...

可视化ETL平台-Kettle的安装及简单使用

本章知识简介 主线A: 自连接查询; 主线B: 安装JDK与Kettle; 主线C: 使用Kettle工具. 本章目标&#xff1a; 1: 知道使用一张表可以实现自连接查询; [了解]注意: 左表、右表都是同一张表 2: 了解Kettle环境的安装流程; [了解]a.安装JDKb.安装Kettle 3: 熟悉使用kettle将txt数…...

java8 动态加载jar包至系统的classpath

1. io.test包 创建MyMain.java类&#xff0c;创建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、题目 若将一个正整数化为二进制数&#xff0c;在此二进制数中&#xff0c;我们将数字1的个数多于数字0的个数的这类二进制数称为好数。 例如&#xff1a; (13)10 (1101)2&#xff0c;其中1的个数为3&#xff0c;0的个数为1&#xff0c;则此数是好数&#xff1b; (10)10 (1…...

数字孪生城市:智慧城市的未来蓝图

在当今数字化时代&#xff0c;智能技术的广泛应用正在改变人们的生活和工作方式。数字孪生城市作为未来新型智慧城市演进的重要方向&#xff0c;数字孪生城市是一种将城市物理世界的各个方面转化为数字形式的技术&#xff0c;通过网络空间与物理世界之间的实时数据交换和仿真分…...

Java篇图书管理系统

目录 前言 一. 图书管理系统的核心 二. 图书管理系统基本框架 2.1 book包 2.1.1 Book&#xff08;书籍类&#xff09; 2.1.2 Booklist (书架类&#xff09; 2.2 user包 2.2.1 User类 2.2.2 Administrator(管理员类) 2.2.3 Visitor&#xff08;用户类&#xff09; 2.…...

BUUCTF之web篇

第一题 [极客大挑战 2019]EasySQL 打开靶机后可以看到这是一个登陆的页面 我们可以尝试两种方式登录 弱口令爆破&#xff08;burpsuite&#xff09; 通过SQL注入里的万能密码来跳过账户和密码验证的过程 这里就需要万能密码aor true # 在这里单引号的作用是结束用户名或者密码…...

010——二叉树(2)线索化

引入&#xff1a; 问题1&#xff1a; n个节点的二叉树&#xff0c;用二叉链表存储&#xff0c;问在这个二叉链表中一共有 __个指针域? 其中&#xff0c;有 __个指针域不为NULL&#xff0c;__个指针域为NULL? 答&#xff1a;2n n-1 n1 在二叉链表中&#xf…...

鸿蒙拍照小助手02

项目文件目录 为了确保项目文件目录清晰,以下是完整的项目文件目录结构: code 拍照小助手/ │ ├── entry/ │ ├── src/ │ │ ├── main/ │ │ │ ├── js/ │ │ │ │ └── 默认/ │ │ │ │ ├── 页面/ │ │ │ │ │ ├── 主页/ │ │ │ │ │ │ ├…...

lua while循环

软考鸭微信小程序 过软考,来软考鸭! 提供软考免费软考讲解视频、题库、软考试题、软考模考、软考查分、软考咨询等服务 Lua作为一种小巧精致的语言&#xff0c;特别适用于嵌入其他程序提供脚本支持。在编程中&#xff0c;循环结构是不可或缺的一部分&#xff0c;而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 类是文件和目录路径名的抽象表示&#xff0c;主要用于文件和目录的创建、查找和删除等操作。 1.2 构造方法 public File(String pathname) &#xff1a;通过…...

241023-RHEL非管理员安装Docker并开放指定宿主机端口部署Gitlab

A. RHEL非管理员安装Docker 要在没有管理员权限的情况下离线安装 Docker 和 Docker Compose&#xff0c;虽然受到一定限制&#xff0c;仍有一些可行的步骤可以帮助你在有限权限下完成这项任务。需要注意的是&#xff0c;这种方式适用于本地用户环境下的 Docker 安装&#xff0…...

python ubuntu安装加速

ubuntu升级python到python3.11&#xff08;可能是全网最靠谱的方法&#xff0c;亲测有效&#xff09;_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 上节主要讲完了多线程的一些基础知识&#xff0c;这节通过代码进一步理解多线程&#x1fae1; 多线程 Java标准库中提供了Thread类&#xff0c;以程序员们编写多线程代码&#xff0c;我们可以查看官方文档进一步了解Thread的特性以及提供的接口。 类似于Sy…...

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…...

Java 语言特性(面试系列1)

一、面向对象编程 1. 封装&#xff08;Encapsulation&#xff09; 定义&#xff1a;将数据&#xff08;属性&#xff09;和操作数据的方法绑定在一起&#xff0c;通过访问控制符&#xff08;private、protected、public&#xff09;隐藏内部实现细节。示例&#xff1a; public …...

DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径

目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...

日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする

日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...

SciencePlots——绘制论文中的图片

文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了&#xff1a;一行…...

【Linux】C语言执行shell指令

在C语言中执行Shell指令 在C语言中&#xff0c;有几种方法可以执行Shell指令&#xff1a; 1. 使用system()函数 这是最简单的方法&#xff0c;包含在stdlib.h头文件中&#xff1a; #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

1.3 VSCode安装与环境配置

进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件&#xff0c;然后打开终端&#xff0c;进入下载文件夹&#xff0c;键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...

多模态大语言模型arxiv论文略读(108)

CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题&#xff1a;CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者&#xff1a;Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…...

Docker 本地安装 mysql 数据库

Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker &#xff1b;并安装。 基础操作不再赘述。 打开 macOS 终端&#xff0c;开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...