IO多路转接之epoll

news2024/9/23 11:23:34

目录

一. epoll的实现原理

二.  epoll的相关接口

2.1 epoll_create -- 创建epoll模型

2.2 epoll_ctl -- 对epoll模型进行控制

2.3 epoll_wait -- 等待epoll所关注的事件就绪

2.4 epoll相关接口的使用方法

三. Epoll服务器的模拟实现

3.1 EpollServer类的声明

3.2 EpollServer类的实现

四. 总结


一. epoll的实现原理

在通过select和poll实现IO多路转接时,都需要程序员来维护一个数组,用来随时控制要被关心的事件(fd)。同时,使用select和poll实现IO多路转接,都需要对用户维护的数组进行遍历,遍历操作的时间复杂度为O(N),这会很大程度上消耗计算资源,降低效率。

为了解决poll和select的这些缺陷,epoll被提了出来,相比于select和poll,epoll在有事件就绪时,不需要逐个遍历检查每个被关注的文件描述符是否就绪,希望被关注的事件不需要程序员自己维护数组来控制,这提高了效率,降低了程序员的代码编写成本。

在使用epoll实现IO多路转接之前,必须要先在OS内核中建立epoll模型。如图1.1所示,epoll模型主要有两部分组成:

  • 一颗红黑树:用来维护用户所关注的事件(fd),一个红黑树节点对应一个被关注的事件,节点中要记录包括文件描述符,所关注事件的类型(读/写/异常)等。
  • 就绪队列:当有事件就绪的时候,会将已经就绪的事件添加到就绪队列中去,从队头拿走就绪事件及其相关的属性信息交给用户层,就能对已经就绪的事件进行响应。

相比于直接遍历数组查找某个节点O(N)的时间复杂度,使用红黑树查找的时间复杂度为O(logN),这样就提高了OS内核管理事件的效率。同时,通过就绪队列维护已经就绪的事件,避免了在wait成功之后在遍历数组的过程中确定具体是哪个事件就绪,这进一步降低了资源的消耗,提高效率。

图1.1 epoll模型

二.  epoll的相关接口

2.1 epoll_create -- 创建epoll模型

函数原型:int epoll_create(size_t size)

头文件:#include <sys/epoll.h>

函数参数:在Linux 2.6.8版本之后参数size就已经被弃用,这里是为了向前兼容,在调用接口时只需要传一个大于0的参数即可。

返回值:如果创建成功,返回新创建的epoll模型的文件描述符epfd,如果创建失败返回-1。

epoll_create函数所执行的工作,就是在操作系统内核中,创建一个如图1.1所示的epoll模型,即:一颗红黑树和一个就绪队列。epoll_create接口返回值表示被创建的epoll模型的对应fd值,可见OS是将epoll模型当做文件来处理的,这符合Linux下一切接文件的观点。

2.2 epoll_ctl -- 对epoll模型进行控制

函数原型:int epoll_create(int epfd, int op, int fd, struct epoll_event *event)

头文件:#include <sys/epoll.h>

函数参数:

  • epfd -- 所要进行操作的epoll模型对应的文件描述符。
  • op -- 用于指定所要进行的操作。
  • fd -- 用于对epoll进行操作的文件描述符,如指明要添加关注的事件。
  • event -- 输入型参数,告知OS要关注的事件的属性信息。

返回值:如果函数执行成功返回0,失败返回-1。

在该接口函数的参数中,epfd为通过epoll_create创建epoll模型获取的文件描述符,op用于指定所进行的操作的类型,表2.1为op的可选值及其对应的意义。

表2.1 op的可选值及其意义
op意义
EPOLL_CTL_ADD将指定事件添加到epoll模型中进行关注
EPOLL_CTL_DEL删除epoll模型中对某个事件的关注
EPOLL_CTL_MOD改变epoll模型中某个事件被关注的状态(event)

