本文包含以下内容:epoll工作原理本文不包含以下内容:epoll的用法epoll的缺陷我很喜欢epoll这类简单易用,不深入的东西principlebuthavegreatuses,尽管可能比较老,select和poll的缺点,epoll对于经常需要处理数万个连接的网络服务应用可以说是革命性的。对于普通的本地应用,select和poll可能非常有用,但是对于像C10K这样的高并发网络场景,select和poll就捉襟见肘了。看他们的APIintselect(intnfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,structtimeval*timeout);intpoll(structpollfd*fds,nfds_tnfds,inttimeout);它们有一个共同点,users监控的文件描述符集合需要打包为参数,作为参数传入。每次调用时,这个集合都会从用户空间复制到内核空间。原因是内核没有这个集合的内存。对于大多数应用来说,这完全是一种浪费,因为应用需要监控的描述符在大部分时间里基本没有变化,可能会有变化,但变化不大。epoll对epoll的改进这个改进正是它的实现方式。它需要完成以下两件事。添加描述符---内核可以记录用户关心哪些文件的哪些事件。事件---内核可以记录哪些文件的哪些事件是真实发生的。当用户来获取它时,可以将结果提供给用户。由于需要添加描述符,因此内核需要一个数据结构来记忆。这个数据结构很简单,如下图所示。epoll_instance,它有一个链表头。链表上的元素epoll_item是用户添加的。每个项目记录描述符fd和感兴趣事件的组合。事件的类型有很多种,其中POLLIN表示可读事件是用户使用最多的。例如:当一个TCPsocket收到一个消息时,它会变成可读的;当管道接收到对端发送的数据时,它将变得可读;当一个timerfd对应的定时器超时后,就会变为可读Read;现在你需要将这些可读事件与之前的epoll_instance关联起来。在Linux中,每个文件描述符在内核中都有对应的structfile结构。这个结构文件有一个private_data指针。根据文件的实际类型,它们指向不同的数据结构。那么我能想到的最方便的方法就是在epoll_item中添加一个指向structfile的指针,在structfile中添加一个指向epollitem的指针。为了记录有事件的文件,我们还需要在epoll_instance中添加一个就绪列表readylist,在private_data指针指向的各种数据结构中添加一个返回structfile的指针,在epollitem中添加一个挂载点字段,当一个文件可读时,它对应的epoll项被附加到epoll_instance。之后,用户可以通过系统调用读取就绪列表,知道哪些文件已经就绪。嗯,以上纯粹是我个人想到的epoll的一般工作方式,肯定有很多缺陷。但是epoll的实际实现和上面类似。让我们谈谈创建一个epoll实例。像上面的epoll_instance,内核需要一个数据结构来存储和记录用户的注册项。这个结构体就是内核中的structeventpoll。当用户使用epoll_create(2)或epoll_create1(2)时,内核fs/eventpoll.c实际上会创建这样一个结构。/**创建内部数据结构(“structeventpoll”)。*/error=ep_alloc(&ep);this结构中比较重要的部分是几个链表,但是在刚创建实例的时候都是空的,后面可以看到它们的作用。epoll_create()最终会返回一个文件描述符给用户,方便用户操作epoll实例,所以在创建epoll实例后,内核会分配一个文件描述符fd和对应的structfile结构/**创建所有设置eventpoll文件所需的项目。也就是说,*一个文件结构和一个空闲文件描述符。*/fd=get_unused_fd_flags(O_RDWR|(flags&O_CLOEXEC));file=anon_inode_getfile("[eventpoll]",&eventpoll_fops,ep,O_RDWR|(flags&O_CLOEXEC));最后将它们与刚才的epoll实例关联起来,然后返回给用户fdep->file=file;fd_install(fd,文件);返回fd;完成后epoll实例变成这样。向epoll实例添加文件描述符用户可以通过epoll_ctl(2)向epoll实例添加监听描述符和感兴趣的事件。和前面的epoll项一样,内核真正创建的是一个叫做structepitem的结构作为注册表项。如下图所示,epoll实例为了在描述符较多的情况下有更高的查找效率,将每一个structepitem以红黑树的形式进行组织(代替上例中的链表)。structepitem结构体中的ffd用于记录关联的文件字段,同时也作为本项加入红黑树的key;它将作为挂载点附加到epoll实例中的ep->rdllist链表。fllink的作用是将其附加到fd对应文件的file->f_tfile_llink链表中作为挂载点。一般来说,这个链表最多只有一个元素。除非发生重复。pwqlist是一个链表头,用来连接轮询等待队列。虽然是链表,但实际上,链表最多只会附加一个元素。创建structepitem的代码在fs/evnetpoll.c的ep_insert()中if(!(epi=kmem_cache_alloc(epi_cache,GFP_KERNEL)))return-ENOMEM;之后,每个字段都将被初始化/*Iteminitializationfollowhere...*/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->event=*event;epi->nwait=0;epi->next=EP_UNACTIVE_PTR;然后设置局部变量epqstructep_pqueueepq;epq.epi=epi;init_poll_funcptr(&epq.pt,ep_ptable_queue_proc);epq的数据结构是structep_pqueue,它是polltable的一个包装器(增加了一个指向structepitem*的指针)structep_pqueue{poll_tablept;structepitem*epi;}轮询表包含一个函数和一个事件掩码typedefvoid(*poll_queue_proc)(structfile*,wait_queue_head_t*,structpoll_table_struct*);typedefstructpoll_table_struct{poll_queue_proc_qproc;无符号长_key;//存储感兴趣的事件掩码}poll_table;这个轮询表用在什么地方?答案是,它用在structfile_operations的poll操作中(这个和本文开头提到的sel一样ect`poll`不是东西)structfile_operations{//省略代码...unsignedint(*poll)(structfile*,structpoll_table_struct*);//代码省略...}不同的文件有不同的poll实现,但是一般都是按照下面的形式实现的无符号整型事件=0;poll_wait(file,&privatedata->wqh,wait);如果(文件可读)事件|=POLLIN;returnevents;}他们主要实现了两个函数将XXX放到文件私有数据的等待队列中(一般在file->private_data队列头wait_queue_head_twqh有一个等待队列),至于XXX是什么,各种类型的实现offiles不同,根据poll_table参数查询是否有事件,有则返回。有兴趣的读者可以通过timerfd_poll()或pipe_poll()来实现poll_wait的实现非常简单。就是调用poll_table中设置的函数,以文件的私有等待队列为参数。staticinlinevoidpoll_wait(structfile*filp,wait_queue_head_t*wait_address,poll_table*p){if(p&&p->_qproc&&wait_address)p->_qproc(filp,wait_address,p);}回到ep_insert()所以这里设置的poll_table是ep_ptable_queue_proc()。然后revents=ep_item_poll(epi,&epq.pt);查看它的实现,可以看到它其实是在主动调用文件的poll函数。这里我们以TCPsocket文件为例(毕竟网络应用最广泛)等待){sock_poll_wait(文件,sk_sleep(sk),等待);//会调用poll_wait()//代码省略...}可以看到最后调用了poll_wait(),所以注册的ep_ptable_queue_proc()会执行structepitem*epi=ep_item_from_epqueue(pt);结构eppoll_entry*pwq;pwq=kmem_cache_alloc(pwq_cache,GFP_KERNEL),另外一个structeppoll_entry是分配结构。其实它和structepitem结构是一一对应的。然后是一些初始化init_waitqueue_func_entry(&pwq->wait,ep_poll_callback);//设置func:ep_poll_callbackpwq->whead=whead;pwq->base=epi;add_wait_queue(whead,&pwq->wait)list_add_tail(&pwq->llink,&epi->pwqlist);epi->nwait++;最重要的是设置pwd->wait.func=ep_poll_callback现在,structepitem和structeppoll_entry之间的关系如下。文件可读后,对于TCPsocket,当收到peer消息时,会调用初始设置的sk->sk_data_ready函数voidsock_init_data(structsocket*sock,structsock*sk){//代码省略...sk->sk_data_ready=sock_def_readable;//省略代码...}最终会调用__wake_up_common,它将遍历挂在socket.wq上的等待队列*当前,*下一步;list_for_each_entry_safe(curr,next,&q->task_list,task_list){flags=curr->flags;如果(curr->func(curr,mode,wake_flags,key)&&(flags&WQ_FLAG_EXCLUSIVE)&&!--nr_exclusive)break;然后,沿着图中红色的轨迹,它会调用我们设置的ep_poll_callback,那么下一步就是让epoll实例知道有文件已经是可读的了。首先从输入参数中取出当前表项epi和ep。结构epitem*epi=ep_item_from_wait(wait);structeventpoll*ep=epi->ep;然后把epi挂到ep的就绪队列if(!ep_is_linked(&epi->rdllink)){list_add_tail(&epi->rdllink,&ep->rdllist)}然后唤醒阻塞在epoll实例中(如果有)的用户。waitqueue_active(&ep->wq)用户获取事件谁可能被阻塞在epoll实例的等待队列中呢?当然是用户使用epoll_wait从epoll实例中获取感兴趣事件的描述符。epoll_wait将调用ep_poll()函数。if(!ep_events_available(ep)){/**我们没有任何可用的事件返回给调用者。*我们需要在这里睡觉,当事件可用时我们将被*ep_poll_callback()唤醒。*/init_waitqueue_entry(&wait,current);__add_wait_queue_exclusive(&ep->wq,&wait);如果没有事件,我们就把自己挂在epoll实例的等待队列上,然后进入睡眠……如果有事件,那么我们就把事件返回给用户ep_send_events(ep,events,maxevents)referenceepoll的实现
