IO多路复用--epoll

一.初识epoll

1.epoll是Linux内核为处理大批量文件描述符而作了改进的poll;
2.epoll是Linux下多路复用IO接口select/poll的增强版本;
3.epoll能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

二.epoll相关的系统调用

1.epoll_create
(1)函数原型:

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

//在linux2.6.8之后,size参数是被忽略的

(2)作用:用来创建一个epoll句柄(或者说是epoll对象);
(3)返回值:调用epoll_create函数,成功之后会返回一个文件描述符,这个文件描述符也就是创建出来的epoll对象,所以当用完之后需要调用close函数来关闭这个文件描述符,否则会导致文件描述符用尽。
2.epoll_ctl
(1)函数原型:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

(2)作用:对epoll对象epfd,对于文件描述符fd执行什么样的操作,可以将fd注册进epfd中,也可以删除epfd中的fd,还可以修改fd监听的事件。
(3)函数参数:

epfd:是epoll_create函数创建的epoll对象
op:对该epfd的相关操作
fd:需要监听的文件描述符
event:是一个结构体的地址,该结构体用来告诉内核需要监听的时间是什么,需要监听的是什么类型的数据

(4)epoll_event结构体

 struct epoll_event {
         uint32_t events;      /* Epoll events */
         epoll_data_t data;      /* User data variable */
     }__EPOLL_PACKED;

  typedef union epoll_data {
        void *ptr;
         int fd;
         uint32_t u32;
         uint64_t u64;
     } epoll_data_t;    //保存触发事件的某个文件描述符相关的数据

(5)epoll_ctl第二个参数op的取值:

EPOLL_CTL_ADD:注册新的文件描述符到epoll对象epfd中
EPOLL_CTL_DEL:在epfd中,根据文件文件描述符fd删除指定的fd对应的相关信息
EPOLL_CTL_MOD:修改已经注册的fd的监听事件

3.epoll_wait
(1)函数原型:

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

(2)作用:收集在epfd中已经就绪的事件
(3)参数:

events:这里的events是一个struct epoll_event的数组,该数组里面存放内核拷贝至用户空间中已经就绪的文件描述符相关信息,该数组的每个元素都是一个epoll_event的结构体
maxevents:表示events数组的大小
timeout:表示超时时间,以毫秒为单位,如果为0则不会阻塞立即返回,-1是永久阻塞

(4)返回值:

大于0:返回IO中已经准备好的文件描述符的个数
-1:表示函数调用出错
等于0:表示超时

三.epoll的工作原理

1.每一个epoll对象都有一个独立的eventpoll结构体,该结构体内容如下:

//eventpoll结构体
struct eventpoll {
    spinlock_t lock; /* Protect the access to this structure */ 

    struct mutex mtx;

    wait_queue_head_t wq; /* Wait queue used by sys_epoll_wait(),epoll文件的等待队列(是一个双链表)*/
/*调用epoll_wait的进程可能在此队列上睡眠, 等待ep_poll_callback()函数唤醒或超时 */

    wait_queue_head_t poll_wait;     
    struct list_head rdllist; /* List of ready file descriptors (就绪链表,双向链表,用于存放将要通过epoll_wait返回给用户的满足条件的文件描述符的相关结构)*/
    struct rb_root rbr;   /* RB tree root used to store monitored fd structs (红黑树的根节点用于存储所有添加到epoll中的需要监控的事件)*/
    struct epitem *ovflist; /* This is a single linked list(单链表) that chains all the "struct epitem" that happened while transferring
ready events to userspace w/out holding ->lock. */
/*(如果正在向用户空间传递事件,此时状态就绪的文件描述符相关的结构会暂时放在该队列上,否则会直接添加到就绪队列rdllist中。) */    

    struct wakeup_source *ws;    /* wakeup_source used when ep_scan_ready_list is running */ 

    struct user_struct *user; /* The user that created the eventpoll descriptor */
    struct file *file;                
int visited; 
    struct list_head visited_list_link;
};

