epoll简介

内核事件表
1、epoll使用一组函数来完成任务,而不是单个函数
2、epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像select和poll一样每次调用都要重复传入文件描述符集或是事件集。
3、但是epoll需要使用一个额外的文件描述符,来唯一标志内核中的这个事件表
     使用 int epoll_create(int size) 来创建,
          size现在并不其作用,只是给内核一个提示,告诉它事件表需要多大
     使用 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 操作epoll的内核事件表
          fd参数是要操作的文件描述符,op参数指定操作类型,包含:
             1、EPOLL_CTL_ADD
             2、EPOLL_CTL_MOD
             3、EPOLL_CTL_DEL
          event参数指定事件,类型如下:
             struct epoll_event
             {
             _uint32_t events;     //epoll事件
             epoll_data_t data;    //用户数据
             }
             其中,events成员描述事件类型,与poll类型差不多,只是在其前面加‘E’,比如可读事件是EPOLLIN。但是epoll有两个额外的事件类型,EPOLLET, EPOLLONESHOT
             其中,epoll_data_t类型如下:
             typedef union epoll_data
             {
             void *ptr;
             int fd;
             uint32_t u32;
             uint64_t u64;
          }epoll_data_t;
             可以看出,这里使用union,只能使用其中一个。

epoll_wait函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数成功时返回就绪的文件描述符个数
该函数如果检测到事件,就将所有就绪事件从内核事件表中复制到它的第二个参数events指向的参数中。这就是epoll函数高效率的一个因素,如下:


比较

//使用poll监听
int nr = poll(fds, MAXEVENT_NUMBER, -1);

for(int i=0; i<MAXEVENT_NUMBER; i++)
{
    if(fds[i].revents & POLLIN)
    {
        int sockfd = fds[i].fd;
        //deal with sockfd
    }
}


//使用epoll
int nr = epoll_wait(epollfd, events, MAXEVENT_NUMBER, -1);

for(int i=0; i<nr; i++)            //体现在这里
{
    int sockfd = events[i].data.fd;
    //deal with sockfd
}



LT和ET模式

epoll对文件描述符的工作方式有两种: 1、LT(Level Trigger) 2、ET(Edge Trigger)
默认工作方式是LT模式
对于采用LT工作模式的文件描述符,当epoll_wait侦测到其上有事件发生并将此事件通知应用进程后,应用程序可以不立即处理该事件。这样,应用程序下次调用epoll_wait时,epoll_wait还是会再次向应用程序通告此事件,直到该时间被处理
对于ET模式,当epoll_wait侦测到其上有事件发生并将此事件通知应用进程后,应用程序必须立刻处理事件,后续调用epoll_wait将不再通知此事件
下面给出使用epoll的两种模式的例子:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>

typedef int bool;
#define false 0
#define true 1

#define MAX_EVENT 1024
#define BUFFER_SIZE 10

void lt(struct epoll_event *events, int number, int epollfd, int listenfd);
void et(struct epoll_event *events, int number, int epollfd, int listenfd);

void setnonblocking(int fd);
void addfd(int epollfd, int fd, bool enable_et);

int main(int ac, char *av[])
{
    if(ac != 3)
    {
        fprintf(stderr, "Usage: %s address port",av[0]);
    }

    char *ip = av[1];
    int port = atoi(av[2]);
    int ret;

    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET, ip, &addr.sin_addr);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        perror("socket error");
        exit(1);
    }

    ret = bind(sock, (struct sockaddr *)&addr, sizeof(addr));
    if(ret < 0)
    {
        perror("bind error");
        exit(1);
    }

    ret = listen(sock, 10);
    if(ret < 0)
    {
        perror("listen error");
        exit(1);
    }
                //以上可忽略
                //将监听套接字的读事件注册在内核事件表中,等待新的连接
    struct epoll_event events[MAX_EVENT];
    int epollfd = epoll_create(5);
    if(epollfd == -1)
    {
        perror("listen error");
        exit(1);
    }
    addfd(epollfd, sock, true);

    while(1)
    {
        //等待epoll的返回
        ret = epoll_wait(epollfd, events, MAX_EVENT, -1);

        if(ret < 0)
        {
            perror("listen error");
            exit(1);
        }
        lt(events, ret, epollfd, sock);        //使用LT模式
        //et(events, ret, epollfd, sock);        //使用LT模式
    }

    close(sock);
    return 0;
}

