Linux epoll 源码分析 2

Posted 底层技术研究

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux epoll 源码分析 2相关的知识,希望对你有一定的参考价值。

继上一篇 ,我们来继续看下 epoll_ctl 方法。


// fs/eventpoll.c

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,

    struct epoll_event __user *, event)

{

  ...

  struct fd f, tf;

  struct eventpoll *ep;

  struct epitem *epi;

  struct epoll_event epds;

  ...

  if (ep_op_has_event(op) &&

      copy_from_user(&epds, event, sizeof(struct epoll_event)))

    goto error_return;

  ...

  f = fdget(epfd);

  ...

  tf = fdget(fd);

  ...

  ep = f.file->private_data;

  ...

  epi = ep_find(ep, tf.file, fd);

  ...

  switch (op) {

  case EPOLL_CTL_ADD:

    if (!epi) {

      epds.events |= POLLERR | POLLHUP;

      error = ep_insert(ep, &epds, tf.file, fd, full_check);

    } else

    ...

    break;

  case EPOLL_CTL_DEL:

    if (epi)

      error = ep_remove(ep, epi);

    else

      error = -ENOENT;

    break;

  case EPOLL_CTL_MOD:

    if (epi) {

      if (!(epi->event.events & EPOLLEXCLUSIVE)) {

        epds.events |= POLLERR | POLLHUP;

        error = ep_modify(ep, epi, &epds);

      }

    } else

      error = -ENOENT;

    break;

  }

  ...

  return error;

}


该方法的大体操作为


1. 如果 ep_op_has_event 返回true,则拷贝用户提供的event到方法的私有变量里。这样,在该方法调用完成之后,用户的 epoll_event 对象还是可以重用的。


// fs/eventpoll.c

static inline int ep_op_has_event(int op)

{

  return op != EPOLL_CTL_DEL;

}


2. 通过epfd找到eventpoll对应的文件。


3. 通过fd找到要被监听的目标文件,比如socket文件。


4. 从epfd对应文件的private_data字段获取eventpoll实例。


5. 调用ep_find方法,查找eventpoll实例中是否监听了目标文件。


6. 如果op是EPOLL_CTL_ADD,则调用ep_insert方法,如果是EPOLL_CTL_DEL,则调用ep_remove方法,如果是EPOLL_CTL_MOD,则调用ep_modify方法,来执行进一步的操作。


由代码我们还能看到,如果op是EPOLL_CTL_ADD或EPOLL_CTL_MOD,内核会自动帮我们注册POLLERR和POLLHUP事件,这在epoll_ctl的man文档中也有提到。


7. 返回error状态给用户。


至此,epoll_ctl 方法的大体轮廓已经有了,现在我们继续看下 ep_insert、ep_remove、ep_modify 这三个方法。


先看下ep_insert方法


// fs/eventpoll.c

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,

         struct file *tfile, int fd, int full_check)

{

  ...

  struct epitem *epi;

  struct ep_pqueue epq;

  ...

  if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))

    return -ENOMEM;

  ...

  // 初始化epitem实例

  ...

  epq.epi = epi;

  init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

  ...

  revents = ep_item_poll(epi, &epq.pt, 1);

  ...

  ep_rbtree_insert(ep, epi);

  ...

  if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {

    list_add_tail(&epi->rdllink, &ep->rdllist);

    ...

    if (waitqueue_active(&ep->wq))

      wake_up_locked(&ep->wq);

    ...

  }

  ...

  return 0;

  ...

}


该方法的大体操作为


1. 为要被监听的目标文件分配一块内存,类型为epitem,然后对其初始化。


// fs/eventpoll.c

struct epitem {

  union {

    /* 在epitem被放到eventpoll实例的红黑树数据结构中时使用 */

    struct rb_node rbn;

    ...

  };


  // 当epitem代表的文件的被监听事件就绪时

  // 是通过这个字段把epitem放到eventpoll实例的rdllist队列中

  struct list_head rdllink;

  ...

  // 用于存放被监听的文件和其对应的fd

  struct epoll_filefd ffd;


  /* epitem被注册到的eventpoll */

  struct eventpoll *ep;

  ...

  /* 用户指定的要监听事件及私有数据 */

  struct epoll_event event;

};


2. 初始化ep_pqueue实例。


该实例的作用是,当 ep_ptable_queue_proc 方法被回调时,通过ep_pqueue实例可以拿到epitem实例。ep_ptable_queue_proc 方法我们后面还会详说。


// fs/eventpoll.c

struct ep_pqueue {

  poll_table pt;

  struct epitem *epi;

};

...

static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)

{

  pt->_qproc = qproc;

  pt->_key   = ~0UL; /* all events enabled */

}


3. 调用ep_item_poll方法,将epitem等相关信息组成的实例,放到被监听文件的事件变动通知队列中,这样当被监听文件有事件变化时,就会调用该队列里各个实例的回调方法,看是否有其感兴趣的事件发生。


该方法还会检查文件中有哪些事件已经发生,并返回我们感兴趣的那些事件。


4. 调用ep_rbtree_insert方法,把epitem实例放入到eventpoll实例的红黑树数据结构中。


5. 如果ep_item_poll方法返回的事件中有我们感兴趣的事件,则将epitem实例放到eventpoll实例的rdllink列表中,然后调用 wake_up_locked 方法,通知那些因调用 epoll_wait 方法而阻塞的线程,有你感兴趣的事件发生,你可以在rdllink列表中查看了。


