我是靠谱客的博主 内向纸飞机,最近开发中收集的这篇文章主要介绍浅谈epoll介绍用户态server-client epoll实现内核中的epoll追踪内核中的epoll_create内核中的epoll_ctl内核中的epoll_wait,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

对于Linux内核多路复用技术相比大家都有一定的了解,从select、poll到epoll,无一不是对前者的升级,这篇文建将要简单讲解epoll在内核中的实现。本文中提及的内核版本是 4.20.11.注意这里的epoll并不是evolution poll,而是event poll,二者没有必要的联系,epoll也并不是针对poll进行的改进,再加上select,三者为单线程多任务的模拟。比如我们了解的redis、nginx都有用到epoll技术,感兴趣的朋友可以自行谷歌或必应。

图片来自网络,侵删。

Table of Contents

介绍

用户态server-client epoll实现

内核中的epoll追踪

内核中的epoll_create

内核中的epoll_ctl

内核中的epoll_wait


介绍

在Linux内核4.20.111 include/linux/syscalls.h里有这样的声明。

asmlinkage long sys_epoll_create(int size);

在用户态中他长这样(参见man epoll_create)

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);

如man手册所说,size参数从Linux 2.6.8被废弃

 

epoll_create() creates a new epoll(7) instance.  Since Linux 2.6.8,
the size argument is ignored, but must be greater than zero; see
NOTES.

用户态server-client epoll实现

这里有一篇关于epoll的socket S-C代码实现:

https://rtoax.blog.csdn.net/article/details/81047943

内核中的epoll追踪

就像上面服务端-客户端(S-C)实现一样,很多小伙伴认为epoll是属于内核中的网络,实则不然,其实它属于文件系统,别忘了,epoll_create的函数返回值是个fd(文件描述符),再看一下代码的存放路径:

linux-4.20.11fseventpoll.c

而对于用户态的epoll_create,在改代码中的系统调用为

SYSCALL_DEFINE1(epoll_create1, int, flags)
{
  return do_epoll_create(flags);
}
SYSCALL_DEFINE1(epoll_create, int, size)
{
  if (size <= 0)
    return -EINVAL;
  return do_epoll_create(0);
}

当然,参数 size 被弃用。

我们已经找到了epoll_create的系统调用原型

/*
 * Open an eventpoll file descriptor.
 */
static int do_epoll_create(int flags);

内核中的epoll_create

当然是先看结构体(为简化只保留关键代码)

struct file;
struct eventpoll;
struct epitem;
struct epoll_event;
struct eppoll_entry;
struct ep_pqueue;
struct epoll_filefd;

简化的流程如下图

内核中的epoll_ctl

系统调用原型为

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
    struct epoll_event __user *, event);

再看这一段代码

  switch (op) {
  case EPOLL_CTL_ADD:
    if (!epi) {
      epds.events |= EPOLLERR | EPOLLHUP;
      error = ep_insert(ep, &epds, tf.file, fd, full_check);
    } else
      error = -EEXIST;
    if (full_check)
      clear_tfile_check_list();
    break;
  case EPOLL_CTL_DEL:
    if (epi)
      error = ep_remove(ep, epi);
    else
      error = -ENOENT;
    break;
  case EPOLL_CTL_MOD:
    if (epi) {
      if (!(epi->event.events & EPOLLEXCLUSIVE)) {
        epds.events |= EPOLLERR | EPOLLHUP;
        error = ep_modify(ep, epi, &epds);
      }
    } else
      error = -ENOENT;
    break;
  }

这段代码告诉我们ADD和DEL两个操作是如何操作的。

首先看下epoll_ctl涉及到的关键全局变量。

然后,开始进行slab分配,关注一句

if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
    return -ENOMEM;

其中kmem_cache_alloc 涉及到slab的只是,此处不做详述,功能是从缓存epi_cache中申请固定页的内存。当然这个类型是struct epitem *epi;这是epoll的一项,也就是一个fd(文件描述符)。接下来初始化一系列链表头

/* Item initialization follow here ... */
  INIT_LIST_HEAD(&epi->rdllink);
  INIT_LIST_HEAD(&epi->fllink);
  INIT_LIST_HEAD(&epi->pwqlist);

设定fd,用于红黑树节点的索引。

/* Setup the structure that is used as key for the RB tree */
static inline void ep_set_ffd(struct epoll_filefd *ffd,
            struct file *file, int fd)
{
  ffd->file = file;
  ffd->fd = fd;
}

将fllink节点插入tfile的f_ep_links为头结点的双向链表中。

list_add_tail_rcu(&epi->fllink, &tfile->f_ep_links);

将item插入红黑树(红黑树:一种平衡二叉树,可用于快速索引,一个整形的遍历最高也就十几次的计算复杂度)

/*
   * Add the current item to the RB tree. All RB tree operations are
   * protected by "mtx", and ep_insert() is called with "mtx" held.
   */
  ep_rbtree_insert(ep, epi);