(1)可以看到:每一个epoll对象包含的eventpoll结构体中,有两个很重要的成员,一个是红黑树,另一个是就绪队列(双向链表);
(2)在event_pool结构体中,还包含一个成员ovflist,里面暂时存放状态就绪的文件描述符的相关数据结构的单链表,可以发现该链表结点的数据类型是epitem结构体,epitem结构体如下:

//epitem结构体
struct epitem{ 
    struct rb_node rbn;    /* 红黑树结点 */ 
    struct list_head rdllink; /* 双向链表结点,用于连接eventpoll就绪列表*/
    struct epitem *next;
    struct epoll_filefd ffd; /* 关联的文件描述符,事件句柄信息 */
    int nwait; /* 轮询操作次数*/ 
    struct list_head pwqlist;
    struct eventpoll *ep; /* 指向其所属的eventpool对象*/ 
    struct list_head fllink; 
    struct wakeup_source __rcu *ws;  /* wakeup_source used when EPOLLWAKEUP is set */
    struct epoll_event event; /* 当前的epitem关心哪些events,这个数据是调用epoll_ctl从用户空间传来的,期待发生的事件类型*/ 
};
2.epoll_create函数执行的操作

(1)epoll_create函数调用epoll_create1函数,epoll_create1函数里面调用ep_alloc函数,ep_alloc函数会调用相关的函数初始化epoll对象的就绪队列(一个空的就绪队列,当某个文件描述符指定的事件发生时,就会将该文件描述符相应的信息放在该就绪队列中);
(2)ep_alloc函数还会调用相关函数初始化红黑树;
(3)epoll_create1函数还会调用 anon_inode_getfd()函数,该函数会分配一个空闲描述符,会创建一个匿名文件,将该空闲文件描述符与匿名文件相关联,该空闲文件描述符就是调用epoll_create函数成功后的返回值(也就是epoll对象);

3.epoll_ctl函数执行的操作
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

(1)从用户空间拷贝关心的事件和相应的文件描述符。
(2)epoll_ctl函数会在红黑树里面查找fd是否存在,如果存在就可以找到fd在红黑树对应的位置;
(3)根据op,采取不同的操作:

补充知识:红黑树是一棵相对平衡的二叉搜索树,一般是K-V模型(既有key,也有value),对红黑树的增删查改都需要先找到key,红黑树的效率很高,增删查改的时间复杂度为,其中N为结点的个数;并且红黑树中不能存在key值相同的两个结点(有一个特例:STL里面的multiset/multimap使用红黑树实现的,它们的key值可以冗余)。

  • 如果op是EPOLL_CTL_ADD,表示将fd及相应关心的事件添加至红黑树中,fd是一个文件描述符可以作为唯一标识,所以采用fd作为红黑树的key值,由于红黑树是一棵搜索二叉树,并且key值不能冗余,所以如果对应的fd已经存在于红黑树中,就不会再次插入进红黑树;如果fd不存在,则将fd插入进相应的位置;(这就避免了重复插入,重复插入会降低效率);
  • 如果op是EPOLL_CTL_DEL,表示删除fd及相应事件,会从红黑树里根据fd的值将fd及其对应的epitem结构删除,因为删除是根据fd来删,不关心相应的事件,所以第三个参数可以填NULL;
  • 如果op是EPOLL_CTL_MOD,表示修改fd对应的事件,也就是根据fd,找到fd对应的epitem结构,修改epitem结构里面的事件信息,也就是在红黑树里面根据key的值修改value的值。

(4)当把文件描述符及相应的关心事件插入进红黑树之后,内核会注册回调函数,并初始化回调函数epollcallback(该回调函数会与网卡驱动建立关系),当文件描述符所关心的某个事件就绪之后,网卡驱动程序调用该回调函数,会将就绪的文件描述符及epitem结构插入进就绪队列中(该就绪队列是一个双向链表);

4.epoll_wait函数执行的操作
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

