分类
linux file system

linux epoll 机制分析–如何就绪

在上一节中,我们讨论了如何使用epoll。这一节,我们将讨论,epoll是如何由不就绪变成就绪状态。
在上一节中,我们已经知道如果使用epoll监控某个文件状态改变,需要调用epoll_ctl系统调用将文件加入监控集合中。分析代码发现,再加入时是使用ep_insert函数完成一些初始操作。因此,状态改变的方式一定跟ep_insert的操作有关系。
首先查看重要的数据结构 struct epitem.结构如下

struct epitem {
        union {
                /* RB tree node links this structure to the eventpoll RB tree */
                struct rb_node rbn;
                /* Used to free the struct epitem */
                struct rcu_head rcu;
        };          //还没有发生事件时,红黑树。

        /* List header used to link this structure to the eventpoll ready list */
        struct list_head rdllink;       //如果就绪,放入epoll就序列表

        /*
         * Works together "struct eventpoll"->ovflist in keeping the
         * single linked chain of items.
         */
        struct epitem *next;

        /* The file descriptor information this item refers to */
        struct epoll_filefd ffd;        //存放等待事件发生的文件

        /* Number of active wait queue attached to poll operations */
        int nwait;      

        /* List containing poll wait queues */
        struct list_head pwqlist;       //等待列表

        /* The "container" of this item */
        struct eventpoll *ep;           //表明属于哪个epoll句柄

        /* List header used to link this item to the "struct file" items list */
        struct list_head fllink;

        /* wakeup_source used when EPOLLWAKEUP is set */
        struct wakeup_source __rcu *ws;

        /* The structure that describe the interested events and the source fd */
        struct epoll_event event;           //响应的事件
};

这个结构体,就是被监控文件在epoll中的一个实例。每次增加监控,多增加一个这样的结构。
分析ep_insert中的三处关关键代码。其他代码都是错误检查初始化等就不分析了。
第一处关键代码

        INIT_LIST_HEAD(&epi->rdllink);
        INIT_LIST_HEAD(&epi->fllink);
        INIT_LIST_HEAD(&epi->pwqlist);
        epi->ep = ep;
        ep_set_ffd(&epi->ffd, tfile, fd);
        epi->event = *event;
        epi->nwait = 0;
        epi->next = EP_UNACTIVE_PTR;
        if (epi->event.events & EPOLLWAKEUP) {
                error = ep_create_wakeup_source(epi);
                if (error)
                        goto error_create_wakeup_source;
        } else {
                RCU_INIT_POINTER(epi->ws, NULL);
        }           //以上都是初始化epi结构体

        /* Initialize the poll table using the queue callback */
        epq.epi = epi;
        init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);       //初始化回调函数,这个回调函数会在目标文件的poll执行时回调。目的是把epi注册到目标文件的事件通知链中。

init_poll_funcptr 函数代码如下

static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
        pt->_qproc = qproc;
        pt->_key   = ~(__poll_t)0; /* all events enabled */
}

ep_table_queue_proc 函数代码如下

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);  //添加到事件列表中。
                list_add_tail(&pwq->llink, &epi->pwqlist);
                epi->nwait++;
        } else {
                /* We have to signal that an error occurred */
                epi->nwait = -1;
        }
}

ep_poll_callback 函数是epoll的核心函数,当监控句柄有事件发生时,这个函数被执行,判断是否为关心事件,如果是关心事件,则将epi结构体放入ep就绪列表中。最后我们回分析ep_poll_callback函数。
epoll_insert第二处关键代码

        revents = ep_item_poll(epi, &epq.pt, 1);        //这处代码是注册事件

        /*
         * We have to check if something went wrong during the poll wait queue
         * install process. Namely an allocation for a wait queue failed due
         * high memory pressure.
         */
        error = -ENOMEM;
        if (epi->nwait < 0)
                goto error_unregister;

        /* Add the current item to the list of active epoll hook for this file */
        spin_lock(&tfile->f_lock);
        list_add_tail_rcu(&epi->fllink, &tfile->f_ep_links);
        spin_unlock(&tfile->f_lock);

ep_item_poll函数 代码如下:

static __poll_t ep_item_poll(const struct epitem *epi, poll_table *pt,
                                 int depth)
{       
        struct eventpoll *ep;
        bool locked;

        pt->_key = epi->event.events;
        if (!is_file_epoll(epi->ffd.file))          //如果被监控文件也是epoll则走下面逻辑。否则执行被监控文件的poll函数。
                return epi->ffd.file->f_op->poll(epi->ffd.file, pt) &
                       epi->event.events;

        ep = epi->ffd.file->private_data;
        poll_wait(epi->ffd.file, &ep->poll_wait, pt);
        locked = pt && (pt->_qproc == ep_ptable_queue_proc);

        return ep_scan_ready_list(epi->ffd.file->private_data,
                                  ep_read_events_proc, &depth, depth,
                                  locked) & epi->event.events;
} 

到这里我们需要看被监控文件的poll函数。这里使用eventfd模块的poll函数作为例子分析。

static __poll_t eventfd_poll(struct file *file, poll_table *wait)
{
        struct eventfd_ctx *ctx = file->private_data;
        __poll_t events = 0;
        u64 count;

        poll_wait(file, &ctx->wqh, wait);       //select 和poll这个函数都返回空,epoll会执行这个函数。
        count = READ_ONCE(ctx->count);

        if (count > 0)
                events |= EPOLLIN;
        if (count == ULLONG_MAX)
                events |= EPOLLERR;
        if (ULLONG_MAX - 1 > count)
                events |= EPOLLOUT;

        return events;
}

poll_wait函数代码。

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

_qproc这里就是ep_ptable_queue_proc 函数。这样就将 epoll的epi挂在到目标文件的事件通知链上了。如果事件发生,则会执行ep_poll_callback函数代码。接下来查看ep_poll_callback代码。如果对事件发生时会执行ep_poll_callback代码有疑问的可以看一下前面写的eventfd的分析。
ep_poll_callback 执行了一系列检查以后,将文件放入就绪列表中。至此,目标文件状态改变,epoll也能监控到。 ep_poll_callback中还加入唤醒ep_wait的操作,让ep_wait执行检查,ep_wait在上一节中介绍。
到这里,我们已经了解epoll的主要执行流程,其他细节不同的内核版本可能不一样,可以通过看代码详细了解。

发表评论

电子邮件地址不会被公开。 必填项已用*标注