[Linux]:IO多路转接之epoll

news2024/12/23 0:32:41

1. IO 多路转接之epoll

1.1 epoll概述

epoll是Linux内核为处理大规模并发网络连接而设计的高效I/O多路转接技术。它基于事件驱动模型,通过在内核中维护一个事件表,能够快速响应多个文件描述符上的I/O事件,如可读、可写、异常等,避免了像selectpoll那样频繁地遍历文件描述符集合,从而大大提高了系统的性能和响应速度。epoll主要适用于需要处理大量并发连接的服务器应用场景,如Web服务器、邮件服务器、实时通信服务器等,能够轻松应对高并发情况下的I/O处理需求,为用户提供高效、稳定的服务。

1.2 epoll相关函数

1.2.1 epoll_create
  1. 函数原型:int epoll_create(int size);
  2. 参数说明:
    • size:从Linux 2.6.8版本开始,该参数已被忽略,但必须大于0。
  3. 返回值说明:
    • 成功时返回一个非负的文件描述符,用于后续的epoll操作。这个文件描述符将指向内核中的epoll实例。
    • 失败时返回 -1,并设置相应的错误码。可能的错误码包括ENOMEM(内存不足)等。
1.2.2 epoll_ctl函数
  1. 函数原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  2. 参数说明:
    • epfdepoll_create返回的epoll文件描述符。
    • op:操作类型,有以下几种取值:
      • EPOLL_CTL_ADD:向epoll实例中添加一个文件描述符及其关注的事件。
      • EPOLL_CTL_MOD:修改已注册文件描述符关注的事件。
      • EPOLL_CTL_DEL:从epoll实例中删除一个文件描述符。
    • fd:要操作的文件描述符。
    • event:指向struct epoll_event结构体的指针,用于指定要关注的事件类型和相关数据。
  3. 返回值说明:
    • 成功时返回0。
    • 失败时返回 -1,并设置相应的错误码。可能的错误码包括EBADF(文件描述符无效)、EEXIST(添加已存在的文件描述符)、ENOENT(删除不存在的文件描述符)、ENOMEM(内存不足)等。

struct epoll_event结构体用于描述epoll事件,其定义如下:

struct epoll_event {
    uint32_t events;  // 表示关注的事件类型,如EPOLLIN(可读)、EPOLLOUT(可写)等
    epoll_data_t data;  // 用于存储与文件描述符相关的用户数据
};

// epoll_data_t结构体定义
typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

其中struct epoll_event 结构中有两个成员,第一个成员 events 表示的是需要监视的事件,第二个成员 data 是一个联合体结构,一般选择使用该结构当中的 fd,表示需要监听的文件描述符。

其中 events 的常用取值如下:

事件取值含义
EPOLLIN表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT表示对应的文件描述符可以写
EPOLLPRI表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR表示对应的文件描述符发送错误
EPOLLHUP表示对应的文件描述符被挂断,即对端将文件描述符关闭了
EPOLLET将epoll的工作方式设置为边缘触发(Edge Triggered)模式
EPOLLONESHOT只监听一次事件,当监听完这次事件之后,如果还需要继续监听该文件描述符的话,需要重新将该文件描述符添加到epoll模型中

这些取值实际都是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。所以可以通过按位或操作符实现同时关心两种方式。