fd用于指定对epoll进行操作的文件描述符,可以为listen文件描述符,也可为普通文件描述符,假设op传EPOLL_CTL_ADD,那么所epoll_ctl所进行的操作就是将fd加入到epoll模型中进行关注。

struct epoll_event 类型数据定义如下,该类型包含两个成员,其中一个为uint32_t类型成员events,用于控制该事件的属性是可读、可写还是异常等。events可以为表示4.2中宏的集合,还有一个为联合自定义类型数据,其中该联合类型可以传四种不同的数据表达不同的意义,但一般使用fd来指定文件描述符。

struct epoll_event类型的定义: 

typedef union epoll_data 
{
    void      *ptr;
    int        fd;
    uint32_t   u32;
    uint64_t   u64;
} epoll_data_t;

struct epoll_event 
{
    uint32_t   events;   /* Epoll events */
    epoll_data_t data;   /* User data variable */
};
表4.2 events的可选宏及对应含义
events可选宏含义
EPOLLIN对应文件描述符可读
EPOLLOUT对应文件描述符可写
EPOLLPRI对应文件描述符带有紧急数据可读(TCP紧急指针置1的报文)
EPOLLERR对应文件描述符异常
EPOLLHUP对应文件描述符被挂起
EPOLLSHOT对应文件描述符只被监视一次,监视完一次后会移出epoll模型

2.3 epoll_wait -- 等待epoll所关注的事件就绪

函数原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

头文件:#include <sys/epoll.h>

函数参数:

  • epfd -- 进行等待的epoll模型文件描述符。
  • events -- 输出型参数,用于存放已经就绪了的事件相关的属性信息,如文件描述符fd以及就绪事件的类型(可读/可写/...)等。
  • maxevents -- 一次wait所能获取到的最大的就绪事件数量。
  • timeout -- 最长阻塞等待时间,传-1表示一直阻塞等待,传0表示完全非阻塞。

返回值:如果等待成功,返回已经就绪的事件的数量,返回0表示在设定的阻塞时间内没有事件就绪,返回-1表示等待失败。

相对于select和poll在等待成功后需要遍历整个数组来确定具体哪一个文件描述符就绪,epoll_wait等待到的就绪事件相关属性信息会被放置在events所指向的空间的前n个位置,其中n为就绪事件数量,即epoll_wait的返回值。因此,只需要遍历events[0] ~ events[n-1]即可,events[0] ~ events[n-1]中记录的事件一定是已经就绪了的

如果已经就绪了的事件多于maxevents会发生什么情况呢?这时会先拿取maxevents个已经就绪的事件,剩余的等到下一轮epoll_wait再进行提取处理,并不会造成任何错误。

2.4 epoll相关接口的使用方法

通过epoll实现IO多路转接,需要按照以下三步操作执行:

  1. 通过epoll_create创建epoll模型。
  2. 通过epoll_ctl添加对特定事件的关注。
  3. 通过epoll_wait等待所关注的事件的一个或多个就绪。

为了方便调用epoll相关的接口函数,代码2.1将epoll的3个相关接口函数进行了封装。在代码2.1中包含了log.hpp头文件,里面是日志打印函数的声明和实现,详见代码2.2。

代码2.1:对epoll接口的封装(Epoll.hpp头文件)

#pragma once

#include "log.hpp"
#include <cstring>
#include <cerrno>
#include <sys/epoll.h>

namespace Epoll
{
    static const int default_size = 1024;
    
    // 创建Epoll模型函数
    static int EpollCreate(int size = default_size)
    {
        int epfd = epoll_create(size);
        if(epfd < 0)
            logMessage(FATAL, "Epoll create fail, %d:%s\n", errno, strerror(errno));
        else
            logMessage(NORMAL, "Epoll create success, epfd:%d\n", epfd);
        return epfd;
    }

    // 进行Epoll控制函数
    static int EpollCtl(int epfd, int op, int fd, uint32_t events)
    {
        struct epoll_event event;
        event.events = EPOLLIN;
        event.data.fd = fd;

        int ret = epoll_ctl(epfd, op, fd, &event);
        if(ret < 0) logMessage(ERROR, "Epoll control fail, %d:%s\n", errno, strerror(errno));
        else logMessage(NORMAL, "Epoll control success, ret:%d\n", ret);

        return ret;
    }

