[转]epoll是如何监控多个描述符及如何获得通知

原文出自:http://blog.chinaunix.net/uid-23629988-id-3569332.html


我们起初聊的是TCP/IP协议栈的自下而上的流程。当我说到内核在选择了正确的socket以后,会唤醒在这个socket上等待的进程,通知他们有新数据包来了。这时,该朋友说到,这是同步模式,那epoll是如何实现的呢?这里先插一句,我认为该朋友的说法有问题。对于前者,可以说是同步模式,其实我觉得他更想强调的是阻塞模式。而无论是将其说成阻塞模式,还是同步模式,epoll都不是相反的。聊这个问题的时候,我当时对epoll的太少了,也就没有对epoll的实现发表什么看法。只是简单的聊了聊epoll的一些特点,以及与select的对比。

这几天找了点空余时间,带着这个问题,看了一些epoll的资料。

关于epoll本身的文章和资料已经有了很多,大多数都是关于epoll的应用,或者是将其与poll,select进行对比,也有部分是分析epoll的源码的,但是都是分析epoll的API在内核的实现。对于我这个简单的问题,网上貌似没有直接的答案。这两天,看了看代码,大致明白了epoll如何同时监控多个描述符及如何获得通知。

1. 无论是select还是epoll,都是基于poll的机制实现的。而poll是VFS要求的一个成员函数,每个具体文件系统的实现,都有对应的poll实现(socket也是一个虚拟的文件系统)。
2. 无论是select还是epoll,其实仍然是阻塞模式。只不过select和epoll在阻塞调用中,可以监控多个文件描述符,还可以设置一个超时。

select的实现代码相对于epoll,要简单很多,是以轮询的方式查询各个描述符。下面看看epoll是如何做到的?

首先看ep_insert函数,这个函数用于插入新的监控描述符。

  1.     /* Initialize the poll table using the queue callback */
  2.     epq.epi = epi;
  3.     init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

  4.     /*
  5.      * Attach the item to the poll hooks and get current event bits.
  6.      * We can safely use the file* here because its usage count has
  7.      * been increased by the caller of this function. Note that after
  8.      * this operation completes, the poll callback can start hitting
  9.      * the new item.
  10.      */
  11.     revents = tfile->f_op->poll(tfile, &epq.pt);
这几行代码是关键的代码。这里的epq像一个粘合剂,把epoll和内核本身的poll机制黏在了一起。epq.epi = epi,这个epi对应了epoll一个监控描述符对象在epoll中的实例,然后init_poll_funcptr设置了epq的poll table的回调函数,完成了epq的初始化。然后调用该文件描述符对应的poll实现函数。

这里就要跳转到具体的poll函数了,以socket文件描述符为例,当该socket为UDP时候,对应的poll实现函数为udp_poll。

  1. unsigned int udp_poll(struct file *file, struct socket *sock, poll_table *wait)
  2. {
  3.     unsigned int mask = datagram_poll(file, sock, wait);
  4.     struct sock *sk = sock->sk;

  5.     /* Check for false positives due to checksum errors */
  6.     if ((mask & POLLRDNORM) && !(file->f_flags & O_NONBLOCK) &&
  7.      !(sk->sk_shutdown & RCV_SHUTDOWN) && !first_packet_length(sk))
  8.         mask &= ~(POLLIN | POLLRDNORM);

  9.     return mask;

  10. }
这个代码很简单。关键函数是datagram_poll

  1. unsigned int datagram_poll(struct file *file, struct socket *sock,
  2.              poll_table *wait)
  3. {
  4.     struct sock *sk = sock->sk;
  5.     unsigned int mask;

  6.     sock_poll_wait(file, sk_sleep(sk), wait);
  7.     mask = 0;
  8.     
  9.     //处理事件
  10.     ...... ......

  1.     return mask;
  2. }
sk_sleep(sk)就是sk上的wait队列。那么就需要进入sock_poll_wait

  1. static inline void sock_poll_wait(struct file *filp,
  2.         wait_queue_head_t *wait_address, poll_table *p)
  3. {
  4.     if (p && wait_address) {
  5.         poll_wait(filp, wait_address, p);
  6.         /*
  7.          * We need to be sure we are in sync with the
  8.          * socket flags modification.
  9.          *
  10.          * This memory barrier is paired in the wq_has_sleeper.
  11.         */
  12.         smp_mb();
  13.     }
  14. }
  1. static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
  2. {
  3.     if (p && wait_address)
  4.         p->qproc(filp, wait_address, p);
  5. }
看到这里,绕了一圈,又回到了起点。。。需要查看ep_ptable_queue_proc的实现
  1. static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
  2.                  poll_table *pt)
  3. {
  4.     struct epitem *epi = ep_item_from_epqueue(pt);
  5.     struct eppoll_entry *pwq;

  6.     if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
  7.         init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
  8.         pwq->whead = whead;
  9.         pwq->base = epi;
  10.         add_wait_queue(whead, &pwq->wait);
  11.         list_add_tail(&pwq->llink, &epi->pwqlist);
  12.         epi->nwait++;
  13.     } else {
  14.         /* We have to signal that an error occurred */
  15.         epi->nwait = -1;
  16.     }
  17. }
这里的代码很明确,获得epoll中的监控描述符的实例epi,然后创建一个epoll的等待节点,并将其放到参数whead的队列中。对应上面的例子中,即sock的等待队列中。这样,就将epoll和具体对应的描述联系起来。当对应的描述符执行唤醒操作时,就会利用上面的关联,唤醒epoll。

