Linux--多路转接之epoll

上一篇:Linux–多路转接之select

epoll

epoll 是 Linux 下多路复用 I/O 接口 select/poll 的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率。它是 Linux 下多路复用 API 的一个选择,相比 selectpollepoll 提供了更高的性能,并且使用起来也更加方便。

epoll的工作原理

在这里插入图片描述
在这里插入图片描述

eventpoll框架的核心在于它能够高效地处理多个文件描述符上的事件,避免了传统I/O多路复用机制(如select和poll)中的轮询开销。eventpoll通过以下方式实现:

  • 注册文件描述符:当文件描述符被注册到eventpoll时,会创建一个epitem(eventpoll item)结构体,用于表示该文件描述符及其关心的事件类型。这个epitem会被插入到eventpoll的红黑树(rbtree)中,以便快速查找和管理。
  • 等待事件发生:通过调用epoll_wait()系统调用,应用程序会在eventpoll的等待队列(wq)上等待。此时,指定的回调函数是default_wake_function,用于在事件发生时唤醒等待的线程。
  • 事件通知:当被监测的文件描述符上有事件发生时,会调用ep_poll_callback()回调函数,将相应的epitem插入到eventpoll的就绪链表(rdllist)中。epoll_wait()会从这个链表中取出epitem,并将对应的事件通知给应用程序。

注意:以上操作均有系统自主完成

epoll 的相关系统调用

epoll_create()

创建一个 epoll 的句柄.

#include <sys/epoll.h>  int epoll_create(int size);

size 参数用于告诉内核这个监听列表(epoll 实例)打算同时监视多少个文件描述符。

返回值:
如果调用成功,epoll_create 返回一个新的文件描述符,该描述符用于后续的 epoll_ctl()和 epoll_wait()调用。
如果调用失败,则返回 -1,并设置 errno 以指示错误原因。

epoll_ctl()

允许程序在 epoll 实例中添加、修改或删除文件描述符(file descriptors)的监听事件.

#include <sys/epoll.h>  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数说明

  • epfd:由 epoll_create () 函数生成的 epoll 实例的文件描述符。
  • op:指定要执行的操作,常用的值包括:
    EPOLL_CTL_ADD:向 epoll 实例注册新的文件描述符和事件。
    EPOLL_CTL_MOD:修改已注册的文件描述符的事件。
    EPOLL_CTL_DEL:从 epoll 实例中删除一个文件描述符。
  • fd:要操作的目标文件描述符,即要注册、修改或删除的文件描述符。
  • event:指向 struct epoll_event 结构体的指针,该结构体包含了要注册或修改的事件信息。对于 EPOLL_CTL_DEL 操作,该参数可以为 NULL。
typedef union epoll_data {  void    *ptr;  int      fd;  uint32_t u32;  uint64_t u64;  
} epoll_data_t;  struct epoll_event {  uint32_t     events;      /* 事件类型 */  epoll_data_t data;        /* 与事件相关的数据 */  
};
  • events:这是一个位掩码,用于指示发生的事件类型。常见的事件类型包括:
    EPOLLIN:表示对应的文件描述符可以进行读操作。
    EPOLLOUT:表示对应的文件描述符可以进行写操作。
    EPOLLERR:表示发生错误。
    EPOLLHUP:表示挂起(hang up)事件,比如对端关闭了连接。
    EPOLLET:将事件设置为边缘触发(Edge Triggered)模式,这是与水平触发(Level Triggered)模式相对的一种触发模式。
    EPOLLONESHOT:用于确保事件被触发一次后,除非再次使用 epoll_ctl 重新注册,否则不再接收该事件。

  • data:这是一个联合体,可以存储与事件相关的数据。它提供了多种方式来关联事件和特定的数据或文件描述符:
    ptr:可以指向任意类型的数据,通常用于存储用户自定义的数据结构指针。
    fd:直接存储文件描述符的值,当只需要管理文件描述符时,这种方式更为直接(常用)。
    u32u64:分别提供了32位和64位的无符号整数存储,这些字段可以用来存储特定的值或标识符。

epoll_wait()

程序调用 epoll_wait 时,它会阻塞当前线程,直到注册在 epoll 实例上的文件描述符上有事件发生,或者超时时间到达