    // Epoll等待函数
    static int EpollWait(int epfd, struct epoll_event* events, int maxevents, int timeout)
    {
        return epoll_wait(epfd, events, maxevents, timeout);
    }
}

代码2.2:日志打印函数的声明和实现(log.hpp头文件)

#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
 
#define DEBUG  0
#define NORMAL 1
#define WARING 2
#define ERROR  3
#define FATAL  4
 
static const char* g_levelMap[5] = 
{
    "DEBUG",
    "NORMAL",
    "WARING",
    "ERROR",
    "FATAL"
};
 
static void logMessage(int level, const char *format, ...)
{
    // 1. 输出常规部分
    time_t timeStamp = time(nullptr);
    struct tm *localTime = localtime(&timeStamp);
    printf("[%s]  %d-%d-%d, %02d:%02d:%02d\n", g_levelMap[level], localTime->tm_year, localTime->tm_mon, \
                localTime->tm_mday, localTime->tm_hour, localTime->tm_min, localTime->tm_sec);
    
    // 2. 输出用户自定义部分
    va_list args;
    va_start(args, format);
    vprintf(format, args);
    va_end(args);
}

三. Epoll服务器的模拟实现

3.1 EpollServer类的声明

在EpollServer中,要包含以下成员变量:epoll模型文件描述符、listen套接字、端口号、ip地址以及指向存放就绪事件相关属性信息的空间的指针。

要有以下成员函数:构造函数和析构函数、服务器运行函数Start、就绪事件处理函数Handler、接收客户端连接请求函数Accepter、对端数据读取函数Reciever。

#pragma once

#include "Sock.hpp"
#include "Epoll.hpp"
#include <unistd.h>

static const int maxevents = 64;

class EpollServer
{
public:
    EpollServer(uint16_t port = 8080, const std::string& ip = "");   // 构造函数
    void Start();    // 启动运行函数
    ~EpollServer();  // 析构函数

private:
    void Handler(int n);            // 就绪事件处理函数
    void Accepter(int listenSock);  // 连接接收函数
    void Reciever(int fd);          // 信息读取函数

    int _listenSock;   // 监听套接字
    int _epfd;         // epoll套接字
    uint16_t _port;    // 服务进程端口号
    std::string _ip;   // 服务器ip 
    struct epoll_event* _ptr_events;   // 指向就绪队列的指针
};

3.2 EpollServer类的实现

  • 构造函数:首先要进行关于tcp通信的常规准备工作,获取listen套接字、绑定端口号、设置监听状态,之后要创建epoll模型、开辟一块内存空间用于存放wait到的就绪事件并将listen套接字添加到epoll模型中去。
  • 析构函数:判断listen套接字和epoll模型文件描述符是否>=0,如果是,就close掉它们。再判断_ptr_events是否为nullptr,如果否,要delete[]释放动态申请的内存空间。
  • 服务器运行函数Start:常驻进程,执行while死循环,每层while循环都调用epoll_wait检测事件就绪的情况,如果epoll_wait返回值大于0,那么调用Handler函数处理就绪事件。
  • 就绪事件处理函数Handler:接收一个参数n表示已就绪事件的数量,遍历_ptr_events[0] ~ ptr_events[n-1],根据就绪的是listen文件描述符还是普通文件描述符,分类进行后续处理,listen文件描述符调用Accepter函数接收对端连接,普通文件描述符调用Reciever函数接收数据。
  • 接收对端连接函数Accepter:获取对端连接,分配文件描述符fd,并将fd添加到epoll模型中去。
  • 数据读取函数Reciever:调用read函数读取客户端发送的数据,如果read返回值>0,那么就执行对应操作处理读取到的数据,如果read返回值为0,那么表示客户端关闭,要将对应的文件描述符fd从epoll模型中删除。

