Linux--多路转接之epoll

news2024/11/28 20:39:09

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

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

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

相关文章

自建 Bitwarden 密码管理器

大佬零度解说的文件修改,与原文不太一样,详细请看&#xff1a;自建 Bitwarden 密码管理器&#xff01;完全免费开源&#xff0c;轻量级&#xff0c;安全又可靠&#xff01;-零度解说 教程&#xff1a;你的密码真的安全吗&#xff1f;Bitwarden 密码管理器&#xff01;一键部署…

【Redis】缓存预热、雪崩、击穿、穿透、过期删除策略、内存淘汰策略

Redis常见问题总结&#xff1a; Redis常见问题总结Redis缓存预热Redis缓存雪崩Redis缓存击穿Redis缓存穿透 Redis 中 key 的过期删除策略数据删除策略 Redis内存淘汰策略一、Redis对过期数据的处理&#xff08;一&#xff09;相关配置&#xff08;二&#xff09;内存淘汰流程&a…

el-table表格里面有一条横线

表格里面 有一条横线&#xff0c; 出现原因&#xff1a;是自定义了表格头.使用了固定列&#xff08;fixed&#xff09;&#xff0c;定宽。就很难受。。。 添加样式文件&#xff1a; <style lang"scss" scoped>::v-deep {.el-table__fixed-right {height: 100%…

植物大战僵尸杂交版之后要出联机版植物大战僵尸?(内测中,可在安卓手机上玩,文末附下载链接)

继植物大战僵尸杂交版之后给大家介绍一个杂交版作者正在酝酿的“植物大战僵尸射击版” 植物大战僵尸射击版介绍 《植物大战僵尸杂交版》的创作者“潜艇伟伟迷”即将推出PVZ改版新作——《植物大战僵尸射击版》。游戏将支持PC、手游和web端&#xff0c;提供单人、双人、三人、…

取证之FTK Imager学习笔记

一、FTK Imager制作镜像详细教程 1、文件-创建磁盘镜像 2、参数详解&#xff1a; 1&#xff09;物理驱动器 整个驱动器&#xff0c;如&#xff1a;识别到的是整块硬盘、U盘等&#xff0c;而不管你分几个分区&#xff1b; 2&#xff09;逻辑驱动器&#xff08;L&#xff09…

数据结构-5.9.树的存储结构

一.树的逻辑结构&#xff1a; 二.双亲表示法(顺序存储)&#xff1a; 1.树中除了根结点外每一颗树中的任意一个结点都只有一个父结点(双亲结点)&#xff1b; 2.结点包括结点数据和指针&#xff1b; 3.上述图片中右边的顺序存储解析&#xff1a;比如A结点左边的0&#xff0c;就…

机器学习:知识蒸馏(Knowledge Distillation,KD)

知识蒸馏&#xff08;Knowledge Distillation&#xff0c;KD&#xff09;作为深度学习领域中的一种模型压缩技术&#xff0c;主要用于将大规模、复杂的神经网络模型&#xff08;即教师模型&#xff09;压缩为较小的、轻量化的模型&#xff08;即学生模型&#xff09;。在实际应…

Java基础 03

⭐输入法的原理&#xff1a;⭐ 1.输入法本质就是输入字符的编码 2. Unicode对应16位编码-->所有字符都是16进制&#xff08;也就是16进制&#xff09; 码点&#xff1a;一套编码表中&#xff0c;单个字符对应的代码串叫做“码点” 3.变量 Java中所有应用的变量都要声明且…

Python面向对象编程:继承和多态③

文章目录 一、继承1.1 什么是继承1.2 定义父类和子类1.3 子类重写父类的方法1.4 多继承 二、多态2.1 什么是多态2.2 多态的实现2.3 抽象类和接口 三、综合详细例子3.1 项目结构3.2 模块代码init.pyshape.pycircle.pyrectangle.py 3.3 主程序代码main.py 3.4 运行结果 四、总结 …

实用篇—高效批量复制INSERT语句,并去除某列