#include <sys/epoll.h>  int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数说明

  • epfd:由 epoll_create 函数生成的 epoll 实例的文件描述符。
  • events:指向 struct epoll_event 数组的指针,用于存储发生的事件。当 epoll_wait 返回时,该数组将被填充有发生事件的文件描述符和事件类型的信息。
  • maxevents:指定 events 数组的最大长度,即 epoll_wait 一次可以处理的最大事件数。
  • timeout:指定等待 I/O 事件发生的超时时间(毫秒)。如果设置为 -1,则 epoll_wait 将无限期地等待,直到有事件发生。如果设置为 0,则 epoll_wait 将立即返回,无论是否有事件发生。如果设置为一个正整数,则 epoll_wait 将等待指定的毫秒数,如果在这段时间内有事件发生,则返回;否则返回 0,表示超时。

返回值

  • 成功时,epoll_wait 返回发生事件的文件描述符数量。如果返回 0,则表示在指定的超时时间内没有事件发生。
  • 如果发生错误,epoll_wait 返回 -1,并设置 errno 以指示错误原因。

epoll的工作方式

水平触发(Level Triggered, LT)

工作原理:在水平触发模式下,只要被监控的文件描述符上有可读写事件发生(即数据到达但未被读取,或可写空间可用但未被写入),epoll_wait就会通知用户程序
如果数据到达但是没有被读取,或者可写空间可用但是没有被写入,epoll_wait会再次通知用户程序,直到相应的操作被执行。

特点:

  • 通知次数:只要条件满足,就会不断地通知。
  • 读写策略:可以更灵活地处理读写,不需要连续读取或写入直到遇到错误。
  • 效率:由于频繁的通知,可能会引起较多的上下文切换,影响效率。
  • 编程复杂度:相对容易理解和使用。

边缘触发(Edge Triggered, ET)

工作原理:边缘触发模式是一种更高效的触发方式。在这种模式下,epoll_wait仅在状态变化时通知用户程序一次,比如从无数据到有数据,或者从不可写变为可写
当收到一个可读事件时,需要一直读取数据,直到返回EAGAIN错误(表示没有更多数据可读)。同样,对于可写事件,需要一直写入数据,直到不能再写入为止。

知次数:只在状态发生变化时通知一次。
读写策略:读操作需要一直进行,直到遇到EAGAIN错误;写操作也是如此,需要一直写,直到无法继续写入。
效率:减少了系统调用的次数,提高了应用程序的效率。
编程复杂度:要求程序必须更加小心地处理事件,以避免错过任何事件,这使得编程变得更加复杂。

主要区别

.水平触发(LT)边缘触发(ET)
通知次数只要条件满足,就会不断地通知只在状态发生变化时通知一次
读写策略可以更灵活地处理读写读操作需要一直进行,直到遇到EAGAIN错误;写操作也是如此
效率可能会引起较多的上下文切换,影响效率减少了系统调用的次数,提高了应用程序的效率
编程复杂度相对容易理解和使用要求程序必须更加仔细地处理事件,以避免错过任何事件,编程复杂度高

epoll_server实例(LT方式)

我们将对上一篇的select_server进行一定的修改即可;

epollServer.hpp

