epoll用法介绍


 

引言

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

ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉调用者,然后它会假设调用者知道文件描述符已经就绪,并且不会再为那个文件描述 符发送更多的就绪通知,直到调用者做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fdIO操作(从而导致它再次变成未就绪),内核不会发送更多的通知。该模式与select有不同的语义,只有当从不可读变为可读时才触发。上面那种情况,还有1k可读,所以不会触发,当继续读,直到返回EAGAIN时,变为不可读,如果再次变为可读就触发。默认可读,调用者可以随便读,直到发生EAGAIN。可读时读和不读,怎么读都由调用者自己决定,中间epoll不管。EAGAIN后不可读了,等到再次可读,epoll会再通知一次。理解ET模式最重要的就是理解状态的变化,对于监听可读事件时,如果是socket是监听socket,那么当有新的主动连接到来为状态发生变化;对一般的socket而言,协议栈中相应的缓冲区有新的数据为状态发生变化。但是,如果在一个时间同时接收了N个连接(N>1),但是监听socketaccept了一个连接,那么其它未 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:注册新的fdepfd中;
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:只监听一次事件,当监听完这次事件之后,就会把这个fdepoll的队列中删除。如果还需要继续监听这个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];
    //
生成用于处理acceptepoll专用的文件描述符
    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模式时应该采用while1)这样的方式来读取连接。本文还给出了一个简单的来说明epoll的用法,本例只是演示作用,对于实际应用中应考虑上述多用户情况,以及采用epoll+线程池的方法。对于参考资料2中的例子,并不会按作者说的那样只输出5字节,而是在出发EPOLLOUT之后还会出发EPOLLIN事件,也就是会出来后面的67890五个字节,对作者的这个例子研究了好久,才明白是这样的,不知道是不是没有深刻理解man的原因,得再好好看看man

参考资料

1Linux man page

2http://www.cppblog.com/converse/archive/2008/04/29/48482.html

你可能感兴趣的:(epoll用法介绍)