• IO复用之epoll系列


    epoll是什么?

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

                                                                    ----摘自百度百科

    在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。

    相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在include/linux/posix_types.h头文件有这样的声明:

    #define __FD_SETSIZE    1024

    表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目。

    epoll的相关接口

    创建一个文件句柄

     #include <sys/epoll.h>
     int epoll_create(int size);

    创建一个 epoll 对象,这里类似于创建管道,但是这里返回的是一个标识该软件资源的文件描述符,在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽

    epoll的事件注册函数

    #include <sys/epoll.h>
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    第一个参数是epoll_create()的返回值,

    第二个参数表示动作,用三个宏来表示:

      EPOLL_CTL_ADD:注册新的fd到epfd中;

      EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

      EPOLL_CTL_DEL:从epfd中删除一个fd;

    第三个参数是需要监听的文件描述符,

    第四个参数为一个结构体指针,这个结构体中的信息为告诉内核需要监听什么事件 

    struct epoll_event结构如下:

    struct epoll_event {
    
      __uint32_t events;  /* Epoll events */
    
      epoll_data_t data;  /* User data variable */
    
    };
    
    
    typedef union epoll_data 
     {
             void        *ptr;
             int          fd;
             uint32_t     u32;
             uint64_t     u64;
     } epoll_data_t;
     

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

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

    EPOLLOUT:    表示对应的文件描述符可以写;

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

    EPOLLERR:     表示对应的文件描述符发生错误;

    EPOLLHUP:     表示对应的文件描述符被挂断;

    EPOLLET:      将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

    EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

    data 为联合体,用来保存用户自定制数据,传什么类型的数据,就对联合体里面的哪个数据进行赋值

    这里的data 在一般情况下用保存对应的文件描述符

    epoll的事件等待函数

    #include <sys/epoll.h>
     int epoll_wait(int epfd, struct epoll_event *events,
                          int maxevents, int timeout);

    第一个参数为我们创建的epoll模型

    第二个参数为事件数组

    第三个为数组大小

    第四个参数为超时时间

    epoll对文件描述符的两种操作模式

    Edge Triggered (ET)  边缘触发只有数据到来,才触发,不管缓存区中是否还有数据。

    Level Triggered (LT)  电平触发只要有数据都会触发。

    假如有这样一个例子:

    1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符

    2. 这个时候从管道的另一端被写入了2KB的数据

    3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作

    4. 然后我们读取了1KB的数据

    5. 调用epoll_wait(2)......

    Edge Triggered 工作模式:

    如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。

       i    基于非阻塞文件句柄

       ii   只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

    Level Triggered 工作模式

    相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

    然后详细解释ET, LT:

    LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

    ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。

    另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,

    读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取

    while(rs)
        {
            buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
            if(buflen < 0) {
                // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读
                // 在这里就当作是该次事件已处理处.
                if(errno == EAGAIN)
                    break;
                else
                    return;
            } else if(buflen == 0) {
                // 这里表示对端的socket已正常关闭.
            }
            if(buflen == sizeof(buf)
                    rs = 1;   // 需要再次读取
                    else
                    rs = 0;
        }

    还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法.

    epoll的工作原理

    1.创建epoll模型 
    调用epoll_create()之后,内核会做3件事情 
    (1)在操作系统底层(硬件驱动,网卡)构建会调机制 
    (2)在操作系统层构建一颗红黑树(一种相对平衡的二叉搜索树),树的每个节点用来保存用户关心的事件(即用户关心的文件描述符和所关心的事件类型) 
    (3)在操作系统层构建一个就绪队列,保存众多事件中已经就绪的事件 
    2.用户控制事件 
    (1) 用户通过调用epoll_ctl()实现实现告诉操作系统,你现在要关心的文件描述符和关心的事件类型 
    (2)操作系统会将这一事件保存在红黑树中 
    3.内核激活事件 
    (1)操作系统得知网卡(文件)上面有数据就绪时(硬件机制),激活该事件,将其存入就绪队列中 
    (2)用户调用epoll_wait()返回时,返回的为就绪队列中就绪的事件 
    我们说的epoll_wait()实现是O(1)的时间复杂度,只需要关注就绪队列是否为空,不为空就将事件复制到用户态

    代码实例:

    #include <iostream>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <errno.h>
    using namespace std;
    #define MAXLINE 5
    #define OPEN_MAX 100
    #define LISTENQ 20
    #define SERV_PORT 5000
    #define INFTIM 1000
    void setnonblocking(int sock)//将套接字设置为非阻塞
    {
        int opts;
        opts=fcntl(sock,F_GETFL);
        if(opts<0)
        {
            perror("fcntl(sock,GETFL)");
            exit(1);
        }
        opts = opts|O_NONBLOCK;
        if(fcntl(sock,F_SETFL,opts)<0)
        {
            perror("fcntl(sock,SETFL,opts)");
            exit(1);
        }
    }
    int main(int argc, char* argv[])
    {
        int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
        ssize_t n;
        char line[MAXLINE];
        socklen_t clilen;
        if ( 2 == argc )
        {
            if( (portnumber = atoi(argv[1])) < 0 )
            {
                fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
                return 1;
            }
        }
        else
        {
            fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
            return 1;
        }
        struct epoll_event ev,events[20]; //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
        epfd=epoll_create(256); //生成用于处理accept的epoll专用的文件描述符
        struct sockaddr_in clientaddr;
        struct sockaddr_in serveraddr;
        listenfd = socket(AF_INET, SOCK_STREAM, 0);
        setnonblocking(listenfd); //把socket设置为非阻塞方式
        ev.data.fd=listenfd; //设置与要处理的事件相关的文件描述符
        ev.events=EPOLLIN|EPOLLET;  //设置要处理的事件类型    
    
        epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); //注册epoll事件
        bzero(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        char *local_addr="127.0.0.1";
        inet_aton(local_addr,&(serveraddr.sin_addr)); 
        serveraddr.sin_port=htons(portnumber);
        bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
        listen(listenfd, LISTENQ);
        maxi = 0;
        for ( ; ; ) {
             nfds=epoll_wait(epfd,events,20,500); //等待epoll事件的发生
            for(i=0;i<nfds;++i) //处理所发生的所有事件
            {
                if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
                {
                    connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
                    if(connfd<0){
                        perror("connfd<0");
                        exit(1);
                    }
                    char *str = inet_ntoa(clientaddr.sin_addr);
                    cout << "accapt a connection from " << str << endl;
                    ev.data.fd=connfd; //设置用于读操作的文件描述符
                    ev.events=EPOLLIN|EPOLLET; //设置用于注测的读操作事件
                    epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //注册ev
                }
                else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。
                {
                    cout << "EPOLLIN" << endl;
                    if ( (sockfd = events[i].data.fd) < 0)
                        continue;
                    if ( (n = read(sockfd, line, MAXLINE)) < 0) {
                        if (errno == ECONNRESET) {
                            close(sockfd);
                            events[i].data.fd = -1;
                        } else
                            std::cout<<"readline error"<<std::endl;
                    } else if (n == 0) {
                        close(sockfd);
                        events[i].data.fd = -1;
                    }
                    line[n] = '/0';
                    cout << "read " << line << endl;
                    ev.data.fd=sockfd;  //设置用于写操作的文件描述符
                    ev.events=EPOLLOUT|EPOLLET; //设置用于注测的写操作事件
                    epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要处理的事件为EPOLLOUT
                }
                else if(events[i].events&EPOLLOUT) // 如果有数据发送
                {
                    sockfd = events[i].data.fd;
                    write(sockfd, line, n);
                    ev.data.fd=sockfd; //设置用于读操作的文件描述符
                    ev.events=EPOLLIN|EPOLLET; //设置用于注测的读操作事件
                    epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);  //修改sockfd上要处理的事件为EPOLIN
                }
            }
        }
        return 0;
    }
    宝剑锋从磨砺出 梅花香自苦寒来
  • 相关阅读:
    转:JMeter5的If Controller操作解析
    .NET Core优秀的应用逻辑分层框架设计
    socket阻塞导致拿不到信息
    2018年开始了,我们还是说说2017吧
    PHP 学习 遇到坑的第一章
    记一次高并发情况,服务器和代码修改过程记录。
    IIS 提高连接的并发数,和CPU的使用率。
    2017年总结
    在 safari 浏览器 onclick 出现延迟的现象
    微信JS-api 注意事项
  • 原文地址:https://www.cnblogs.com/GHzcx/p/9486836.html
Copyright © 2020-2023  润新知