#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <sys/epoll.h>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Log.hpp"using namespace socket_ns;class EpollServer
{const static int gnum = 64;
public:EpollServer(uint16_t port = 8080): _port(port),_listensock(std::make_unique<TcpSocket>()),_epfd(-1){// 1. 创建listensockInetAddr addr("0", _port);//0表示任意ip_listensock->BuildListenSocket(addr);// 2. 创建epoll模型_epfd = ::epoll_create(128);//返回值是epoll的fdif (_epfd < 0){LOG(FATAL, "epoll_create error\n");exit(5);}LOG(DEBUG, "epoll_create success, epfd: %d\n", _epfd);// 3. 只有一个listensock, listen sock 关心的事件:读事件struct epoll_event ev; //结构体包含事件的信息ev.events = EPOLLIN;//事件可读ev.data.fd = _listensock->SockFd(); //将listenfd放入到信息中epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->SockFd(), &ev);}//对事件的处理void handlerEvent(int num){for (int i = 0; i < num; i++)//可处理多个事件{// 逐一将事件取出uint32_t revents = _revs[i].events;int sockfd = _revs[i].data.fd;// 读事件就绪if (revents & EPOLLIN){if (sockfd == _listensock->SockFd())//监听fd,表示将创建连接fd{InetAddr clientaddr;int newfd = _listensock->Accepter(&clientaddr); // 不会被阻塞,事件已知被响应if (newfd < 0)continue;// 获取新链接成功struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = newfd;epoll_ctl(_epfd, EPOLL_CTL_ADD, newfd, &ev);//将新事件添加到epoll中LOG(DEBUG, "_listensock ready, accept done, epoll_ctl done, newfd is: %d\n", newfd);}else//表示连接的fd有事情发生{char buffer[1024];ssize_t n = ::recv(sockfd, buffer, sizeof(buffer), 0); //接收客户端数据if (n > 0){LOG(DEBUG, "normal fd %d ready, recv begin...\n", sockfd);buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo_string = "server echo# ";echo_string += buffer;::send(sockfd, echo_string.c_str(), echo_string.size(), 0);//将结果返回}else if (n == 0)//表示连接已被断开,没有断开无数据传输将阻塞于epoll{LOG(DEBUG, "normal fd %d close, me too!\n", sockfd);// 对端连接关闭了::epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);::close(sockfd);}else{::epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr); // 这里表示将epoll中的sockfd删除::close(sockfd);//而fd是拷贝进去的,只是将拷贝在epoll中的fd擦除,对应的fd事件还没有被关闭}}}}}//循环执行void Loop(){int timeout = -1;//表示epoll阻塞等待,直到有事件发生while (true){int n = ::epoll_wait(_epfd, _revs, gnum, timeout);//用于等待事件的发生switch (n){case 0://规定时间内无事件发生LOG(DEBUG, "epoll_wait timeout...\n");break;case -1://发生错误LOG(DEBUG, "epoll_wait failed...\n");break;default://有事件发生LOG(DEBUG, "epoll_wait haved event ready..., n : %d\n", n);handlerEvent(n);break;}}}~EpollServer(){_listensock->Close();//关闭listen_fdif (_epfd >= 0)//关闭epoll的fd::close(_epfd);}
private:uint16_t _port; //端口号std::unique_ptr<Socket> _listensock; //监听sockint _epfd; //epoll的fdstruct epoll_event _revs[gnum];//事件数组,存储对应事件
};

在这里插入图片描述
_epfd: epoll是Linux底层中一种高效的I/O多路复用机制,所以也是属于一种事件,需要在用户层创建对应的文件描述符用于表示对epoll的创建;
_revs : 虽然在底层有红黑树来进行存储对应的事件,但是在用户层是无法了解到底层的存储执行的,因为epoll的底层全由系统来完成的,用户无法操作,所以还需要一个事件数组来存储对应的事件。

初始化:
在这里插入图片描述
128是设置这次的最大事件管理数量,相比于select来说他是无上限的,比较灵活;
在这里插入图片描述
对于事件的控制 :将事件信息包含在ev结构体中即可;

Loop:
在这里插入图片描述
这里我们将事件数组放入到函数中,当 epoll_wait 返回时,该数组将被填充有发生事件的文件描述符和事件类型的信息。这样就不用我们手动添加到事件数组中。
正是因为底层的红黑树会先存储着对应的事件信息,当被监测的文件描述符上有事件发生时,将相应的epitem插入到eventpoll的就绪链表(rdllist)中。epoll_wait()会从这个链表中取出epitem,放到_revs中,所以调用该函数会存到事件数组中。

handlerEvent:
在这里插入图片描述
EPOLLIN是0x001, revents如果对应位上是可读的(如:0x003)那么就能表示读事件就绪了;

main.cc

#include "epollServer.hpp"
#include "Log.hpp"#include <iostream>
#include <memory>// ./selectserver port
int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Usage: " << argv[0] << " port" << std::endl;return 0;}uint16_t port = std::stoi(argv[1]);EnableScreen();std::unique_ptr<EpollServer> svr = std::make_unique<EpollServer>(port);svr->Loop();return 0;
}

在这里插入图片描述
在这里插入图片描述

注:ET模式相对来说比较复杂,需要涉及到非阻塞的程序,等下一篇Reactor再详细展示。