(1)当用户调用epoll_wait函数时,用户需要自己传入一个epoll_event数组,以及该数组的大小,还会传入一个超时时间;
(2)epoll_wait函数首先会检查用户传入色参数是否合法(events数组是否可写,maxevents是否为有效大小);
(3)epoll_wait主要调用ep_poll函数获取事件,ep_poll函数会检查就绪队列是否为空,如果就绪队列为空,就将该进程加入到eventpoll的等待队列中,等待被回调函数唤醒或者到了超时事件,如果过了超时时间还没有被唤醒,epoll_wait函数也会返回,返回0,如果就绪队列不为空,就将就绪队列里面的事件拷贝至用户传入的events数组中,并且将就绪文件描述符的数量返回给用户。
(4)因为一次不会有多个文件描述符相应的事件就绪,也就是就绪队列不会很长,则内核将就绪队列中的内容拷贝至用户空间的时间复杂度接近于O(1),这也是epoll高效的体现。
(5)epoll_wait函数成功返回的是就绪的文件描述符的个数,也就是events数组里面有效内容的大小,通过遍历events数组,就可以得到相应的文件描述符和事件,根据不同的文件描述符就可以采取不同的操作。

四.epoll使用过程

epoll使用过程分为3步:

1.调用epoll_create创建一个epoll句柄(epoll对象);
2.调用epoll_ctl函数对需要监控的文件描述符进行增删改;
3.调用epoll_wait函数等待文件描述符就绪。

五.epoll的优缺点

1.epoll等待的文件描述符没有上限;
2.随着文件描述符的增加,epoll的性能并没有下降,因为epoll采用基于事件的就绪通知方式,哪怕文件描述符比较多,一旦监听的某个文件描述符就绪,也会迅速激活这个文件描述符;
3.调用epoll_wait获取就绪的文件描述符速度会很快,因为调用epoll_wait直接从内核维护的就绪链表里面取,而一次就绪的文件描述符不会很多,使得就绪链表不会很长,这样操作的时间复杂度为O(1);
4.epoll的缺点:当多连接中有很多活跃的文件描述符不适合用epoll,反而会使得epoll的性能下降,因为当活跃的文件描述符比较多时,就绪队列就会很长,拷贝就会变得很慢。所以epoll比较适用于多连接且只有一部分比较活跃。

六.epoll与select的区别

1.epoll文件描述符的数量没有上限,select文件描述符的数量有限制;
(1)因为epoll将需要监控的文件描述符放在红黑树里面,只要内存足够大,就能打开很多的文件描述符,它所监控的文件描述符是最大可以打开文件的数目;
(1)select文件描述符的大小取决于fd_set的大小,fd_set一般为1024字节,而文件描述符对应一位,也就是一般最多为1024*8个文件描述符;
2.epoll执行过程由三个函数来完成,虽然比select一个函数看起来复杂,但是因为epoll将“等”文件描述符这个过程拆成三步,避免了做一些重复的工作;
3.每次调用select都需要将文件描述符从用户空间拷贝至内核空间,当文件描述符很多时,这个代价很大,而epoll将文件描述符放至红黑树里,只要被监听的文件描述符存在红黑树中,下次就不用插入,避免了重复拷贝,epoll保证了每个描述符在整个过程中只会拷贝一次。
4.每次调用select之后都需要遍历从内核传来的所有文件描述符,而epoll只有调用epoll_wait函数后,遍历events数组,因为每次就绪的文件描述符不是很多,就会使得遍历的次数比较少,不用遍历所有的文件描述符。
5.select将输入输出文件描述符用一个参数表示,没有将其分离开来,而epoll采用epoll_ctl函数获得输入的文件描述符,采用epoll_wait函数获得输出文件描述符。

七.epoll的使用

采用epoll编写简单的HTTP服务器,浏览器访问时可以在浏览器页面上显示“hello world”


#include<stdio.h>                                                                                                                       
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/epoll.h>
#include<netinet/in.h>

