多路转接之epoll
本篇博客介绍: 多路转接之epoll
多路转接之epoll
- 初识epoll
- epoll相关系统调用
- epoll的工作原理
- epoll服务器编写
- 成员变量
- 构造函数
- 循环函数
- HandlerEvent函数
- epoll的优缺点
我们学习epoll分为四部分
- 快速理解部分概念 快速的看一下部分接口
- 讲解epoll的工作原理
- 手写epoll服务器
- 工作模式
并且在这四个部分的内容学习完毕之后我们学习一下Reactor模式
初识epoll
按照man手册的说法
epoll是为了处理大量句柄而做出改进的poll
它在2.5.44内核中被引入到Linux
也是目前来说最常用的一种多路转接IO方式
epoll相关系统调用
epoll函数有三个相关的系统调用 分别是
- epoll_create
- epoll_ctl
- epoll_wait
epoll_create函数
epoll_create函数的作用是创建一个epoll模型 函数原型如下
int epoll_create(int size);
参数说明:
- 目前来说 epoll_create的参数是被废弃的 我们设置为256或者512就行 这样设计的原因是为了向前兼容
返回值说明:
- 返回一个epoll模型 (实际上就是一个文件描述符)
epoll_ctl函数
epoll_ctl函数的作用是对创建出来的epoll模型进行操控 函数原型如下
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
- int epfd 标识一个我们的IO模型
- int op operator 表示我们想要做出什么样的操作
- int fd 表示我们需要添加的文件描述符
- epoll_event *event 表示我们需要关心哪些事件
返回值说明:
- 函数成功调用返回0 失败返回-1 同时错误码将被设置
epoll_wait函数
epoll_wait函数的作用是监视我们关心的关键描述符 函数原型如下
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数说明:
- int epfd 标识我们的epoll模型
- struct epoll_event *events 输出型参数 内核会拷贝已经就绪的事件到这里面
- int maxevents events数组的元素个数
- int timeout 和poll函数中的timeout一样 等待事件 单位是毫秒
epoll的工作原理
我们之前的学习的多路转接函数 无论是select还有poll 它们都需要我们做下面的操作
- 让我们维护一个第三方的数组
- 都需要遍历整个数组
- 都需要经历用户到内核 内核到用户的事件通知
而我们的epoll工作模式则不同
操作系统硬件上的工作模式如下
这是一个缩略版的操作系统图
那么现在问题来了 操作系统是如何知道硬件里面有数据了呢?
(这个硬件可以是网卡 可以是键盘等等)
具体解释如下图
而epoll的工作原理如下
还是该图
当我们创建一个epoll模型之后操作系统底层会帮助我们维护一颗红黑树
红黑树的节点里面维护着很多元素 其中最重要的是两个
- 文件描述符
- 事件
所以说这颗红黑树解决的是用户通知内核的问题
用户通知内核自己要关心哪些文件描述符的哪些事件之后 操作系统就会生成一个节点然后插入到这颗红黑树当中
而这颗红黑树就是对应我们select和poll当中的数组
只不过此时它就由操作系统进行维护了
而我们内核通知用户的则是通过消息队列通知
我们可以这么理解 在内核维护的红黑树旁边有一个消息队列
每当有fd的事件就绪的时候就会在该队列上添加一个元素
于是我们用户读取的时候时间复杂度变为了O(1)
操作系统什么时候构建就绪队列节点呢?
操作系统在调用驱动的时候构建就绪队列节点
在生成红黑树节点的时候 在驱动中 每个节点都会生成一个自己的回调函数
于是在经历了硬件中断到读取数据的过程后 操作系统会调用驱动中的回调函数来获取该节点的数据 并且根据这些数据(fd和events)构建就绪节点 最后将构建好的节点插入到队列中
我们将上面的一整套机制称为epoll模型
那么我们现在再来回顾下epoll的三个函数
- epoll_create
- epoll_ctl
- epoll_wait
它们的作用分别是
- epoll_create : 创建epoll模型 包括红黑树 就绪队列 回调函数等
- epoll_ctl : 对于红黑树的节点进行注册
- epoll_wait : 获取就绪队列中的内容
为什么epoll_create返回一个文件描述符 而epoll_ctl和epoll_wait需要用到这个文件描述符呢?
这个问题最本质的原因是因为文件描述符表是随进程的 具体理解我们可以看下图
我们都知道每个进程都对应一个PCB结构 而每个PCB结构中都会有一个file struct结构体 这个结构体中有一个文件数组 每个下标对应一个文件描述符
而epoll_create的本质就是打开了一个文件 所以被分配了一个文件描述符
在这个文件中有个void* p指针 可以找到我们上面说的那些红黑树 就绪队列等等
这里还有一些关于epoll服务器的一些小细节
epoll底层维护的红黑树key值是什么呢?
是fd文件描述符 它是一个绝佳的天然key值 既不会重复 又容易排序
用户需要关系os对于fd和event的管理吗
不需要 os会在底层完成这些事
epoll为什么高效呢
- 因为epoll底层维护的是红黑树结构 对比数组来说增删改查有着天然的优势
- 我们不需要主动去询问哪些文件是否就绪 os会自动将其添加到就绪队列中
- 在寻找就绪文件的时候 由于我们使用的是就绪队列 时间复杂度是O(1) 而遍历数组的时间复杂度则是O(N)
epoll有线程安全问题嘛?
没有
实际上就绪队列是一个经典的生产者消费者模型 os生成数据 而用户消费数据 所以说这个队列实际上是一个临界资源 所以说操作系统在底层对其做了一些加锁处理 让他变为线程安全的
如果底层没有就绪事件 我们上层应该怎么办呢?
根据timeout参数来决定
- 如果timeout为0 则是非阻塞
- 如果timeout为-1 则是阻塞
- 如果timeout大于0 则表示我们要等待多少毫秒之后去读取
epoll服务器编写
接下来我们开始设计一个epoll服务器
成员变量
首先作为一个基于TCP协议的服务器 我们必须要有listen套接字和端口号
int _listensock; uint16_t _port;
其次作为一个epoll服务器 我们还必须要有一个epfd作为句柄来标识一个epoll模型
int _epfd;
此外我们还需要设置一个数组来接收epoll_wait的数据
struct epoll_event* _revs; int _revs_num;
构造函数
ep_server(const int& port = default_port) :_port(port) { // 1. create listensock _listensock = Sock::Socket(); Sock::Bind(_listensock , _port); Sock::Listen(_listensock); // 2. create epoll_epfd = epoll::createepoll(); logMessage(DEBUG , "create epoll_server success, epfd: %d , listensock: %d " ,_epfd , _listensock); // 3. append listen socket to epollif(epoll::epollctl(_epfd , EPOLL_CTL_ADD , _listensock , EPOLLIN)) {logMessage(DEBUG , "epollctl add success %d");} else {exit(6);} }
我们这里不直接使用epoll的原生函数来进行操作 而是进行一下封装
封装后的epoll类如下
class epoll
{ public: static const int gsize = 256; public: static int createepoll() { int epfd = epoll_create(gsize); if (epfd > 0) { return epfd; } else {// errexit(5);}} static bool epollctl(int epfd , int oper , int sock , uint32_t events) { struct epoll_event ev; ev.data.fd = sock; ev.events = events; int ret = epoll_ctl(epfd , oper , sock , &ev); return ret == 0; }static int epollwait(int epfd , struct epoll_event res[] , int num , int timeout) { return epoll_wait(epfd , res , num , timeout); }
};
循环函数
我们服务器肯定不是只accept一次就完事了 所以说我们需要设计一个循环函数来重复执行accept的动作
我们分析下 首先我们每次循环肯定是要检测一次epoll就绪队列中有没有数据的 如果有的话 我们就直接从这个里面拿数据 并且把这个数据拿出来
特别注意 如果是listen套接字中的数据 我们还需要往 struct_events
中添加数据
每次循环的大概代码如下
int n = epoll_wait(_epfd, _revs, _num, timeout); switch (n) { case 0: logMessage(NORMAL, "timeout ..."); break; case -1: logMessage(WARNING, "epoll_wait failed, code: %d, errstring: %s", errno, strerror(errno)); break; default: logMessage(NORMAL, "have event ready"); //HandlerEvent(n); break;
我们将处理函数重新封装
HandlerEvent函数
在每次循环的时候我们成功使用epoll_wait拿到了就绪队列里的数据之后会走到这里
这里我们要进行判断 到底是listensock就绪了还是普通sock套接字就绪了
如果是listensock套接字就绪就代表我们要接收一个新的请求 如果是普通sock就绪就代表我们可以读取请求了
void HandlerEvent(int readyNum) { logMessage(DEBUG, "HandlerEvent in"); for (int i = 0; i < readyNum; i++) { uint32_t events = _revs[i].events; int sock = _revs[i].data.fd; if (sock == _listensock && (events & EPOLLIN)) { //_listensock读事件就绪, 获取新连接 std::string clientip; uint16_t clientport; int fd = Sock::Accept(sock, &clientip, &clientport); if (fd < 0) { logMessage(WARNING, "accept error"); continue; } // 获取fd成功,可以直接读取吗??不可以,放入epoll struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = fd; epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev); } else if (events & EPOLLIN) { // 普通的读事件就绪 // 依旧有问题 char buffer[1024];// 把本轮数据读完,就一定能够读到一个完整的请求吗??int n = recv(sock, buffer, sizeof(buffer), 0);if (n > 0){buffer[n] = 0;logMessage(DEBUG, "client# %s", buffer);// TODOstd::string response = func_(buffer);send(sock, response.c_str(), response.size(), 0);}else if (n == 0){// 建议先从epoll移除,才close fdepoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);close(sock);logMessage(NORMAL, "client quit");} else{// 建议先从epoll移除,才close fdepoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);close(sock);logMessage(ERROR, "recv error, code: %d, errstring: %s", errno, strerror(errno));}}else{}}logMessage(DEBUG, "HandlerEvent out");}
其实到这里 我们简单的epoll服务器就做完了
我们接下来还要学习下epoll服务器的工作模式
epoll的优缺点
相关文章:

多路转接之epoll
本篇博客介绍: 多路转接之epoll 多路转接之epoll 初识epollepoll相关系统调用epoll的工作原理epoll服务器编写成员变量构造函数 循环函数HandlerEvent函数epoll的优缺点 我们学习epoll分为四部分 快速理解部分概念 快速的看一下部分接口讲解epoll的工作原理手写epo…...

删除排序链表中的重复节点II(C++解法)
题目 给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。 示例 1: 输入:head [1,2,3,3,4,4,5] 输出:[1,2,5]示例 2: 输入:head [1…...

uniapp自定义tab切换css样式、uni-forms中input下拉等标签字体、过宽、溢出样式一系列调整(附加实战举例)
一、uniapp自定义tab切换css样式 <view class="tabs-container"><view class="tabs-list">...

windows server 2016-IIS静态服务器-设置详细过程
文章目录 1.打开仪表盘新建角色2.iis功能模块3.启动服务器4.优点 1.打开仪表盘新建角色 2.iis功能模块 能选上的尽量选上,除非知道自己用不上。 然后确认,下一步,安装。 3.启动服务器 搜索IIS,启动IIS管理器。 启动网站。 右…...

不一样的编程方式 —— 协程(设计原理与汇编实现)
主要通过以下9个方面来了解协程的原理: 目录 1、为什么使用协程 1.3、协程的适用场景 2、协程的原语操作 3、协程的切换 3.1、汇编实现 4.协程的运行流程 5.协程的结构体定义(我们其实可以参照线程或者进程的状态来设计) 5.1、多状态集合设计 6.协程的调度…...

Thinkphp6项目在虚拟机无法指向pulic的目录访问的方法
以阿里云虚拟主机为例,服务器环境为 LAMP,Apache2.4 php7.2 mysql5.7 1.根目录新建 index.php 文件,将以下内容放入文件中 <?php include ./public/index.php;2.将 public 目录下的 admin.php、backend 文件夹、static 文件夹、tinymc…...

数据结构(超详细讲解!!)第十八节 串(堆串)
1.定义 假设以一维数组heap [MAXSIZE] 表示可供字符串进行动态分配的存储空间,并设 int start 指向heap 中未分配区域的开始地址(初始化时start 0) 。在程序执行过程中,当生成一个新串时,就从start指示的位置起&#…...

idea集成测试插件替代postman
idea集成测试插件替代postman 兄弟萌,你再测试接口是否无bug是否流畅的时候是否还在使用“postman”来回切换进行测试呢? 页面切换进行测试,有没有感觉很麻烦呢? 打开postman,输入接口地址,有没有感觉很麻烦…...

clusterprolifer go kegg msigdbr 富集分析应该使用哪个数据集,GO?KEGG?Hallmark?
关注微信:生信小博士 5 Overview of enrichment analysis Chapter 5 Overview of enrichment analysis | Biomedical Knowledge Mining using GOSemSim and clusterProfiler 5.1.2 Gene Ontology (GO) Gene Ontology defines concepts/classes used to describ…...
Linux学习笔记1-入门
前言:之前的基于单片机的闭环控制步进电机项目其实已经完成了,但很多时间都花在调试和生产上,实在没时间去做总结笔记,现在又开始做新项目了,从单片机到了Linux,想用这个平台来督促自己继续学习,…...
怎样更有效的运营Etsy店铺?
大家都知道,Etsy作为一个重要的电商平台,给很多人提供了不少机会。但是如何取得etsy店铺运营的成功呢?第一步就是选好辅助工具。 什么是指纹浏览器? VMLogin指纹浏览器(www.vmlogin.com.cn) 是一种工具,通过伪装用户…...

Vue 项目中如何使用Bootstrap5(简单易懂)
Vue 项目中如何使用Bootstrap5(简单易懂) 安装在 src/main.js 文件下引入包在vue文件中使用 Bootstrap官网(中文):https://www.bootcss.com/ Bootstrap5文档:https://v5.bootcss.com/docs/getting-started/…...

k8s 资源预留
KUBERNETES资源管理之–资源预留 Kubernetes 的节点可以按照 Capacity 调度。node节点本身除了运行不少驱动 OS 和 Kubernetes 的系统守护进程,默认情况下 pod 能够使用节点全部可用容量, 除非为这些系统守护进程留出资源,否则它们将与 pod 争…...

微信小程序自定义弹窗阻止滑动冒泡catchtouchmove之后弹窗内部内容无法滑动
自定义弹窗 如图所示: 自定义弹窗内部有带滚动条的盒子区域 问题: 在盒子上滑动,页面如果超出一屏的话,也会跟着一起上下滚动 解决方案:给自定义弹窗 添加 catchtouchmove 事件,阻止冒泡即可 网上不少…...

Linux 命令速查
Network ping ping -c 3 -i 0.01 127.0.0.1 # -c 指定次数 # -i 指定时间间隔 日志 一般存放位置: /var/log,包含:系统连接日志 进程统计 错误日志 常见日志文件说明 日志功能access-logweb服务访问日志acct/pacct用户命令btmp记录失…...

第22期 | GPTSecurity周报
GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区,集成了生成预训练 Transformer(GPT)、人工智能生成内容(AIGC)以及大型语言模型(LLM)等安全领域应用的知识。在这里,您可以…...
JavaScript前端 console 控制台详细解析与代码实例
JavaScript Console(控制台)是一个重要的工具,可以用于调试和测试 JavaScript 代码。在浏览器中,你可以使用控制台来查看 JavaScript 输出、测试代码、调试错误等。在本文中,我们将详细介绍控制台的常用功能和代码实例…...

idea中启动多例项目配置
多实例启动 日常本地开发微服务项目时,博主想要验证一下网关的负载均衡以及感知服务上下线能力时,需要用到多实例启动。 那么什么是多实例启动嘞?简单说就是能在本地同时启动多个同一服务。打个比方项目中有一个 MobileApplication 服务&…...

Activiti7流程结束监听事件中,抛出的异常无法被spring全局异常捕捉
ProcessRuntimeEventListener activiti7中,提供了ProcessRuntimeEventListener监听器,用于监听流程实例的结束事件 /*** 流程完成监听器*/ Slf4j Component public class ProcessCompleteListener implements ProcessRuntimeEventListener<ProcessC…...
Android 默认关闭自动旋转屏幕功能
Android 默认关闭自动旋转屏幕功能 接到客户邮件想要默认关闭设备的自动旋转屏幕功能,具体修改参照如下: /vendor/mediatek/proprietary/packages/apps/SettingsProvider/res/values/defaults.xml - <bool name"def_accelerometer_rotati…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...

React第五十七节 Router中RouterProvider使用详解及注意事项
前言 在 React Router v6.4 中,RouterProvider 是一个核心组件,用于提供基于数据路由(data routers)的新型路由方案。 它替代了传统的 <BrowserRouter>,支持更强大的数据加载和操作功能(如 loader 和…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...
Java 二维码
Java 二维码 **技术:**谷歌 ZXing 实现 首先添加依赖 <!-- 二维码依赖 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.1</version></dependency><de…...
scikit-learn机器学习
# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...
4. TypeScript 类型推断与类型组合
一、类型推断 (一) 什么是类型推断 TypeScript 的类型推断会根据变量、函数返回值、对象和数组的赋值和使用方式,自动确定它们的类型。 这一特性减少了显式类型注解的需要,在保持类型安全的同时简化了代码。通过分析上下文和初始值,TypeSc…...

Qemu arm操作系统开发环境
使用qemu虚拟arm硬件比较合适。 步骤如下: 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载,下载地址:https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...