【Hello Linux】多路转接之 epoll

news2025/1/12 13:31:58

本篇博客介绍: 多路转接之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 它们都需要我们做下面的操作

  1. 让我们维护一个第三方的数组
  2. 都需要遍历整个数组
  3. 都需要经历用户到内核 内核到用户的事件通知

而我们的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为什么高效呢

  1. 因为epoll底层维护的是红黑树结构 对比数组来说增删改查有着天然的优势
  2. 我们不需要主动去询问哪些文件是否就绪 os会自动将其添加到就绪队列中
  3. 在寻找就绪文件的时候 由于我们使用的是就绪队列 时间复杂度是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 epoll
      if(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    
      {
        // err
        exit(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);
                          // TODO
                          std::string response = func_(buffer);
                          send(sock, response.c_str(), response.size(), 0);
                      }
                      else if (n == 0)
                      {
                          // 建议先从epoll移除,才close fd
                          epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                          close(sock);
                          logMessage(NORMAL, "client quit");
                      }                                                                                                         
                      else
                      {
                          // 建议先从epoll移除,才close fd
                          epoll_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的优缺点

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

找不到msvcr120.dll怎么办?电脑缺失msvcr120.dll的修复方法

msvcr120.dll 是 Microsoft Visual C Redistributable Package 中的一个动态链接库文件&#xff0c;它包含了 C 运行时库的一些功能。这个文件通常与 Visual C 2010 编译器一起使用&#xff0c;用于支持一些大型游戏和应用程序的运行。msvcr120.dll 文件的主要作用是提供 C 语言…

远程代码执行渗透测试—Server2128

远程代码执行渗透测试 任务环境说明&#xff1a; √ 服务器场景&#xff1a;Server2128&#xff08;开放链接&#xff09; √服务器场景操作系统&#xff1a;Windows √服务器用户名&#xff1a;Administrator密码&#xff1a;pssw0rd 1.找出靶机桌面上文件夹1中的文件RCEBac…

【AI视野·今日Robot 机器人论文速览 第四十六期】Tue, 3 Oct 2023

AI视野今日CS.Robotics 机器人学论文速览 Tue, 3 Oct 2023 Totally 76 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers Generalized Animal Imitator: Agile Locomotion with Versatile Motion Prior Authors Ruihan Yang, Zhuoqun Chen, Jianhan M…

RabbitMQ-网页使用消息队列

1.使用消息队列 几种模式 从最简单的开始 添加完新的虚拟机可以看到&#xff0c;当前admin用户的主机访问权限中新增的刚添加的环境 1.1查看交换机 交换机列表中自动新增了刚创建好的虚拟主机相关的预设交换机。一共7个。前面两个 direct类型的交换机&#xff0c;一个是…

运行程序时msvcr110.dll丢失的解决方法,msvcr110.dll丢失5的个详细解决方法

在使用电脑的过程中&#xff0c;我们经常会遇到各种问题&#xff0c;其中之一就是 msvcr110.dll 丢失的问题。msvcr110.dll 是 Microsoft Visual C Redistributable 的一个组件&#xff0c;用于支持使用 Visual C 编写的应用程序。如果您的系统中丢失了这个文件&#xff0c;您可…

保姆级Anaconda安装教程

一.anaconda下载 建议使用清华大学开源软件镜像站进行下载&#xff0c;使用官网下载速度比较慢。 anaconda清华大学开源软件镜像站 &#xff1a; https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/ 一路next即可&#xff0c;注意添加环境变量得选项都勾上。 二.验证…

C++list模拟实现

list模拟实现 1.链表结点2.类模板基本框架3.构造4.插入普通迭代器实现4.1尾插4.2普通迭代器实现4.3对比list和vector的iterator4.4迭代器的价值4.5insert4.6尾插头插复用写法 5.删除erase5.1erase5.2尾删头删复用写法 6.析构emptysizeclear6.1clear6.2size6.3 empty6.4 析构 7.…

深度学习笔记之微积分及绘图

深度学习笔记之微积分及绘图 学习资料来源&#xff1a;微积分 %matplotlib inline from matplotlib_inline import backend_inline from mxnet import np, npx from d2l import mxnet as d2lnpx.set_np()def f(x):return 3 * x ** 2 - 4 * xdef numerical_lim(f, x, h):retur…

浅谈yolov5中的anchor

默认锚框 YOLOv5的锚框设定是针对COCO数据集中大部分物体来拟定的&#xff0c;其中图像尺寸都是640640的情况。 anchors参数共3行&#xff1a; 第一行是在最大的特征图上的锚框 第二行是在中间的特征图上的锚框 第三行是在最小的特征图上的锚框 在目标检测中&#xff0c;一…

[sping] spring core - 依赖注入

[sping] spring core - 依赖注入 所有代码实现基于 Spring Boot3&#xff0c;core 的概念很宽广&#xff0c;这里的 core concept 主要指的就是 Inversion of Control 和 Dependency Injection&#xff0c;其他的按照进度应该是会被放到其他的 section 记录 之前有写过 IoC 和…

如何在Keil和IAR环境编译生成的bin文件添加CRC校验值

之前写过一篇文章介绍过 CRC 的原理和应用。在程序升级的情况下&#xff0c;我们可以在烧录下载的 bin 文件添加 CRC 校验值&#xff0c;以校验我们获取的bin文件是否正确。 下面我打算使用 APM32F407 的工程代码&#xff0c;介绍下如何在 Keil 环境和 IAR 环境对编译生成的 b…

线程概念,实现方式以及多线程模型

1.线程引入 有的进程可能需要“同时”做很多事&#xff0c;而传统的进程只能串行地执行一系列程序。 为此&#xff0c;引入了“线程”&#xff0c;来增加并发度。 可以把线程理解为“轻量级进程”。线程是一个基本的CPU执行单元&#xff0c;也是程序执行流的最小单位。引入线…

GEE15:获取不同遥感指数的时间序列及不同指数间的关系

GEE 1. 不同遥感指数间的时间序列分析2. 不同指数之间的关系 1. 不同遥感指数间的时间序列分析 GPP数据在一定程度上和植被指数&#xff08;如NDVI和EVI&#xff09;有着显著的相关性&#xff0c;那么其相关性如何&#xff1f;如何从时间序列的角度来思考呢&#xff1f;下面我将…

【Spring Cloud】深入探索统一网关 Gateway 的搭建,断言工厂,过滤器工厂,全局过滤器以及跨域问题

文章目录 前言为什么需要网关以及网关的作用网关的技术实现 一、Gateway 网关的搭建1.1 创建 Gateway 模块1.2 引入依赖1.3 配置网关1.4 验证网关是否搭建成功1.5 微服务结构分析 二、Gateway 断言工厂2.1 Spring 提供的断言工厂2.2 示例&#xff1a;设置断言工厂 三、Gateway …

【算法训练-字符串 三】字符串相加

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是【字符串相加】&#xff0c;使用【字符串】这个基本的数据结构来实现&#xff0c;这个高频题的站点是&#xff1a;CodeTop&#xff0c;筛选条件为&…

visual studio禁用qt-vsaddin插件更新

visual studio里qt-vsaddin插件默认是自动更新的&#xff0c;由于qt-vsaddin插件新版本的操作方式与老版本相差较大&#xff0c;且新版本不稳定&#xff0c;容易出Bug&#xff0c;所以需要禁用其自动更新&#xff0c;步骤如下&#xff1a;     点击VS2019菜单栏上的【扩展】–…

制作pcb流畅

首先选择一款好用的软件。嘉立创&#xff0c;对新手友好&#xff0c;上手快&#xff0c;每个月还有免费的pcb打样卷。有官方的一对一客服服务。 作为一个新手我在绘制pcb时常进行如下几步。 1、绘制原理图 根据实际情况找到芯片对应的原理图&#xff0c;并添加自己需要的外设。…

Linux系统编程系列之互斥锁和读写锁

一、什么是互斥锁和读写锁 互斥锁是一种并发机制&#xff0c;用于控制多个线程对共享资源的访问。 读写锁是一种并发机制&#xff0c;用于控制多个线程对共享资源的访问。 二、特性 1、互斥锁 当一个线程获得了互斥锁并进入临界区&#xff08;对共享资源进行访问&#xff09;时…

鱼眼相机去畸变(图像拉直/展开/矫正)算法及实战总结

本文介绍两种方法 1、经纬度矫正法 2、棋盘格矫正法 一、经纬度矫正法 1、算法说明 经纬度矫正法&#xff0c; 可以把鱼眼图想象成半个地球&#xff0c; 然后将地球展开成地图&#xff0c;经纬度矫正法主要是利用几何原理&#xff0c; 对图像进行展开矫正。 经过P点的入射光线…

操作系统-《王道 操作系统》

概念、功能和目标 概念 什么是操作系统 功能和目标 作为系统的管理者向上提供方便简易的服务作为最接近底层硬件的层次 特征 并发 共享 并发与共享的关系—互为存在条件 虚拟 异步 发展与分类 单道批处理系统多道批处理系统 优点&#xff1a;多道程序并发执行&#xff0c…