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.

 

你可能感兴趣的:(linux,工作,socket,IO,struct,events)