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. FreeRTOS - 如何根据FreeRTOS提供的功能(信号量、任务通知、队列等)设计程序

    原文地址:http://www.cnblogs.com/god-of-death/p/6917837.html 1.二值信号量 就像一个标志位,事件产生置一,事件处理后直零 用于任务之间的同步,即一个 ...

  2. 美国选举问题/完全背包/Knapsack

    using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Knap ...

  3. android中dip、dp、px、sp和屏幕密度

    1. dip: device independent pixels(设备独立像素). 不同设备有不同的显示效果,这个和设备硬件有关,一般我们为了支持WVGA.HVGA和QVGA 推荐使用这    这个 ...

  4. Mysql优化小记1

    在项目开发中,需要写个windows服务从sqlserver复制数据到mysql(5.6.13 Win64(x86_64)),然后对这些数据进行计算分析.每15分钟复制一次,每次复制大概200条数据, ...

  5. 启动hbase输出ignoring option PermSize=128m; support was removed in 8.0告警信息

    ./start-hbase.sh starting master, logging to /home/hadoop/hbase-1.2.4/bin/../logs/hbase-hadoop-maste ...

  6. B. Complete the Word(Codeforces Round #372 (Div. 2)) 尺取大法

    B. Complete the Word time limit per test 2 seconds memory limit per test 256 megabytes input standar ...

  7. fundamentals of the jQuery library

    1.why is jquery Only 32kB minified and gzipped. Can also be included as an AMD module Supports CSS3 ...

  8. python mysql插入数据遇到的错误

    1.数据插入的时候报错:not enough arguments for format string,大概意思就是说没有足够的参数格式化字符串. 我的数据库插入方法是这样的 def add_data( ...

  9. iptables 操作

    iptables --list 查看列表 iptables删除规则 iptables -nL --line-number Chain INPUT (policy ACCEPT)num target p ...

  10. VO、DTO、DO、PO的概念、区别和用处

    转至:http://qixuejia.cnblogs.com/ 本篇文章主要讨论一下我们经常会用到的一些对象:VO.DTO.DO和PO. 由于不同的项目和开发人员有不同的命名习惯,这里我首先对上述的概 ...