int server_init(int port)
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        return -1;
    }

    int opt = 1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    local.sin_port = htons(port);

    int ret = bind(sock,(struct sockaddr*)&local,sizeof(local));
    if(ret < 1)
    {
        perror("bind");
        return -2;
    }

    ret = listen(sock,5);
    if(ret < 0)
    {
        perror("listen");
        return -3;
    }
    return sock;
}

void hander_request(int epfd,struct epoll_event ev, int new_sock)
{
    char buf[10240] = {0};
    ssize_t size = read(new_sock,buf,sizeof(buf)-1);
    if(size < 0)                                                                                                                        
    {
        perror("read");
        return;
    }
    else if(size == 0)
    {
        //说明对端关闭连接
        //需要关闭new_sock,并且将new_sock从epfd中删除
        printf("client close\n");
        close(new_sock);
        epoll_ctl(epfd,EPOLL_CTL_DEL,new_sock,NULL);
        return;
    }
    else
    {
        buf[size] = '\0';
        printf("%s",buf);
        //并且向客户端返回响应
        //需要将listen_sock文件描述符关心的读就绪变为写就绪
        ev.data.fd = new_sock;
        ev.events = EPOLLOUT;
        epoll_ctl(epfd,EPOLL_CTL_MOD,new_sock,&ev);
    }
}
//处理连接的函数
void hander_connect(int epfd,int listen_sock)                                                                                           
{
    //首先调用accept获取连接
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
    if(new_sock < 0)
    {
        perror("accept");
        return;
    }

    //将new_sock添加至epoll对象中
    struct epoll_event ev;
    ev.data.fd = new_sock;
    ev.events = EPOLLIN;
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
    if(ret < 0)
    {
        perror("epoll_ctl");
        return;
    }
}

int main(int argc,char* argv[])
{
    if(argc != 2)                                                                                                                       
    {
        printf("Usage %s [port]\n",argv[0]);
        return 1;
    }
    //初始化服务器
    int listen_sock = server_init(atoi(argv[1]));
    if(listen_sock < 0)
    {
        perror("server_init fail\n");
        return 2;
    }

    //创建一个epoll对象
    int epfd = epoll_create(256);
    if(epfd < 0)
    {
        perror("epoll_create");
        return 3;
    }

    //将listen_sock及相应的事件添加至epoll模型中
    struct epoll_event ev;
    ev.data.fd = listen_sock;
    ev.events = EPOLLIN;
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);                                                                            
    if(ret < 0)
    {
        perror("epoll_ctl");
        return 4;
    }
    struct epoll_event events[100];
    int timeout = -1;
    while(1)
    {
       int size =  epoll_wait(epfd, events,sizeof(events)/sizeof(events[0]), timeout);
       if(size < 0)
       {
           perror("epoll_wait");
           continue;
       }
       else if(size == 0)
       {
           printf("timeout\n");
           continue;
       }
       else
       {
           //就绪的数组events里面包含就绪的文件描述符
           //需要遍历整个events数组
           int i = 1;                                                                                                                   
           for(;i<size;++i)   //注意这里遍历的最大范围为size
           {
               //根据events里的每个元素进行相关判断
               int fd = events[i].data.fd;
               if(events[i].events & EPOLLIN)
               {
                   //读就绪
                   if(fd == listen_sock)
                   {
                       //listen_sock就绪
                       hander_connect(epfd,listen_sock);
                   }
                   else
                   {
                       //new_sock就绪
                       //通过new_sock处理客户端的请求
                       hander_request(epfd,ev,fd);   //此时的fd就是new_sock
                   }
               }

               //判断文件描述符是否为写就绪
               if(events[i].events & EPOLLOUT)
               {
                   char* response = "HTTP/1.1 200 OK\n\n\n<h1>hello world</h1>";
                   write(fd,response,strlen(response));
                   close(fd);
                   epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);                                                                               
               }
           }
       }

    }
    return 0;
}             

运行结果:
这里写图片描述

猜你喜欢

转载自blog.csdn.net/xu1105775448/article/details/81238064