《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

相关推荐
一颗青果1 小时前
IP分片与组装
网络·网络协议·tcp/ip
不会kao代码的小王1 小时前
服务器、存储与网络核心知识全解析
运维·服务器·网络
Xの哲學1 小时前
Linux Workqueue 深度剖析: 从设计哲学到实战应用
linux·服务器·网络·算法·边缘计算
nix.gnehc1 小时前
Anolis23 环境下 Docker 与私有 Harbor 仓库完整部署指南
运维·docker·容器
nnerddboy1 小时前
嵌入式面试题:1.协议:IIC、SPI、TCP/IP
网络·网络协议·tcp/ip
xiep14383335101 小时前
Ubuntu 24.04.3 LTS 搭建离线仓库安装docker-ce
linux·ubuntu·docker
云安全干货局1 小时前
深度解析:高防 IP 如何实现 “隐藏源站 IP”?核心技术原理拆解
网络·网络安全·高防ip
物理与数学1 小时前
linux 内存区域(Zone)
linux·linux内核
代码游侠2 小时前
学习笔记——ARM Cortex-A 裸机开发实战指南
linux·运维·开发语言·前端·arm开发·笔记