IO多路转接之epoll

一、epoll

epoll是为处理大批量句柄而作了改进的poll,它是在2.5.44内核中被引进的(epoll(4) is a newAPI introducedinLinuxkernel2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

二、epoll的3个相关的系统调用

2.1、 epoll_create

创建一个epoll的句柄:

自从linux2.6.8之后,size参数是被忽略的。用完之后,必须调用closeO关闭。

2.2、epoll_wait

收集在epoll监控的事件中已经发送的事件:

(1)参数events是分配好的epoll_event结构体数组。

(2)epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。

(3)maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。

(4)参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。

(5)epfd就是epoll_create返回的文件描述符。

(6)如果函数调用成功,返回对应I/O上已准备好的文件描述符数目(已经就绪的fd的个数),如返回0表示已超时,返回小于0表示函数失败。

(7)epoll_event结构体:

2.3、epoll_ctl

epoll的事件注册函数:

它不同于select0是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

(1)第一个参数是epoll_createO的返回值(epoll的句柄)。

(2)第二个参数表示动作,用三个宏来表示。

(3)第三个参数是需要监听的fd。

(4)第四个参数是告诉内核需要监听什么事。

第二个参数op的取值:

(1)EPOLL_CTL_ADD:注册新的fd到epfd中。

(2)EPOLL_CTL_MOD:修改已经注册的fd的监听事件。

(3)EPOLL_CTL_DEL:从epfd中删除一个fd。

events可以是以下几个宏的集合:

(1)EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。

(2)EPOLLOUT:表示对应的文件描述符可以写。

(3)EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。

(4)EPOLLERR:表示对应的文件描述符发生错误。

(5)EPOLLHUP:表示对应的文件描述符被挂断。

(6)EPOLLET:将EPOLL设为边缘触发(EdgeTriggered)模式,这是相对于水平触发(Level

Triggered)来说的。

(7)EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL红黑树里。

三、epoll的工作原理

3.1原理解释以及图解

1、当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。

2、每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。

3、这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

4、而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法。

5、这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.。

6、在epoll中,对于每一个事件,都会建立一个epitem结构体。

7、当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。

8、如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户,这个操作的时间复杂度是0(1)。

3.2总结epoll的使用过程:

(1)调用epoll_create创建一个epoll句柄。

(2)调用epoll_ctl,将要监控的文件描述符进行注册。

(3)调用epoll_wait,等待文件描述符就绪。

epoll的优点(和select的缺点对应)

(1)接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开。

(2)数据拷贝轻量:只在合适的时候调用EPOLL_CTLADD将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)

(3)事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪,这个操作时间复杂度O(1),即使文件描述符数目很多,效率也不会受到影响。

(4)没有数量限制:文件描述符数目无上限

四、epoll代码:

4.1nocopy.hpp文件:

为了防止epoll拷贝,创建一个nocopy类:

cpp 复制代码
#pragma once
#include <iostream>

class nocopy
{
public:
    nocopy()
    {
    }
    nocopy(const nocopy &) = delete;
    nocopy &operator=(const nocopy &) = delete;
};

4.2Sock.hpp文件

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Log.hpp"

const int backlog = 10;
extern log lg;

enum
{
    SocketError = 2,
    BindError,
    ListenError
};

class Sock
{
public:
    Sock()
    : _sockfd(-1)
    {
    }

    ~Sock()
    {
        if (_sockfd >= 0)
        {
            close(_sockfd);
        }
    }

    // 创建套接字
    void Socket()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            lg.logmessage(Fatal, "socket fail, %s, %d", errno, strerror(errno));
            exit(SocketError);
        }
        int opt = -1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }

    // 创建一个绑定接口
    void Bind(const uint16_t port)
    {
        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;

        if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg.logmessage(Fatal, "bind fail, %s, %d", errno, strerror(errno));
            exit(BindError);
        }
    }

    void Listen()
    {
        if (listen(_sockfd, backlog) < 0)
        {
            lg.logmessage(Fatal, "listen fail, %s, %d", errno, strerror(errno));
            exit(ListenError);
        }
    }

    int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(_sockfd, (struct sockaddr *)&peer, &len);
        if (newfd < 0)
        {
            lg.logmessage(Warning, "accept fail, %s, %d", errno, strerror(errno));
            return -1;
        }

        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        // 获取远端主机的信息
        *clientport = ntohs(peer.sin_port);
        *clientip = ipstr;

        return newfd;
    }

    bool Connect(const std::string &ip, const uint16_t &port)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);

        int n = connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer));
        if (n == -1)
        {
            std::cerr << "connect to" << ip << ":" << port << std::endl;
            return false;
        }
        return true;
    }

    void Close()
    {
        close(_sockfd);
    }

    int Fd()
    {
        return _sockfd;
    }

private:
    int _sockfd;
};

4.3 Log.hpp文件

cpp 复制代码
#pragma once

#include <iostream>
#include <stdarg.h>
#include <time.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include <fcntl.h>
#include<string.h>

#define SIZE 1024

// 设置日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define logFile "log.txt"

