epoll源码分析(二)

epoll源码分析(二)

  • epoll源码分析(二)
    • epoll_ctl() 函数实现
      • 总结


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源码分析(二)_第1张图片

从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不用每次都轮询遍历数据, 而是自动唤醒回调, 更加的高效. 并且回调函数也只是在进程加入的时侯才设置, 而且只设置一次.

你可能感兴趣的:(UNIX高级编程随笔,linux深入理解,unix编程学习)