欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

epoll源码分析(二)

程序员文章站 2022-06-14 11:17:13
...

epoll源码分析(二)


epoll_ctl() 函数实现

struct ep_pqueue
{
    poll_table pt;
    struct epitem *epi;
};
  1. 调用copy_from_user将数据从用户空间拷贝到内核空间
  2. 通过fget()获得获得epoll_create()创建的匿名文件的文件指针.
  3. 进行 epoll_ctl() 传入的 op方法的判断.
SYSCALL_DEFINE4(epoll_create, int, epfd, int, op, int, fd, struct epoll_event __user*, event)
{
    struct epoll_event epds;
    ...
    // 从用户态拷贝到内核态
    if(ep_op_has_event(op) && copy_from_user(&epds, event, sizeof(struct epoll_event)))
        goto error_return;
        // 获取调用 epoll_create()函数返回的文件描述符后, 获得创建匿名文件的文件指针.
    file = fget(epfd);
    if (!file)
        goto error_return;
    tfile = fget(fd);
    if (!tfile)
        goto error_fput;
    ...
    epi = ep_find(ep, tfile, fd);   // 查找的原因在于ep是否已经存在了
    /*
    ep_find : 二叉查找文件
        1. 通过将ep_set_ffd()将文件描述符和文件指针加入到一个结构体中
        2. 调用ep_cmp_ffd()进行节点与要查找的文件进行比较, 二叉搜索
    */
    error = -EINVAL;
    // 调用 epoll_ctl()函数的方法
    switch (op) 
    {
    case EPOLL_CTL_ADD: // 添加
        if (!epi) 
        {
            epds.events |= POLLERR | POLLHUP;
            // 调用ep_insert()函数, 设置好回调函数
            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_insert设置回调函数. 那么现在也就讲解回调函数的实现.

ep_insert

  1. 调用kmem_cache_alloc申请缓存空间
  2. 调用ep_set_ffd将文件描述符和文件指针加入ffd结构体中
  3. 调用init_poll_funcptr()设置回调函数为ep_ptable_queue_proc
  4. 掉用ep_rbtree_insert() 将ep, 加入到ep1中
  5. struct epitem epi插入到红黑树中

对目标文件的监听是由一个epitem结构的监听项变量维护的,所以在ep_insert函数里面,首先调用kmem_cache_alloc函数,从slab分配器里面分配一个epitem结构监听项,然后对该结构进行初始化.

static int ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *file, int fd)
{
    ...
    struct epitem *epi;
    struct ep_pqueue epq;
    // 从slab里面分配缓存空间
    if(!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
        return -ENOMEM;

    // 初始化
    INIT_LIST_HEAD(&epi->rdllink);
    INIT_LIST_HEAD(&epi->fllink);
    INIT_LIST_HEAD(&epi->pwqlist);
    ...

    // 保存 epi 以便回调时使用
    epq.epi = epi;
    // 设置好 poll 回调函数为ep_ptable_queue_proc
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
    // 调用事件 poll 函数来获取当前事件位, 利用它来调用注册函数 ep_ptable_queue_proc
    revents = tfile->f_op->poll(tfile, &epq.pt);
    ...

    // 将其加入到红黑树中
    ep_rbtree_insert(ep, epi);
    /*
    ep_rbtree_insert : 插入二叉树中
        1. 通过ep_cmp_ffd进行二叉搜索
        2. 调用rb_link_node rb_insert_color 将 ep结构加入到epi中
    */

    // 返回的事件位(revent)与最初设置的事件位(events)相与进行判断, 判断是否有时间到来, 同时还要保证返回给epoll_wait()的就绪队列不为空
    if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) 
    {
        list_add_tail(&epi->rdllink, &ep->rdllist);
        // 检查等待队列是否为空
        if (waitqueue_active(&ep->wq))
            wake_up_locked(&ep->wq);
        // 等待队列不为空, 则增加唤醒次数
        if (waitqueue_active(&ep->poll_wait))
            pwake++;
    }
    ...
}

同样ep_insert函数又去调用init_poll_funcptr 将函数 init_ptable_queue_proc()来设置为回调函数. 那么init_poll_funcptrinit_ptable_queue_proc 具体我们也来看一下吧

// 将qproc注册到 poll_table_struct 中
// 因为执行 f_op->poll() 时. XXX_poll() 函数会执行 poll_wait() 回调函数, 而 poll_wait()又会调用 poll_table_struct 中的 qproc
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
    pt->qproc = qproc;
}

ep_ptable_queue_proc函数

  1. 将申请缓存空间
  2. 设置 poll 被唤醒时要调用的回调函数
  3. 将其加入到等待队列中

首先将eppoll_entrywhead指向fd的设备等待队列, 再初始化eppoll_entrybase变量指向epitem,最后通过add_wait_queue将epoll_entry挂载到fd的设备等待队列上。完成这个动作后,epoll_entry已经被挂载到fd的设备等待队列.

// 当 poll 函数唤醒时就调用该函数
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt)
{
    // 从注册的结构中struct ep_pqueue中获取项epi
    struct epitem *epi = ep_item_from_epqueue(pt);

    // eppoll_entry主要完成epitem和epitem事件发生时的callback(ep_poll_callback)函数之间的关联
    struct eppoll_entry *pwq;
    // 申请eppoll_entry 缓存, 加入到等待队列中, 和链表中
    if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache,GFP_KERNEL))) 
    {
        // 初始化等待队列函数的入口. 也就是 poll 醒来时要调用的回调函数
        init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
        pwq->whead = whead;
        pwq->base = epi;
        // 加入到等待队列中
        add_wait_queue(whead, &pwq->wait);
        // 将等待队列 llink 的链表挂载到 eptiem等待链表中
        list_add_tail(&pwq->llink, &epi->pwqlist);
        epi->nwait++;
    } 
    ...
}