//设置描述符为非阻塞
void setnonblocking(int fd)
{
    int op = fcntl(fd, F_GETFL);
    op = op | O_NONBLOCK;
    fcntl(fd, F_SETFL, op);
}

//将事件注册进内核事件表
void addfd(int epollfd, int fd, bool enable_et)
{
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = fd;
    if(enable_et)
    {
        event.events |= EPOLLET;
    }
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

//LT模式
//当epoll_wait侦测到其上有事件发生并将此事件通知应用进程后,应用程序可以不立即处理该事件。这样,应用程序下次调用epoll_wait时,epoll_wait还是会再次向应用程序通告此事件,直到该时间被处理
//注意,这里已经使用非阻塞
void lt(struct epoll_event *events, int number, int epollfd, int listenfd)
{
    int i;
    char buf[BUFFER_SIZE];
    for(i=0; i<number; i++)
    {
        int sock = events[i].data.fd;
        //如果是监听套接字有就绪事件
        if(sock == listenfd)
        {
            int connfd = accept(listenfd, NULL, NULL);
            if(connfd < 0)
            {
                perror("accept error");
                exit(1);
            }
            addfd(epollfd, connfd, false);
        }
        //连接套接字有就绪事件
        else if(events[i].events & EPOLLIN)
        {
            //这里我们只读一次,因为如果我们没有全部读完,这个描述符会出现在下一次的epoll_wait里,我依旧可以继续读
            printf("lt :event trigger once \n");
            memset(buf, '\0', BUFFER_SIZE);
            int ret = recv(sock, buf, BUFFER_SIZE-1, 0);
            if(ret <= 0)
            {
                close(sock);
                continue;
            }
            printf("get %d bytes from the side\n", ret);
            if(ret < BUFFER_SIZE-1)        //表示读完了
                close(sock);
        }
        else
        {
            printf("something else happened\n");
        }
    }
}

//ET模式
//对于ET模式,当epoll_wait侦测到其上有事件发生并将此事件通知应用进程后,应用程序必须立刻处理事件,后续调用epoll_wait将不再通知此事件
void et(struct epoll_event *events, int number, int epollfd, int listenfd)
{
    char buf[BUFFER_SIZE];
    int ret;
    int i;
    for(i=0; i<number; i++)
    {
        int sock = events[i].data.fd;
        if(sock == listenfd)
        {
            int connfd = accept(listenfd, NULL, NULL);
            if(connfd < 0)
            {
                perror("accept error");
                exit(1);
            }
            addfd(epollfd, connfd, false);
        }
        else if(events[i].events & EPOLLIN)
        {
            printf("et :event trigger once\n");
            //这里我们使用循环读,是因为事件只通知一次,我们必须一次就把所有数据都读完
            while(1)
            {
                ret = recv(sock, buf, BUFFER_SIZE-1, 0);
                //非阻塞读什么时候会报错呢?当没有数据可读的时候,非阻塞读就会报错,但是根据epoll返回,说明有数据可读,返回错误的话表示我们读完了
                if(ret < 0)
                {
                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                    {
                        close(sock);
                        break;
                    }
                }
                else if(ret == 0)
                {
                    close(sock);
                }
                else
                    printf("get %d bytes form the side\n", ret);

            }
        }
        else
        {
            printf("something else happened\n");
        }
    }
}



首先,使用默认的LT模式得到的结果:
客户端:
$ telnet xxx.xxx.xxx.xxx 13000
Trying xxx.xxx.xxx.xxx...
Connected to Ben.
Escape character is '^]'.
jasndfknasfnsla
Connection closed by foreign host.


服务器端:
lt :event trigger once
get 9 bytes from the side
lt :event trigger once
get 8 bytes from the side


然后使用ET模式得到的结果为:
客户端为:
$ telnet xxx.xxx.xxx.xxx 13000
Trying xxx.xxx.xxx.xxx...
Connected to Ben.
Escape character is '^]'.
asfnaslfnaklnf
Connection closed by foreign host.


服务器端为:
et :event trigger once
get 9 bytes form the side
get 7 bytes form the side
可以很明显的看出差别



EPOLLONESHOT事件
    现在我们仔细看ET模式,虽然ET模式指明下一次epoll_wait调用不会因为这次文件描述符没有被读完而再次被通知,但是如果进程在读取完某个socket上的数据后在处理这些数据时,该socket上又有新的数据可读(EPOLLIN再次被触发),在并发处理的情况下,另一个线程(进程)被唤醒来读取这些数据    ,于是就同时有两个线程在操作一个socket的局面,这当然不是我们锁期望的。 我们期望的是任何时刻一个socket只被一个线程处理
    这里就用到了EPOLLONESHOT。
    以下为使用线程情况下需要使用EPOLLONESHOT的主要代码部分(都是非阻塞的):

//主函数中
//在epoll_wait的通知事件中,监听socket被通知
connfd = accept(...);
event.data.fd = connfd;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
//在epoll_wait的通知事件中,某个连接socket的EPOLLIN事件被通知
pthread_create(...,worker,..);


//worker函数主要部分
while(1)
{
    ret = recv(....);
    if(ret == 0)
    {
        //foreign closet the socket,对端终止连接
        close(fd);
        break;
    }
    else if(ret < 0)
    {
        if(errno == EAGAIN)
        {
            //已无数据可读
            //重置已注册的这个事件,以确保下一次这个socket可读时,能被再次触发,被某个空闲线程处理
            reset_oneshot(epollfd, fd);
            printf("read later\n")
            break;
        }
    }
    else
    {
        printf("get content %s\n",buf);
        //模拟处理数据的操作,假设需要花5秒,保证我们在处理这个socket上的数据时是独占着这个socket的
        sleep(5);
    }
}


这样保证了一个socket同一时刻只有一个线程在处理,在不同时刻可能被不同线程处理。



最后,进行select, poll, epoll的简单总结性比较
     select的参数类型fd_set没有将文件描述符和事件绑定,仅仅是一个文件描述符集合,因此,select提供了三个参数分别指示读、写、异常,使得select不能处理更多类型事件。
     另一方面由于内核对描述符集合的修改,下次调用select不得不重置这3个集合
     poll相对聪明些,文件描述符和事件绑定起来,任何事件被统一处理,编程接口简洁。且无需像select每次调用都要重置,内核只修改revents成员。
     但是,select和poll一样,返回的是整个用户注册的事件集合,所以时间复杂度为O(n)
     epoll则不同。
     epoll在内核中维护一个事件表,每次epoll_wait都是从该内核事件表中取得用户注册的事件,而无需反复从用户空间读入这些事件。
     epoll_wait系统调用的events参数仅用来返回就绪事件,这使得应用程序索引就绪文件描述符的复杂度为O(1)

其次,select和poll都只能工作在LT模式,而epoll则可以使用ET模式
    从实现原理上看,select和poll采用的都是轮询方式,每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序,因此它们检测事件的复杂度为O(N)
    epoll则采用回调方式,内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪事件队列,内核最后在适当的时机将该就绪事件队列的内容拷贝到用户空间。因此,epoll无需轮询整个描述符集合,复杂度为O(1)
    但是,如果在活动连接较多时,回调函数触发过于频繁,效率就不见得高
    因此, epoll适用于连接数量多,但活动连接较少的情况。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值