• 利用epoll写一个"迷你"的网络事件库


      epoll是linux下高性能的IO复用技术,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

      为什么会出现IO复用技术呢,比如在Web应用中,大量的请求连接事件,如果采用多进程方式处理,也就是一个连接对应一个fork来处理,这样开销太大了,毕竟创建进程还是很耗资源的;如果采用多线程方式处理,也就是一个连接对应一个线程来处理,当请求并发量上去的话,系统中就会充斥着很多处理线程,毕竟一个系统创建线程是有一定上限的。这时,就需要我们的IO复用技术了。常见的网络模型中,有多进程+IO复用编程模型,也有多线程+IO复用编程模型,比如大名鼎鼎的nginx默认采用的就是多进程+IO复用技术来处理网络请求的;开源网络库libevent也是基于IO复用技术来完成网络数据处理的。

    epoll系列函数

      epoll是Linux特有的IO复用函数,它在实现和使用上与select和poll有很大差异,首先,epoll使用一组函数来完成操作,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核上的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集合事件表。但epoll需要使用一个额外的文件描述符,来唯一标识内核中这个事件表,这个文件描述符使用如下epoll_create函数创建:

    #include <sys/epoll.h>  
    int epoll_create(int size);  // 返回:成功返回创建的内核事件表对应的描述符,出错-1  

      size参数现在并不起作用,只是给内核一个提示,告诉它内核表需要多大,该函数返回的文件描述符将用作其他所有epoll函数的第一个参数,以指定要访问的内核事件表。用epoll_ctl函数操作内核事件表

    #include <sys/epoll.h>  
    int epoll_ctl(int opfd, int op, int fd, struct epoll_event *event);  // 返回:成功返回创建的内核事件表对应的描述符,出错-1  

      fd参数是要操作的文件描述符,op指定操作类型,操作类型有3种

    • EPOLL_CTL_ADD:往事件表中注册fd上的事件
    • EPOLL_CTL_MOD:修改fd上的注册事件
    • EPOLL_CTL_DEL:删除fd上的注册事件

      event指定事件类型,它是epoll_event结构指针类型:

    struct epoll_event  
    {  
        __uint32_t events;  /* epoll事件 */  
        epoll_data_t data;  /* 用户数据 */  
    };  

      其中events描述事件类型,epoll支持的事件类型和poll基本相同,表示epoll事件类型的宏是在poll对应的宏加上”E”,比如epoll的数据可读事件是EPOLLIN,但epoll有两个额外的事件类型-EPOLLET和EPOLLONESHOT,它们对于高效运作非常关键,data用于存储用户数据,其类型epoll_data_t定义如下:

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

      epoll_data_t是一个联合体,其4个成员最多使用的是fd,它指定事件所从属的目标文件描述符,ptr成员可用来指定fd相关的用户数据,但由于opoll_data_t是一个联合体,我们不能同时使用fd和ptr,如果要将文件描述符嗯哼用户数据关联起来,以实现快速的数据访问,则只能使用其他手段,比如放弃使用fd成员,而在ptr指针指向的用户数据中包含fd。

    #include <sys/epoll.h>  
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  // 返回:成功返回就绪的文件描述符个数,出错-1  

      timeout参数的含义与poll接口的timeout参数相同,maxevents参数指定最多监听多少个事件,它必须大于0。

      epoll_wait如果检测到事件,就将所有就绪的事件从内核事件表(由epfd指定)中复制到events指定的数组中,这个数组只用来输epoll_wait检测到的就绪事件,而不像select和poll的参数数组既传递用于用户注册的事件,有用于输出内核检测到就绪事件,这样极大提高了应用程序索引就绪文件描述符的效率。

    epoll原理与实现

      epoll是怎么实现的呢?其实很简单,从这3个方法就可以看出,它比select聪明的避免了每次频繁调用“哪些连接已经处在消息准备好阶段”的 epoll_wait时,是不需要把所有待监控连接传入的。这意味着,它在内核态维护了一个数据结构保存着所有待监控的连接。这个数据结构就是一棵红黑树,它的结点的增加、减少是通过epoll_ctrl来完成的。

    图片来源于CSDN:高性能网络编程5--IO复用与并发编程

      图中左下方的红黑树由所有待监控的连接构成。左上方的链表,同是目前所有活跃的连接。于是,epoll_wait执行时只是检查左上方的链表,并返回左上方链表中的连接给用户。这样,epoll_wait的执行效率能不高吗?

      最后,再看看epoll提供的2种玩法ET和LT,即翻译过来的边缘触发和水平触发。其实这两个中文名字倒也有些贴切。这2种使用方式针对的仍然是效率问题,只不过变成了epoll_wait返回的连接如何能够更准确些。
    例如,我们需要监控一个连接的写缓冲区是否空闲,满足“可写”时我们就可以从用户态将响应调用write发送给客户端 。但是,或者连接可写时,我们的“响应”内容还在磁盘上呢,此时若是磁盘读取还未完成呢?肯定不能使线程阻塞的,那么就不发送响应了。但是,下一次epoll_wait时可能又把这个连接返回给你了,你还得检查下是否要处理。可能,我们的程序有另一个模块专门处理磁盘IO,它会在磁盘IO完成时再发送响应。那么,每次epoll_wait都返回这个“可写”的、却无法立刻处理的连接,是否符合用户预期呢?
      于是,ET和LT模式就应运而生了。LT是每次满足期待状态的连接,都得在epoll_wait中返回,所以它一视同仁,都在一条水平线上。ET则不然,它倾向更精确的返回连接。在上面的例子中,连接第一次变为可写后,若是程序未向连接上写入任何数据,那么下一次epoll_wait是不会返回这个连接的。ET叫做 边缘触发,就是指,只有连接从一个状态转到另一个状态时,才会触发epoll_wait返回它。可见,ET的编程要复杂不少,至少应用程序要小心的防止epoll_wait的返回的连接出现:可写时未写数据后却期待下一次“可写”、可读时未读尽数据却期待下一次“可读”。 
      当然,从一般应用场景上它们性能是不会有什么大的差距的,ET可能的优点是,epoll_wait的调用次数会减少一些,某些场景下连接在不必要唤醒时不会被唤醒(此唤醒指epoll_wait返回)。但如果像我上面举例所说的,有时它不单纯是一个网络问题,跟应用场景相关。当然,大部分开源框架都是基于ET写的,框架嘛,它追求的是纯技术问题,当然力求尽善尽美。

    基于epoll的"迷你"网络事件库

      网络事件库封装了底层IO复用函数,同时提供给外部使用的接口,提供的接口可以多种多样,但是一般有添加事件、删除事件、开始事件循环等接口。为了展示下网络事件库的是如何封装IO复用函数,同时学习epoll的使用,"迷你"网络事件库-tomevent今天诞生了 :) (ps:tomevent采用C++语言实现)。

      既然是网络事件库,那首先需要定义一个事件的结构,LZ这里就使用Event结构体了,事件结构体中包含监听的文件描述符、事件类型、回调函数、传递给回调函数的参数,当然,这只是一个简单的事件结构,如果还需要其他信息可另外添加。

    /**
     * event struct.
     */
    struct Event {
        int fd;                                 /* the fd want to monitor */
        short event;                            /* the event you want to monitor */
        void *(*callback)(int fd, void *arg);   /* the callback function */
        void *arg;                               /* the parameter of callback function */
    };

      定义一个事件处理接口IEvent,该接口定义了3个基本的事件操作函数,也就是添加事件、删除事件、开始事件循环。定义IEvent接口,与具体的底层IO技术解耦,使用具体的IO复用类来实现该接口,比如对应select的SelectEvent,或者是对应poll的PollEvent,当然,这里就用epoll对应的EpollEvent来实现IEvent接口(ps:c++中接口貌似应该称为抽象类,不过这里称为接口更合适一点)。

    /**
     * the interface of event.
     */
    class IEvent {
    public:
        virtual int addEvent(const Event &event) = 0;
        virtual int delEvent(const Event &event) = 0;
        virtual int dispatcher() = 0;
    
        virtual ~IEvent() { }
    };

      IEvent的实现类EpollEvent,其中封装了epoll相关的函数。EpollEvent有3个成员,分别是pollCreateSize、epollFd、events,pollCreateSize表示调用epoll_create时传递的参数值,epollFd表示epoll_create的返回值,events是记录事件的map,events中记录了监听事件的信息,当事件来临时被用到。

    class EpollEvent : public IEvent {
    public:
        EpollEvent() : EpollEvent(16) {
    
        }
        EpollEvent(int createSize) {
            if (createSize < 16) {
                createSize = 16;
            }
    
            epollCreateSize = createSize;
            initEvent();
        }
    
        virtual int addEvent(const Event &event);
        virtual int delEvent(const Event &event);
        virtual int dispatcher();
    
    private:
        int initEvent() {
            int epollFd = epoll_create(this->epollCreateSize);
            if (epollFd <= 0) {
                perror("create_create error:");
                return epollFd; /* here epollFd is -1 */
            }
    
            this->epollFd = epollFd;return 0;
        }
    
        int epollCreateSize;
        int epollFd;
    
        //Event event;
        map<int, Event> events;
    };
    int EpollEvent::addEvent(const Event &event) {
        struct epoll_event epollEvent;
    
        epollEvent.data.fd = event.fd;
        epollEvent.events = event.event;
        int retCode = epoll_ctl(this->epollFd, EPOLL_CTL_ADD, event.fd, &epollEvent);
        if (retCode < 0) {
            perror("epoll_ctl error:");
            return retCode;
        }
    
        /* add event to this->events */
        this->events[event.fd] = event;return 0;
    }
    
    int EpollEvent::delEvent(const Event &event) {
        struct epoll_event epollEvent;
    
        epollEvent.data.fd = event.fd;
        epollEvent.events = event.event;
        int retCode = epoll_ctl(this->epollFd, EPOLL_CTL_DEL, event.fd, &epollEvent);
        if (retCode < 0) {
            perror("epoll_ctl error:");
            return retCode;
        }
    
        this->events.erase(event.fd);
        return 0;
    }
    
    int EpollEvent::dispatcher() {
        struct epoll_event epollEvents[32];
    
        //cout << "epoll_wait before" << endl;
        int nEvents = epoll_wait(epollFd, epollEvents, 32, -1);
        if (nEvents <= 0) {
            perror("epoll_wait error:");
            return -1;
        }
        //cout << "epoll_wait after nEvent" << endl;
        for (int i = 0; i < nEvents; i++) {
            int fd = epollEvents[i].data.fd;
            Event event = this->events[fd];
            if (event.callback) {
                event.callback(fd, event.arg);
            }
        }
        return 0;
    }

      到这里整个tomevent的框架代码就结束了,那么该如何使用呢,以下是一个测试用例。使用tomevent来同时监听2个文件描述符,一个是标准输入(fd为0),另一个是提供UDP服务的一个文件描述符。

    void *test(int fd, void *arg) {
        cout << "****************test(): fd=" << fd << endl;
    
        char buff[256];
        int len = recvfrom(fd, buff, sizeof(buff), 0, NULL, NULL);
        if (len > 0) {
            buff[len] = '';
            cout << buff << endl;
        }
        else {
            perror("recvfrom error:");
        }
        cout << "****************test()**********" << endl;
    }
    
    void *inTest(int fd, void *arg) {
        cout << "****************inTest(): fd=" << fd << endl;
    
        char buff[256];
        int len = read(fd, buff, sizeof(buff));
        if (len > 0) {
            buff[len] = '';
            cout << buff << endl;
        }
        else {
            perror("read stdin error:");
        }
        cout << "****************inTest()**********" << endl;
    }
    
    int main(int argc, char **argv) {
        int listenFd = -1;
        int connFd = -1;
        struct sockaddr_in servAddr;
    
        listenFd = socket(AF_INET, SOCK_DGRAM, 0);
    
        memset(&servAddr, 0, sizeof(servAddr));
        servAddr.sin_family = AF_INET;
        servAddr.sin_port = htons(8080);
        servAddr.sin_addr.s_addr = INADDR_ANY;
        bind(listenFd, (struct sockaddr *)&servAddr, sizeof(servAddr));
    
        listen(listenFd, 5);
    
        Event event, inEvent;
        EpollEvent eventBase;
    
        event.fd = listenFd;
        event.event = EPOLLIN;
        event.arg = NULL;
        event.callback = test;
    
        inEvent.fd = 0;
        inEvent.event = EPOLLIN;
        inEvent.arg = NULL;
        inEvent.callback = inTest;
    
        eventBase.addEvent(event);
        eventBase.addEvent(inEvent);
    
        for (; ;) {
            eventBase.dispatcher();
        }
    
        return 0;
    }

      以下是测试结果 ,同时提供UDP服务和响应键盘输入。

    参考

      1、epoll-百度百科

      2、高性能网络编程5--IO复用与并发编程

      3、Libevent初探

  • 相关阅读:
    jquery,字符串转json对象,json对象转字符串
    Oracle,跳出游标循环
    oracle常用函数使用大全 Oracle除法(转)
    Oracle 数字操作。数字函数。mod(),trunc(),round(),ceil(),floor的使用
    Oracle 和sqlserver 字符串补齐
    js中数组的splice()方法
    详解JavaScript的splice()方法
    使用GDB命令行调试器调试C/C++程序
    代码为什么需要重构
    Spring事务管理要点总结
  • 原文地址:https://www.cnblogs.com/luoxn28/p/5814924.html
Copyright © 2020-2023  润新知