6. 返回。


下面,我们再以tcp socket文件为例,具体看下ep_item_poll是如何做的。


如果是tcp socket类型的文件,ep_item_poll方法最终会调用tcp_poll方法。


// net/ipv4/tcp.c

unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)

{

  ...

  struct sock *sk = sock->sk;

  const struct tcp_sock *tp = tcp_sk(sk);

  ...

  sock_poll_wait(file, sk_sleep(sk), wait);

  ...

  if (state == TCP_LISTEN)

    return inet_csk_listen_poll(sk);

  ...

}

EXPORT_SYMBOL(tcp_poll);


该方法的参数中,file为tcp socket文件,wait为ep_item_poll方法传过来的poll_table实例,该实例被上面的init_poll_funcptr方法初始化,使其_qproc字段指向ep_ptable_queue_proc 方法,这个方法一会会被用到。


该方法调用了sock_poll_wait方法,传入一些参数。其中 sk_sleep(sk) 参数可以认为是 tcp socket事件变动通知队列。


// include/net/sock.h

static inline void sock_poll_wait(struct file *filp,

    wait_queue_head_t *wait_address, poll_table *p)

{

  if (!poll_does_not_wait(p) && wait_address) {

    poll_wait(filp, wait_address, p);

    ...

  }

}


sock_poll_wait 方法又调用了 poll_wait 方法。


// include/linux/poll.h

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)

{

  if (p && p->_qproc && wait_address)

    p->_qproc(filp, wait_address, p);

}


poll_wait 方法又调用了poll_table中的_qproc字段指向的方法,即上面提到的 ep_ptable_queue_proc 方法。


我们看下 ep_ptable_queue_proc 干了什么事。


// fs/eventpoll.c

static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,

         poll_table *pt)

{

  struct epitem *epi = ep_item_from_epqueue(pt);

  struct eppoll_entry *pwq;


  if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {

    init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);

    pwq->whead = whead;

    pwq->base = epi;

    if (epi->event.events & EPOLLEXCLUSIVE)

      add_wait_queue_exclusive(whead, &pwq->wait);

    else

      add_wait_queue(whead, &pwq->wait);

    ...

  } else {

    ...

  }

}


该方法作用就是把epitem实例、ep_poll_callback事件回调方法等,组成一个实例eppoll_entry,然后添加到whead指向的队列中,即 tcp socket 的 sk_sleep(sk) 事件通知队列。


这样,当tcp socket有事件发生时,就会回调 ep_poll_callback 方法,该方法会根据该事件是否是我们感兴趣的事件,决定是否唤醒因调用 epoll_wait 而阻塞的线程。


在看 ep_poll_callback 方法的具体实现之前,我们回头再看下 tcp_poll 方法的剩余内容。


在调用完 sock_poll_wait 方法之后,tcp_poll方法会检查当前已经就绪的事件。


比如上面代码中,我们以 listen socket 为例,tcp_poll方法会调用 inet_csk_listen_poll 方法。


// include/net/inet_connection_sock.h

static inline unsigned int inet_csk_listen_poll(const struct sock *sk)

{

  return !reqsk_queue_empty(&inet_csk(sk)->icsk_accept_queue) ?

      (POLLIN | POLLRDNORM) : 0;

}


当 listen socket 的 accept 队列不为空时,该方法会返回 POLLIN 事件,即,通知应用层可以accept了。


好,继续看上面说到的 ep_poll_callback 方法。


当tcp socket有事件发生时,比如收到数据,就会调用这个方法,执行和epoll相关的逻辑。


// fs/eventpoll.c

static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)

{

  ...

  struct epitem *epi = ep_item_from_wait(wait);

  struct eventpoll *ep = epi->ep;

  ...

  if (key && !((unsigned long) key & epi->event.events))

    goto out_unlock;

  ...

  if (!ep_is_linked(&epi->rdllink)) {

    list_add_tail(&epi->rdllink, &ep->rdllist);

   ...

  }

  ...

  if (waitqueue_active(&ep->wq)) {

    ...

    wake_up_locked(&ep->wq);

  }

  ...

}


该方法中的参数key表示的就是发生的事件,ep_poll_callback方法先检查key中是否包含我们感兴趣的事件,如果包含,则将 epitem实例添加到eventpoll的rdllink队列中,然后再调用 wake_up_locked 方法,将那些因调用epoll_wait而阻塞的线程唤醒,告知它们可以到eventpoll实例的rdllink队列中去查看,有哪些监听文件已经发生了我们感兴趣的事件。


至此,ep_insert方法涉及到的逻辑算是全部讲完了。


结合第一篇文章的内容,现在epoll体系的知识已经形成了一个逻辑闭环。


限于篇幅原因,ep_remove和ep_modify方法我们会在下一篇文章中分析。

以上是关于Linux epoll 源码分析 2的主要内容,如果未能解决你的问题,请参考以下文章

Linux epoll 源码分析 1

epoll源码分析以及在Redis中的实现

Linux epoll模型详解及源码分析

Linux epoll模型详解及源码分析

epoll源码分析(基于linux-5.1.4)

面试必问的epoll技术,从内核源码出发彻底搞懂epoll