55 Linux epoll高效IO实战指南

🔥个人主页: Milestone-里程碑

❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>

🌟心向往之行必能至

一 .epoll初识

不会出现poll随着用户增加,效率会发生一定地下降

按照man⼿册的说法: 是为处理⼤批量句柄⽽作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它⼏乎具备了之前所说的⼀切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知⽅法.

二.epoll的相关接口

2.1 epoll_create

bash 复制代码
EPOLL_CREATE(2)          Linux Programmer's Manual          EPOLL_CREATE(2)

NAME
       epoll_create, epoll_create1 - open an epoll file descriptor

SYNOPSIS
       #include <sys/epoll.h>

       int epoll_create(int size);

创建⼀个epoll的句柄. epoll模型
• ⾃从linux2.6.8之后,size参数是被忽略的.
• ⽤完之后, 必须调⽤close()关闭

2.2 epoll_ctl

bash 复制代码
EPOLL_CTL(2)             Linux Programmer's Manual             EPOLL_CTL(2)

NAME
       epoll_ctl - control interface for an epoll file descriptor

SYNOPSIS
       #include <sys/epoll.h>

       int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epfd :即epoll_create的返回值

fd,即用户要内核帮忙监视的socket

event

其中下面的data是存放用户写入的数据的,常用的是fd,内核不修改

bash 复制代码
       The event argument describes the object linked to the file descriptor fd.  The struct epoll_event is defined as:

           typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

           struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };

       The data member of the epoll_event structure specifies data that the kernel should save and then return (via epoll_wait(2)) when this file descriptor becomes ready.

       The events member of the epoll_event structure is a bit mask composed by ORing together zero or more of the following available event types:
 
 EPOLLIN
              The associated file is available for read(2) operations.

       EPOLLOUT
              The associated file is available for write(2) operations.

       EPOLLRDHUP (since Linux 2.6.17)
              Stream socket peer closed connection, or shut down writing half of connection.  (This flag is especially useful for writing simple code to detect peer shutdown when using edge-triggered monitoring.)

       EPOLLPRI
              There is an exceptional condition on the file descriptor.  See the discussion of POLLPRI in poll(2).

       EPOLLERR
              Error condition happened on the associated file descriptor.  This event is also reported for the write end of a pipe when the read end has been closed.

2.3 epoll_wait

bash 复制代码
NAME
       epoll_wait, epoll_pwait - wait for an I/O event on an epoll file de?
       scriptor

SYNOPSIS
       #include <sys/epoll.h>

       int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
      

events即:内核通知用户,你让我关心的fd们,上面的哪些事件就绪了

maxevents是数组长度

timeout 同poll 毫秒

三. epoll的使用

bash 复制代码
#pragma once

#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Log.hpp"

using namespace SocketModule;
using namespace LogModule;

