IO复用之epoll系列

epoll是什么?

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率

                                                                ----摘自百度百科

在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。

相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在include/linux/posix_types.h头文件有这样的声明:

#define __FD_SETSIZE    1024

表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目。

epoll的相关接口

创建一个文件句柄

 #include <sys/epoll.h>
 int epoll_create(int size);

 

创建一个 epoll 对象,这里类似于创建管道,但是这里返回的是一个标识该软件资源的文件描述符,在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽

 

epoll的事件注册函数

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

第一个参数是epoll_create()的返回值,

第二个参数表示动作,用三个宏来表示:

  EPOLL_CTL_ADD:注册新的fd到epfd中;

  EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

  EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的文件描述符,

第四个参数为一个结构体指针,这个结构体中的信息为告诉内核需要监听什么事件 

struct epoll_event结构如下:

struct epoll_event {

  __uint32_t events;  /* Epoll events */

  epoll_data_t data;  /* User data variable */

};

typedef union epoll_data 
 {
         void        *ptr;
         int          fd;
         uint32_t     u32;
         uint64_t     u64;
 } epoll_data_t;
 

events可以是以下几个宏的集合:

EPOLLIN :     表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT:    表示对应的文件描述符可以写;

EPOLLPRI:      表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:     表示对应的文件描述符发生错误;

EPOLLHUP:     表示对应的文件描述符被挂断;

EPOLLET:      将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

 

data 为联合体,用来保存用户自定制数据,传什么类型的数据,就对联合体里面的哪个数据进行赋值

这里的data 在一般情况下用保存对应的文件描述符

 

epoll的事件等待函数

#include <sys/epoll.h>
 int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);

第一个参数为我们创建的epoll模型

第二个参数为事件数组

第三个为数组大小

第四个参数为超时时间

epoll对文件描述符的两种操作模式

Edge Triggered (ET)  边缘触发只有数据到来,才触发,不管缓存区中是否还有数据。

Level Triggered (LT)  电平触发只要有数据都会触发。

假如有这样一个例子:

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(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

然后详细解释ET, LT:

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

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。

另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,

读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取

while(rs)
    {
        buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
        if(buflen < 0) {
            // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读
            // 在这里就当作是该次事件已处理处.
            if(errno == EAGAIN)
                break;
            else
                return;
        } else if(buflen == 0) {
            // 这里表示对端的socket已正常关闭.
        }
        if(buflen == sizeof(buf)
                rs = 1;   // 需要再次读取
                else
                rs = 0;
    }

还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法.

epoll的工作原理

1.创建epoll模型 
调用epoll_create()之后,内核会做3件事情 
(1)在操作系统底层(硬件驱动,网卡)构建会调机制 
(2)在操作系统层构建一颗红黑树(一种相对平衡的二叉搜索树),树的每个节点用来保存用户关心的事件(即用户关心的文件描述符和所关心的事件类型) 
(3)在操作系统层构建一个就绪队列,保存众多事件中已经就绪的事件 
2.用户控制事件 
(1) 用户通过调用epoll_ctl()实现实现告诉操作系统,你现在要关心的文件描述符和关心的事件类型 
(2)操作系统会将这一事件保存在红黑树中 
3.内核激活事件 
(1)操作系统得知网卡(文件)上面有数据就绪时(硬件机制),激活该事件,将其存入就绪队列中 
(2)用户调用epoll_wait()返回时,返回的为就绪队列中就绪的事件 
我们说的epoll_wait()实现是O(1)的时间复杂度,只需要关注就绪队列是否为空,不为空就将事件复制到用户态

 

代码实例:

#include <iostream>
#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>
using namespace std;
#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 argc, char* argv[])
{
    int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;
    if ( 2 == argc )
    {
        if( (portnumber = atoi(argv[1])) < 0 )
        {
            fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
            return 1;
        }
    }
    else
    {
        fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
        return 1;
    }
    struct epoll_event ev,events[20]; //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
    epfd=epoll_create(256); //生成用于处理accept的epoll专用的文件描述符
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    setnonblocking(listenfd); //把socket设置为非阻塞方式
    ev.data.fd=listenfd; //设置与要处理的事件相关的文件描述符
    ev.events=EPOLLIN|EPOLLET;  //设置要处理的事件类型    

    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); //注册epoll事件
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr)); 
    serveraddr.sin_port=htons(portnumber);
    bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    for ( ; ; ) {
         nfds=epoll_wait(epfd,events,20,500); //等待epoll事件的发生
        for(i=0;i<nfds;++i) //处理所发生的所有事件
        {
            if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
                if(connfd<0){
                    perror("connfd<0");
                    exit(1);
                }
                char *str = inet_ntoa(clientaddr.sin_addr);
                cout << "accapt a connection from " << str << endl;
                ev.data.fd=connfd; //设置用于读操作的文件描述符
                ev.events=EPOLLIN|EPOLLET; //设置用于注测的读操作事件
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //注册ev
            }
            else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。
            {
                cout << "EPOLLIN" << endl;
                if ( (sockfd = events[i].data.fd) < 0)
                    continue;
                if ( (n = read(sockfd, line, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) {
                        close(sockfd);
                        events[i].data.fd = -1;
                    } else
                        std::cout<<"readline error"<<std::endl;
                } else if (n == 0) {
                    close(sockfd);
                    events[i].data.fd = -1;
                }
                line[n] = '/0';
                cout << "read " << line << endl;
                ev.data.fd=sockfd;  //设置用于写操作的文件描述符
                ev.events=EPOLLOUT|EPOLLET; //设置用于注测的写操作事件
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要处理的事件为EPOLLOUT
            }
            else if(events[i].events&EPOLLOUT) // 如果有数据发送
            {
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                ev.data.fd=sockfd; //设置用于读操作的文件描述符
                ev.events=EPOLLIN|EPOLLET; //设置用于注测的读操作事件
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);  //修改sockfd上要处理的事件为EPOLIN
            }
        }
    }
    return 0;
}

 

posted @ 2018-08-16 16:53  GHzz  阅读(219)  评论(0编辑  收藏  举报