linux epoll使用详解

Linux2.6内核中epoll用法详解linux

引言编程

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描述符socket

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

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

      4. 而后咱们读取了1KB的数据ui

      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")ordie"createsocketerror@";
my $msg_out = "1234567890";
print socketmsg_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.

相关文章
相关标签/搜索