I/O多路复用之epoll学习总结

一、epoll的系统调用函数
epoll的实现和select和poll有很大的区别,首先epoll是由一组函数来实现的而不是单个函数;其次epoll把关心的文件描述符上的事件放在内核的一个事件表里面,从而不需要向select和poll那样每次调用都要重复传入文件描述符集或者事件集。但是epoll需要一个额外的文件描述符来标识内核中的这个时间表。
1、epoll_create创建一个epoll模型(内核事件表);

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

size参数并不起作用,它只是给内核一个提示,告诉它事件表需要多大。这个函数的返回值作为其他两个epoll函数的第一个参数,用来指定要访问的内核事件表;
创建一个内核事件表其实就是在Linux内核中创建一颗空的红黑树和空的就绪队列,具体见下文。。。
2、epoll_ctl维护该epoll模型,主要的体现就是采用回调机制激活红黑树中的结点到就绪队列中;

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

epoll_ctl是epoll的事件注册函数,它不同于select函数的是select是在监听事件的时候告诉内核要监听什仫类型的事件,epoll_ctl是先注册要监听的事件类型;
epfd:是epoll_create()的返回值,用来指定监听的是哪一个epoll模型;
op:指定操作类型,一般由三个宏来表示:

EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;

fd:指定需要监听的文件描述符fd;
event:指定参数,它是epoll_event结构指针类型,它的结构如下:

typedef union epoll_data {
        void        *ptr;
        int          fd;
        __uint32_t   u32;
        __uint64_t   u64;
} epoll_data_t;

struct epoll_event {
       __uint32_t   events;      /* epoll事件 */
       epoll_data_t data;        /* 用户数据 */
};

epoll_data是一个联合体,其中fd使用的最多,表示指定事件的文件描述符;ptr可以用来指定与fd相关的用户数据。如果要将文件描述符和用户数据关联起来,可以让ptr指向的用户数据这种包含fd。
在epoll_event中的events成员是从以下几个宏来体现的:

EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(LevelTriggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里;

3、epoll_wait在该文件上等待特定文件描述符上的就绪事件,它是在一段超时事件内等待一组文件描述符上的事件;

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

返回值:成功返回等待文件描述符的个数;失败返回-1并设置错误码;超时返回0;
epfd:是epoll_create()的返回值,用来指定监听的是哪一个epoll模型;
events:这个参数也是epoll_event结构体的一个成员;
epoll_wait()如果检测到事件,就将所有的就绪事件从内核事件表(epfd所指定的epoll模型)中复制它的第二个参数events中去,这个结构体类型的数组只用于输出检测到的就绪事件,而不是向select一样是一个输入输出型参数,这样就极大地提高了应用程序查找就绪文件描述符的效率(这里采用的是mmap技术);
maxevents:该参数用来指定最多监听多少个事件,它必须大于0;
timeout:这个参数和poll接口中的timeout参数类似,是用来设置超时时间的;
二、ET模式和LT模式
epoll对文件描述符的操作有两种方式:LT(水平触发)和ET(边沿触发),epoll的默认工作方式是LT,在这种模式下epoll相当于一个相对较高效的poll。ET模式是epoll相对较为高效的工作方式;
对于LT工作模式的文件描述符,当检测到有事件发生并将此事件通知应用程序之后,应用程序可以不立即处理该事件,当下一次调用epoll_wait()的时候,epoll_wait()还会再次向应用程序通告此事件,直到该事件被处理;
对于ET工作模式的文件描述符,当检测到有事件发生并将此事件通知应用程序之后,应用程序就会立即处理该数据,因为后续的epoll调用将不再向应用程序通知这一事件。ET模式在很大程度上降低了同一个epoll事件被触发的次数,因为比LT模式效率要高。
每个使用ET模式的文件描述符都应该是非阻塞的。如果文件描述符是阻塞的,那仫读或者写操作会因为没有后续事件而一直处于阻塞状态(饥渴状态);
三、epoll的工作原理

epoll的工作原理
四、epoll的优点
1>、底层采用回调机制激活某个节点,将已经就绪的文件描述符添加到就绪队列中去;
2>、当有新的事件发生的时候,该结点会被插入到epoll模型中的红黑树中去,红黑树增删查改效率比较高,它的时间复杂度为O(N*lgN);
3>、epoll_wait获得就绪的文件描述符是从就绪队列中获得的,它的时间复杂度为O(1),这也是epoll的时间复杂度同时也是epoll高效的原因;
4>、epoll所关心的文件描述符是无上限的;
5>、将就绪队列中的数据结点映射到epoll_wait的events结构体中,节省了一次内核态至用户态的拷贝,这是mmap技术(内存映射技术);
6>、就绪事件的陈列方式不同,epoll访问的是就绪队列避免了访问无价值的数据,而select是访问的是一个数组;
五、一个关于epoll的例子

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#define SIZE 10240

//创建一个结构体类型,里面保存一个文件描述符和一段缓冲区
typedef struct epbuf
{
    int fd;
    char buf[SIZE];
}epbuf_t,*epbuf_p,**epbuf_pp;

static epbuf_p alloc_epbuf(int fd)
{
    epbuf_p ptr=(epbuf_p)malloc(sizeof(epbuf_t));
    if(NULL == ptr)
    {
        perror("malloc");
        exit(6);
    }
    ptr->fd=fd;
    return ptr;
}

static void del_epbuf(epbuf_p ptr)
{
    if(NULL != ptr)
    {
        free(ptr);
        ptr=NULL;
    }
}

int StartUp(const char *ip,int port)
{
    assert(ip);
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket error");
        exit(2);
    }
    //端口号复用
    int opt=1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(port);
    local.sin_addr.s_addr=inet_addr(ip);
    if(bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0)
    {
        perror("bind error");
        exit(3);
    }

    if(listen(sock,10) < 0)
    {
        perror("listen error");
        exit(4);
    }
    return sock;
}