1.2.3 epoll_wait函数
  1. 函数原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  2. 参数说明:
    • epfdepoll_create返回的epoll文件描述符。
    • events:指向struct epoll_event结构体数组的指针,用于接收就绪事件的信息。
    • maxeventsevents数组的大小,即最多能接收的就绪事件数量,该值不能大于创建 epoll 模型时传入的 ``size` 值。
    • timeout:超时时间,单位为毫秒。取值如下:
      • -1:阻塞等待,直到有事件就绪或捕获到信号。
      • 0:非阻塞等待,立即返回,无论是否有事件就绪。
      • 大于0:在指定时间内阻塞等待,超时后返回。
  3. 返回值说明:
    • 成功时返回就绪事件的数量。
    • 超时返回0。
    • 失败返回 -1,并设置相应的错误码。可能的错误码包括EBADF(文件描述符无效)、EFAULTevents数组指针无效)、EINTR(被信号中断)等。

1.3 epoll 的工作原理

epoll 的工作原理是基于eventpoll结构体。当进程调用epoll_create函数时,内核创建eventpoll结构体,其中有红黑树rbr和就绪队列rdlist

struct eventpoll{
	//...
	//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监视的事件
	struct rb_root rbr;
	//就绪队列中则存放着将要通过epoll_wait返回给用户的满足条件的事件
	struct list_head rdlist;
	//...
}

红黑树存储要监视的事件对应的文件描述符和事件类型,epoll_ctl函数用于对红黑树进行增删改操作。文件描述符可作为红黑树的键值,设置EPOLLONESHOT选项的事件就绪后会从红黑树自动删除,没设置则一直存在,除非手动删除。

就绪队列存放已就绪的事件,epoll_wait函数用于从就绪队列获取这些事件。每个事件对应一个epitem结构体,红黑树和就绪队列的节点分别基于epitem中的rbnrdllink成员,ffd记录文件描述符,event记录事件类型。

struct epitem{
	struct rb_node rbn; //红黑树节点
	struct list_head rdllink; //双向链表节点
	struct epoll_filefd ffd; //事件句柄信息
	struct eventpoll *ep; //指向其所属的eventpoll对象
	struct epoll_event event; //期待发生的事件类型
}

在回调机制方面,添加到红黑树的事件会和设备驱动程序建立ep_poll_callback回调方法。与selectpoll不同,epoll不需要操作系统主动轮询检测事件是否就绪。当监视的事件就绪时,自动调用回调方法,将事件添加到就绪队列。

总结来说,epoll的使用过程就是三步:首先用epoll_create创建epoll模型;接着用epoll_ctl注册要监控的文件描述符;最后用epoll_wait等待文件描述符就绪。而且因为就绪队列可能被多个执行流访问,eventpoll结构中有用于保护的锁,本身是线程安全的,还涉及等待队列wq来处理多个执行流访问同一epoll模型的情况。

1.4 服务端代码

然后我们就可以编写一个基于 epoll多路转接的 TCP 服务端,并且编写流程也与 select服务端类似:

#include "Sock.hpp"
#include <sys/epoll.h>

#define NUM 1024

// EpollServer类,用于实现基于epoll的服务器功能
class EpollServer
{
public:
    // 构造函数,初始化服务器监听端口
    EpollServer(int port)
        : _port(port)
    {
    }

    // 初始化服务器相关设置,包括创建、绑定和监听套接字,同时创建epoll模型
    void InitPollServer()
    {
        // 创建套接字
        _listensock.Socket();
        // 将套接字绑定到指定端口
        _listensock.Bind(_port);
        // 开始监听套接字
        _listensock.Listen();

        // 创建epoll模型,参数NUM用于指定监听事件数量的上限
        _epfd = epoll_create(NUM);
        if (_epfd < 0)
        {
            // 如果创建失败,输出错误信息并退出程序
            std::cerr << "epoll_create error" << std::endl;
            exit(4);
        }
    }

    // 运行服务器,处理客户端连接和数据读取等操作
    void Run()
    {
        // 将监听套接字添加到epoll模型中,监听可读事件
        AddEvent(_listensock.Fd(), EPOLLIN);
        while (true)
        {
            struct epoll_event revs[NUM];
            int num;
            // 调用epoll_wait等待事件发生,根据返回值进行不同处理
            switch (num = epoll_wait(_epfd, revs, NUM, -1))
            {
            case 0:
                // epoll_wait返回0,表示超时
                std::cout << "time out..." << std::endl;
                break;
            case -1:
                // epoll_wait返回-1,表示发生错误
                std::cerr << "epoll error" << std::endl;
                break;
            default:
                // epoll_wait返回其他值,表示有就绪事件,调用处理函数进行处理
                HandlerEvent(revs, num);
                break;
            }
        }
    }

    // 析构函数,关闭监听套接字和epoll模型对应的文件描述符
    ~EpollServer()
    {
        if (_listensock.Fd() >= 0)
        {
            _listensock.Close();
        }
        if (_epfd >= 0)
        {
            close(_epfd);
        }
    }

private:
    // 处理就绪事件的函数
    void HandlerEvent(struct epoll_event revs[], int num)
    {
        for (int i = 0; i < num; i++)
        {
            int fd = revs[i].data.fd;
            // 如果是监听套接字且有可读事件,表示有新的客户端连接
            if (fd == _listensock.Fd() && revs[i].events & EPOLLIN)
            {
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                memset(&peer, 0, len);
                std::string clientip;
                uint16_t clientport;

                // 接受新的客户端连接,获取客户端的套接字描述符、IP地址和端口号
                int sock = _listensock.Accept(&clientip, &clientport);
                std::cout << "get a new link[" << clientip << ":" << clientport << "]" << std::endl;

                // 将新连接的套接字添加到epoll模型中,监听可读事件
                AddEvent(sock, EPOLLIN);
            }

            // 如果不是监听套接字且有可读事件,表示有数据可读,进行数据读取和处理
            else if (revs[i].events & EPOLLIN)
            {
                char buffer[1024];
                ssize_t n = read(fd, buffer, sizeof(buffer) - 1);

                if (n > 0)
                {
                    // 如果读取到数据,添加字符串结束符并输出数据内容
                    buffer[n] = 0;
                    std::cout << "echo# " << buffer << std::endl;
                }
                else if (n == 0)
                {
                    // 如果读取到的字节数为0,表示客户端已断开连接,关闭对应的套接字并从epoll模型中删除该事件
                    std::cout << "client quit..." << std::endl;
                    close(fd);
                    epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
                }
                else
                {
                    // 如果读取发生错误,输出错误信息,关闭对应的套接字并从epoll模型中删除该事件
                    std::cerr << "read error" << std::endl;
                    close(fd);
                    epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
                }
            }
        }
    }

    // 将指定套接字添加到epoll模型中,设置要监听的事件类型
    void AddEvent(int sockfd, uint32_t event)
    {
        struct epoll_event ev;
        ev.events = event;
        ev.data.fd = sockfd;
        // 使用epoll_ctl函数将套接字和事件添加到epoll模型中,操作类型为添加
        epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
    }

private:
    Sock _listensock;
    int _port;
    int _epfd; // epoll模型对应的文件描述符
};

我们同样可以使用 telnet进行测试。

1.5 epoll工作模式

epoll有水平触发(LT,Level Triggered)和边缘触发(ET,Edge Triggered)两种工作方式,这和数字电路中的高电平触发、上升沿触发类似。

epoll 默认工作模式是水平触发(LT)模式。

1.5.1 水平触发模式

当文件描述符就绪时,epoll_wait会返回该文件描述符,并且如果应用程序没有对该文件描述符进行完全的读写操作(即缓冲区数据没有被全部处理完),那么下次epoll_wait调用时仍然会立即返回该文件描述符,直到应用程序完成对该文件描述符的I/O操作,使得其状态不再就绪。

例如,一个套接字有可读数据(接收缓冲区中有数据),在水平触发模式下,epoll_wait会一直通知应用程序该套接字可读,直到应用程序将接收缓冲区中的数据全部读取完。

1.5.2 边缘触发模式

如果想将 epoll 改为ET工作模式,则需要在添加事件时设置EPOLLET选项。

当文件描述符的状态发生变化(如从不可读到可读,或从不可写到可写)时,epoll_wait会返回该文件描述符。之后,只有当该文件描述符的状态再次发生变化时,epoll_wait才会再次通知应用程序。这意味着应用程序在处理事件时必须一次性将数据读取完或写入完,否则可能会错过后续的I/O事件。

对于一个监听套接字,当有新的连接请求到来时(从无连接请求到有连接请求,状态变化),epoll_wait会返回该监听套接字的就绪事件。如果应用程序接受了连接并得到新的连接套接字,那么这个新的连接套接字在有数据可读(从无数据到有数据,状态变化)时,epoll_wait会通知应用程序。但如果应用程序在处理可读事件时没有将接收缓冲区中的数据全部读取完,下次epoll_wait不会再次通知,直到有新的数据到达导致状态再次变化。

epoll 的 ET(边缘触发)工作模式中,仅当底层就绪事件从无到有或从有到更多时才通知用户。这就要求用户在读事件就绪时需一次性读完所有数据,写事件就绪时要一次性写满发送缓冲区,否则可能因为此后底层再也没有事件就绪而失去读写机会。

对于读操作,要循环调用 recv 函数。当底层读事件就绪,持续循环调用 recv,直至某次调用时实际读取字节数小于期望读取字节数,这表明底层数据已读完。不过,存在一种情况,即最后一次调用 recv 时实际读取字节数和期望读取字节数相等,但此时底层数据恰好也读完了,若再次调用 recv,由于底层无数据,recv 函数将阻塞。这种阻塞问题严重,以单进程服务器为例,若 recv 阻塞且数据不再就绪,服务器就相当于瘫痪了。因此,在 ET 模式下循环调用 recv 读取数据时,必须将对应的文件描述符设置为非阻塞状态。

写操作同理,写数据时要循环调用 send 函数,并且也要将对应的文件描述符设置为非阻塞状态。总之,在 ET 工作模式下,recvsend 操作的文件描述符必须设置为非阻塞状态,这是强制要求。

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

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

相关文章

Spring Security 认证流程,长话简说

一、代码先行 1、设计模式 SpringSecurity 采用的是 责任链 的设计模式&#xff0c;是一堆过滤器链的组合&#xff0c;它有一条很长的过滤器链。 不过我们不需要去仔细了解每一个过滤器的含义和用法,只需要搞定以下几个问题即可&#xff1a;怎么登录、怎么校验账户、认证失败…

API 接口进行多分支管理的方法

原文链接&#xff1a;API 接口进行多分支管理的方法

链表类算法【leetcode】

链表的定义 面试时&#xff0c;需要自己手写... // 单链表 struct ListNode {int val; // 节点上存储的元素ListNode *next; // 指向下一个节点的指针ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数 }; 【构造函数可以省略&#xff0c;C默认生成一个构造函数…

重构开发之道,Blackbox.AI为技术注入智能新动力

本文目录 一、引言二、Blackbox.AI实战体验2.1 基于网页界面生成前端代码进行应用开发2.2 与AI助手实现实时智能对话2.3 重塑大型文件交互方式2.4 链接Github仓库进行对话编程 三、总结 一、引言 在生产力工具加速进化的浪潮中&#xff0c;Blackbox.AI开始崭露头角&#xff0c…

【STM32F1】——9轴姿态传感器JY901与IIC通信

【STM32F1】——9轴姿态传感器JY901与IIC通信 一、简介 本篇主要对9轴姿态传感器JY901的调试过程进行总结,实现了以下功能。 IIC通信采集+串口收发:使用STM32F103C8T6的GPIO口模拟IIC,从JY901读取数据,并通过USART1串口发送到PC。二、JY901介绍 电压:3.3-5V量程:X/Z轴 …

Linux网络——自定义协议与序列化

一、协议 协议是一种 " 约定 ". socket api 的接口 , 在读写数据时 , 都是按 " 字符串 " 的方式来发送接收的。如 果我们要传输一些 " 结构化的数据 "&#xff0c;依然可以通过协议。 其实&#xff0c;协议就是双方约定好的结构化的数据。…

Windows,虚拟机Ubuntu和开发板三者之间的NFS服务器搭建

Windows,虚拟机Ubuntu和开发板三者之间的NFS服务器搭建 &#xff08;1&#xff09;虚拟机 ubuntu 要使用桥接模式&#xff0c;不能使用其他模式 &#xff08;2&#xff09;通过网线将PC和开发板网口直连:这样的连接&#xff0c;开发板是无法连接外网的 &#xff08;3&#xff…

更改Ubuntu22.04锁屏壁纸

更改Ubuntu22.04锁屏壁纸 sudo apt install gnome-shell-extensions gnome-shell-extension-manager安装Gnome Shell 扩展管理器后&#xff0c;打开“扩展管理器”并使用搜索栏找到“锁屏背景”扩展

大模型推理优化技术-KV Cache

近两年大模型火出天际&#xff1b;同时&#xff0c;也诞生了大量针对大模型的优化技术。本系列将针对一些常见大模型优化技术进行讲解。 大模型推理优化技术-KV Cache大模型推理服务调度优化技术-Continuous batching大模型底显存推理优化-Offload技术大模型推理优化技术-KV C…

力扣 LeetCode 24. 两两交换链表中的节点(Day2:链表)

解题思路&#xff1a; 暂存节点tmp和tmp1 注意&#xff1a;while (cur.next ! null && cur.next.next ! null)表示为偶数和奇数时的循环停止条件&#xff0c;并且while语句中的顺序不可交换&#xff0c;交换会报空指针异常 class Solution {public ListNode swapPai…

动态规划-背包问题——494.目标和

1.状态表示 题目来源 494.目标和——力扣 测试用例 2.算法原理 1.状态表示 首先我们需要将问题简化&#xff0c;这里需要找到能将数组组合计算成为指定数字target的添加方式&#xff0c;那么我们就可以将数字分为两类&#xff0c;一类是前面添加""的&#xff0c;另…

哪些因素会导致充电器的充电速度变慢?-纳米软件

充电器的充电速度变慢可能由多种原因引起。以下是一些常见的因素&#xff1a; 一、充电器本身的问题 充电头功率不足&#xff1a;不同的充电头有不同的输出功率&#xff0c;如果使用的充电头功率较低&#xff0c;那么充电速度就会变慢。例如&#xff0c;一些老旧的充电头可能…

刷题强训(day06) -- 大数加法、链表相加、大数乘法

目录 1、大数加法 1.1 题目 1.2 思路 1.3 代码实现 2、链表相加&#xff08;二&#xff09; 2.1 题目 2.2 思路 2.3 代码实现 3、大数乘法 3.1 题目 3.2 思路 3.3 代码实现 1、大数加法 1.1 题目 1.2 思路 这道题可以模拟列竖式相加解答&#xff0c; 将每一位都转…

数字后端教程之Innovus report_property和get_property使用方法及应用案例

数字IC后端实现Innovus中使用report_property可以报告出各种各样object的属性&#xff0c;主要有cell&#xff0c;net&#xff0c;PG Net&#xff0c;Pin&#xff0c;时钟clock&#xff0c;时序库lib属性&#xff0c;Design属性&#xff0c;timing path&#xff0c;timin arc等…

网络基础 - 网段划分篇

我们知道&#xff0c;IP 地址(IPv4 地址)由 “网络标识(网络地址)” 和 “主机标识(主机地址)” 两部分组成&#xff0c;例如 192.168.128.10/24&#xff0c;其中的 “/24” 表示从第 1 位开始到多少位属于网络标识&#xff0c;那么&#xff0c;剩余位就属于主机标识了&#xf…

python实战(八)——情感识别(多分类)

一、任务目标 本文使用的是来自Kaggle的一个情感识别数据集&#xff0c;这个数据集的总数据量是5934条&#xff0c;标签为anger、fear、joy三种情感的其中一种&#xff0c;很明显是一个多分类任务。这里&#xff0c;我们将使用微调技巧进行深度学习建模&#xff0c;同时我们会比…

23423234

c语言中的小小白-CSDN博客c语言中的小小白关注算法,c,c语言,贪心算法,链表,mysql,动态规划,后端,线性回归,数据结构,排序算法领域.https://blog.csdn.net/bhbcdxb123?spm1001.2014.3001.5343 给大家分享一句我很喜欢我话&#xff1a; 知不足而奋进&#xff0c;望远山而前行&am…

opencv入门学习总结

opencv学习总结 不多bb&#xff0c;直接上代码&#xff01;&#xff01;&#xff01; 案例一&#xff1a; import cv2 # 返回当前安装的 OpenCV 库的版本信息 并且是字符串格式 print(cv2.getVersionString()) """ 作用&#xff1a;它可以读取不同格式的图像文…

MySQL 中的索引下推功能

看到索引&#xff0c;应该大家都可以联想到这个是和查询效率有关系的&#xff0c;既然有这个功能&#xff0c;那么那句古话说的好啊&#xff1a;存在即合理。那么这个就是说有了这个功能&#xff0c;可以提升查询效率。 什么是索引下推 我们先有一个大概的理解&#xff1a;在…

重拾CSS,前端样式精读-媒体查询

前言 本文收录于CSS系列文章中&#xff0c;欢迎阅读指正 说到媒体查询&#xff0c;大家首先想到的可能是有关响应式的知识点&#xff0c;除此之外&#xff0c;它还可以用于条件加载资源&#xff0c;字体大小&#xff0c;图像和视频的优化&#xff0c;用户界面调整等等方面&am…