《epoll深度解析:从原理到使用,解锁Linux高并发I/O的核心能力(终篇)》

**前引:**在Linux系统的高并发领域,I/O处理效率直接决定了服务的性能上限。当我们面对每秒数万甚至数十万的连接请求时,传统的"一连接一线程"模型会因线程切换开销暴增而迅速崩溃,而早期的I/O多路转接技术如select和poll,也早已暴露出身法笨重的短板------select受限于FD_SETSIZE的1024文件描述符限制,poll虽突破了数量约束,却需在用户态与内核态间频繁拷贝事件数组,在高并发场景下性能损耗呈指数级上升!

目录

【一】epoll介绍

【二】接口使用说明

【二】epoll模型

【四】epoll_create

(1)函数原型

(2)参数

(3)返回值

【五】epoll_ctl

(1)函数原型

(2)参数

(1)第一个参数

(2)第二个参数

(3)第三个参数

(4)第四个参数

(3)返回值

【六】epoll_wait

(1)函数原型

(2)参数

(1)第一个参数

(2)第二个参数

(3)第三个参数

(4)第四个参数

(3)返回值

【七】epoll模型使用

(1)封装epoll接口

(2)功能实现

【八】epoll的两种工作模式

(1)LT模式(默认)

(2)ET模式


【一】epoll介绍

用来关心你设置的文件描述符:与select和epoll功能类似,但结构完全不同

【二】接口使用说明

epoll 的使用需要和前面的 select 与 poll 区别,它一般情况下由三个接口组成!如下讲解:

cpp 复制代码
头文件都是:<sys/epoll.h>

【二】epoll模型

epoll 模型其实涉及到:红黑树+就绪队列+回调机制

**红黑树:**用来高效管理需要监听的文件描述符,节点通常是一个结构体类型

**就绪队列:**将已经就绪的文件事件放入就绪队列

**回调机制:**用来管理红黑树和运行队列,不用用户自己操作,只需要添加关心的文件描述符即可

比如:将网卡拿到的数据交给TCP运行队列;节点的增删...

【四】epoll_create

(1)函数原型
cpp 复制代码
int epoll_create(int size);
(2)参数

说明:要监控的 fd 数量,系统会动态调整,可以任意传一个正数

(3)返回值

创建一个 epoll 模型,并返回 epoll 模型对应的文件描述符,理解返回值需要先看 epoll 模型

【五】epoll_ctl

(1)函数原型
cpp 复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
(2)参数
(1)第一个参数

说明:epoll_create 创建的 epoll 模型文件描述符

(2)第二个参数

说明:三种操作选项

  • EPOLL_CTL_ADD:把 fd 加入监控列表(新安排任务)
  • EPOLL_CTL_MOD:修改已监控 fd 的事件类型(调整任务)
  • EPOLL_CTL_DEL:把 fd 从监控列表移除(取消任务)
(3)第三个参数

说明:要添加的目标文件描述符

(4)第四个参数

说明:epoll_event类型的结构体指针,对添加的文件描述符作说明

cpp 复制代码
struct epoll_event 
{
    uint32_t events;  // 要监控的事件类型(比如"能读了""能写了")
    epoll_data_t data;// 关联数据(用来标识这个fd,方便后续处理)
};

// data是个联合体(可以存不同类型的数据):
typedef union epoll_data 
{
    void    *ptr;   // 指针(比如存fd对应的业务数据)
    int      fd;    // 直接存fd本身(最常用)
    uint32_t u32;   // 32位整数
    uint64_t u64;   // 64位整数(比如位图标记)
} epoll_data_t;
  • events常用取值
    • EPOLLIN:fd 有数据可以读(比如 socket 收到了客户端消息)
    • EPOLLOUT:fd 可以写数据(比如 socket 可以给客户端发消息了)
    • EPOLLERR:fd 发生了错误
    • 可以用 "或运算" 组合(比如EPOLLIN | EPOLLOUT表示同时监控读写)
(3)返回值

说明:返回0:说明事件添加成功;返回-1:errno

【六】epoll_wait

(1)函数原型
cpp 复制代码
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
(2)参数
(1)第一个参数

说明:epoll 模型文件描述符(epoll_create的返回值)

(2)第二个参数

说明:epoll_event类型的数组,用来存储信息(即当有事件就绪,操作系统自动给你填)

cpp 复制代码
struct epoll_event 
{
    uint32_t events;  // 要监控的事件类型(比如"能读了""能写了")
    epoll_data_t data;// 关联数据(用来标识这个fd,方便后续处理)
};

// data是个联合体(可以存不同类型的数据):
typedef union epoll_data 
{
    void    *ptr;   // 指针(比如存fd对应的业务数据)
    int      fd;    // 直接存fd本身(最常用)
    uint32_t u32;   // 32位整数
    uint64_t u64;   // 64位整数(比如位图标记)
} epoll_data_t;
(3)第三个参数

说明:最多关心的事件个数(比如:应用层的我最多只关心2个,剩余事件下次会继续报告给你)

(4)第四个参数

说明:超时时间设置(毫秒)

  • -1:一直阻塞,直到有事件发生
  • 0:立即返回(不管有没有事件)
  • 正数:最多等这么久,没事件就超时返回
(3)返回值

说明:触发事件的 fd 数量

返回0:没有事件就绪

返回>0:事件就绪数量

返回-1:出错,设置errno

【七】epoll模型使用

如果有问题,可以私信我!"功能实现"中的思路和前面章节中的select思路讲解,一模一样!

(1)封装epoll接口

封装上面三个接口,这里没什么好说的。声明:里面的size是用来判断当前套接字个数的

cpp 复制代码
#include"TCP_Network.h"

#define max_num_size 10