最后一次的调用, ep_poll_callback主要就只是将要回调事件的文件描述符(fd)加入到 epoll的监听队列中.

ep_poll_callback :

  1. 加锁
  2. 进行事件状态的判断, 没有事件, goto out_unlink
  3. event_poll是否加入就绪队列中, goto is_list
  4. 调用list_add_tail 加入链表
  5. 将epi结构体加入就绪队列中
  6. is_list段 : 调用waitqueue_active 判断就绪队列是否为空, 不为空计数值增加,
  7. out_unlock段 :解除所有的锁
  8. 退出

ep_poll_callback函数主要的功能是将被监视文件的等待事件就绪时,将文件对应的epitem实例添加到就绪队列中,当用户调用epoll_wait()时,内核会将就绪队列中的事件报告给用户.

// poll 到来时, 调用的回调函数. 判断poll 事件是否到来, 是否加入到就绪队列中了
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    struct epitem *epi = ep_item_from_wait(wait);
    ...
    // 事件epi在是否在准备列表中
    if (ep_is_linked(&epi->rdllink))
        goto is_linked;
    // 重要 : 将 fd 加入到 epoll 监听的就绪队列中
    list_add_tail(&epi->rdllink, &ep->rdllist);
    ...
}

关于回调epoll 的回调函数设置的源码已经说的比较清楚了, 一层调用一层函数, 只需要在触发的时侯将其调用就行. 而epoll_ctl函数根据监听的事件,为目标文件申请一个监听项,并将该监听项挂到eventpoll结构的红黑树里面。

总结

epoll源码分析(二)

从SYSCALL_DEFINE4(epoll_ctl, …)开始, 函数首先就分配空间, 将结构从用户空间复制到内核空间中, 在进行方法(op)判断之前, 先采用ep_find函数进行查找, 以确保该数据已经设置好回调函数了, 然后使用fget函数获取该epoll的匿名文件的文件描述符, 最后进行方法(op)判断, 确定是EPOLL_CTL_ADD, EPOLL_CTL_MOD还是 EPOLL_CTL_DEL.

这里主要讲的是EPOLL_CTL_ADD, 所以当是选择加入时, 就调用ep_insert函数, 将回调函数设置为ep_ptable_queue_proc函数, 也就是将消息到达后, 需要自动启动ep_ptable_proc函数, 进而调用ep_poll_callback函数, 该函数就是把来的消息所对应的结构和文件信息加入到就绪链表中, 以便之后调用 epoll_wait 可以直接从就绪队列链表中夺得就绪的文件. 也正是这样, epoll的回调函数使epoll不用每次都轮询遍历数据, 而是自动唤醒回调, 更加的高效. 并且回调函数也只是在进程加入的时侯才设置, 而且只设置一次.