epoll源码重要部分详解

一、epoll相关的数据结构

  最重要的两个数据结构是红黑树和就绪链表红黑树用于管理所有的文件描述符fd,就绪链表用于保存有事件发生的文件描述符。

  当向系统中添加一个fd时,就创建一个epitem结构体。eventpoll用于管理所有的epitem。


struct epitem 
{
    struct rb_node  rbn;        // 用于主结构管理的红黑树
    struct list_head  rdllink;  // 事件就绪链表
    struct epitem  *next;       // 用于主结构体中的链表
 	struct epoll_filefd  ffd;   // 这个结构体对应的被监听的文件描述符信息
 	int  nwait;                 // poll操作中事件的个数
    struct list_head  pwqlist;  // 双向链表,保存着被监视文件的等待队列
    struct eventpoll  *ep;      // 该项属于哪个主结构体(多个epitm从属于一个eventpoll)
    struct list_head  fllink;   // 双向链表,用来链接被监视的文件描述符对应的struct
    struct epoll_event  event;  // 注册的感兴趣的事件,也就是用户空间的epoll_event
}

struct eventpoll
{
    spin_lock_t       lock;         // 对本数据结构的访问
    struct mutex      mtx;          // 防止使用时被删除
    /*
     * 等待队列可以看作保存进程的容器,在阻塞进程时,将进程放入等待队列;
     * 当唤醒进程时,从等待队列中取出进程
     */ 
    wait_queue_head_t   wq;         // sys_epoll_wait() 使用的等待队列
    wait_queue_head_t   poll_wait;  // file->poll()使用的等待队列
    struct list_head    rdllist;    // 就绪链表
    struct rb_root      rbr;        // 用于管理所有fd的红黑树(树根)
    struct epitem      *ovflist;    // 将事件到达的fd进行链接起来发送至用户空间
}

在这里插入图片描述

二、epoll_create函数

  epoll_create函数的功能是创建eventpoll。

long sys_epoll_create(int size) 
{
    struct eventpoll *ep;
    ep_alloc(&ep); // 为ep分配内存并进行初始化
 	fd = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
    return fd;
}

三、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 epoll_event epds;
    error = -FAULT;

    // 判断参数的合法性,将 __user *event 复制给 epds。
    if(ep_op_has_event(op) && copy_from_user(&epds,event,sizeof(struct epoll_event)))
    	goto error_return; //省略跳转到的代码
    file  = fget (epfd); // epoll fd 对应的文件对象
    tfile = fget(fd);    // fd 对应的文件对象

    // 在create时存入进去的(anon_inode_getfd),现在取用。
    ep = file->private->data;
    mutex_lock(&ep->mtx);

    // 防止重复添加(在ep的红黑树中查找是否已经存在这个fd)
    epi = epi_find(ep, tfile, fd);

    switch(op)
    {
    	...
        case EPOLL_CTL_ADD:  // 增加监听一个fd
        	if (!epi)
            {
                epds.events |= EPOLLERR | POLLHUP;     // 默认包含POLLERR和POLLHUP事件。
                error = ep_insert(ep, &epds, tfile, fd);  // 在ep的红黑树中插入这个fd对应的epitm结构体。
            } else  // 重复添加(在ep的红黑树中查找已经存在这个fd)。
                error = -EEXIST;
            break;
        ...
    }
    return error;
}

四、ep_insert函数

  ep_insert函数的功能是插入一个文件描述符到红黑树上。

static int ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *tfile, int fd)
{
	/* 初始化epitem */

	// 设置监听注册函数为ep_ptable_queue_proc, &epq.pt->qproc = ep_ptable_queue_proc
	init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

	/*
	 * 执行f_op->poll(tfile, &epq.pt)时,XXX_poll(tfile, &epq.pt)函数会执行poll_wait(),
	 * poll_wait()会调用epq.pt.qproc函数,即ep_ptable_queue_proc。
	 */
	revents = tfile->f_op->poll(tfile, &epq.pt);

   	spin_lock(&tfile->f_ep_lock);

   	// 将epitem与它需要监听的文件链接起来,list_add_tail是内核函数,表示添加一个结点到链表尾部
   	list_add_tail(&epi->fllink, &tfile->f_ep_lilnks);
   	spin_unlock(&tfile->f_ep_lock);

	// 将epitem插入红黑树
	ep_rbtree_insert(ep, epi);

	/*
	 * 如果要监视的fd状态改变,并且还没有加入到就绪链表中,则将当前的
	 * epitem加入到就绪链表中。如果有进程正在等待该文件的状态就绪,则
	 * 唤醒一个等待的进程。
	 */
    if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) 
    {
        list_add_tail(&epi->rdllink, &ep->rdllist);

        /* 唤醒epoll_wait()当前epoll实例的用户 */
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);

        /* 当前epoll文件已就绪 */
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }
}

