epoll介绍及使用

一、 介绍
    Epoll 是一种高效的管理socket的模型,相对于select和poll来说具有更高的效率和易用性。传统的select以及poll的效率会因为 socket数量的线形递增而导致呈二次乃至三次方的下降,而epoll的性能不会随socket数量增加而下降。标准的linux-2.4.20内核不支持epoll,需要打patch。本文主要从linux-2.4.32和linux-2.6.10两个内核版本介绍epoll。

select运行机制

select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。

select机制的问题

  1. 每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
  3. 为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)

poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。也就是说,poll只解决了上面的问题3,并没有解决问题1,2的性能开销问题。


二、 Epoll的使用
epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数:
所用到的数据结构

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 /
      };

    结构体epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件,其中epoll_data 联合体用来保存触发事件的某个文件描述符相关的数据,例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段以便后面的读写操作在这个文件描述符上进行。epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件可能的取值为:EPOLLIN :表示对应的文件描述符可以读;

EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:表示对应的文件描述符设定为edge模式;
  • 水平触发(LT):默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件
  • 边缘触发(ET): 当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次)。

所用到的函数:
1、epoll_create函数
    函数声明:int epoll_create(int size)
    该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。在linux-2.4.32内核中根据size大小初始化哈希表的大小,在linux2.6.10内核中该参数无用,使用红黑树管理所有的文件描述符,而不是hash。
2、epoll_ctl函数
    函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
    该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
    参数:epfd:由 epoll_create 生成的epoll专用的文件描述符;
                op:要进行的操作例如注册事件,可能的取值

如果调用成功返回0,不成功返回-1

EPOLL_CTL_ADD 注册、
EPOLL_CTL_MOD 修改、
EPOLL_CTL_DEL 删除
fd:关联的文件描述符;
event:指向epoll_event的指针;

3、epoll_wait函数
函数声明:int epoll_wait(int epfd,struct epoll_event   events,int maxevents,int timeout)
该函数用于轮询I/O事件的发生;
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值(ms);-1永不超时,直到有事件产生才触发,0立即返回。
返回发生事件数。-1有错误。

有关EPOLLOUT(写)监听的使用,理解起来有点麻烦。因为监听一般都是被动操作,客户端有数据上来需要读写(被动的读操作),EPOLIN监听事件很好理解,但是服务器给客户发送数据是个主动的操作,写操作如何监听呢?

 如果将客户端的socket接口都设置成 EPOLLIN | EPOLLOUT(读,写)两个操作都设置,那么这个写操作会一直监听,有点影响效率。EPOLLOUT(写)监听的使用场,一般说明主要有以下三种使用场景:

  1. 对客户端socket只使用EPOLLIN(读)监听,不监听EPOLLOUT(写),写操作一般使用socket的send操作。

    如果发送遇到EAGAIN/EWOULDBLOCK 就去加入EPOLLOUT,等待直到socket输出缓冲区可以写,那么epoll_wait就能触发EPOLLOUT,再去写数据。见示例2

  2. 客户端的socket初始化为EPOLLIN(读)监听,有数据需要发送时,对客户端的socket修改为EPOLLOUT(写)操作,这时EPOLL机制会回调发送数据的函数,发送完数据之后,再将客户端的socket修改为EPOLL(读)监听
  3. 对客户端socket使用EPOLLIN 和 EPOLLOUT两种操作,这样每一轮epoll_wait循环都会回调读,写函数,这种方式效率不是很好

简单的应用
跟网上的其它代码修改了一下,可以直接编译运行

#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>
#include <memory.h>

using namespace std;

#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

