zoukankan      html  css  js  c++  java
  • 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.

  • 相关阅读:
    DRUPAL-PSA-CORE-2014-005 && CVE-2014-3704 Drupal 7.31 SQL Injection Vulnerability /includes/database/database.inc Analysis
    WDCP(WDlinux Control Panel) mysql/add_user.php、mysql/add_db.php Authentication Loss
    Penetration Testing、Security Testing、Automation Testing
    Tomcat Server Configuration Automation Reinforcement
    Xcon2014 && Geekpwn2014
    phpMyadmin /scripts/setup.php Remote Code Injection && Execution CVE-2009-1151
    Linux System Log Collection、Log Integration、Log Analysis System Building Learning
    The Linux Process Principle,NameSpace, PID、TID、PGID、PPID、SID、TID、TTY
    Windows Management Instrumentation WMI Security Technology Learning
    IIS FTP Server Anonymous Writeable Reinforcement, WEBDAV Anonymous Writeable Reinforcement(undone)
  • 原文地址:https://www.cnblogs.com/xiaOt119/p/2538767.html
Copyright © 2011-2022 走看看