五、ep_poll_callback函数

  所以ep_poll_callback函数主要的功能是当被监视文件的等待事件就绪时,将文件描述符对应的epitem实例添加到就绪链表中,导致rdlist不空,进程被唤醒,epoll_wait()得以继续执行,之后内核会将就绪链表中的事件从内核空间拷贝到用户空间。

// 将epitem挂载到资源文件的监听队列上
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt)
{
    /* 获取epitem */
    struct epitem *epi = ep_item_from_epqueue(pt);
    struct eppoll_entry *pwq;

    /* 从slab分配一个eppoll_entry结构,然后进行相应的初始化 */
    if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) 
    {
        /* 
         * 初始化一个等待队列节点,其中唤醒函数设置为ep_poll_callback
         * 唤醒回调函数为ep_poll_callback!!!
         */
        init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
        
        /* 还要保存资源文件监听队列的队列头whead */
        pwq->whead = whead;
        pwq->base = epi;

        /* 
         * 将eppoll_entry挂载到资源文件的监听队列中,
         * add_wait_queue表示将队列元素加入到等待队列头部,并设置非互斥等待
         */
        add_wait_queue(whead, &pwq->wait);

        list_add_tail(&pwq->llink, &epi->pwqlist);
        /* 增加等待计数 */
        epi->nwait++;
    }
    else
    {
    	epi->nwait = -1;
    }
}

static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    int pwake = 0;
    unsigned long flags;

    /* 通过eppoll_entry中的wait获取对应的epitem */
    struct epitem *epi = ep_item_from_wait(wait);

    /* 获取epitem所属的eventpoll */
    struct eventpoll *ep = epi->ep;

    /* spinlock加锁(不允许休眠):保护eventpoll的访问 */
    spin_lock_irqsave(&ep->lock, flags);

    /* 如果我们想要监听的事件events为空,那么资源文件就绪时,nothing to do */
    if (!(epi->event.events & ~EP_PRIVATE_BITS))
        goto out_unlock;

    /* 判断当前fd状态是否是我们关心的事件events */
    if (key && !((unsigned long) key & epi->event.events))
        goto out_unlock;

    if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) 
    {
        if (epi->next == EP_UNACTIVE_PTR) 
        {
            epi->next = ep->ovflist;
            ep->ovflist = epi;
        }
        goto out_unlock;
    }

    /* 如果epitem没有被挂载到所属eventpoll中的就绪链表,就将其添加到就绪链表尾 */
    if (!ep_is_linked(&epi->rdllink))
        list_add_tail(&epi->rdllink, &ep->rdllist);

    /* 唤醒epoll_wait()当前epoll实例的用户 */
    if (waitqueue_active(&ep->wq))
        wake_up_locked(&ep->wq);

    /* 当前epoll文件已就绪 */
    if (waitqueue_active(&ep->poll_wait))
        pwake++;

out_unlock:
    spin_unlock_irqrestore(&ep->lock, flags);
    if (pwake)
        ep_poll_safewake(&ep->poll_wait);

    return 1;
}

六、epoll_wait函数

  epoll_wait调用ep_poll,当rdlist(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。然后就将就绪的events和data发送到用户空间(ep_send_events()),如果ep_send_events()返回的事件数为0,并且还有超时时间剩余(jtimeout),那么我们retry,期待不要空手而归。

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events, int, maxevents, int, timeout)
{
    int error;
    struct file *file;
    struct eventpoll *ep;

    /* 参数验证 */
    if (maxevents <= 0 || maxevents > EP_MAX_EVENTS)
        return -EINVAL;

    /* 验证events数组区域,当前用户是否能够访问 */
    if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event))) 
    {
        error = -EFAULT;
        goto error_return;
    }

    error = -EBADF;

    /* 获取eventpoll文件描述符对应的struct file结构 */
    file = fget(epfd);
    if (!file)
        goto error_return;

    error = -EINVAL;

    /* 验证epfd指向的文件是否是epoll文件 */
    if (!is_file_epoll(file))
        goto error_fput;

    /* 取出挂载到epoll文件中的eventpoll */
    ep = file->private_data;

    /* 调用ep_poll()等待事件的到来 */
    error = ep_poll(ep, events, maxevents, timeout);

error_fput:
    fput(file);
