Chinaunix首页 | 论坛 | 博客
  • 博客访问: 317767
  • 博文数量: 18
  • 博客积分: 2049
  • 博客等级: 大尉
  • 技术积分: 664
  • 用 户 组: 普通用户
  • 注册时间: 2007-07-06 17:38
文章分类

全部博文(18)

文章存档

2012年(1)

2011年(8)

2010年(4)

2009年(2)

2008年(3)

分类: 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粘合代码,诸如将filpfd关联,不说了。

另外,值得一提的,就是那个size参数,其实是棒锤,什么也不顶。

epoll_ctl()系统调用

前面做参数合法检查和退出时打扫战场的代码我就忽略了。这个函数的骨干逻辑如下:

    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;
    }

这是ep_find()调用的唯一地方,它的作用是在在一棵红黑树查找满足已经添加过的“事件结构”,毫无疑问,这是为DELMOD操作的预备动作。而我们将主要关心ADD,所以忽略它吧。此外一个值得注意的地方,从以上代码可以看到,对于ADDMOD操作, POLLERR | POLLHUP都是必定会通知用户空间程序的,即使你不指定它们。

OK,对于ADD操作,就剩下ep_insert(ep, &epds, tfile, fd) 需要解释了。这个函数首先分配一个epievent poll item)结构,然后初始化上面的几个链表什么的,没什么大不了是不是?但最关键的代码就隐藏在这个函数里面,其中最重要的代码是这么几行:


struct ep_pqueue epq;
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
revents = tfile->f_op->poll(tfile, &epq.pt);

(当然,这个函数也会把epi插入到之前提到的红黑树中)

init_poll_funcptr()倒是很简单:只有一行代码,它的意思其实就是:


epq.pt.qproc = ep_ptable_queue_proc;


稍微解释一下这里的数据结构, ep_pqueue只是用来粘合epoll()Linux 多路复用(multiplexing)机制的一个连接件,epq.epi是连接件的epoll()这一端,epq.ptLinux 多路复用(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)
{
    unsigned int mask;
    struct sock *sk = sock->sk;
    struct tcp_sock *tp = tcp_sk(sk);

    poll_wait(file, sk->sk_sleep, wait);
    if (sk->sk_state == TCP_LISTEN)
        return inet_csk_listen_poll(sk);
    /* …... */
        if ((tp->rcv_nxt != tp->copied_seq) &&
         (tp->urg_seq != tp->copied_seq ||
         tp->rcv_nxt != tp->copied_seq + 1 ||
         sock_flag(sk, SOCK_URGINLINE) || !tp->urg_data))
            mask |= POLLIN | POLLRDNORM;
    /* …... */
    return mask;
}

你可能注意到了,上面的tcp_poll()tfile->f_op->poll()的参数不同,这是因为tcp_poll()是通过socket层包装过的,只是参数不同,但本质没有区别。这个函数的代码按功能分成两部分,前面的poll_wait()用于支持多路复用,filewait分别是前面的tfile&epq.ptsk->sk_sleepTCP的等待队列,如果TCP有需要通知的事件(例如收到数据包了)就通知在这个等待队列上面的等待者;后面是TCP自己判断是否产生了有效事件(POLLIN等)的逻辑。poll_wait()的逻辑也是非常简单的,相当于:


    if (&epq.pt && sk->sk_sleep)
        &epq.qt->qproc(tfile, sk->sk_sleep, &epq.qt);


也就是说,会调用 ep_ptable_queue_proc(),这个函数做了什么呢?去掉错误处理:


pwq = kmem_cache_alloc(pwq_cache, SLAB_KERNEL))) {
    init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
    pwq->whead = whead;
    pwq->base = epi;
    add_wait_queue(whead, &pwq->wait);

这是啥意思?其实意图是很简单的,向sk->sk_sleep这个等待队列里放置一个“等待者”。通常意义上的唤醒等待者,会唤醒对应的某个任务,然后在下次发生中断时进行任务切换。但这里的唤醒只会导致运行 ep_poll_callback,这个函数的核心代码是:


list_add_tail(&epi->rdllink, &ep->rdllist);
    if (waitqueue_active(&ep->wq))
        __wake_up_locked(&ep->wq, TASK_UNINTERRUPTIBLE |
                 TASK_INTERRUPTIBLE);

首先把这个epievent poll item)添加到这个事件池的“Ready Event Linked List”的尾部中,然后唤醒等待在这个事件池的任务(如果有的话),也就是调用了epoll_wait()的用户空间线程。

epoll_wait() 系统调用


明白了上述过程,epoll_wait()就格外简单了。首先,它让当前线程休眠在等待队列 ep->wq上。直到因为”Ready Event Linked List“里面有数据有个唤醒它。然后,它会调用ep_events_transfer(ep, events, maxevents)将等到的按顺序事件复制到用户指定的参数中。这就不解释其余的代码了。

其他epoll_XXX() 系统调用

RHEL6的内核(主要基于2.6.32)里,还有两个与epoll有关的系统调用:epoll_pwait()epoll_create1()。这些只是上面三个系统调用的包装而已。pwait是做了与进程信号有关的工作,create1则是用来补漏的。


epoll over epoll

上面讲到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/cscopepyGTK GUI前端,开发中,吼吼~ 虽然没有Windows上的Source Insight那么完善,但总算不用太眼馋了,也够用了,整张Screenshot

See



下面是本文的PDF:
文件:epoll.pdf
大小:400KB
下载:下载
阅读(2727) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~