前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux epoll 源码分析 2

Linux epoll 源码分析 2

作者头像
KINGYT
发布2019-05-30 19:19:01
1.5K0
发布2019-05-30 19:19:01
举报

继上一篇 Linux epoll 源码分析 1,我们来继续看下 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方法我们会在下一篇文章中分析。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-02-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档