• linux epoll使用详解


    Linux2.6内核中epoll用法详解

    引言

    epoll是linux2.6内核中才有的机制,其他版本内核中是没有的,是Linux2.6内核引入的多路复用IO的一种方式,用于提高网络IO 性能的方法。在linux网络编程中,很长一段时间都是采用select来实现多事件触发处理的。Select存在如下几个方面的问题:一是每次调用时要 重复地从用户态读入参数,二是每次调用时要重复地扫描文件描述符,三是每次在调用开始时,要把当前进程放入各个文件描述符的等待队列。在调用结束后,又把 进程从各个等待队列中删除。Select采用轮询的方式来处理事件触发,当随着监听socket的文件描述符fd的数量增加时,轮询的时间也就越长,造成 效率低下。而且linux/posix_types.h中有#define __FD_SETSIZE 1024(也有说2048的)的定义,也就是说linux select能监听的最大fd数目是1024个,虽然能通过内核修改此参数,但这是治标不治本。

        epoll的出现可以有效的解决select效率低下的问题,epoll把参数拷贝到内核态,在每次轮询时不会重复拷贝。epoll有ET和LT两种工 作模式,ET是高速模式只能以非阻塞方式进行,LT相当于快速的select,可以才有阻塞和非阻塞两种方式,epoll通过把操作拆分为 epoll_create,epoll_ctl,epoll_wait三个步骤避免重复地遍历要监视的文件描述符。

    Epoll介绍

    epoll机制可以运转在两种模式下:Edge Triggered (ET)和Level Triggered (LT)。首先来看一下man手册中的一个例子:

          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/select,在poll能用的地方epoll都可以用,因为他们 具有同样的职能。即使使用ET模式的epoll,在收到多个数据包的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl处理文件句柄就成为调用者必须作的事情。

          以上是man手册对epoll中两种模式的简要介绍,这里有必要对两种模式进行详细的介绍:

    LT是缺省的工作方式,并且同时支持block和no-block socket;在这种做法中,内核会告诉调用者一个文件描述符是否就绪了,然后调用者可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会 继续通知调用者的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。LT模式跟select有一样的语义。就 是如果可读就触发。比如某管道原来为空,如果有一个进程写入2k数据,就会触发。如果处理进程读取1k数据,下次轮询时继续触发。该模式下,默认不可读, 只有epoll通知可读才是可读,否则不可读。

    ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉调用者,然后它会假设调用者知道文件描述符已经就绪,并且不会再为那个 文件描述 符发送更多的就绪通知,直到调用者做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未 就绪),内核不会发送更多的通知。该模式与select有不同的语义,只有当从不可读变为可读时才触发。上面那种情况,还有1k可读,所以不会触发,当继 续读,直到返回EAGAIN时,变为不可读,如果再次变为可读就触发。默认可读,调用者可以随便读,直到发生EAGAIN。可读时读和不读,怎么读都由调 用者自己决定,中间epoll不管。EAGAIN后不可读了,等到再次可读,epoll会再通知一次。理解ET模式最重要的就是理解状态的变化,对于监听 可读事件时,如果是socket是监听socket,那么当有新的主动连接到来为状态发生变化;对一般的socket而言,协议栈中相应的缓冲区有新的数 据为状态发生变化。但是,如果在一个时间同时接收了N个连接(N>1),但是监听socket只accept了一个连接,那么其它未 accept的连接将不会在ET模式下给监听socket发出通知,此时状态不发生变化;对于一般的socket,如果对应的缓冲区本身已经有了N字节的 数据,而只取出了小于N字节的数据,那么残存的数据不会造成状态发生变化。

    Epoll的调用很简单只涉及到三个函数分别是:

    1.int epoll_create(int size);

    创建一个epoll的句柄,size用来告诉内核这个监听的数目最大值。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的 值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗 尽。

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

    epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

    EPOLL_CTL_ADD:注册新的fd到epfd中;

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

    EPOLL_CTL_DEL:从epfd中删除一个fd;

    第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,数据结构如下:

    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 */

    };

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

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

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

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

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

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

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

    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,就会把这个fd从epoll的队列中删除。如果还需要继续监听这个socket的话,需要再次把这个fd加入到EPOLL队列里

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

    等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这 个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻 塞)。该函数返回需要处理的事件数目,如返回0表示已超时。      

    epoll简单例子

    下面给出一个简单使用epoll的例子以加深理解

    服务端代码:

    #include <strings.n>

    #include<sys/types.h>

    #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>

    #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 i, maxi, listenfd, connfd, sockfd,epfd,nfds;

        ssize_t n;

        char line[MAXLINE];

        socklen_t clilen;

        //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件

        struct epoll_event ev,events[20];

        //生成用于处理accept的epoll专用的文件描述符

        epfd=epoll_create(256);

        struct sockaddr_in clientaddr;

        struct sockaddr_in serveraddr;

    clilen=sizeof(clientaddr);

        listenfd = socket(AF_INET, SOCK_STREAM, 0);

        //把socket设置为非阻塞方式

       setnonblocking(listenfd);

        //设置与要处理的事件相关的文件描述符

        ev.data.fd=listenfd;

        //设置要处理的事件类型

        ev.events=EPOLLIN|EPOLLET;

        //ev.events=EPOLLIN;

        //注册epoll事件

        epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

        bzero(&serveraddr, sizeof(serveraddr));

        serveraddr.sin_family = AF_INET;

        char *local_addr="127.0.0.1";

        inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);

        serveraddr.sin_port=htons(SERV_PORT);

        bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));

        listen(listenfd, LISTENQ);

        maxi = 0;

        for ( ; ; ) {

            //等待epoll事件的发生

            nfds=epoll_wait(epfd,events,20,500);

            //处理所发生的所有事件     

            for(i=0;i<nfds;++i)

            {

                if(events[i].data.fd==listenfd)//有客户连接

                {

                    connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);

                    if(connfd<0){

                        perror("connfd<0");

                        exit(1);

                    }

                    setnonblocking(connfd);

                    char *str = inet_ntoa(clientaddr.sin_addr);

                    printf( "accapt a connection from %s /n",str);

                    //设置用于读操作的文件描述符

                    ev.data.fd=connfd;

                    //设置用于注测的读操作事件

                    ev.events=EPOLLIN|EPOLLET;

                    //ev.events=EPOLLIN;

                    //注册ev

                    epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);

                }

                else if(events[i].events&EPOLLIN) //客户端socket可读事件

                {

                   recv(events[i].data.fd, line,MAXLINE,0);

    printf(“recv line %s /n ”, line);

           }

        }

        return 0;

    }

    客户端的代码用perl写的如下:

    #!/usr/bin/perl

    use IO::Socket;

    my $host = "127.0.0.1";

    my $port = 5000;

    my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";

    my $msg_out = "1234567890";

    print $socket $msg_out;

    print "now send over, go to sleep/n";

    while (1)

    {

        sleep(1);

    }

      同时运行服务端和客户端程序,会发现服务端在接收5字节数据之后就不会在触发EPOLLIN事件了,因为采用的是ET模式,客户端发送的10字 节数据中,只读取了5字节的数据,还有5字节数据可读,也就是状态未发生改变。所以服务端不会在触发EPOLLIN事件。而如果把ET模式改成LT模式, 那么服务端还是会触发EPOLLIN事件,将剩余的5字节数据读取。

    总结

    本文主要介绍了linux epoll的使用方法,对其中的epoll的两种模式进行了详细的分析,在服务器处理中要等待用户socket连接,由于epoll的性能较高,可以有效 的处理用户请求。对于多用户连接时还要要注意在服务端accept时,有可能同时到达多个连接,由于采用ET模式,此时服务器端可能只会读取一个连接而忽 略其他连接,所以采用ET模式时应该采用while(1)这样的方式来读取连接。本文还给出了一个简单的来说明epoll的用法,本例只是演示作用,对于 实际应用中应考虑上述多用户情况,以及采用epoll+线程池的方法。对于参考资料2中的例子,并不会按作者说的那样只输出5字节,而是在出发 EPOLLOUT之后还会出发EPOLLIN事件,也就是会出来后面的67890五个字节,对作者的这个例子研究了好久,才明白是这样的,不知道是不是没 有深刻理解man的原因,得再好好看看man.

  • 相关阅读:
    5个人的晚餐
    replace(),indexOf(),substring(),split(),join(),——各种小知识点
    2016-11-01——孤独留给自己,开心让给别人
    CMS3.0——初次邂逅express
    项目三(集团官网)——总结(2) 递归创建子目录
    项目三(集团官网)——总结(1) cookie
    jQuery_pager.js分页
    thinkjs——修改where默认条件为or
    优化之——查询数据库
    go net库
  • 原文地址:https://www.cnblogs.com/xiaOt119/p/2538767.html
Copyright © 2020-2023  润新知