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的主要内容,如果未能解决你的问题,请参考以下文章