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. 创建listensock
        InetAddr addr("0", _port);//0表示任意ip
        _listensock->BuildListenSocket(addr);

        // 2. 创建epoll模型
        _epfd = ::epoll_create(128);//返回值是epoll的fd
        if (_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_fd
        if (_epfd >= 0)//关闭epoll的fd
            ::close(_epfd);
    }
private:
    uint16_t _port; //端口号
    std::unique_ptr<Socket> _listensock; //监听sock
    int _epfd; //epoll的fd

    struct 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在处理大量并发连接时能够更加高效地利用系统资源。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

用于病理图像诊断的跨尺度多实例学习|文献速递-基于深度学习的医学影像分类,分割与多模态应用

Title 题目 Cross-scale multi-instance learning for pathological image diagnosis 用于病理图像诊断的跨尺度多实例学习 01 文献速递介绍 病理学是诊断炎症性肠病&#xff08;如克罗恩病&#xff09;的金标准&#xff08;Gubatan等&#xff0c;2021&#xff1b;Yeshi等…

机器学习:opencv--人脸检测以及微笑检测

目录 前言 一、人脸检测的原理 1.特征提取 2.分类器 二、代码实现 1.图片预处理 2.加载分类器 3.进行人脸识别 4.标注人脸及显示 三、微笑检测 前言 人脸检测是计算机视觉中的一个重要任务&#xff0c;旨在自动识别图像或视频中的人脸。它可以用于多种应用&#xff0…

Vxe UI vue vxe-table select 下拉框选项列表数据量超大过大时卡顿解决方法

Vxe UI vue vxe-table vxe-grid select 下拉框选项列表数据量超大过大时卡顿解决方法 查看 github vxe-table 官网 vxe-table 本身支持虚拟滚动&#xff0c;数据量大也是支持的&#xff0c;但是如果在可编辑表格中使用下拉框&#xff0c;下拉框的数据量超大时&#xff0c;可能…

int QSqlQuery::size() const

返回结果的大小&#xff08;返回的行数&#xff09; 或者返回-1 &#xff08;如果大小不能被决定 或者 数据库不支持报告查询的大小信息&#xff09; 注意&#xff1a;对于非查询语句&#xff0c;将返回-1&#xff08;isSelect()返回false&#xff09; 如果查询不是活跃的&…

JS 分支语句

目录 1. 表达式与语句 1.1 表达式 1.2 语句 1.3 区别 2. 程序三大流控制语句 3. 分支语句 3.1 if 分支语句 3.2 双分支 if 语句 3.3 双分支语句案例 3.3.1 案例一 3.3.2 案例二 3.4 多分支语句 1. 表达式与语句 1.1 表达式 1.2 语句 1.3 区别 2. 程序三大流控制语…

基于方块编码的图像压缩matlab仿真,带GUI界面

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 编码单元的表示 4.2编码单元的编码 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 下图是随着方块大小的变化&#xff0c;图像的压缩率以及对应的图像质量指标PSN…

初始爬虫13(js逆向)

为了解决网页端的动态加载&#xff0c;加密设置等&#xff0c;所以需要js逆向操作。 JavaScript逆向可以分为三大部分&#xff1a;寻找入口&#xff0c;调试分析和模拟执行。 1.chrome在爬虫中的作用 1.1preserve log的使用 默认情况下&#xff0c;页面发生跳转之后&#xf…

echarts显示隐藏柱状图柱子的背景色

showBackground: true, //控制是否显示背景色backgroundStyle: {// color: rgba(180, 180, 180, 0.4) //背景色的颜色color: red} 关键代码是 showBackground: true, //控制是否显示背景色 设置为false或者直接而不写就是不显示背景色&#xff0c;默认是不显示背景色 true的时…

windows客户端SSH连接ubuntu/linux服务器,三种网络连接:局域网,内网穿透(sakuraftp),虚拟局域网(zerotier)

windows客户端SSH连接ubuntu/linux服务器&#xff0c;三种网络连接&#xff1a;局域网&#xff0c;内网穿透&#xff08;sakuraftp&#xff09;&#xff0c;虚拟局域网&#xff08;zerotier&#xff09; 目录 SSH简述、三种网络连接特点SSH简述局域网内连接内网穿透&#xff08…

商业级免费OCR利器!Surya OCR:支持90+种语言识别,复杂布局识别,表格解析全覆盖!