代码3.2:EpollServer的实现(EpollServer.cc源文件)

#include "EpollServer.hpp"

EpollServer::EpollServer(uint16_t port, const std::string& ip)
    : _listenSock(-1), _epfd(-1), _port(port)
    , _ip(ip), _ptr_events(nullptr)
{
    // 1.获取监听套接字
    _listenSock = Sock::Socket();
    if(_listenSock < 0) exit(1);

    // 2.绑定端口号
    if(Sock::Bind(_listenSock, _ip, _port) < 0) exit(2);

    // 3.设置监听状态
    if(Sock::Listen(_listenSock) < 0) exit(3);

    // 4.创建epoll模型
    _epfd = Epoll::EpollCreate();
    if(_epfd < 0) exit(4);

    // 5.为_events开辟内存空间并进行初始化
    _ptr_events = new epoll_event[maxevents];

    // 6.将listenSock添加到epoll模型中
    if(Epoll::EpollCtl(_epfd, EPOLL_CTL_ADD, _listenSock, EPOLLIN) < 0) exit(5);

    logMessage(NORMAL, "EpollServer init success!\n");
}

// Epoll服务器启动运行函数
void EpollServer::Start()
{
    while(1)
    {
        // 对epoll进行等待
        int n = Epoll::EpollWait(_epfd, _ptr_events, maxevents, -1);

        switch(n)
        {
        case 0:
            logMessage(DEBUG, "epoll wait time out!\n");
            break;
        case -1:
            logMessage(ERROR, "epoll wait error, %d:%s\n", errno, strerror(errno));
            break;
        default:
            Handler(n);
            break;
        }
    }
}

// 析构函数
EpollServer::~EpollServer()
{
    if(_listenSock >= 0) close(_listenSock);
    if(_epfd >= 0) close(_epfd);
    if(_ptr_events) delete[] _ptr_events;
}

// 就绪事件处理函数
void EpollServer::Handler(int n)
{
    // 遍历_events,查找已经就绪的事件
    for(int i = 0; i < n; ++i)
    {
        // 分listen套接字和普通套接字两种情况讨论
        if(_ptr_events[i].data.fd == _listenSock) Accepter(_listenSock);
        else Reciever(_ptr_events[i].data.fd);
    }
}

// 连接接收函数
void EpollServer::Accepter(int listenSock)
{
    std::string cli_ip;   // 发起连接的客户端ip
    uint16_t cli_port;    // 客户端端口号
    int fd = Sock::Accept(listenSock, cli_ip, cli_port);
    if(fd < 0) return;

    // 将新增的fd添加到epoll模型中去
    Epoll::EpollCtl(_epfd, EPOLL_CTL_ADD, fd, EPOLLIN);
    logMessage(NORMAL, "Add a new fd to epoll success, fd:%d\n", fd);
}

// 信息读取函数
void EpollServer::Reciever(int fd)
{
    char buffer[1024];
    ssize_t n = read(fd, buffer, 1023);

    // 如果读取成功
    if(n > 0)
    {
        buffer[n - 1] = '\0';
        printf("Client# %s\n", buffer);
    }
    else if(n == 0)
    {
        // 对端关闭,将fd从epoll模型中拿走
        Epoll::EpollCtl(_epfd, EPOLL_CTL_DEL, fd, EPOLLIN);
        if(n == 0)
        {
            // logMessage(NORMAL, "Remove fd from epoll success, fd:%d\n", fd);
            printf("Remove fd from epoll success, fd:%d\n", fd);
            close(fd);
        }
    }
    else  // 读取数据失败
    {
        logMessage(ERROR, "Read message fail, %d:%s\n", errno, strerror(errno));
    }
}