epoll的优点

  • 支持水平触发(LT)和边缘触发(ET)
  • 接口简单易用
  • 没有最大文件描述符数量的限制 :select 和 poll 都有文件描述符数量的限制,而 epoll 则没有。
  • 只管理“活跃”的连接:epoll 会检查注册在其上的所有 socket,只将那些真正活跃的 socket 返回给用户,即减少了无效的等待时间。
  • 高效处理大量并发连接:epoll能够高效地处理大量并发连接,尤其适用于只有少量活跃连接的大量并发场景。它通过内核与用户空间共享一个事件表来跟踪所有需要监控的文件描述符,当文件描述符的状态发生变化时,内核会通知用户空间,从而避免了传统方法中的线性扫描。
  • 提高CPU利用率:epoll在等待事件就绪时,如果就绪队列中没有事件,会主动让出CPU,从而提高了CPU的利用率。这使得epoll在处理大量并发连接时能够更加高效地利用系统资源。

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

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

相关文章

DevExpress WPF v24.1新版亮点:PDF查看器、富文本编辑器功能升级

DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强大互动功能的XAML基础应用程序&#xff0c;这些应用程序专注于当代客户的需求和构建未来新一代支持触摸的解决方案。 DevExpress WPF控件日…

1971. 寻找图中是否存在路径

有一个具有 n 个顶点的 双向 图&#xff0c;其中每个顶点标记从 0 到 n - 1&#xff08;包含 0 和 n - 1&#xff09;。图中的边用一个二维整数数组 edges 表示&#xff0c;其中 edges[i] [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接&#x…

Vue3 学习笔记(一)Vue3 介绍及环境部署

一、Vue.js 简介 1、Vue.js 是什么&#xff1f; Vue.js&#xff08;读音 /vjuː/, 类似于 view&#xff09; 是一套构建用户界面的渐进式框架。Vue 只关注视图层&#xff0c; 采用自底向上增量开发的设计。Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件…

性能工具之JMeter 通过Java API生成 BeanShell PreProcessor 脚本

文章目录 一、前言二、实现代码三、代码示例四、最后 一、前言 对于上一篇文章&#xff08;性能工具之 HAR 格式化转换JMeter JMX 脚本文件&#xff09;还是有点问题。大家在使用的情况需要注意。 如果多个接口相同 path 路径且不同参数进行查询如&#xff1a; 上面接口如果…

【前端】如何制作一个自己的网页(15)

有关后代选择器的具体解释&#xff1a; 后代选择器 后代选择器使用时&#xff0c;需要以空格将多个选择器间隔开。 比如&#xff0c;这里p span&#xff0c;表示只设置p元素内&#xff0c;span元素的样式。 <style> /* 使用后代选择器设置样式 */ p span { …

java--多态(详解)

目录 一、概念二、多态实现的条件三、向上转型和向下转型3.1 向上转型3.2 向下转型 四、重写和重载五、理解多态5.1练习&#xff1a;5.2避免在构造方法中调用重写的方法&#xff1a; 欢迎来到权权的博客~欢迎大家对我的博客提出指导这是我的博客主页&#xff1a;点击 一、概念…

Java毕业设计 基于SpringBoot发卡平台

Java毕业设计 基于SpringBoot发卡平台 这篇博文将介绍一个基于SpringBoot发卡平台&#xff0c;适合用于Java毕业设计。 功能介绍 首页 图片轮播 商品介绍 商品详情 提交订单 文章教程 文章详情 查询订单  查看订单卡密 客服   后台管理 登录 个人信息 修改密码 管…

Selenium爬虫技术:如何模拟鼠标悬停抓取动态内容

介绍 在当今数据驱动的世界中&#xff0c;抓取动态网页内容变得越来越重要&#xff0c;尤其是像抖音这样的社交平台&#xff0c;动态加载的评论等内容需要通过特定的方式来获取。传统的静态爬虫方法难以处理这些由JavaScript生成的动态内容&#xff0c;Selenium爬虫技术则是一…

字典如何与选择器一起使用

背景&#xff1a;开发过程中会遇到某些字段需要做成下拉框。如下图&#xff1a; 组件 | Element里有select选择器这个组件可以实现下拉框的效果 我们可能会想到创一个辅助表来存储这些下拉数据像这样 这样虽然能实现&#xff0c;但是在实际开发中是不合理的&#xff0c;如果有…

个税自然人扣缴客户端数据的备份与恢复(在那个文件夹)

一&#xff0c;软件能够正常打开&#xff0c;软件中的备份与恢复功能 1&#xff0c;备份 您按照下面的方法备份一下哦~ 进入要备份的自然人软件&#xff0c;点击左侧系统设置→→系统管理→→备份恢复&#xff1b; 在备份设置里&#xff0c;点击“备份到选择路径”&#xff0c;…

