全部博文(18)
分类: LINUX
2010-08-01 10:47:55
看官兄弟姐妹们,你们都点过“理解epoll的实现”的标题了,我想就不用再说epoll()如果使用的废话云云了。直接进入正题吧:
epoll_create()系统调用首先,我们注意epoll_create()返回的是一个文件描述符,这是epoll()与select()/poll()族系统调用的一点显著不同。有文件描述符就一定有对应的文件系统,这个特殊文件系统,在RHEL5.4内核(主要基于2.6.18,混合了部分2.6.2x的代码)里叫eventpollfs。不过,这个用来借壳“充数”的文件系统在更新的内核里已经看不到了,因为新内核(至少2.6.32以后是没有了)提供了更轻量级的方法实现“借壳”。
“借壳”本身与epoll()的核心逻辑关系不大,我们略过不表。无论有没有这个特殊文件系统,这个打开文件的对应的file_operations都实现了两个方法:一个是release,一个poll。前一个我不用介绍了,从名字上猜含义不难。后一个就比较有趣了,这意味着epoll_create()返回文件描述符本身也是可以select()/poll()/epoll()的,我们可以复合epoll!
epoll_create()创建完对应的inode()之后,就是做一些常规的VFS粘合代码,诸如将filp与fd关联,不说了。
另外,值得一提的,就是那个size参数,其实是棒锤,什么也不顶。
epoll_ctl()系统调用前面做参数合法检查和退出时打扫战场的代码我就忽略了。这个函数的骨干逻辑如下:
epi = ep_find(ep, tfile, fd); |
这是ep_find()调用的唯一地方,它的作用是在在一棵红黑树查找满足已经添加过的“事件结构”,毫无疑问,这是为DEL和MOD操作的预备动作。而我们将主要关心ADD,所以忽略它吧。此外一个值得注意的地方,从以上代码可以看到,对于ADD/MOD操作, POLLERR | POLLHUP都是必定会通知用户空间程序的,即使你不指定它们。
OK,对于ADD操作,就剩下ep_insert(ep, &epds, tfile, fd) 需要解释了。这个函数首先分配一个epi(event poll item)结构,然后初始化上面的几个链表什么的,没什么大不了是不是?但最关键的代码就隐藏在这个函数里面,其中最重要的代码是这么几行:
struct ep_pqueue epq; |
(当然,这个函数也会把epi插入到之前提到的红黑树中)
init_poll_funcptr()倒是很简单:只有一行代码,它的意思其实就是:
epq.pt.qproc = ep_ptable_queue_proc; |
稍微解释一下这里的数据结构, ep_pqueue只是用来粘合epoll()与Linux 多路复用(multiplexing)机制的一个连接件,epq.epi是连接件的epoll()这一端,epq.pt是Linux 多路复用(multiplexing)机制的另一端。 ep_ptable_queue_proc,是一个函数,稍后介绍。tfile,是要需要监听的那个fd对应的filp(内核里描述打开文件的结构体)。这个结构体内的f_op->poll()方法是实现Linux 多路复用(multiplexing)机制的关键点。因为监听的文件可能是socket,可能是一个裸设备,也可能是某个文件,所以,只要想支持多路复用,协议栈、设备驱动、文件系统都必须支持这个方法。以TCP为例,它的poll实现是tcp_poll函数:
unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait) |
你可能注意到了,上面的tcp_poll()与tfile->f_op->poll()的参数不同,这是因为tcp_poll()是通过socket层包装过的,只是参数不同,但本质没有区别。这个函数的代码按功能分成两部分,前面的poll_wait()用于支持多路复用,file和wait分别是前面的tfile和&epq.pt,sk->sk_sleep是TCP的等待队列,如果TCP有需要通知的事件(例如收到数据包了)就通知在这个等待队列上面的等待者;后面是TCP自己判断是否产生了有效事件(POLLIN等)的逻辑。poll_wait()的逻辑也是非常简单的,相当于:
if (&epq.pt && sk->sk_sleep) |
也就是说,会调用 ep_ptable_queue_proc(),这个函数做了什么呢?去掉错误处理:
pwq = kmem_cache_alloc(pwq_cache, SLAB_KERNEL))) { |
这是啥意思?其实意图是很简单的,向sk->sk_sleep这个等待队列里放置一个“等待者”。通常意义上的唤醒等待者,会唤醒对应的某个任务,然后在下次发生中断时进行任务切换。但这里的唤醒只会导致运行 ep_poll_callback,这个函数的核心代码是:
list_add_tail(&epi->rdllink, &ep->rdllist); |
首先把这个epi(event poll item)添加到这个事件池的“Ready Event Linked List”的尾部中,然后唤醒等待在这个事件池的任务(如果有的话),也就是调用了epoll_wait()的用户空间线程。
epoll_wait() 系统调用
在RHEL6的内核(主要基于2.6.32)里,还有两个与epoll有关的系统调用:epoll_pwait(),epoll_create1()。这些只是上面三个系统调用的包装而已。pwait是做了与进程信号有关的工作,create1则是用来补漏的。
上面讲到epoll_create()返回的本身就是文件描述符,而eventpollfs则是一个支持多路复用的特殊文件系统。这样,我们就是可以对epoll_create()的结果做epoll了。为了避免可能的无穷递归,epoll的实现对此也做了相应的处理。这就是ep->poll_wait成员和ep_poll_safewake()函数的用意所在了,这块的代码就不粘了,点到即止吧。
OK,为什么要epoll over epoll呢?试想我们有几千个socket需要处理时,而我们不希望平等对待这几千个socket,要将其分成128个优先级。上面已经看到,对于一个epoll文件(epoll_create()返回的那个fd)中的事件,内核是平等对待的,就是一个简单的FIFO队列。使用复合epoll,就可以快速地分辨出哪些优先级上的socket有数据包来临,而不需要逐个判断每个就绪连接的优先级了,从而可以优先处理那高优先级的socket,嗯,再改吧改吧,似乎是个很好的QoS机制的基础。
借这个文章,宣传一下自己的ctags/cscope的pyGTK GUI前端,开发中,吼吼~ 虽然没有Windows上的Source Insight那么完善,但总算不用太眼馋了,也够用了,整张Screenshot:
See
|