在数据库管理中&#xff0c;常常需要将数据从一个表复制到另一个表。使用 Navicat 等工具可以方便地导出多条 INSERT 语句&#xff0c;但有时我们不需要某些列&#xff08;如 ID 列&#xff09;。本文将介绍如何在 Navicat 中复制多条 INSERT 语句&#xff0c;并去除 ID 列以便…

C语言笔记 14

函数原型 函数的先后关系 我们把自己定义的函数isPrime()写在main函数上面 是因为C的编译器自上而下顺序分析你的代码&#xff0c;在看到isPrime的时候&#xff0c;它需要知道isPrime()的样子——也就是isPrime()要几个参数&#xff0c;每个参数的类型如何&#xff0c;返回什么…

python画图|在三维空间的不同平面上分别绘制不同类型二维图

【1】引言 前序已经完成了基础的二维图和三维图绘制教程探索&#xff0c;可直达的链接包括但不限于&#xff1a; python画图|3D参数化图形输出-CSDN博客 python画三角函数图|小白入门级教程_正余弦函数画图python-CSDN博客 在学习过程中&#xff0c;发现一个案例&#xff1…

XGBoost回归预测 | MATLAB实现XGBoost极限梯度提升树多输入单输出

回归预测 | MATLAB实现XGBoost极限梯度提升树多输入单输出 目录 回归预测 | MATLAB实现XGBoost极限梯度提升树多输入单输出预测效果基本介绍模型描述程序设计参考资料预测效果 基本介绍 XGBoost的全称是eXtreme Gradient Boosting,它是经过优化的分布式梯度提升库,旨在高效、…

优化UVM环境(三)-环境发包较多时,会触发timeout

书接上回&#xff1a; 优化UVM环境&#xff08;一&#xff09;-环境结束靠的是timeout&#xff0c;而不是正常的objection结束 优化UVM环境&#xff08;二&#xff09;-将error/fatal红色字体打印&#xff0c;pass绿色字体打印 环境发包较多时&#xff0c;会触发timeout 解决…

SpringBoot +Vue3前后端分离项目入门基础实例五

项目说明 项项目名称使用框架说明后端项目springboot_vue_element_demoSpringBoot + MyBatis-plus + MySQL完成基本的增删改查操作API前端项目vue-projectVue3 + ElementUI plus + axios界面展示,调用后端API项目文档目录 SpringBoot +Vue3前后端分离项目入门基础实例一 Spri…

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

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

【C++】- STL之vector模拟实现

1.vector的介绍 vector是表示可变大小数组的序列容器。vector采用的连续存储空间来存储元素。意味着也可以采用下标对vector的元素进行访问&#xff0c;和数组一样高效。但是它的大小是可以动态改变的&#xff0c;而且它的大小会被容器自动处理。vector使用动态分配数组来存储它…

三子棋(C 语言)

目录 一、游戏设计的整体思路二、各个步骤的代码实现1. 菜单及循环选择的实现2. 棋盘的初始化和显示3. 轮流下棋及结果判断实现4. 结果判断实现 三、所有代码四、总结 一、游戏设计的整体思路 &#xff08;1&#xff09;提供一个菜单让玩家选择人机对战、玩家对战或者退出游戏…

企业电子印章主要通过以下几种方式进行防伪

企业电子印章主要通过以下几种方式进行防伪&#xff1a; 一、数字证书和加密技术 数字证书认证 企业电子印章依托数字证书&#xff0c;数字证书由权威的第三方数字认证机构颁发&#xff0c;确保了印章使用者的身份真实性。 数字证书如同企业在数字世界的身份证&#xff0c;包…

Python 工具库每日推荐 【sqlparse】

文章目录 引言SQL解析工具的重要性今日推荐:sqlparse工具库主要功能:使用场景:安装与配置快速上手示例代码代码解释实际应用案例案例:SQL查询分析器案例分析高级特性自定义格式化处理多个语句扩展阅读与资源优缺点分析优点:缺点:总结【 已更新完 Python工具库每日推荐 专…