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

还有,假如发送端流量大于接收端的流量(意思是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);
    )
    {
        perror("fcntl(sock,GETFL)");
        exit();
    }
    opts = opts|O_NONBLOCK;
    )
    {
        perror("fcntl(sock,SETFL,opts)");
        exit();
    }
}
int main(int argc, char* argv[])
{
    int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;
     == argc )
    {
        ])) <  )
        {
            fprintf(stderr,]);
            ;
        }
    }
    else
    {
        fprintf(stderr,]);
        ;
    }
    ]; //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
    epfd=epoll_create(); //生成用于处理accept的epoll专用的文件描述符
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, );
    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 = ;
    for ( ; ; ) {
         nfds=epoll_wait(epfd,events,,); //等待epoll事件的发生
        ;i<nfds;++i) //处理所发生的所有事件
        {
            if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
                ){
                    perror("connfd<0");
                    exit();
                }
                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;
                )
                    continue;
                ) {
                    if (errno == ECONNRESET) {
                        close(sockfd);
                        events[i].data.fd = -;
                    } else
                        std::cout<<"readline error"<<std::endl;
                } ) {
                    close(sockfd);
                    events[i].data.fd = -;
                }
                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
            }
        }
    }
    ;
}

IO复用之epoll系列的更多相关文章

  1. IO复用——epoll系列系统调用

    1.内核事件表 epoll是Linux特有的I/O复用函数.epoll把用户关心的文件描述上的事件放在内核里的一个事件表中,并用一个额外的文件描述符来标识该内核事件表.这个额外文件描述符使用函数epo ...

  2. IO复用(Reactor模式和Preactor模式)——用epoll来提高服务器并发能力

    上篇线程/进程并发服务器中提到,提高服务器性能在IO层需要关注两个地方,一个是文件描述符处理,一个是线程调度. IO复用是什么?IO即Input/Output,在网络编程中,文件描述符就是一种IO操作 ...

  3. select、poll、epoll三组IO复用

    int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout)//其中n ...

  4. linux基础编程:IO模型:阻塞/非阻塞/IO复用 同步/异步 Select/Epoll/AIO(转载)

      IO概念 Linux的内核将所有外部设备都可以看做一个文件来操作.那么我们对与外部设备的操作都可以看做对文件进行操作.我们对一个文件的读写,都通过调用内核提供的系统调用:内核给我们返回一个file ...

  5. IO复用: select 和poll 到epoll

    linux 提供了select.poll和epoll三种接口来实现多路IO复用.下面总结下这三种接口. select 该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经 ...

  6. Select、Poll、Epoll IO复用技术

    简介 目前多进程方式实现的服务器端,一次创建多个工作子进程来给客户端提供服务, 但是创建进程会耗费大量资源,导致系统资源不足 IO复用技术就是让一个进程同时为多个客户端端提供服务 IO复用技术 之 S ...

  7. IO复用的三种方法(select,poll,epoll)深入理解

    (一)IO复用是Linux中的IO模型之一,IO复用就是进程告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程处理,从而不会在单个IO上阻塞了,Linux中,提供 ...

  8. Linux网络编程-IO复用技术

    IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了.Linux中,提 ...

  9. Libevent的IO复用技术和定时事件原理

    Libevent 是一个用C语言编写的.轻量级的开源高性能网络库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大:源代码相当精炼.易 ...

随机推荐

  1. HDU 3507斜率优化dp

    Print Article Time Limit: 9000/3000 MS (Java/Others)    Memory Limit: 131072/65536 K (Java/Others)To ...

  2. Nginx的启动、停止、平滑重启

    转载自:http://www.xj123.info/2572.html 启动Nginx /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/ngi ...

  3. 照片EXIF信息的读取和改写的JAVA实现

    由于项目需要对照片的EXIF信息进行处理,因此在网上搜索了一番.捣鼓出来了,写下,总结. 需要用到2个jar包,metadata-extractor-2.3.1和mediautil-1.0.这2个ja ...

  4. Hibernate入门(3)- 持久对象的生命周期介绍

    在hibernate中对象有三种状态:瞬时态(Transient). 持久态(Persistent).脱管态或游离态(Detached).处于持久态的对象也称为PO(Persistence Objec ...

  5. JS之document例题讲解1(两张表之间数据转移、日期时间选择、子菜单下拉、用div做下拉菜单、事件总结)

    作业一:两个列表之间数据从一个列表移动到另一个列表 <div style="width:600px; height:500px; margin-top:20px"> & ...

  6. hdu1002 A + B Problem II(大数题)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1002 A + B Problem II Time Limit: 2000/1000 MS (Java/ ...

  7. Perl6 Bailador框架(3):路径匹配

    use v6; use Bailador; =begin pod 注意的是, 当/:one设置时 虽然你有/admin或/about, 但这个/:one不会跟现有的匹配 只跟没有的匹配: 也就是说, ...

  8. metlnfo 5.3.1 sql注入复现

    首先还是要说的是metlnfo是伪全局变量机制 所以如下: /admin/include/global.func.php function save_met_cookie(){ global $met ...

  9. 网络设备之pci_driver

    每个pci驱动都有一个pci_driver实例,用以描述驱动名称,支持的设备信息,以及对应的操作函数: /* 描述一个pci设备,每个pci驱动必须创建一个pci_driver实例 */ struct ...

  10. redis可编译

    redis-3.0.7 可编译 redis-3.0.7.tar.gz twemproxy-master.zip keepalived-1.2.19.tar.gz openssl-1.0.1s.tar. ...