当前位置: 首页 > Linux

深度解密epoll是如何工作的?

时间:2023-04-06 05:11:19 Linux

epoll是linux平台下独特的多路复用IO实现。与传统的select相比,epoll在性能上有了很大的提升。本文主要讲解epoll的实现原理,epoll的使用可以参考相关书籍或文章。相关视频推荐面试中严重“千篇一律”的网络原理tcp/udp、网络编程epoll/reactor6种epoll设计,让面试官你挂了,他顶不顶。epoll原理解析及三手四手处理LinuxC++后台服务器开发架构师免费学习地址epoll创建要使用epoll,首先需要调用epoll_create()函数创建epoll句柄。epoll_create()函数定义如下:intepoll_create(intsize);由于历史原因遗留了参数大小。现在不起作用。当用户调用epoll_create()函数时,会进入内核空间,调用sys_epoll_create()内核函数创建epoll句柄。sys_epoll_create()函数的代码如下:asmlinkagelongsys_epoll_create(intsize){interror,fd=-1;结构事件轮询*ep;错误=-EINVAL;if(size<=0||(error=ep_alloc(&ep))<0){fd=error;转到错误返回;}fd=anon_inode_getfd("[eventpoll]",&eventpoll_fops,ep);if(fd<0)ep_free(ep);error_return:returnfd;}sys_epoll_create()主要做了两件事:调用ep_alloc()函数创建并初始化一个eventpoll对象。调用anon_inode_getfd()函数将eventpoll对象映射到文件句柄,并返回文件句柄。我们先来看看eventpoll对象。eventpoll对象用于管理epoll监控的文件列表。它的定义如下:structeventpoll{...wait_queue_head_twq;...结构list_headrdllist;结构rb_rootrbr;...};下面解释一下eventpoll对象的各个成员的作用:wq:等待队列,当调用epoll_wait(fd)时,进程会被加入到eventpoll对象的wq等待队列中。rdllist:保存已经准备好的文件列表。rbr:使用红黑树来管理所有被监控的文件。下图是eventpoll对象与被监控文件的关系:由于被监控文件是通过epitem对象来管理的,所以上图中的节点都是以epitem对象的形式存在的。为什么要用红黑树来管理监控文件?这是为了通过文件句柄快速找到对应的epitem对象。红黑树是一种平衡二叉树。不了解的可以参考相关文档。【文章福利】:小编整理了一些个人认为比较好的学习书籍和视频资料分享到群档。如果需要,可以自己添加!~点击加入(需要自己去取)给epoll添加一个文件句柄。在上一节中,我们介绍了如何创建epoll。接下来介绍如何将需要监控的文件添加到epoll中。可以通过调用epoll_ctl()函数将需要监控的文件添加到epoll中,其原型如下:longepoll_ctl(intepfd,intop,intfd,structepoll_event*event);下面解释各个参数的作用:epfd:通过调用epoll_create()函数返回的文件句柄。op:要执行的操作,有3个选项:EPOLL_CTL_ADD:表示要执行添加操作。EPOLL_CTL_DEL:表示要进行删除操作。EPOLL_CTL_MOD:表示要进行修改操作。fd:要监听的文件句柄。事件:告诉内核要监视什么。它的定义如下:structepoll_event{__uint32_tevents;/*epoll事件*/epoll_data_t数据;/*用户数据变量*/};events可以是以下宏的集合:EPOLLIN:表示可以读取对应的文件句柄(包括结束SOCKET常闭);EPOLLOUT:表示可以写入对应的文件句柄;EPOLLPRI:表示对应的文件句柄有紧急数据需要读取;EPOLLERR:表示对应的文件句柄有错误;EPOLLHUP:表示挂起对应的文件句柄;EPOLLET:设置EPOLL为EdgeTriggered模式,相对于LevelTriggered。EPOLLONESHOT:只听一个事件。监听到这个事件后,如果需要继续监听这个socket,需要再次将这个socket加入到EPOLL队列中。data用于保存用户定义的数据。epoll_ctl()函数将调用sys_epoll_ctl()内核函数。sys_epoll_ctl()核函数的实现如下:asmlinkagelongsys_epoll_ctl(intepfd,intop,intfd,structepoll_event__user*event){...file=fget(epfd);tfile=fget(fd);...ep=文件->私有数据;mutex_lock(&ep->mtx);epi=ep_find(ep,tfile,fd);错误=-EINVAL;switch(op){caseEPOLL_CTL_ADD:if(!epi){epds.events|=POLLERR|POLLHUP;error=ep_insert(ep,&epds,tfile,fd);}否则错误=-EEXIST;休息;...}mutex_unlock(&ep->mtx);...returnerror;}sys_epoll_ctl()函数会根据传入的不同ops的值进行不同的操作,比如传入EPOLL_CTL_ADD,表示添加操作,则调用ep_insert()函数进行添加操作.下面继续分析添加操作ep_insert()函数的实现:staticintep_insert(structeventpoll*ep,structepoll_event*event,structfile*tfile,intfd){...error=-ENOMEM;//申请epitem对象if(!(epi=kmem_cache_alloc(epi_cache,GFP_KERNEL)))gotoerror_return;//初始化epitem对象INIT_LIST_HEAD(&epi->rdllink);INIT_LIST_HEAD(&epi->fllink);INIT_LIST_HEAD(&epi->pwqlist);epi->ep=ep;ep_set_ffd(&epi->ffd,tfile,fd);epi->事件=*事件;epi->nwait=0;epi->next=EP_UNACTIVE_PTR;epq.epi=epi;//相当于:epq.pt->qproc=ep_ptable_queue_procinit_poll_funcptr(&epq.pt,ep_ptable_queue_proc);//调用被监控文件的poll接口。//这个接口是各个文件系统实现的,比如socket,那么这个接口就是tcp_poll()。revents=tfile->f_op->poll(tfile,&epq.pt);...ep_rbtree_insert(ep,epi);//将epitem对象添加到epoll的红黑树中进行管理spin_lock_irqsave(&ep->lock,flags);//如果被监控的文件已经可以配对相应的读写操作//然后将文件添加到epoll的就绪队列rdllink中,并唤醒调用epoll_wait()的进程。if((revents&event->events)&&!ep_is_linked(&epi->rdllink)){list_add_tail(&epi->rdllink,&ep->rdllist);如果(waitqueue_active(&ep->wq))wake_up_locked(&ep->wq);如果(waitqueue_active(&ep->poll_wait))pwake++;}spin_unlock_irqrestore(&ep->lock,flags);...返回0;...}被监控的文件是通过epitem对象进行管理的,也就是说被监控的文件会被封装成一个epitem对象,然后添加到eventpoll对象的红黑树中进行管理(比如ep_rbtree_insert(ep,epi)在上面的代码中)tfile->f_op->poll(tfile,&epq.pt)这行代码的作用是调用被监控文件的poll()接口。如果被监视的文件是套接字句柄,那么tcp_poll()将被调用。让我们看看tcp_poll()做了什么:unsignedinttcp_poll(structfile*file,structsocket*sock,poll_table*wait){structsock*sk=sock->sk;...poll_wait(文件,sk->sk_sleep,等待);...returnmask;}每个socket对象都有一个等待队列(waitqueue,等待队列请参考文章:等待队列原理及实现),用于存放等待socket状态变化的进程。从上面代码可以看出,tcp_poll()调用了poll_wait()函数,poll_wait()最终会调用ep_ptable_queue_proc()函数。ep_ptable_queue_proc()函数实现如下:结构eppoll_entry*pwq;if(epi->nwait>=0&&(pwq=kmem_cache_alloc(pwq_cache,GFP_KERNEL))){init_waitqueue_func_entry(&pwq->waitcall,ep_poll_back;pwq->whead=whead;pwq->base=epi;add_wait_queue(whead,&pwq->wait);list_add_tail(&pwq->llink,&epi->pwqlist);epi->nwait++;}else{epi->nwait=-1;}}ep_ptable_queue_proc()函数的主要工作是添加当前epitem对象加入到socket对象的等待队列中,并将唤醒函数设置为ep_poll_callback(),即当socket状态发生变化时,会触发调用ep_poll_callback()函数。ep_poll_callback()函数实现如下:staticintep_poll_callback(wait_queue_t*wait,unsignedmode,intsync,void*key){...//将就绪文件加入就绪队列list_add_tail(&epi->rdllink,&ep->rdllist);is_linked://通过调用epoll_wait()唤醒阻塞的进程if(waitqueue_active(&ep->wq))wake_up_locked(&ep->wq);...return1;}ep_poll_callback()函数的主要工作是将就绪文件添加到eventepoll对象的就绪队列中,然后通过调用epoll_wait()唤醒阻塞的进程。等待被监控文件的状态变化将被监控文件句柄加入epoll后,可以调用epoll_wait()等待被监控文件状态变化。epoll_wait()调用将阻塞当前进程。当被监控文件的状态发生变化时,epoll_wait()调用将返回。epoll_wait()系统调用的原型如下:longepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);各参数含义:epfd:调用epoll_create()函数创建的epoll句柄。events:用于存放就绪文件列表。maxevents:事件数组的大小。timeout:设置等待的超时时间。epoll_wait()函数会调用sys_epoll_wait()内核函数,sys_epoll_wait()函数最终会调用ep_poll()函数。我们来看看ep_poll()函数的实现:staticintep_poll(structeventpoll*ep,structepoll_event__user*events,intmaxevents,longtimeout){...//如果就绪文件列表为空if(list_empty(&ep->rdllist)){//将当前进程加入epoll的等待队列init_waitqueue_entry(&wait,current);等待.flags|=WQ_FLAG_EXCLUSIVE;__add_wait_queue(&ep->wq,&wait);对于(;;){set_current_state(TASK_INTERRUPTIBLE);//设置当前进程进入休眠状态if(signal_pending(current)){//接收信号并退出res=-EINTR;休息;}spin_unlock_irqrestore(&ep->lock,flags);jtimeout=schedule_timeout(jtimeout);//放弃CPU,切换到其他进程执行spin_lock_irqsave(&ep->lock,flags);}//这里有3种情况会执行://1.监控文件集合中有就绪文件//2.设置超时时间,超时//3.收到信号__remove_wait_queue(&ep->wq,&等等);set_current_state(TASK_RUNNING);}/*是否有准备好的文件?*/eavail=!list_empty(&ep->rdllist);spin_unlock_irqrestore(&ep->lock,flags);如果(!res&&eavail&&!(res=ep_send_events(ep,events,maxevents))&&jtimeout)转到重试;returnres;}ep_poll()函数主要做了以下几件事:判断监听的文件集合中是否有就绪文件,如果有则返回,如果没有则将当前进程加入epoll的等待队列,然后进入休眠状态。进程会休眠,直到出现以下情况:被监控的文件集合中有就绪文件,设置超时时间,收到超时时间。如果有就绪文件,则调用ep_send_events()函数将就绪文件复制到events参数中。返回就绪文件的数量。最后我们通过一张图总结一下epoll的原理:下面用文字描述一下这个过程:调用epoll_create()函数创建并初始化一个eventpoll对象。通过调用epoll_ctl()函数,将监听到的文件句柄(如socket句柄)封装成一个epitem对象,加入到eventpoll对象的红黑树中进行管理。通过调用epoll_wait()函数等待被监控文件的状态发生变化。当被监听文件的状态发生变化时(比如socket接收到数据),文件句柄对应的epitem对象会被加入到eventpoll对象的就绪队列rdllist中。并将就绪队列的文件列表复制到epoll_wait()函数的events参数中。通过调用epoll_wait()函数唤醒被阻塞(休眠)的进程。