int myread(int sock,char *buf)
{
    int len=0;
    int total=0;
    while((len=read(sock,buf+total,1024) > 0) && (len == 1024))
    {
        //读成功并且读到的值与期望值相符合
        total += len;
    }
    //没有读到1024个字节,更新total
    if(len > 0 && len < 1024){
        total += len;
    }
    buf[total]='\0';
    return total;
}

int set_fd_nonblock(int sock)  //设置非阻塞
{
    int old_opt=fcntl(sock,F_GETFL);
    int new_opt=old_opt | O_NONBLOCK;
    fcntl(sock,F_SETFL,new_opt);
    return old_opt;
}

static void Usage(const char *proc)
{
    printf("Usage:%s [local_ip] [local_port]\n",proc);
}

int main(int argc,char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    int listenfd=StartUp(argv[1],atoi(argv[2]));

    int epfd=epoll_create(256);  //创建一个epoll模型,其实底层就是创建一颗红黑树和就绪队列

    if(epfd < 0)
    {
        perror("epoll_create error");
        return 5;
    }

    struct epoll_event ev;
    ev.events=EPOLLIN | EPOLLET;   //工作模式是ET模式
    ev.data.ptr=alloc_epbuf(listenfd);

    set_fd_nonblock(listenfd);      //如果工作在ET模式则必须设置非阻塞

    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);  //将监听套接字添加到epoll模型中

    while(1)
    {
        struct epoll_event evs[32];
        int max_evs=32;
        int timeout=1000;   //设置超时时间
        //int timeout=-1;   //此时说明阻塞
        int nums=0;
        switch(nums=epoll_wait(epfd,evs,max_evs,timeout))
        {
            case -1:
                perror("epoll_wait error");
                break;
            case 0:
                printf("timeout...\n");
                break;
            default:
                {
                    int i=0;
                    for(;i<nums;i++){
                        int sock=((epbuf_p)(evs[i].data.ptr))->fd;
                        if((sock == listenfd) && (evs[i].events & EPOLLIN)){
                            //监听事件到来并且发生了EPOLLIN事件
                            struct sockaddr_in client;
                            socklen_t len=sizeof(client);
                            int new_sock=accept(sock,(struct sockaddr *)&client,&len);
                            if(new_sock < 0){
                                perror("accept");
                                continue;
                            }

                            printf("get a new client,ip is %s,port is %d\n",\
                                    inet_ntoa(client.sin_addr),ntohs(client.sin_port));

                            ev.events=EPOLLIN | EPOLLET;    //ET

                            set_fd_nonblock(new_sock);

                            ev.data.ptr=alloc_epbuf(new_sock);
                            epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
                        }//if
                        else if((sock != listenfd) && (evs[i].events & EPOLLIN)){
                            //普通事件到达并且发生了EPOLLIN事件
                            char *buf=((epbuf_p)(evs[i].data.ptr))->buf;
                            //ssize_t s=read(sock,buf,SIZE-1);
                            ssize_t s=myread(sock,buf);
                            if(s > 0){  //读成功
                                buf[s-1]='\0';
                                printf("client# %s\n",buf);
                                //修改事件的状态并将其添加到就绪队列中去
                                ev.events=EPOLLOUT;    
                                epoll_ctl(epfd,EPOLL_CTL_MOD,sock,&ev);
                            }
                            else if(s == 0){  //有客户端退出
                                printf("client is quit\n");
                                del_epbuf(evs[i].data.ptr);
                                epoll_ctl(epfd,EPOLL_CTL_DEL,sock,NULL);
                                close(sock);
                            }
                            else{  //读失败
                                perror("read");
                                del_epbuf(evs[i].data.ptr);
                                epoll_ctl(epfd,EPOLL_CTL_DEL,sock,NULL);
                                close(sock);
                            }
                        }//else if
                        else if((sock != listenfd) && (evs[i].events & EPOLLOUT))
                        {
                            //是普通事件并且发生了EPOLLOUT事件
                            char *msg="HTTP/1.0 200 OK\r\n<html><h1>HELLO EPOLL!!!</h1></html>\n";
                            ssize_t ret=write(sock,msg,strlen(msg));
                            if(ret < 0)
                            {
                                perror("write");
                                return 6;
                            }
                            del_epbuf(evs[i].data.ptr);
                            epoll_ctl(epfd,EPOLL_CTL_DEL,sock,NULL);
                            close(sock);
                        }
                        else   //其他事件不做任何处理
                        {}
                    }//for
                }
                break;
        }
    }
    return 0;
}

使用telnet测试结果展示:
epoll
在这里就分享结束了~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值