上文书说到,epoll是如何加到每个监控描述符的wait queue中,这只是第一步。上次也提过,epoll实际上也是一个阻塞操作,只不过是可以同时监控多个文件描述符。下面看一下epoll_wait->ep_poll的实现。

epoll既然是阻塞的,必然需要wait queue。但是这个不能使用监控的文件描述符的wait queue,epoll自己本身也是一个虚拟的文件系统。epoll_create的返回值也是一个文件描述符。Unix下,一切皆是文件嘛。

所以epoll的实现代码如下:

  1.         init_waitqueue_entry(&wait, current);
  2.         __add_wait_queue_exclusive(&ep->wq, &wait);

  3.         for (;;) {
  4.             /*
  5.              * We don't want to sleep if the ep_poll_callback() sends us
  6.              * a wakeup in between. That's why we set the task state
  7.              * to TASK_INTERRUPTIBLE before doing the checks.
  8.              */
  9.             set_current_state(TASK_INTERRUPTIBLE);
  10.             if (ep_events_available(ep) || timed_out)
  11.                 break;
  12.             if (signal_pending(current)) {
  13.                 res = -EINTR;
  14.                 break;
  15.             }

  16.             spin_unlock_irqrestore(&ep->lock, flags);
  17.             if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
  18.                 timed_out = 1;

  19.             spin_lock_irqsave(&ep->lock, flags);
  20.         }
  21.         __remove_wait_queue(&ep->wq, &wait);
这里epoll_wait是将当前进程添加到epoll自身的wait queue中。那么问题来了,前文说到epoll已经将当前进程加到了各个监控描述符的wait queue中。现在这里又有了一个epoll自身的wait queue。这是为什么呢?
回答这个问题,需要我们再跳回ep_ptable_queue_proc——不记得这个函数的同学,请翻看前面的文章。这个函数调用init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);,将epoll当前进程的wait queue节点的回调函数设置为ep_poll_callback。对比epoll调用的init_waitqueue_entry函数,这个函数设置wait queue节点的回调函数为default_wake_function。

那么当监控文件描述符执行wakeup动作时,比如一个socket收到数据时,调用sk_data_ready->sock_def_readable->wake_up_interruptible_sync_poll->....最终会执行wait_queue节点的回调函数。对于epoll来说,即ep_poll_callback。

  1. static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
  2. {
  3.     int pwake = 0;
  4.     unsigned long flags;
  5.     struct epitem *epi = ep_item_from_wait(wait);
  6.     struct eventpoll *ep = epi->ep;

  7.     spin_lock_irqsave(&ep->lock, flags);

  8.     /*
  9.      * If the event mask does not contain any poll(2) event, we consider the
  10.      * descriptor to be disabled. This condition is likely the effect of the
  11.      * EPOLLONESHOT bit that disables the descriptor when an event is received,
  12.      * until the next EPOLL_CTL_MOD will be issued.
  13.      */
  14.     if (!(epi->event.events & ~EP_PRIVATE_BITS))
  15.         goto out_unlock;

  16.     /*
  17.      * Check the events coming with the callback. At this stage, not
  18.      * every device reports the events in the "key" parameter of the
  19.      * callback. We need to be able to handle both cases here, hence the
  20.      * test for "key" != NULL before the event match test.
  21.      */
  22.     if (key && !((unsigned long) key & epi->event.events))
  23.         goto out_unlock;

  24.     /*
  25.      * If we are transferring events to userspace, we can hold no locks
  26.      * (because we're accessing user memory, and because of linux f_op->poll()
  27.      * semantics). All the events that happen during that period of time are
  28.      * chained in ep->ovflist and requeued later on.
  29.      */
  30.     if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
  31.         if (epi->next == EP_UNACTIVE_PTR) {
  32.             epi->next = ep->ovflist;
  33.             ep->ovflist = epi;
  34.         }
  35.         goto out_unlock;
  36.     }

  37.     /* If this file is already in the ready list we exit soon */
  38.     if (!ep_is_linked(&epi->rdllink))
  39.         list_add_tail(&epi->rdllink, &ep->rdllist);

  40.     /*
  41.      * Wake up ( if active ) both the eventpoll wait list and the ->poll()
  42.      * wait list.
  43.      */
  44.     if (waitqueue_active(&ep->wq))
  45.         wake_up_locked(&ep->wq);
  46.     if (waitqueue_active(&ep->poll_wait))
  47.         pwake++;

  48. out_unlock:
  49.     spin_unlock_irqrestore(&ep->lock, flags);

  50.     /* We have to call this outside the lock */
  51.     if (pwake)
  52.         ep_poll_safewake(&ep->poll_wait);

  53.     return 1;
  54. }
这个函数的注释相当清楚,可以清晰的知道每一行代码的用途。其中

  1. if (waitqueue_active(&ep->wq))
  2.         wake_up_locked(&ep->wq);
这两行代码,检测了epoll自身的wait queue上是否有等待的节点,如果有的话,就执行唤醒动作。对于epoll的使用者来说,如果用户态正阻塞在epoll_wait中,那么ep->wq一定不为空,这时就会被唤醒。将该进程移到就绪队列中。

这两篇文章基本上理清了epoll如何监控多个描述符及如何获得通知的过程。对于如何监控来说,还欠缺了epoll内部结构,如何保存的各个描述符,如何维护的信息等。不过这样的文章网上已经有了很多。也许以后我会针对这个问题,再写两篇文章吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值