Epoll模型

Epoll模型

一、底层核心原理

每一个epoll模型内部都维护了一棵红黑树,当调用epoll_ctl并执行EPOLL_CTL_ADD操作时,会将新增的文件描述符(fd)及关联数据构造成一个新结点,存入这棵红黑树中。此外epoll模型中还维护了一个就绪队列 :当文件描述符上有事件就绪时,底层会触发中断,操作系统会将该文件描述符对应的红黑树结点放入就绪队列中。当执行epoll_wait时,就能直接拿到所有已经就绪的事件对应的fd,由程序员自行处理。

二、为什么用红黑树存储 fd 数据?

因为epoll模型可监测的 fd 数量没有硬上限,当存在大量 fd 时,使用红黑树既可以对 fd 进行高效管理,也能实现快速查找与增删操作。

三、事件结点如何加入就绪队列?

红黑树的每一个结点中,存储的不只有 fd,还包含其他指针字段。当某个结点有事件就绪时,不需要重新拷贝整个结点再添加到就绪队列,只需要通过结点内的就绪队列指针,将结点链入就绪队列即可(头插或尾插方式),全程无需拷贝结点数据。

四、epoll IO 多路转接的整体使用逻辑

epoll 是 IO 多路转接模型的一种实现,相比 select/poll 效率更高。

使用 epoll 的大致流程为:

  1. 调用epoll_create创建一个 epoll 模型,获取该模型的句柄(本质也是一个文件描述符)

  2. 调用epoll_ctl,将需要监听的文件描述符添加到 epoll 模型中进行事件检测

  3. 在事件循环中调用epoll_wait接口,等待就绪的文件描述符

五、核心接口详细介绍
1. epoll_create

功能:创建一个 epoll 模型实例,返回该实例的句柄(文件描述符)。

说明:该函数的参数在新版本内核中已被弃用,传入一个大于 0 的数字即可。

函数原型:

c++ 复制代码
#include <sys/epoll.h> 
int epoll_create(int size);

使用样例:

c++ 复制代码
// 1.创建epoll模型 
_epfd = epoll_create(128); 
if(_epfd < 0) 
{ 
    std::cout << "epoll create error, " << strerror(errno) << std::endl;
    exit(-1); 
}
2. epoll_ctl

功能:通过 epoll 句柄,对被监听的文件描述符执行增、删、改操作。成功返回 0,失败返回 - 1。

函数原型:

c++ 复制代码
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数说明:

  • epfd:epoll 模型的句柄

  • op:操作类型

    • EPOLL_CTL_ADD:将 fd 添加到 epoll 模型中进行监听

    • EPOLL_CTL_MOD:修改 fd 对应的监听事件

    • EPOLL_CTL_DEL:将 fd 从 epoll 模型中移除

  • fd:目标文件描述符

  • event:需要监听的事件结构体

其中epoll_event结构体定义如下:

  • events字段:用于设置需要监听的事件,常用的有EPOLLIN(可读事件)、EPOLLOUT(可写事件)
  • data字段:用户自定义数据,一般将 fd 存入该字段的fd成员中
c++ 复制代码
struct epoll_event 
{ 
    uint32_t events; /* Epoll events */ 
    epoll_data_t data; /* User data variable */ 
}; 
typedef union epoll_data 
{ 
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;

使用样例:

c++ 复制代码
// 2.将listen套接字加入到epoll模型中 
// 先构建一个事件结构体 
struct epoll_event ev;
ev.events |= EPOLLIN;
ev.data.fd = _socket->GetSockfd();
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _socket->GetSockfd(), &ev); // 成功返回0 
if(n != 0) 
{
    std::cout << "epoll control error, " << strerror(errno) << std::endl;
    exit(-1);
}
3. epoll_wait

功能:检查 epoll 模型中是否有文件描述符的事件就绪。如果有就绪事件,会将这些就绪的 epoll 事件放到events缓冲区中;成功时返回就绪事件的个数,超时返回 0,出错返回 - 1。

函数原型:

c++ 复制代码
#include <sys/epoll.h> 
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

参数说明:

  • epfd:epoll 模型的句柄

  • events:存放就绪事件的输出缓冲区

  • maxevents:缓冲区最多能容纳的事件个数

  • timeout:超时时间,单位是毫秒

使用样例:

c++ 复制代码
// 定义常量,表示就绪事件缓冲区的最大容量
const static int gnum = 64;    
// 定义存放就绪事件的缓冲区
struct epoll_event _events[gnum];
// 调用epoll_wait进行等待,超过1秒就会返回,继续向下执行
int n = epoll_wait(_epfd, _events, gnum, 1000);
if(n == 0)
{
    // 没有事件就绪,超时返回
    std::cout << "time out..." << std::endl;
}
else if(n > 0)
{
    // 有n个事件就绪,开始处理事件
    HandlerEvents(n);
}
else
{
    if(errno == EINTR)
        continue;
    std::cout << "epoll wait fail, " << strerror(errno) << std::endl;
    exit(-1);
}

代码样例------EpollServerDemo

EpollServer.hpp

c++ 复制代码
#pragma once
#include <iostream>
#include <memory>
#include "TcpSocket.hpp"
#include <unordered_map>
#include <unistd.h>
#include <sys/epoll.h>