class log
{
public:
    log()
    {
        PrintMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        PrintMethod = method;
    }

    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        default:
            return "None";
        }
    }

    void logmessage(int level, const char *format, ...) // 后面的省略号表示可变参数
    {

        char leftbuffer[SIZE];

        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);

        snprintf(leftbuffer, sizeof(leftbuffer), "[%s],[%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        char rightbuffer[SIZE];

        va_list s;
        va_start(s, format);
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        char logtxt[SIZE * 3];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        // printf("%d-%d-%d %d:%d:%d\n",ctime->tm_year + 1900, ctime->tm_mon, ctime->tm_mday, ctime->tm_hour,ctime->tm_min,ctime->tm_sec);

        //printf("%s", logtxt);
        PrintLog(level,logtxt);
        // 格式:默认部分+自定义部分(可变参数部分)
    }

    void PrintLog(int level, const std::string& logtxt)
    {
        switch(PrintMethod)
        {
            case Screen:
                std::cout << logtxt << std::endl;
            break;
            case Onefile:
                printOneFile(logFile, logtxt);
            break;
            case Classfile:
                printClassFile(level, logtxt);
            break;
            default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    { 
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);
        if(fd < 0)
        {
            return  ;
        }
        write(fd,logtxt.c_str(),logtxt.size());
        close(fd);

    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = logFile;
        filename += ".";
        filename += levelToString(level);
        printOneFile(filename, logtxt);
    }
    ~log()
    {
    }
private:
    int PrintMethod;
    std::string path;
};


log lg;

4.4 epoll.hpp文件

对epoll函数接口的封装:

cpp 复制代码
#pragma once

#include <sys/epoll.h>
#include <string>
#include <cerrno>
#include <cstring>
#include "nocopy.hpp"

class Epoller : public nocopy
{
    static const int size = 128;

public:
    Epoller()
    {
        _epfd = epoll_create(size);
        if (_epfd == -1)
        {
            lg.logmessage(Error, "epoll_create error:%s", strerror(errno));
        }
        else
        {
            // 创建成功
            lg.logmessage(Info, "epoll_create success:%d", _epfd);
        }
    }

    int EpollerWait(struct epoll_event revents[], int num)
    {
        int n = epoll_wait(_epfd, revents, num, _timeout);
        return n;
    }

    int EpollerUpdate(int oper, int sockfd, uint32_t event)
    {
        int n = 0;
        if (oper == EPOLL_CTL_DEL)
        {
            // 如果是删除,最后一个参数可以不要了
            n = epoll_ctl(_epfd, oper, sockfd, nullptr);
            if (n != 0)
            {
                lg.logmessage(Error, "delete epoll_ctl error");
            }
        }
        else
        {
            // 修改或添加
            struct epoll_event ev;
            ev.events = event;
            ev.data.fd = sockfd;

            n = epoll_ctl(_epfd, oper, sockfd, &ev);
            if (n != 0)
            {
                lg.logmessage(Error, "epoll_ctl error");
            }
        }
        return n;
    }

    ~Epoller()
    {
        if (_epfd >= 0)
        {
            close(_epfd);
        }
    }

private:
    int _epfd;
    int _timeout = {3000};
};

4.5 EpollServer.hpp文件

cpp 复制代码
#pragma once
#include <sys/epoll.h>
#include <iostream>
#include <memory>
#include "Sock.hpp"
#include "Log.hpp"
#include "Epoll.hpp"
#include "nocopy.hpp"

const uint16_t defaultport = 8080;
uint16_t EVENT_IN = (EPOLLIN);
uint16_t EVENT_OUT = (EPOLLOUT);

class EpollServer : public nocopy
{
    static const int num = 64;

public:
    EpollServer(uint16_t port = defaultport)
        : _port(port), _listensocket_ptr(new Sock), _epoller_ptr(new Epoller)
    {
    }

    void Init()
    {
        _listensocket_ptr->Socket();
        _listensocket_ptr->Bind(_port);
        _listensocket_ptr->Listen();

        lg.logmessage(Info, "create listensock success, listensockfd: %d", _listensocket_ptr->Fd());
    }

    void Dispatcher(struct epoll_event revs[], int num)
    {
        // 遍历
        for (size_t i = 0; i < num; i++)
        {
            // 获取文件描述符
            int fd = revs[i].data.fd;
            // 获取事件
            uint32_t events = revs[i].events;

            if (events & EVENT_IN)
            {
                // 读事件
                if (fd == _listensocket_ptr->Fd())
                {
                    std::string clientip;
                    uint16_t clientport;
                    // 获取了一个新链接
                    int sock = _listensocket_ptr->Accept(&clientip, &clientport);
                    if (sock > 0)
                    {
                        // 不能直接读,因为可能没数据,需要交给epoll替我们关心
                        _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
                        lg.logmessage(Info, "get a new link, client message:clientip:%s,clientport:%d", clientip.c_str(), clientport);
                    }
                    else
                    {
                        continue;
                    }
                }
                else
                {
                    // 其他的文件描述符就绪,进行读取操作
                    char buffer[1024];
                    ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
                    if (n > 0)
                    {
                        buffer[n] = 0;
                        std::cout << "get a message:" << buffer << std::endl;

                        // write
                        std::string echo = buffer;
                        echo += "Server Echo# ";
                        write(fd, echo.c_str(), echo.size());
                    }
                    else if (n == 0)
                    {
                        lg.logmessage(Info, "client quit, close fd is:%d", fd);
                        // 从epoll中移除了文件描述符(一定是先移除在关闭)
                        _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
                        close(fd);
                    }
                    else
                    {
                        lg.logmessage(Warning, "read error, fd is:%d", fd);
                        // 从epoll中移除了文件描述符(一定是先移除在关闭)
                        _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
                        close(fd);
                    }
                }
            }
            else if (events & EVENT_OUT)
            {
                // 写事件
            }
            else
            {
            }
        }
    }