❤️ 如果你也关注大模型与 AI 的发展现状&#xff0c;且对大模型应用开发非常感兴趣&#xff0c;我会快速跟你分享最新的感兴趣的 AI 应用和热点信息&#xff0c;也会不定期分享自己的想法和开源实例&#xff0c;欢迎关注我哦&#xff01; 微信订阅号&#xff5c;搜一搜&…

机器学习实战27-基于双向长短期记忆网络 BiLSTM 的黄金价格模型研究

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下机器学习实战27-基于双向长短期记忆网络 BiLSTM 的黄金价格模型研究。本文针对黄金价格预测问题&#xff0c;展开基于改造后的长短期记忆网络BiLSTM的黄金价格模型研究。文章首先介绍了项目背景&#xff0c;随后详细…

青少年编程能力等级测评CPA C++(二级)试卷(1)

青少年编程能力等级测评CPA C&#xff08;二级&#xff09;试卷&#xff08;1&#xff09; 一、单项选择题&#xff08;共20题&#xff0c;每题3.5分&#xff0c;共70分&#xff09; CP2_1_1&#xff0e;下列C程序段中&#xff0c;对二维数组arr的正确定义是&#xff08; &am…

PCL滤波器之面试总结

体素滤波器&#xff1a;降采样&#xff0c;减小体量。 处理前 处理后 直通滤波器&#xff1a;获得想要的区域。 接着上一步继续处理 索引滤波&#xff08;ExtractIndices滤波器&#xff09;&#xff1a; 接上图&#xff1a;反选效果&#xff0c;实际上删除的是这几个点,上图…

Qml-Item的函数使用

Qml-Item的函数使用 Item的提供了一些函数用于处理item之间父子关系&#xff0c;焦点链&#xff0c;以及item之间的坐标转换&#xff0c;本文重点示范item之间的坐标转换 Item的函数 函数childAt(real x,real y) &#xff1a;在item所在坐标系中&#xff0c;返回点point(x,y…

python pip安装requirements.txt依赖与国内镜像

python pip安装requirements.txt依赖与国内镜像 如果网络通畅&#xff0c;直接pip安装依赖&#xff1a; pip install -r requirements.txt 如果需要国内的镜像&#xff0c;可以考虑使用阿里的&#xff0c;在后面加上&#xff1a; -i http://mirrors.aliyun.com/pypi/simple --…

(四)Python标识符与保留字

一、标识符规则 标识符用来识别变量、函数、类、模块以及对象的名称。 Python标识符可包括英文字母、数字以及下划线。 限制如下&#xff1a; 1、标识符第一个字符必须是字母表中字母或者下划线&#xff0c;变量名称间不得有空格&#xff1b; 2、Python标识符有大小写之分…

【SRE系列--DNS跨域转发】

1.DNS原理 1.1 简介 DNS(Domain Name Service的缩写)的作用就是根据域名查出IP地址。IP地址是由32位二进制数字组成&#xff0c;人们很难记住这些IP&#xff0c;相反&#xff0c;大家愿意使用比较容易记忆的主机名字。而电脑在处理IP数据报文时&#xff0c;是使用IP地址的&am…

高阶数据结构与算法——红黑树の奥秘

1.认识红黑树 1.1红黑树的概念 红⿊树是⼀棵⼆叉搜索树&#xff0c;他的每个结点增加⼀个存储位来表⽰结点的颜⾊&#xff0c;可以是红⾊或者⿊⾊。通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束&#xff0c;红⿊树确保没有⼀条路径会⽐其他路径⻓出2倍&#xff0c…

JDK安装环境配置保姆间教程

文章介绍了Java编程语言的基本知识&#xff0c;包括其创始人和发布年份&#xff0c;然后详细阐述了如何下载和安装JDK&#xff0c;以及如何配置JAVA_HOME和Path环境变量&#xff0c;以确保Java开发环境的正确设置。最后&#xff0c;作者提到在JDK1.5以后的版本中&#xff0c;无…

Python基础语法条件

注释 注释的作用 通过用自己熟悉的语言&#xff0c;在程序中对某些代码进行标注说明&#xff0c;这就是注释的作用&#xff0c;能够大大增强程序的可读性。 注释的分类及语法 注释分为两类&#xff1a;单行注释 和 多行注释。 单行注释 只能注释一行内容&#xff0c;语法如下…