该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2011-08-10
最后修改:2011-08-10
最近花些功夫在研究Java NIO的JDK源码,发现Selector的实现,除了在唤醒机制上做了手脚,主要依赖操作系统的实现,为了无负担的弄懂Selector,有必要研究一 下操作系统是如何实现选择的。本文主要参考linux-2.6.10内核epoll的实现(poll见上一篇: Java NIO 选择器(Selector) 知识预备 (linux poll))。
本文可能会表现得很肤浅,高手们请直接略过,另外,本文所出现的“政府”字样,乃比喻性质的,或者就认为它是“清政府”好了,请相关人员不要曲解。
上回冒充大侠poll府上走了一遭,感觉还不过瘾,于是计划再到它表哥epoll家去闯闯,可是man了一下之后,我有点退却了,丫的,还以为它表哥是一个人,原来是仨儿:
#include <sys/epoll.h> int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; 首先看看epoll_event是啥玩意儿,应该和pollfd类似吧?
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ }; 对比一下,发现区别不大,epoll_data_t是一个共用体,至少我们可以认为它可以是一个fd,所以较大的不同点就在于epoll_event没有 revents了,上次探索poll的时候不是发现,这是poll一个很关键的地方吗?最终事件是否发生就看它的值了。决心带着这个疑问去探一探。
先介绍一下图中涉及到的各种结构体:
下面结合这幅图大致讲解一下epoll_create、epoll_ctl、epoll_wait都在做些什么:
在给大家大致讲解了epoll涉及到的结构及epoll三兄弟大概在做些什么之后,开始我们的探索之旅吧:
epoll_create先看sys_epoll_create系统调用: asmlinkage long sys_epoll_create(int size) { int error, fd; struct inode *inode; struct file *file; DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d)\n", current, size)); /* Sanity check on the size parameter */ error = -EINVAL; if (size goto eexit_1; /* * Creates all the items needed to setup an eventpoll file. That is, * a file structure, and inode and a free file descriptor. */ error = ep_getfd(&fd, &inode, &file); if (error) goto eexit_1; /* Setup the file internal data structure ( "struct eventpoll" ) */ error = ep_file_init(file); if (error) goto eexit_2; DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d) = %d\n", current, size, fd)); return fd; eexit_2: sys_close(fd); eexit_1: DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d) = %d\n", current, size, error)); return error; } 我们只需要注意到两个函数:ep_getfd和ep_file_init
static int ep_file_init(struct file *file) { struct eventpoll *ep; if (!(ep = kmalloc(sizeof(struct eventpoll), GFP_KERNEL))) return -ENOMEM; memset(ep, 0, sizeof(*ep)); rwlock_init(&ep->lock); init_rwsem(&ep->sem); init_waitqueue_head(&ep->wq); init_waitqueue_head(&ep->poll_wait); INIT_LIST_HEAD(&ep->rdllist); ep->rbr = RB_ROOT; file->private_data = ep; DNPRINTK(3, (KERN_INFO "[%p] eventpoll: ep_file_init() ep=%p\n", current, ep)); return 0; } 从这几行代码可以看出,ep_file_init就做了两件事:
对外看来,epoll_create就做了一件事,那就是创建一个epoll文件,事实上,更关键的是,它创建了一个eventpoll结构体变量,该变量为epoll_ctl和epoll_wait的工作打下了基础。 epoll_ctl展示一下epoll_ctl系统调用先: asmlinkage long sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event) { int error; struct file *file, *tfile; struct eventpoll *ep; struct epitem *epi; struct epoll_event epds; DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p)\n", current, epfd, op, fd, event)); error = -EFAULT; if (EP_OP_HASH_EVENT(op) && copy_from_user(&epds, event, sizeof(struct epoll_event))) goto eexit_1; /* Get the "struct file *" for the eventpoll file */ error = -EBADF; file = fget(epfd); if (!file) goto eexit_1; /* Get the "struct file *" for the target file */ tfile = fget(fd); if (!tfile) goto eexit_2; /* The target file descriptor must support poll */ error = -EPERM; if (!tfile->f_op || !tfile->f_op->poll) goto eexit_3; /* * We have to check that the file structure underneath the file descriptor * the user passed to us _is_ an eventpoll file. And also we do not permit * adding an epoll file descriptor inside itself. */ error = -EINVAL; if (file == tfile || !IS_FILE_EPOLL(file)) goto eexit_3; /* * At this point it is safe to assume that the "private_data" contains * our own data structure. */ ep = file->private_data; down_write(&ep->sem); /* Try to lookup the file inside our hash table */ epi = ep_find(ep, tfile, fd); error = -EINVAL; switch (op) { case EPOLL_CTL_ADD: if (!epi) { epds.events |= POLLERR | POLLHUP; error = ep_insert(ep, &epds, tfile, fd); } else error = -EEXIST; break; case EPOLL_CTL_DEL: if (epi) error = ep_remove(ep, epi); else error = -ENOENT; break; case EPOLL_CTL_MOD: if (epi) { epds.events |= POLLERR | POLLHUP; error = ep_modify(ep, epi, &epds); } else error = -ENOENT; break; } /* * The function ep_find() increments the usage count of the structure * so, if this is not NULL, we need to release it. */ if (epi) ep_release_epitem(epi); up_write(&ep->sem); eexit_3: fput(tfile); eexit_2: fput(file); eexit_1: DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p) = %d\n", current, epfd, op, fd, event, error)); return error; } 记得前文提到过eventpoll结构体包含一个变量struct rb_root rbr,这就是一颗红黑树的根结点,epoll_ctl的ADD、DEL、MOD操作,就是在操作这颗红黑树。先分析一下代码流程:
static int ep_modify(struct eventpoll *ep, struct epitem *epi, struct epoll_event *event) { int pwake = 0; unsigned int revents; unsigned long flags; /* * Set the new event interest mask before calling f_op->poll(), otherwise * a potential race might occur. In fact if we do this operation inside * the lock, an event might happen between the f_op->poll() call and the * new event set registering. */ // 这个就是modify需要修改的地方,即修改对应的events epi->event.events = event->events; /* * Get current event bits. We can safely use the file* here because * its usage count has been increased by the caller of this function. */ // 这个地方不要感到奇怪,说明几点后大家应该就容易理解了: // 1、既然是modify则说明之前已经被add过,不需要重复挂等待队列,因此回调函数为NULL // 2、同时因为NULL参数,即说明不需要回调,也不会有挂等待队列的操作 // 该调用其实就是去file那里收集一下事件而已 revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL); write_lock_irqsave(&ep->lock, flags); /* Copy the data member from inside the lock */ // 这个就是modify需要修改的地方,即修改对应的data epi->event.data = event->data; /* * If the item is not linked to the hash it means that it's on its * way toward the removal. Do nothing in this case. */ // 这个if不准备详细讲,其实很简单,前面不是已经问过file得到revents吗? // 如果当前epi已经被链接的话,就看是否是感兴趣事件发生,如果是,则同样将其 // 添加到eventpoll的rdllist链表中,并notify if (EP_RB_LINKED(&epi->rbn)) { /* * If the item is "hot" and it is not registered inside the ready * list, push it inside. If the item is not "hot" and it is currently * registered inside the ready list, unlink it. */ if (revents & event->events) { if (!EP_IS_LINKED(&epi->rdllink)) { list_add_tail(&epi->rdllink, &ep->rdllist); /* Notify waiting tasks that events are available */ if (waitqueue_active(&ep->wq)) wake_up(&ep->wq); if (waitqueue_active(&ep->poll_wait)) pwake++; } } } write_unlock_irqrestore(&ep->lock, flags); /* We have to call this outside the lock */ if (pwake) ep_poll_safewake(&psw, &ep->poll_wait); return 0; } 可以看到,修改操作其实就修改红黑树中对应的epitem的event值,有个细节点需要注意,也就是内核不放弃任何一次机会,修改过程中也不忘问一下file的事件状态,如果有事件ready则同样将其链接到rdllist链表中。
epoll_wait在讲解了epoll_ctl的过程之后,epoll_wait的确没什么内容了,也不想贴一大堆源码什么的,这里分几个点将其描述一下:
前文已经多次出现一个链表rdllist,该链表位于eventpoll结构体变量中,当ep_poll_callback回调函数被调用时,肯 定会将当前epitem链接进来,或者在ep_insert、ep_modify过程中,如果发现file有事件ready也会将当前epitem链接到 rdllist上,因此,我们可以猜测得到epoll_wait在做什么,看下面关键部分代码:
// 如果rdllist中还没有epitem时,就开始等待了 if (list_empty(&ep->rdllist)) { /* * We don't have any available event to return to the caller. * We need to sleep here, and we will be wake up by * ep_poll_callback() when events will become available. */ // 初始化等待队列,等待队列项对应的线程即为当前线程 init_waitqueue_entry(&wait, current); // 不用多说,先将当前线程挂到等待队列上,之后在调用schedule_timeout // 时,就开始了超时等待了 add_wait_queue(&ep->wq, &wait); for (;;) { /* * We don't want to sleep if the ep_poll_callback() sends us * a wakeup in between. That's why we set the task state * to TASK_INTERRUPTIBLE before doing the checks. */ // 这块内容比较熟悉,在poll讲解过程中也有说明,它与schedule_timeout配合 // 因为会被阻塞,这里先设置线程状态为可中断 set_current_state(TASK_INTERRUPTIBLE); // 整个循环的核心,其实就在看rdllist中是否有数据,或者等待超时 // 应征了前面的说明,epoll_wait只需要等着收集数据即可 if (!list_empty(&ep->rdllist) || !jtimeout) break; // 如果被中断。。。后面部分比较简单,可以参照poll那篇 if (signal_pending(current)) { res = -EINTR; break; } write_unlock_irqrestore(&ep->lock, flags); jtimeout = schedule_timeout(jtimeout); write_lock_irqsave(&ep->lock, flags); } remove_wait_queue(&ep->wq, &wait); set_current_state(TASK_RUNNING); } 其实还有一点需要说明,大家可能也会想到,rdllist中的epitem只能表示对应fd有事件ready,可是自始至终都没看到有地方回写revents,我们怎么知道到底是哪些事件ready了呢?
list_for_each(lnk, txlist) { epi = list_entry(lnk, struct epitem, txlink); /* * Get the ready file event set. We can safely use the file * because we are holding the "sem" in read and this will * guarantee that both the file and the item will not vanish. */ revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL); /* * Set the return event set for the current file descriptor. * Note that only the task task was successfully able to link * the item to its "txlist" will write this field. */ epi->revents = revents & epi->event.events; 看到这段代码,应该很清楚了,只需要遍历链表,再去拿一次就好了,见关键代码:
还有一点我这里故意隐瞒,其实不是我特别想说明的点,对理解epoll影响也不大,那就是收集结果不是直接从rdllist中进行的,这中间还有一个转移的过程,在epoll_wait的最后进行,关键代码如下:
static int ep_collect_ready_items(struct eventpoll *ep, struct list_head *txlist, int maxevents) { int nepi; unsigned long flags; // rdllist里存放的就是当前ready的epitem链表,且至少存在一个epitem struct list_head *lsthead = &ep->rdllist, *lnk; struct epitem *epi; write_lock_irqsave(&ep->lock, flags); // 遍历rdllist链表 for (nepi = 0, lnk = lsthead->next; lnk != lsthead && nepi < maxevents;) { // 先拿到epitem epi = list_entry(lnk, struct epitem, rdllink); lnk = lnk->next; /* If this file is already in the ready list we exit soon */ // 确保不会被重复链接到txlink上 if (!EP_IS_LINKED(&epi->txlink)) { /* * This is initialized in this way so that the default * behaviour of the reinjecting code will be to push back * the item inside the ready list. */ epi->revents = epi->event.events; /* Link the ready item into the transfer list */ // 将epi的txlink链接到ep的txlist上,简单的说 // 将对应的epitem链接到txlist链表上 list_add(&epi->txlink, txlist); nepi++; /* * Unlink the item from the ready list. */ // 因为已经被转移了,所以从rdllist链表中清除 EP_LIST_DEL(&epi->rdllink); } } write_unlock_irqrestore(&ep->lock, flags); return nepi; }
经过这一步,rdllist中当前的结果已经被转移到txlist中,之后如果有新加入到rdllist的话,本次epoll_wait不会再关心,不过可以留到下次再收集。
总结后面详细讲解epoll_create、epoll_ctl、epoll_wait只是为了让大家强化理解前面的那副图,这里讲解epoll并不涉 及到内存映射等优化点,只是为了让大家理解,epoll到底在干什么,到最后,留给大家的,也只是这幅图,或者更简单的一个点:原来回调函数是epoll 比poll高明的地方啊。至于为什么要创建一个文件来承载eventpoll,甚至采用红黑树来保存数据,都只是空间换时间而已。
PS. 本文地址:Java NIO 教程 ,请大家关注:黄金档
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2011-08-11
分析的很深,楼主应该花了不少功夫吧。
期待更多的产出。 |
|
返回顶楼 | |
发表时间:2011-08-11
楼主去看看apache mina 写一边这样的文章就好了
|
|
返回顶楼 | |
发表时间:2011-08-11
现在iteye流行源码风了?
|
|
返回顶楼 | |
发表时间:2011-08-11
NightWatch 写道 现在iteye流行源码风了?
看源码,写感受,挺好的啊。 |
|
返回顶楼 | |
发表时间:2011-08-12
希望楼主多出这样的好文章啊
|
|
返回顶楼 | |
发表时间:2011-08-12
绝对精华贴……
|
|
返回顶楼 | |
发表时间:2011-08-12
最后修改:2011-08-12
楼主想向你请教一个问题。
前段时间看了下java nio selector的java层面的实现(没有深入到native code看cpp代码,且也只是粗略的看),发现一个问题,对于每一个注册了相关事件的socketchannel/serversocketchannel,selector都会为他们开启一个等待线程sun.nio.windowSelectorImpl$selectThread,这让我很迷惑: selector本名多路复用机制,就是为了在一个通道上完成以前需要多个慢速通道才能完成问题,这种机制的好处也不就是在于避免了 thread per socketchannel而频繁导致thread的阴塞与调度吗? 所以对于openjdk里selectorimp.java,最终每个socketchannel一个thread的模式,能给解释下吗?谢谢。 |
|
返回顶楼 | |
发表时间:2011-08-13
最后修改:2011-08-13
很好,有时间再来细看
|
|
返回顶楼 | |
发表时间:2011-08-15
littlecar 写道 楼主想向你请教一个问题。
前段时间看了下java nio selector的java层面的实现(没有深入到native code看cpp代码,且也只是粗略的看),发现一个问题,对于每一个注册了相关事件的socketchannel/serversocketchannel,selector都会为他们开启一个等待线程sun.nio.windowSelectorImpl$selectThread,这让我很迷惑: selector本名多路复用机制,就是为了在一个通道上完成以前需要多个慢速通道才能完成问题,这种机制的好处也不就是在于避免了 thread per socketchannel而频繁导致thread的阴塞与调度吗? 所以对于openjdk里selectorimp.java,最终每个socketchannel一个thread的模式,能给解释下吗?谢谢。 首先要理解windows下的NIO的Selector其实就是用select实现的,select有MAX_SELECTABLE_FDS限制,而NIO的Selector似乎没有这个限制,为什么,这就是SelectThread的作用,SelectThread中有SubSelector可以用于poll,同时WindowsSelector保证了MAX_SELECTABLE_FDS - 1会拥有一个Daemon的SelectThread线程,去完成select工作,因此不是每个Channel会有一个SelectThread,而是每MAX_SELECTABLE_FDS - 1会有一个SelectThread去做select工作。 |
|
返回顶楼 | |