反过来的remove操作

/*
 * Removes a "struct epitem" from the eventpoll RB tree and deallocates
 * all the associated resources. Must be called with "mtx" held.
 */
static int ep_remove(struct eventpoll *ep, struct epitem *epi)

当然,从链表中删除item

list_del_rcu(&epi->fllink)
list_del_init(&epi->rdllink);

从红黑树中删除叶子

rb_erase_cached(&epi->rbn, &ep->rbr);

这里当然涉及到资源释放“RCU-read count use”

call_rcu(&epi->rcu, epi_rcu_free);

讲了ADD,DEL,MOD也就好解释了,无非是索引+修改。

/*
 * Modify the interest event mask by dropping an event if the new mask
 * has a match in the current file status. Must be called with "mtx" held.
 */
static int ep_modify(struct eventpoll *ep, struct epitem *epi,
         const struct epoll_event *event)

然后再将其添加到队尾

list_add_tail(&epi->rdllink, &ep->rdllist);

如下图给出了epoll的create和ctrl的结构体图。

内核中的epoll_wait

wait有两种,用户态接口为epoll_wait和epoll_pwait,二者区别在于一个signal,此处不做讲解,以epoll_wait为例。

epoll_wait的系统调用为

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
    int, maxevents, int, timeout)
{
  return do_epoll_wait(epfd, events, maxevents, timeout);
}

do_epoll_wait函数原型为

/*
 * Implement the event wait interface for the eventpoll file. It is the kernel
 * part of the user space epoll_wait(2).
 */
static int do_epoll_wait(int epfd, struct epoll_event __user *events,
       int maxevents, int timeout);

我们主要关注函数主要做了哪些工作。注意之类将使用wait_queue,可以简单将其理解为一方read阻塞,一方write后,read阻塞被打断,读取缓冲区,当然这也是一种比喻,实际请读者自己脑补,他的数据类型为

wait_queue_entry_t wait;

对于wait只针对关键函数进行调用追踪,先下面这个“事件发生”判断

/* Is it worth to try to dig for events ? */
  eavail = ep_events_available(ep);

以及这个事件真的发生了

/*
   * Try to transfer events to user space. In case we get 0 events and
   * there's still timeout left over, we go trying again in search of
   * more luck.
   */
  if (!res && eavail &&
      !(res = ep_send_events(ep, events, maxevents)) && !timed_out)
    goto fetch_events;

提到一嘴,这里的宏current 代表了运行了这段代码的内核进程,也就是结构task_struct(详情请自行学习进程管理相关知识)。

下面我们就关注ep_send_events这个函数,里面就只scan了一个ready list

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;

  ep_scan_ready_list(ep, ep_send_events_proc, &esed, 0, false);
  return esed.res;
}

初始化一个链表,并肩所有ready的节点拼接到这个链表上

LIST_HEAD(txlist);
list_splice_init(&ep->rdllist, &txlist);

然后调用这个回调proc函数

/*
   * Now call the callback function.
   */
  res = (*sproc)(ep, &txlist, priv);

这个回调函数中将该ready的节点删除,函数原型为

static __poll_t ep_send_events_proc(struct eventpoll *ep, struct list_head *head,
             void *priv);

然后就是这一段epoll_wait将返回的代码

if (!list_empty(&ep->rdllist)) {
    /*
     * Wake up (if active) both the eventpoll wait list and
     * the ->poll() wait list (delayed after we release the lock).
     */
    if (waitqueue_active(&ep->wq))
      wake_up_locked(&ep->wq);
    if (waitqueue_active(&ep->poll_wait))
      pwake++;
  }
  spin_unlock_irq(&ep->wq.lock);

  if (!ep_locked)
    mutex_unlock(&ep->mtx);

  /* We have to call this outside the lock */
  if (pwake)
    ep_poll_safewake(&ep->poll_wait);

当然,其中可能涉及到中断的知识,此文也不做讲解,

static void ep_poll_safewake(wait_queue_head_t *wq)
{
  int this_cpu = get_cpu();

  ep_call_nested(&poll_safewake_ncalls, EP_MAX_NESTS,
           ep_poll_wakeup_proc, NULL, wq, (void *) (long) this_cpu);

  put_cpu();
}

本文就知识简单讲解epoll的实现机制。感兴趣的朋友可以继续阅读源码了解。

最后

以上就是内向纸飞机为你收集整理的浅谈epoll介绍用户态server-client epoll实现内核中的epoll追踪内核中的epoll_create内核中的epoll_ctl内核中的epoll_wait的全部内容,希望文章能够帮你解决浅谈epoll介绍用户态server-client epoll实现内核中的epoll追踪内核中的epoll_create内核中的epoll_ctl内核中的epoll_wait所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(48)

评论列表共有 0 条评论

立即
投稿
返回
顶部