WebGL编程指南 - 颜色与纹理续

设置纹理坐标&#xff08;initVertexBuffers()&#xff09; 从缓冲区到 attribute 变量的流程&#xff1a; // 顶点坐标 function initVertexBuffers(gl) {// 数据准备let verticesTexCoords new Float32Array([// 顶点坐标&#xff0c;纹理坐标-0.5, 0.5, 0.0, 1.0, -0.5, …

图像异常检测评估指标-分类性能

图像异常检测评估指标-分类性能 1. 混淆矩阵 混淆矩阵包括4个用于衡量分类算法性能的基本数值 四个字母代表的含义是&#xff1a;P&#xff08;Positive&#xff09;代表算法将样本预测为正类&#xff0c;N&#xff08;Negative&#xff09;代表算法将样本预测为负类&#xf…

ST7789读取ID错误新思路(以STC32G为例)

1.前言 前两天刚把ST7789写入搞定&#xff0c;这两天想折腾一下读取。最开始是读ID&#xff0c;先是用厂家送的程序&#xff0c;程序里面用的是模拟I8080协议&#xff0c;一切正常。后来我用STC32G的内置LCM模块&#xff0c;发现读取不出来。更神奇的是ID读不出来&#xff0c;…

[项目详解][boost搜索引擎#2] 建立index | 安装分词工具cppjieba | 实现倒排索引

目录 编写建立索引的模块 Index 1. 设计节点 2.基本结构 3.(难点) 构建索引 1. 构建正排索引&#xff08;BuildForwardIndex&#xff09; 2.❗构建倒排索引 3.1 cppjieba分词工具的安装和使用 3.2 引入cppjieba到项目中 倒排索引代码 本篇文章&#xff0c;我们将继续项…

【C++指南】类和对象(四):类的默认成员函数——全面剖析 : 拷贝构造函数

引言 拷贝构造函数是C中一个重要的特性&#xff0c;它允许一个对象通过另一个已创建好的同类型对象来初始化。 了解拷贝构造函数的概念、作用、特点、规则、默认行为以及如何自定义实现&#xff0c;对于编写健壮和高效的C程序至关重要。 C类和对象系列文章&#xff0c;可点击下…

GitLab+Jenkins 实现 Webhook 自动化触发构建

在持续集成和持续部署&#xff08;CI/CD&#xff09;过程中&#xff0c;如何实现代码提交后自动触发构建&#xff1f;今天&#xff0c;我们将通过GitLab与Jenkins的集成&#xff0c;利用Webhook实现自动化触发构建&#xff0c;为你的开发流程注入高效能量&#xff01; 在每次代…

Java 多线程(六)—— 线程池 和 工厂模式

线程池 随着现代计算机的发展&#xff0c;任务越来越多&#xff0c;线程创建也逐渐增加&#xff0c;每次让操作系统创建线程这个开销就有点大&#xff0c;因此&#xff0c;我们诞生了线程池的概念&#xff0c;线程池里面有很多线程&#xff0c;这些线程可以被用户去调用执行任…

Java最全面试题->Java基础面试题->JavaSE面试题->异常面试题

文章目录 异常1.说一下Java中的异常体系&#xff1f;2.Error和Exception的区别3.写出你最常见的 5 个 RuntimeException&#xff1f;4.如何处理异常?5.try()里面有⼀个return语句&#xff0c; 那么后面的finally{}里面的代码会不会被执行&#xff1f;什么时候执行&#xff0c;…

C++ 进阶:类相关特性的深入探讨

⭐在对C 中类的6个默认成员函数有了初步了解之后&#xff0c;现在我们进行对类相关特性的深入探讨&#xff01; &#x1f525;&#x1f525;&#x1f525;【C】类的默认成员函数&#xff1a;深入剖析与应用&#xff08;上&#xff09; 【C】类的默认成员函数&#xff1a;深入剖…

初阶数据结构【3】--单链表(比顺序表还好的一种数据结构!!!)

本章概述 前情回顾单链表实现单链表彩蛋时刻&#xff01;&#xff01;&#xff01; 前情回顾 咱们在上一章博客点击&#xff1a;《顺序表》的末尾&#xff0c;提出了一个问题&#xff0c;讲出了顺序表的缺点——有点浪费空间。所以&#xff0c;为了解决这个问题&#xff0c;我…