error_return:

    return error;
}

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout)
{
    int res, eavail;
    unsigned long flags;
    long jtimeout;
    wait_queue_t wait;

    /* 处理睡眠时间:将毫秒数转化为HZ */
    jtimeout = (timeout < 0 || timeout >= EP_MAX_MSTIMEO) ?
        MAX_SCHEDULE_TIMEOUT : (timeout * HZ + 999) / 1000;

retry:
    /* spinlock加锁:保护eventpoll的访问 */
    spin_lock_irqsave(&ep->lock, flags);

    res = 0;

    /* 如果就绪链表为空,说明还没有任何events就绪 */
    if (list_empty(&ep->rdllist)) 
    {
        /* 初始化等待队列节点,设置等待状态为互斥等待 */
        init_waitqueue_entry(&wait, current);
        wait.flags |= WQ_FLAG_EXCLUSIVE;

        /* 将刚刚初始化的等待队列节点挂载到eventpoll中的等待队列 */
        __add_wait_queue(&ep->wq, &wait);

        for (;;) 
        {
            /* 
             * 设置程序运行状态为可中断阻塞,因为我们希望能够接收到
             * ep_insert()、ep_modify()、ep_poll_callback()的唤醒
             */
            set_current_state(TASK_INTERRUPTIBLE);

            /* events就绪或者超时,跳出循环 */
            if (!list_empty(&ep->rdllist) || !jtimeout)
                break;

            /* 出现未决信号,设置返回值为-EINTR并跳出循环 */
            if (signal_pending(current)) 
            {
                res = -EINTR;
                break;
            }

            spin_unlock_irqrestore(&ep->lock, flags);
            /* 休眠...等待超时或者被就绪资源唤醒 */
            jtimeout = schedule_timeout(jtimeout);
            spin_lock_irqsave(&ep->lock, flags);
        }
        /* 从等待队列中卸载 */
        __remove_wait_queue(&ep->wq, &wait);

        /* 恢复程序运行状态 */
        set_current_state(TASK_RUNNING);
    }

    /* 判断是否有资源就绪 */
    eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;

    spin_unlock_irqrestore(&ep->lock, flags);

    if (!res && eavail && !(res = ep_send_events(ep, events, maxevents)) && jtimeout)
        goto retry;

    return res;
}

七、ep_send_events函数

  ep_send_events函数,它扫描txlist中的每个epitem,调用其关联的fd对应的的poll方法,取得fd上较新的events(防止之前events被更新)即revents,之后将revents和相应的data拷贝(__put_user())到用户空间。如果这个epitem对应的fd是LT模式监听且取得的events是用户所关心的,则将其重新加入回rdlist,如果是ET模式则不再加入rdlist

// 将就绪的events传递到用户空间
static int ep_send_events(struct eventpoll *ep, struct epoll_event __user *events, int maxevents)
{
    struct ep_send_events_data esed;
    esed.maxevents = maxevents;
    esed.events = events;
    return ep_scan_ready_list(ep, ep_send_events_proc, &esed);
}

// 扫描就绪链表
static int ep_scan_ready_list(struct eventpoll *ep,
                  			  int (*sproc)(struct eventpoll *, struct list_head *, void *),
                              void *priv)
{
    int error, pwake = 0;
    unsigned long flags;
    struct epitem *epi, *nepi;
    /* 初始化一个链表 */
    LIST_HEAD(txlist);

    /* mutex加锁 */
    mutex_lock(&ep->mtx);

    /* spinlock加锁:保护eventpoll的访问 */
    spin_lock_irqsave(&ep->lock, flags);

    /*
     * 将eventpoll就绪链表中的所有节点全部链接到txlist上,
     * 之后eventpoll就绪队列为空
     */
    list_splice_init(&ep->rdllist, &txlist);

    /* 
     * 设置eventpoll.ovflist,使得接下来新就绪的events被挂载到
     * eventpoll.ovflist而不是就绪队列 
     */
    ep->ovflist = NULL;
    spin_unlock_irqrestore(&ep->lock, flags);
    error = (*sproc)(ep, &txlist, priv);

    /* spinlock加锁:保护eventpoll的访问 */
    spin_lock_irqsave(&ep->lock, flags);

    /* 
     * 我们在调用ep_send_events_proc()将就绪队列中的事件交付
     * 给用户的期间,新就绪的events被挂载到eventpoll.ovflist
     * 所以我们需要遍历eventpoll.ovflist将所有已就绪的epitem
     * 重新挂载到就绪链表中,等待下一次epoll_wait()进行交付
     */
    for (nepi = ep->ovflist; (epi = nepi) != NULL; nepi = epi->next, epi->next = EP_UNACTIVE_PTR) 
    {
        if (!ep_is_linked(&epi->rdllink))
            list_add_tail(&epi->rdllink, &ep->rdllist);
    }
    ep->ovflist = EP_UNACTIVE_PTR;

    /* 
     * 将调用ep_send_events_proc()之后剩余的未交付的epitem重新splice到
     * eventpoll的就绪链表上 
     */
    list_splice(&txlist, &ep->rdllist);

    /* 
     * 注意到epoll_wait()中,将wait_queue_t的等待状态设置为互斥等待,因此
     * 每次被唤醒的只有一个节点。现在我们已经将eventpoll中就绪队列里的事件
     * 尽量向用户交付了,但是在交付时,可能没有交付完全(1.交付过程中出现了
     * 错误 2.使用了LT模式),也有可能在过程中又发生了新的事件。也就是这次
     * epoll_wait()调用后,还剩下一些就绪资源,那么我们再次唤醒一个等待节点
     * 让别的用户也享用一下资源
     *
     * 从这里已经可以看出内核对于epoll惊群的解决方案:ET模式:
     * 1. 每次只唤醒一个节点
     * 2. 事件交付后不再将事件重新挂载到就绪队列
     */
    if (!list_empty(&ep->rdllist)) 
    {
        /* 唤醒epoll_wait()当前epoll实例的用户 */
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);

        /* 当前epoll文件已就绪 */
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }
    spin_unlock_irqrestore(&ep->lock, flags);
    mutex_unlock(&ep->mtx);
    if (pwake)
        ep_poll_safewake(&ep->poll_wait);
    return error;
}

