当前位置: 首页 > article >正文

[Linux]:IO多路转接之epoll

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.kler.cn/a/392246.html

相关文章:

  • 力扣104 : 二叉树最大深度
  • 更改Ubuntu22.04锁屏壁纸
  • Vue 项目打包后环境变量丢失问题(清除缓存),区分.env和.env.*文件
  • 【C#设计模式(8)——过滤器模式(Adapter Pattern)】
  • 如何用WordPress和Shopify提升SEO表现?
  • 系统架构设计师论文
  • 鸿蒙next版开发:订阅应用事件(ArkTS)
  • EasyExcel 使用多线程按顺序导出数据
  • linux GPIO
  • 【Linux】进程状态与进程优先级
  • torch jit 动态获取buffer
  • upload-labs通关练习
  • 闲鱼监控助手货源获取技巧(轻松找到优质货源的方法)
  • 【大数据测试spark+kafka-详细教程(附带实例)】
  • Unity3D设置3D物体不超出相机视角范围(物体一直保持在相机视角范围内)
  • Android S长按文件或视频或编辑中文字或输入框中文字不会弹出分享菜单
  • 零基础入门转录组下游分析——预后模型之多因素cox模型
  • 小西作业1_third order plant(SPM)
  • Linux也有百度云喔~
  • 在Java中使用ModelMapper简化Shapefile属性转JavaBean实战
  • 信令服务器设计之websocket基础
  • BERT配置详解1:构建强大的自然语言处理模型
  • 【Echarts图轮播显示label】
  • PHP动物收容所管理系统-计算机设计毕业源码94164
  • 初阶C++之C++入门基础
  • OKG Research:用户意图驱动的Web3应用变革