浅谈epoll

对于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.11\fs\eventpoll.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的实现机制。感兴趣的朋友可以继续阅读源码了解。

已标记关键词 清除标记
【为什么还需要学习C++?】 你是否接触很多语言,但从来没有了解过编程语言的本质? 你是否想成为一名资深开发人员,想开发别人做不了的高性能程序? 你是否经常想要窥探大型企业级开发工程的思路,但苦于没有基础只能望洋兴叹?   那么C++就是你个人能力提升,职业之路进阶的不二之选。 【课程特色】 1.课程共19大章节,239课时内容,涵盖数据结构、函数、类、指针、标准库全部知识体系。 2.带你从知识与思想的层面从0构建C++知识框架,分析大型项目实践思路,为你打下坚实的基础。 3.李宁老师结合4大国外顶级C++著作的精华为大家推出的《征服C++11》课程。 【学完后我将达到什么水平?】 1.对C++的各个知识能够熟练配置、开发、部署; 2.吊打一切关于C++的笔试面试题; 3.面向物联网的“嵌入式”和面向大型化的“分布式”开发,掌握职业钥匙,把握行业先机。 【面向人群】 1.希望一站式快速入门的C++初学者; 2.希望快速学习 C++、掌握编程要义、修炼内功的开发者; 3.有志于挑战更高级的开发项目,成为资深开发的工程师。 【课程设计】 本课程包含3大模块 基础篇 本篇主要讲解c++的基础概念,包含数据类型、运算符等基本语法,数组、指针、字符串等基本词法,循环、函数、类等基本句法等。 进阶篇 本篇主要讲解编程中常用的一些技能,包含类的高级技术、类的继承、编译链接和命名空间等。 提升篇: 本篇可以帮助学员更加高效的进行c++开发,其中包含类型转换、文件操作、异常处理、代码重用等内容。
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页