class EpollServer
{
    const static int size = 4096;
    const static int defaultfd = -1;

public:
    EpollServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false), _epfd(defaultfd)
    {
        // 1.创建套接字
        _listensock->BuildTcpSocketMethod(port); // 4
        // 2. 创建epoll模型
        _epfd = epoll_create(256); // 4
        if (_epfd == -1)
        {
            LOG(LogLevel::ERROR) << "epoll_create fail";
            exit(EPOLL_CREATE_ERROR);
        }
        LOG(LogLevel::INFO) << "epoll_create success , _epfd :" << _epfd;
        // 3. 将listensocket设置到内核中
        struct epoll_event ev; // 有没有设置到内核中,有没有rb_tree中新增节点??没有!!
        ev.events = EPOLLIN;
        ev.data.fd = _listensock->Fd(); // 这里用来维护用户的数据 常见的为fd  内核不修改
        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Fd(), &ev);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "add listensockfd failed";
            exit(EPOLL_CTL_ERR);
        }
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            int timeout = -1; // 1000毫秒 1秒
            // rfds: 0000 0000
            int n = epoll_wait(_epfd, _revs, size, timeout); //-1 阻塞
            switch (n)
            {
            case -1:
                LOG(LogLevel::ERROR) << "epoll error";
                break;
            case 0:
                LOG(LogLevel::INFO) << "time out...";
                break;
            default:
                // 有事件就绪,就不仅仅是新连接到来了吧?读事件就绪啊?
                LOG(LogLevel::DEBUG) << "有事件就绪了..., n : " << n;
                Dispatcher(n); // 处理就绪的事件啊!
                break;
            }
        }

        _isrunning = false;
    }
    // 事件派发器
    void Dispatcher(int rnum)
    {
        LOG(LogLevel::DEBUG) << "event ready ..."; // LT: 水平触发模式--epoll默认
        // 合法 : 读事件就绪&& 新连接到来            读事件就绪&&可读
        // 不合法
        for (int i = 0; i < rnum; ++i)
        {
            int sockfd = _revs[i].data.fd;
            uint16_t revent = _revs[i].events;
            if (revent & EPOLLIN)
            {
                if (sockfd == _listensock->Fd()) // 读事件就绪 && 新连接到来
                    Accepter();
                else
                {
                    // 读事件就绪 && 普通socket可读
                    Recver(sockfd);
                }
            }
        }
    }

    // 链接管理器
    void Accepter()
    {
        InetAddr client;
        int sockfd = _listensock->Accept(&client); // accept会不会阻塞?
        if (sockfd >= 0)
        {
            // 获取新链接到来成功, 然后呢??能不能直接
            // read/recv(), sockfd是否读就绪,我们不清楚
            // 只有谁最清楚,未来sockfd上是否有事件就绪?select!
            // 将新的sockfd,托管给select!
            // 如何托管? 将新的fd放入辅助数组!
            LOG(LogLevel::INFO) << "get a new link, sockfd: "
                                << sockfd << ", client is: " << client.StringAddr();
            // 交给epoll
            struct epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = sockfd;
            // 能不能直接recv??? 不能!!!
            // 将新的sockfd添加到内核
            int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
            if (n < 0)
            {
                LOG(LogLevel::WARNING) << "add listensockfd failed";
            }
            else
            {
                LOG(LogLevel::INFO) << "epoll_ctl add sockfd success: " << sockfd;
            }
        }
    }

    // IO处理器
    void Recver(int sockfd)
    {
        char buffer[1024];
        // 我在这里读取的时候,会不会阻塞?
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); // recv写的时候有bug吗?
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say@ " << buffer << std::endl;
        }
        else if (n == 0)
        {
            LOG(LogLevel::INFO) << "clien quit...";
            // 从epoll中移除 &&关闭fd  epoll_ctl:只能合法  所以只能先移除再关闭
            int m = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            if (m > 0)
            {
                LOG(LogLevel::INFO) << "epoll_ctl remove sockfd success: " << sockfd;
            }
            close(sockfd);
        }
        else
        {
            LOG(LogLevel::ERROR) << "recv error";
            // 必须先关闭再修改,反了就成为关闭-1了
            int m = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            if (m > 0)
            {
                LOG(LogLevel::INFO) << "epoll_ctl remove sockfd success: " << sockfd;
            }
            close(sockfd);
        }
    }
    void Stop()
    {
        _isrunning = false;
    }
    ~EpollServer()
    {
        _listensock->Close();
        if (_epfd > 0)
            close(_epfd);
    }

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    int _epfd;
    struct epoll_event _revs[size];
};

四.epoll的工作原理

• 当某⼀进程调⽤epoll_create⽅法时,Linux内核会创建⼀个eventpoll结构体,这个结构体中有
两个成员与epoll的使⽤⽅式密切相关.

bash 复制代码
struct eventpoll{
....
/*红⿊树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给⽤⼾的满⾜条件的事件*/
struct list_head rdlist;
....
};

每⼀个epoll对象都有⼀个独⽴的eventpoll结构体,⽤于存放通过epoll_ctl⽅法向epoll对象中添
加进来的事件.
• 这些事件都会挂载在红⿊树中,如此,重复添加的事件就可以通过红⿊树⽽⾼效的识别出来(红⿊
树的插⼊时间效率是lgn,其中n为树的⾼度).
• ⽽所有添加到epoll中的事件都会与设备(⽹卡)驱动程序建⽴回调关系,也就是说,当响应的事件
发⽣时会调⽤这个回调⽅法.
• 这个回调⽅法在内核中叫ep_poll_callback,它会将发⽣的事件添加到rdlist双链表中.
• 在epoll中,对于每⼀个事件,都会建⽴⼀个epitem结构体.

相关推荐
buhuizhiyuci17 小时前
【QT-百日筑基篇】修真世界摸爬滚打多年,终于黄天不负有心人,突破炼器中期——常用控件QWight的属性
开发语言·c++·qt·gui·图形化
~|Bernard|18 小时前
Linux 定时任务(Cron)完整教程
linux·运维·服务器
小周学学学18 小时前
vmware-horizon第一章:horizon服务器安装
运维·服务器·vmware
m0_5873830018 小时前
社区家政系统开发实战:从需求分析到上线部署全指南
java·spring boot·spring·需求分析
凤山老林18 小时前
动态 i18n 体系落地:Spring Boot 多租户热加载与前后端协同实践
java·spring boot·后端·i18n
小雪崩18 小时前
嵌入式学习 day25:哈希表及排序与查找
linux·c语言·数据结构·学习·排序算法
东风破_18 小时前
TypeScript 高级类型进阶:keyof、Exclude、Record 与类型组合思想
前端·后端·typescript
白露与泡影18 小时前
解密 Pi 的 Harness 工程:Agent 会话如何实现持久化与恢复
java·人工智能·算法
DS随心转插件19 小时前
Grok生成的html怎么导出——AI导出鸭:大模型结构化输出的“最后一公里”工程化解构
前端·人工智能·ai·html·豆包·deepseek·ai导出鸭
এ慕ོ冬℘゜19 小时前
使用 jQuery 动态渲染表格与状态切换
前端·javascript·jquery