四. 总结

  • 相比于通过select和poll实现IO多路转接,epoll不需要程序员维护数组来控制关注的事件,在有事件就绪后也不需要遍历数组查找具体哪个事件就绪,效率较高。
  • epoll底层维护一颗红黑树存储要关心的事件,维护一个就绪队列表示已经就绪的事件。
  • epoll实现IO多路转接,要进行的操作为:epoll_create创建epoll模型 -> epoll_ctl向epoll模型中添加受到关注的文件描述符 -> epoll_wait等待事件就绪。

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

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

相关文章

模拟退火算法应用——求解函数的最小值

仅作自己学习使用 一、问题 需求&#xff1a; 计算函数 的极小值&#xff0c;其中个体x的维数n10&#xff0c;即x(x1,x2,…,x10)&#xff0c;其中每一个分量xi均需在[-20,20]内。因此可以知道&#xff0c;这个函数只有一个极小值点x (0,0,…,0)&#xff0c;且其极小值是0&…

智能对话手机版系统源码:附带完整的搭建教程

移动设备的普及和移动互联网的发展&#xff0c;手机已经成为人们日常生活中最常用的通信工具。因此&#xff0c;针对手机平台开发智能对话系统具有非常重要的意义。首先&#xff0c;手机用户可以更加方便地进行语音对话和信息交互&#xff0c;提高沟通效率。其次&#xff0c;手…

ctfshow sql

180 过滤%23 %23被过滤&#xff0c;没办法注释了&#xff0c;还可以用’1’1来闭合后边。 或者使用--%0c-- 1%0corder%0cby%0c3--%0c--1%0cunion%0cselect%0c1,2,database()--%0c--1%0cunion%0cselect%0c1,2,table_name%0cfrom%0cinformation_schema.tables%0cwhere%0ctable_…

Redis常用操作及应用(一)

一、五种数据结构 二、String结构 1、字符串常用操作 SET key value //存入字符串键值对 MSET key value [key value ...] //批量存储字符串键值对 SETNX key value //存入一个不存在的字符串键值对 GET key //获取一个字符串键值 MGET key [ke…

超好用的制作照片书神器,强到没朋友

在这个快节奏的时代&#xff0c;我们常常忙于工作和生活&#xff0c;而忽略了那些珍贵的回忆。现在&#xff0c;有了超好用的制作照片书神器&#xff0c;你可以轻松地将这些回忆化为实实在在的书册&#xff0c;随时翻阅&#xff0c;感受那份温暖和感动。 接下来给大家推荐一款神…

开源还是闭源(=°Д°=)!!趋势表明,开源技术在诸多领域中日益受到重视

开源和闭源&#xff0c;两种截然不同的开发模式&#xff0c;对于大模型的发展有着重要影响。开源让技术共享&#xff0c;吸引了众多人才加入&#xff0c;推动了大模的创新。而闭源则保护了商业利益和技术优势&#xff0c;为大模型的商业应用提供了更好的保障。 一、开源和闭源的…

长征故事vr互动教育体验系统让师生感同身受

红色文化是贯穿于新民主主义革命、社会主义建设的各个时期&#xff0c;具有深厚的历史价值和文化内涵的先进文化&#xff0c;是高效、优质的教育资源。思政课vr红色数字展馆充分开发和大力弘扬红色文化资源&#xff0c;发挥其独特资源优势和教育功能&#xff0c;应用到教学中&a…

【学习草稿】pid控制基础实现--往水桶注水

pid 1&#xff09;非常通俗易懂的PID控制&#xff08;1&#xff09;https://zhuanlan.zhihu.com/p/37515841 球场上运动至指定地点&#xff08;比例控制&#xff09;&#xff1a;有图【很直观的帮助理解】&有文字分析 2&#xff09;初识PID-搞懂PID概念 https://zhuanlan.…

leetcode刷题日志-11盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xff1a;你不能倾斜容器。 思路…

LeetCode.88合并两个有序数组

LeetCode.88合并两个有序数组 1.问题描述2.解题思路3.代码 1.问题描述 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m 和 n &#xff0c;分别表示 nums1 和 nums2 中的元素数目。 请你 合并 nums2 到 nums1 中&#xff0c;使合并后的数组同…

