EPOLL使用总结

来源:互联网 发布:java表格界面 编辑:程序博客网 时间:2024/05/17 22:39

2010-10-27 kejieleung

 

 

epoll的接口非常简单,一共就三个函数:
1. int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个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,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

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:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到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事件有两种模型:
Edge Triggered (ET) 
    只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
    
i    基于非阻塞文件句柄
    
ii   只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。
    
但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
    ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。


Level Triggered (LT)
    LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

    
    在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。

EPOLLOUT事件的处理策略就是, 先尝试直接发送.如果发送不完整,就buffer住等下一轮再发

 

使用总结

1.EPOLL 默认是水平触发模式LT, 如果要使用边缘触发模式,要将listen的socket事件加上EPOLLET

  ev.data.fd = listen_fd;
  ev.events = EPOLLIN | EPOLLET | EPOLLOUT | EPOLLHUP | EPOLLERR;
  //add server socket fd in to event set
  epoll_ctl( epfd, EPOLL_CTL_ADD, listen_fd, &ev );
 

 

 

2.需要设置非阻塞

    int opts;
    opts = fcntl( sock_fd, F_GETFL );
    if( opts < 0 )
    {
        printf("err:fcntl F_GETFL");
        exit( 1 );
    }
    opts = opts | O_NONBLOCK ;
    if( fcntl( sock_fd, F_SETFL, opts ) < 0 )
    {
        printf("err:fcntl F_SETFL");
        exit( 1 );
    }
 

 

3.如果是以LT方式处理,只要数据未处理完(send、recv内核缓冲有数据)就会不停的收到 EPOLLIN/EPOLLOU消息(已设置监听)。所以在recv时可以先处理一个MAX_LEN, 之后等待下次触发再处理

处理:

1)不将EPOLLOUT事件加入监听

2)在收到EPOLLIN后,需要写时再加入EPOLLIN | EPOLLOUT

3)写完后再设为 EPOLLIN

4)或者改为ET模式

    如果是以ET方式处理,事件只会通知一次,如数据到达可接收,需要使用while循环来接入数据。同样可发送也只会通常一次。send/recv都以收到EAGAIN为标识,如果还没处理完,才会现收到事件通知。

    注意EPOLLOUT事件,如果是LT模式,只要send缓冲区空闲就会一直有这个事件通知,程序需要判断是否有数据可发送。如果是ET模式,只有在accept一个socket时触发一次,除非发送,缓冲区满了,再变为空闲时,才会再通知一次

 

4.处理socket recv/send时返回-1, 连接已断开

 

5.关于accept有个小细节,一定要指定sockaddr_in长度,不然会莫名accept不了

  clnLen = sizeof( sockaddr_in );
  memset( &clnAddr, 0 , sizeof( sockaddr_in ) );
  conn_fd = accept( listen_fd, (sockaddr*)&clnAddr, &clnLen );
 

 

 

6.设置端口复用,可以多个socket绑定同一端口

    int flags = 1;
    if(setsockopt( sock_fd , SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags)) == -1)
    {
        return false;
    }

 

 

 

 

 

 

 

示例代码如下:

 

 

原创粉丝点击