class Epoll
{
public:
    //初始化结构体
    void Install()
    {
        memset(events, 0, sizeof(events));
        for(int i=0;i<max_num_size;i++)
        {
            events[i].data.fd=-1;
        }
    }
    //创建epoll模型
    const int Epoll_creat(int size)
    {
        if (size <= 0)
        {
            log_message(LOG_LEVEL_ERROR, __FILE__, __LINE__, "epoll_create参数非法:size必须>0");
            exit(1);
        }
        //初始化结构体
        Install();

        int epfd = epoll_create(size);
        if(epfd==-1)
        {
            log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误信息:%s",errno,strerror(errno));
            exit(1);
        }
        return epfd;
    }
    //设置关心事件
    const int Epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    {
        //先检查size是否已满
        if (op == EPOLL_CTL_ADD)
        {
            if (size >= max_num_size)
            {
                std::cout << "事件满了,添加失败" << std::endl;
                return -1;
            }
        }
        //先检查size是否为空
        else if (op == EPOLL_CTL_DEL)
        {
            if (size <= 0)
            {
                std::cout << "事件删除失败" << std::endl;
                return -1;
            }
        }
        int ct = epoll_ctl(epfd, op, fd, event);
        if(ct==-1)
        {
            log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误信息:%s",errno,strerror(errno));
            exit(1);
        }
        //更新size
        if (op == EPOLL_CTL_ADD)
        {
            size++;
        }
        else if (op == EPOLL_CTL_DEL)
        {
            size--;
        }
        return ct;
    }
    //收集就绪事件
    const int Epoll_wait(int epfd, int maxevents, int timeout)
    {
        int wa = epoll_wait(epfd, events, maxevents, timeout);
        if(wa==-1)
        {
            log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误信息:%s",errno,strerror(errno));
        }
        return wa;
    }
    //返回信息
    struct epoll_event* Message()
    {
        return events;
    }

private:
    //存储信息的结构体
    struct epoll_event events[max_num_size];
    //关心的事件个数
    int size =0;
};
(2)功能实现

epoll与poll、select的功能实现思路都是一样的,只需要知道epoll的三个接口的功能:

epoll_create:创建一个epoll模型

epoll_ctl:对epoll模型进行操作(增删)

epoll_wait:返回就绪事件

核心思路:先创建--->再添加套接字--->事件就绪就进入任务函数--->listen套接字还是recv进行判断

cpp 复制代码
#include "Epoll.h"

class Media
{
public:
    void Inital()
    {
        _S.Socket();
        _S.Bind();
        _S.Listen();
    }
    void Recv(int fd, int epfd)
    {
        char buffer[1024] = {0};
        ssize_t d = recv(fd, buffer, sizeof(buffer) - 1, 0);
        if (d > 0)
        {
            buffer[d] = 0;
            std::cout << "客户端发送了数据:";
            std::cout << buffer << std::endl;
        }
        else if (d == 0)
        {
            std::cout << "关闭了文件描述符:" << fd << std::endl;
            // 对方断开了连接
            _E.Epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
            // 关闭当前的文件描述符
            close(fd);
        }
        else
        {
            // 读取错误
            _E.Epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
            close(fd);
            // 关闭当前的文件描述符
           
        }
    }
    // 处理事件
    void Handle(int epfd, int wa)
    {
        // 获取准备就绪的事件
        struct epoll_event *events = _E.Message();
        // 遍历事件
        for (int i = 0; i < wa; i++)
        {
            if (events[i].data.fd == -1)
                continue;
            if (events[i].data.fd == _S.Fd())
            {
                std::cout<<"Accept"<<std::endl;
                int d = _S.Accept();
                // 添加读端事件
                struct epoll_event v;
                v.data.fd = d;
                v.events = EPOLLIN;
                int ctl = _E.Epoll_ctl(epfd, EPOLL_CTL_ADD, d, &v);
                if (ctl == 0)
                {
                    std::cout << "事件:" << d << "添加成功" << std::endl;
                }
            }
            else if (events[i].events == EPOLLIN) // 读取数据
            {
                std::cout<<"Recv"<<std::endl;
                Recv(events[i].data.fd, epfd);
            }
            // 写端
            // 监听
        }
    }
    // 关心事件
    void Install()
    {
        // 创建epoll模型
        const int epfd = _E.Epoll_creat(max_num_size);

        // 将listen套接字添加进epoll模型
        int fd = _S.Fd();

        struct epoll_event v;
        v.data.fd = fd;
        v.events = EPOLLIN;
        int ctl = _E.Epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &v);
        if (ctl == 0)
        {
            std::cout << "事件:" << fd << "添加成功" << std::endl;
        }

        for (;;)
        {
            // 等待事件
            int wa = _E.Epoll_wait(epfd, max_num_size, 2000);
            switch (wa)
            {
                
                case 0:
                {
                    std::cout << "暂时没有事件....HHHHHHH" << std::endl;
                    continue;
                }
                case -1:
                {
                    if (errno != EINTR)
                    {
                        std::cout << "Epoll_wait is fatal errno" << std::endl;
                    }
                    break;
                }
                default:
                {
                    Handle(epfd, wa);
                }
            }
        }
    }

private:
    Server _S;
    Epoll _E;
};

【八】epoll的两种工作模式

(1)LT模式(默认)

说明:只要目标套接字(fd)处于 "有事件可处理" 的状态,就会一直通知你

特点:安全、逻辑简单、但是重复提醒同一个套接字效率低,不强制搭配非阻塞IO

(2)ET模式

说明:只要目标套接字(fd)状态发送变化时,才会通知你,且只通知一次

特点:只通知一次,减少不必要的触发,效率极高,但是需要一次性读完数据,必须搭配非阻塞IO

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式