完美解决:Nginx访问PHP出现File not found.

目录 解决方法一&#xff1a; 解决方法二&#xff1a; 遇到 File not found. 出现的问题解决&#xff1a; 解决方法一&#xff1a; 修改nginx的主配置文件。 vi /etc/nginx/nginx.conf location ~ \.php$ { root html; fastcgi_pass …

人工智能-注意力机制之注意力提示

注意力提示 自经济学研究稀缺资源分配以来&#xff0c;人们正处在“注意力经济”时代&#xff0c; 即人类的注意力被视为可以交换的、有限的、有价值的且稀缺的商品。 许多商业模式也被开发出来去利用这一点&#xff1a; 在音乐或视频流媒体服务上&#xff0c;人们要么消耗注意…

读懂毛京波的营销,也就读懂了路特斯的提速转变

“尽管我们交付还不到一年时间&#xff0c;但是今天Emeya一发布&#xff0c;我们的三车主又诞生了&#xff0c;他有Emira、Eletre&#xff0c;马上又定了Emeya&#xff0c;说明他对路特斯还是非常认可的&#xff0c;”路特斯集团CEO冯擎峰欣喜地说道&#xff0c;一个产品能不能…

《C++PrimePlus》第8章 函数探幽

8.1 内联函数 使用内联函数 #include <iostream> using namespace std;inline double square(double x) { return x * x; }int main(){double a;a square(5.0);cout << "a " << a << endl;return 0; } 8.2 引用变量 将引用用作函数参数&…

在JVM中 判定哪些对象是垃圾?

目录 垃圾的条件 1、引用计数法 2、可达性分析 3、强引用 4、软引用 5、弱引用 6、虚引用 判断垃圾的条件 在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;垃圾收集器负责管理内存&#xff0c;其中的垃圾收集算法用于确定哪些对象是垃圾&#xff0c;可以被回收…

0002Java程序设计-springboot在线考试系统小程序

文章目录 **摘 要****目录**系统实现开发环境 编程技术交流、源码分享、模板分享、网课分享 企鹅&#x1f427;裙&#xff1a;776871563 摘 要 本毕业设计的内容是设计并且实现一个基于springboot的在线考试系统小程序。它是在Windows下&#xff0c;以MYSQL为数据库开发平台&…

记一次linux操作系统实验

前言 最近完成了一个需要修改和编译linux内核源码的操作系统实验&#xff0c;个人感觉这个实验还是比较有意思的。这次实验总共耗时4天&#xff0c;从对linux实现零基础&#xff0c;通过查阅资料和不断尝试&#xff0c;直到完成实验目标&#xff0c;在这过程中确实也收获颇丰&…

井盖位移监测系统怎么监测井盖位移

党的二十大报告提出&#xff0c;坚持人民城市人民建、人民城市为人民&#xff0c;提高城市规划、建设、治理水平。秉持依法治理、创新引领的理念&#xff0c;市政府应该坚定推进窨井盖安全管理工作&#xff0c;不断加大排查整治力度&#xff0c;弥补设施安全管理短板&#xff0…

每日一练:“打家劫舍“(House Robber)问题 II

有想要了解打家劫舍初级问题的&#xff0c;可以点击下面链接查看&#xff01; 每日一练&#xff1a;“打家劫舍“&#xff08;House Robber&#xff09;问题 I 1. 问题 假设有房屋形成一个环形&#xff0c;即第一个房屋和最后一个房屋也相邻&#xff0c;每个房屋里都存放着一定…

Java数组的复制、截取(内含例题:力扣-189.轮转数组)

目录 数组的复制、截取&#xff1a; 1、使用Arrays中的copyOf方法完成数组的拷贝 2、使用Arrays中的copyofRange方法完成数组的拷贝 题目链接&#xff1a; 数组的复制、截取&#xff1a; 1、使用Arrays中的copyOf方法完成数组的拷贝 public class Csdn {public static vo…