class EpollServer
{
private:
    const static int N=sizeof(fd_set)*8;
    const static int defaultfd=-1;
    const static int gnum=64;

public:
    EpollServer(uint16_t port)
        :_port(port)
        ,_socket(std::make_unique<TcpSocket>(port))
        ,_epfd(defaultfd)
    {
        // 1.创建epoll模型
        _epfd=epoll_create(128);
        if(_epfd<0)
        {
            std::cout<<"epoll create error, "<<strerror(errno)<<std::endl;            
            exit(-1);
        }

        // 2.将listen套接字加入到epoll模型中
        // 先构建一个事件结构体
        struct epoll_event ev;
        ev.events|=EPOLLIN;
        ev.data.fd=_socket->GetSockfd();
        int n =epoll_ctl(_epfd, EPOLL_CTL_ADD, _socket->GetSockfd(), &ev);  // 成功返回0
        if(n!=0)
        {
            std::cout<<"epoll control error, "<<strerror(errno)<<std::endl;            
            exit(-1);
        }
    }

    void HandlerEvents(int num)
    {
        std::cout<<"有Num:"<<num<<"个事件就绪了"<<std::endl;
    }
    void Loop()
    {
        while(true)
        {
            int n=epoll_wait(_epfd, _events, gnum, 1000);
            if(n==0)
            {
                // 事件没有就绪
                std::cout<<"time out..."<<std::endl;
            }
            else if(n>0)
            {
                // 有n个事件就绪了
                HandlerEvents(n);
            }
            else
            {
                if(errno==EINTR)
                    continue;
                std::cout<<"epoll wait fail, "<<strerror(errno)<<std::endl;
                exit(-1);
            }
        }
    }
private:
    int _epfd;
    uint16_t _port;
    std::unique_ptr<TcpSocket> _socket;
    struct epoll_event _events[gnum];
};

TcpSocket.hpp

c++ 复制代码
#pragma once
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>


class InetAddr
{
public:
    InetAddr()
    {}
    InetAddr(const std::string&ip, uint16_t port)
        :_ip(ip)
        ,_port(port)
    {
        memset(&_sockaddr, 0, sizeof(_sockaddr));
        _sockaddr.sin_family=AF_INET;
        _sockaddr.sin_port=htons(port);
        _sockaddr.sin_addr.s_addr=inet_addr(_ip.c_str());
    }

    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }

    // 将addr里面的数据,拷贝给自己
    void GetInetAddr(sockaddr_in*addr)
    {
        memcpy(&_sockaddr, addr, sizeof(*addr));
        _port=ntohs(addr->sin_port);
        _ip=inet_ntoa(addr->sin_addr);
    }

private:
    struct sockaddr_in _sockaddr;
    std::string _ip;
    uint16_t _port;
};

class TcpSocket
{
public:
    TcpSocket(uint16_t port)
    :_port(port)
    {
        _listensocket=socket(AF_INET, SOCK_STREAM, 0);
        if(_listensocket<0)
        {
            std::cout<<"监听套接字失败"<<std::endl;
            exit(-1);
        }
        // 设置地址复用
        int opt=1;
        setsockopt(_listensocket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

        // bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family=AF_INET;
        local.sin_port=htons(port);
        local.sin_addr.s_addr=INADDR_ANY;

        int n=bind(_listensocket, (struct sockaddr*)&local, sizeof(local));
        if(n<0)
        {
            std::cout<<"绑定失败"<<std::endl;
            exit(-1);
        }
        // 开启监听
        n=::listen(_listensocket, _defaultBacklog);
        if(n<0)
        {
            std::cout<<"TCP监听失败"<<std::endl;
            exit(-1);
        }
    }

    int Accept(InetAddr*addr)
    {
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        int sockfd=accept(_listensocket, (struct sockaddr*)&peer, &len);
        addr->GetInetAddr(&peer);
        return sockfd;
    }

    int GetSockfd()
    {
        return _listensocket;
    }

private:
    uint16_t _port;
    int _listensocket;

    // 类内常量,所有类对象共享一份
    const static int _defaultBacklog=16;
};

Main.cc

c++ 复制代码
#include <iostream>
#include "EpollServer.hpp"

int main()
{
    std::unique_ptr<EpollServer> server=std::make_unique<EpollServer>(8080);
    server->Loop();

    return 0;
}

makefile

c++ 复制代码
test_server:Main.cc
    g++ -g -o $@ $^ -std=c++14

.PHONY:clean
clean:
    rm -f test_server
相关推荐
MartinYeung51 小时前
[论文学习]JBShield:通过激活概念分析与操纵防御大语言模型越狱攻击
人工智能·学习·语言模型
Shadow(⊙o⊙)1 小时前
Linux网络部分——TCP服务端、客户端架构分析多进程、多线程、线程池
c++·tcp/ip·架构
Escalating_xu1 小时前
【Linux】基础 I/O 深度解析:FILE、文件描述符、open/read/write、重定向与缓冲区
java·linux·服务器
qetfw1 小时前
Debian 部署 Discuz! 论坛:Nginx、PHP 与 MariaDB 配置
linux·运维·nginx·debian·php·discuz
MartinYeung51 小时前
[论文学习]SMSR:带平滑检索的签名记忆——针对持久化LLM智能体系统运行时内存投毒的认证防御
人工智能·学习
zh路西法1 小时前
【3D SLAM源码解读系列】(四) GTSAM与iSAM2——从回环约束到位姿图优化
c++·slam·gtsam·pgo·isam2
烛之武2 小时前
Linux 命令速查表
linux·运维·服务器
01二进制代码漫游日记2 小时前
C++基础入门速通
java·开发语言·c++
小马同学-2 小时前
Keepaloved+LVS(DR) +MariaDN主主
linux·运维·lvs