// 将events和data发送到用户空间
static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head, void *priv)
{
    struct ep_send_events_data *esed = priv;
    int eventcnt;
    unsigned int revents;
    struct epitem *epi;
    struct epoll_event __user *uevent;

    /* 
     * 遍历就绪链表,eventcnt记录已交付的events的数量
     * uevent指向esed中封装的events数组,这个数组用于将已就绪events返回给用户
     */
    for (eventcnt = 0, uevent = esed->events;!list_empty(head) && eventcnt < esed->maxevents;) 
    {
        epi = list_first_entry(head, struct epitem, rdllink);

        /* 将epitem从head就绪队列中卸载 */
        list_del_init(&epi->rdllink);

        /* 从资源文件当前状态中提取出我们所关心的events,拿到最新的数据 */
        revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) & epi->event.events;

        /* 如果有我们所关心的events发生 */
        if (revents) 
        {
            /* 
             * 使用__put_user将数据在用户空间和内核空间互相拷贝
             *
             * 将events拷贝到用户空间
             *
             * 若拷贝失败,那么就将该epitem重新添加到就绪链表上,
             * 然后返回已交付的events的数量
             */
            if (__put_user(revents, &uevent->events) || __put_user(epi->event.data, &uevent->data)) 
            {
                /* 复制失败了 */
                list_add(&epi->rdllink, head);
                return eventcnt ? eventcnt : -EFAULT;
            }

            /* 更新已交付的event的数量 */
            eventcnt++;

            /* 指向events数组中的下一元素 */
            uevent++;

            /* 
 			 * 如果是ET模式, epitem是不会再进入到就绪链表,
			 * 除非fd再次发生了状态改变, ep_poll_callback被调用。
			 * 如果是LT模式,不但会将对应的数据返回给用户,并且会将当前的epitem再次加入到rdllist中。
			 * 这样如果下次再次被唤醒就会给用户空间再次返回事件。当然如果这个
			 * 被监听的fd确实没事件也没数据了, epoll_wait会返回一个0。
			 */
            if (epi->event.events & EPOLLONESHOT)
                epi->event.events &= EP_PRIVATE_BITS;
            else if (!(epi->event.events & EPOLLET)) 
            {
                list_add_tail(&epi->rdllink, &ep->rdllist);
            }
        }
    }
    return eventcnt;
}

八、epoll的工作过程详解

  通过上面的源码分析可以看出epoll的工作过程如下:

1. epoll_wait()调用ep_poll(),如果就绪链表rdlist为空,则挂起当前进程,直到rdlist不为空时被唤醒,这个时候会调用ep_send_events()将实际发生的事件reventsdata从内核空间拷贝到用户空间(拷贝调用的是__put_user,并不存在什么共享内存之类的)。

2. 当文件描述符fd的状态改变时(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback() 被调用。

3. ep_poll_callback()将有事件发生的文件描述符(epitem)加入到就绪链表rdlist 中,这时候就绪链表不为空,epoll_wait() 进程被唤醒。

4. ep_send_events()扫描就绪链表,调用每个文件描述符的poll函数返回revents,之后将reventsdata从内核空间拷贝到用户空间。如果是ET模式, epitem是不会再进入到就绪链表,除非fd再次发生了状态改变, ep_poll_callback被调用。如果是LT模式,不但会将对应的数据返回给用户,并且会将当前的epitem再次加入到rdllist中。这样如果下次再次被唤醒就会给用户空间再次返回事件。

转自:http://blog.chinaunix.net/uid/25601623.html

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值