    void Start()
    {
        // 将listen文件描述符加入到epoll中,本质是吧listen文件描述符合他关心的事件添加到红黑树中
        int n = _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listensocket_ptr->Fd(), EVENT_IN); // 只关心读事件

        struct epoll_event revs[num];
        for (;;)
        {
            int n = _epoller_ptr->EpollerWait(revs, num);
            if (n > 0)
            {
                // 有事件就绪
                lg.logmessage(Info, "event happended, fd is %d", revs[0].data.fd);
                // 处理事件
                Dispatcher(revs, n);
            }
            else if (n == 0)
            {
                lg.logmessage(Info, "wait time out!");
            }
            else
            {
                lg.logmessage(Warning, "epoll wait error");
            }
        }
    }

    ~EpollServer()
    {
        _listensocket_ptr->Close();
    }

private:
    std::shared_ptr<Sock> _listensocket_ptr;
    std::shared_ptr<Epoller> _epoller_ptr;
    uint16_t _port;
};

4.6 main.cc文件

cpp 复制代码
#include <memory>
#include "EpollServer.hpp"

int main()
{

    std::unique_ptr<EpollServer> svr(new EpollServer());
    svr->Init();
    svr->Start();

    
    return 0;
}

五、epoll工作方式

epoll有2种工作方式:水平触发(LT)和边缘触发(ET))

5.1水平触发LevelTriggered工作模式

epoll默认状态下就是LT工作模式

(1)当epoll检测到socket上事件就绪的时候,口可以不立刻进行处理.口或者只处理一部分。

(2)假如只读了1K数据,缓冲区中还剩1K数据,在第二次调用epoll_wait时,epoll_wait仍然会立刻返回并通知socket读事件就绪。

(3)直到缓冲区上所有的数据都被处理完,epoll_wait才不会立刻返回。

(4)支持阻塞读写和非阻塞读写

5.2边缘触发EdgeTriggered工作模式

如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志,epoll进入ET工作模式。

(1)当epoll检测到socket上事件就绪时,必须立刻处理。

(2)如上面的例子,虽然只读了1K的数据,缓冲区还剩1K的数据,在第二次调用epoll_wait的时候,epoll_wait 不会再返回了。

(3)也就是说,ET模式下,文件描述符上的事件就绪后,只有一次处理机会,这也就使得我们必须循环读取,直到读取出错,但是由于fd默认是阻塞的,所以在ET模式下,所有的fd都必须是非阻塞的(non_block)。(重点)

(4)ET的性能比LT性能更高(epoll_wait返回的次数少了很多),Nginx默认采用ET模式使用epoll。

(5)只支持非阻塞的读写

注意:select和poll其实也是工作在LT模式下,epoll既可以支持LT,也可以支持ET。

总结:ET相对于LT,ET的通知效率更高,并且ET的IO也更高效,因为每一次都保证要把数据全部取走,那么这也保证了TCP在通知对方的时候,可以告诉对方一个更大的窗口,从而在概率上一次性发送更多数据。

六、epoll的使用场景:

epoll的高性能,是有一定的特定场景的

(1)对于多连接,且多连接中只有一部分连接比较活跃时,比较适合使用epoll。例如,典型的

一个需要处理上万个客户端的服务器,例如各种互联网APP的入口服务器,这样的服务器就很适

合epoll。

(2)如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用

epoll就并不合适,具体要根据需求和场景特点来决定使用哪种lo模型。

相关推荐
舰长1153 小时前
polkit服务没起来,导致防火墙命令卡住
linux·运维·服务器
芊&星3 小时前
靶机应急 | 知攻善防----Linux
linux·运维·服务器·安全
l1t3 小时前
DeepSeek总结的PostgreSQL 19 Beta 的四个特性
数据库·postgresql
程序员阿明4 小时前
docker搭建openPLC runTime
运维·docker·容器
报错小能手4 小时前
讲讲docker
运维·docker·容器
Agent手记4 小时前
生产环节费用智能管控与超支预警方案:基于AGI智能体的精细化治理实践
运维·服务器·人工智能·ai·agi
zcg19424 小时前
图像分割——常用数据和算法
算法
子午4 小时前
基于YOLO的车牌识别检测~Python+YOLOV8算法+车牌定位+车牌检测+深度学习
python·算法·yolo
dxxt_yy4 小时前
精准定位、高效运维:DLJ-1 护航交通电缆安全
运维·安全