/*
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 
};
*/

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\n",argv[0]);
            return 1;
        }
    }
    else
    {
        fprintf(stderr,"Usage:%s portnumber\n",argv[0]);
        return 1;
    }



    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件

    struct epoll_event ev, events[20];
    //生成用于处理accept的epoll专用的文件描述符

    epfd=epoll_create(256);
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    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);
	
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);

    serveraddr.sin_port=htons(portnumber);
    bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    while ( 1 ) {
        //等待epoll事件的发生

        nfds=epoll_wait(epfd, events, 20, 500);
        //处理所发生的所有事件

        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);
                }
                //setnonblocking(connfd);

                char *str = inet_ntoa(clientaddr.sin_addr);
                cout << "accapt a connection from " << str << endl;
                //设置用于读操作的文件描述符

                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)//如果是已经连接的用户,并且收到数据,那么进行读入。

            {
                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;
                //修改sockfd上要处理的事件为EPOLLOUT

                //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

            }
            else if(events[i].events&EPOLLOUT) // 如果有数据发送

            {
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                //设置用于读操作的文件描述符

                ev.data.fd=sockfd;
                //设置用于注测的读操作事件

                ev.events=EPOLLIN|EPOLLET;
                //修改sockfd上要处理的事件为EPOLIN

                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            }
        }
    }
    return 0;
}

示例2

void write_handler2(ev_data *ptr)
{
 
    //判断指针, 可自由发挥
    if(!ptr || !ptr->buffer){
        close(ptr->fd);
        puts(" write_handler2 ptr is empty ");
        return;
    }
 
     //如果没数据,就直接返回了
    if(ptr->nread <= 0){
        printf("buffer is empty , ptr->nread:%d \n",ptr->nread);
        return;
    }
    static struct epoll_event ev ={0,{0}};
 
 
    //要发送的字节数
    int left = ptr->nread - ptr->start_write_index;
 
    //指针位置
    char * pbuffer = ptr->buffer + ptr->start_write_index;
    int nwriten = -1;
    int write_bytes = 0;
    printf("begin write , nread:%d, start_write_index:%d,left:%d\n",
    ptr->nread,ptr->start_write_index,left);
 
 
    while(left > 0){
        nwriten  = write(ptr->fd,pbuffer,left);
 
        //如果有错误
        if(nwriten <= 0){
 
            /*
                socket输出缓冲区满了
                那就加入EPOLLOUT事件,等epoll_wait返回通知你
            */
            if(errno < 0 && ( EWOULDBLOCK == errno || EAGAIN == errno)){
 
                //记录一下, 下一次要从哪里开始
                ptr->start_write_index += write_bytes;
 
                //这个纯粹为了防止重复的epoll_ctl,额外增加负担的,你不做判断也没事
                if(EPOLLOUT & ptr->events)
                    return;
            
                //增加EPOLLOUT事件
                ptr->events |= EPOLLOUT;
                ev.events = ptr->events;
                ev.data.ptr = ptr;
                
                //修改事件
                epoll_ctl(ptr->epoll_fd,EPOLL_CTL_MOD,ptr->fd,&ev);
                printf("socket buff is full , nread:%d,start_write_index:%d,left:%d\n",
                ptr->nread,ptr->start_write_index,left);
                return;
            }
            else{
 
                //如果出错了, 这里你可以自由发挥
                close(ptr->fd);
                free_event(ptr);
                perror("write error");
                return;
            }
        }
        pbuffer += nwriten;
        left -= nwriten;
        write_bytes += nwriten;
    }
 
    //到这里,说明该写的都写了, 重置一下读写位置
    ptr->start_write_index = ptr->nread = 0;
 
    //这个判断纯粹为了防止每次都去epoll_ctl,不加判断也行
    if(EPOLLOUT & ptr->events) {
 
        //把 EPOLLOUT 删了 ,这样就跟原来一样还是EPOLLIN|EPOLLET
        ptr->events &= ~EPOLLOUT;
        ev.events =ptr->events;
        ev.data.ptr = ptr;
        //修改一下
        epoll_ctl(ptr->epoll_fd, EPOLL_CTL_MOD, ptr->fd, &ev);
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值