Epoll实施,第2部分

在发布实施系列第一篇文章的译文时epoll,我们对继续进行周期翻译的可行性进行了调查。超过90%的调查参与者赞成翻译其余文章。因此,今天我们发布该周期的第二本材料的译文。







Ep_insert()函数



功能ep_insert()是实现中最重要的功能之一epoll为了了解它如何准确地epoll从正在观看的文件中获取有关新事件的信息,了解它的工作极为重要可以在文件的第1267行找到



该声明让我们看一下此功能的一些代码片段:ep_insert()fs/eventpoll.c



user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
  return -ENOSPC;


在此代码段中,该函数ep_insert()首先检查当前用户正在观看的文件总数是否不大于中指定的值/proc/sys/fs/epoll/max_user_watches如果为user_watches >= max_user_watches,则该函数立即终止errnoENOSPC



然后,它ep_insert()使用Linux内核slab内存管理机制分配内存:



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


如果函数能够为分配足够的内存struct epitem,则将执行以下初始化过程:



/*  ... */
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;


之后,它将ep_insert()尝试在文件描述符中注册回调。但是,在谈论它之前,我们需要熟悉一些重要的数据结构。



框架poll_tablepoll()VFS实现使用的重要实体(我知道这可能会造成混淆,但是在这里我想解释一下,poll()这里提到的功能是文件操作的实现poll(),而不是系统调用poll())。她在以下时间宣布include/linux/poll.h



typedef struct poll_table_struct {
  poll_queue_proc _qproc;
  unsigned long _key;
} poll_table;


实体poll_queue_proc表示一种类似于以下形式的回调函数:



typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);


一个_key的成员poll_table实际上不是它最初看起来的样子。即,尽管名称暗示某个“键”,但_key实际上,存储了我们感兴趣的事件的掩码。在实现中,将其epoll _key设置为~0(补0)。这意味着它epoll试图接收有关任何类型事件的信息。这是有道理的,因为用户空间应用程序可以随时使用来更改事件掩码epoll_ctl(),接受来自VFS的所有事件,然后在实现中对其进行过滤epoll,这使事情变得更加容易。



为了便于恢复poll_queue_proc原始结构epitem,它epoll使用了一个简单的结构,称为ep_pqueue它用作poll_table带有相应结构指针的包装器epitem(文件fs/eventpoll.c,第243行):



/* -,    */
struct ep_pqueue {
  poll_table pt;
  struct epitem *epi;
};


然后ep_insert()初始化struct ep_pqueue以下代码首先将指向epi结构的ep_pqueue指针写入结构成员,指针epitem对应于我们要添加的文件,然后写入结构ep_ptable_queue_proc()成员并对其进行写入_qprocep_pqueue_key~0



/*      */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);


然后ep_insert()它将调用ep_item_poll(epi, &epq.pt);,这将导致对poll()与文件关联的实现的调用



让我们看一个使用poll()Linux TCP堆栈实现的示例,并了解该实现的功能poll_table



函数tcp_poll()poll()TCP套接字的实现。可以net/ipv4/tcp.c在第436行的文件中找到其代码。这是此代码的片段:



unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
  unsigned int mask;
  struct sock *sk = sock->sk;
  const struct tcp_sock *tp = tcp_sk(sk);

  sock_rps_record_flow(sk);

  sock_poll_wait(file, sk_sleep(sk), wait);

  //  
}


该函数tcp_poll()调用sock_poll_wait(),作为第二个参数sk_sleep(sk)和第三个参数传递wait(这是先前传递给该函数的tcp_poll()poll_table)。



什么事sk_sleep()事实证明,这只是访问特定结构的事件等待队列的吸气剂sock(文件include/net/sock.h,行1685):



static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
  BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
  return &rcu_dereference_raw(sk->sk_wq)->wait;
}


什么是sock_poll_wait()要与事件排队等候办?事实证明,此函数将执行一些简单的检查,然后poll_wait()使用相同的参数进行调用然后,该函数poll_wait()将调用我们指定的回调,并向其传递事件等待队列(文件include/linux/poll.h,第42行):



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


对于epoll实体,它将_qproc在第1091行ep_ptable_queue_proc()的文件中声明的函数fs/eventpoll.c



/*
*  - ,       
*     ,    .
*/
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;
    add_wait_queue(whead, &pwq->wait);
    list_add_tail(&pwq->llink, &epi->pwqlist);
    epi->nwait++;
  } else {
    /*       */
    epi->nwait = -1;
  }
}


首先,它ep_ptable_queue_proc()尝试epitem从正在使用的等待队列中还原与文件相对应的结构。由于它epoll使用包装器结构ep_pqueue,因此epitem从指针还原poll_table是简单的指针操作。



之后,它只ep_ptable_queue_proc()分配所需的内存struct eppoll_entry。此结构充当正在监视的文件的等待队列与该文件的相应结构之间的“胶水” epitem。它epoll知道在哪里等待队列头是文件被监视极为重要。否则,epoll以后将无法注销等待队列。结构体eppoll_entry还包括pwq->wait带有提供过程恢复功能的wait(队列ep_poll_callback()也许pwq->wait这是整个实现中最重要的部分epoll,因为此实体用于解决以下任务:



  1. 监视事件与特定文件的监视。
  2. 如果有这种需要,请恢复其他流程的工作。


然后,它将ep_ptable_queue_proc()附加pwq->wait到目标文件的等待队列(whead)。该函数还将struct eppoll_entrystruct epitemepi->pwqlist添加到链接列表,并增加epi->nwait表示列表长度的值epi->pwqlist



在这里,我有一个问题。为什么epoll使用链表将结构存储eppoll_entryepitem单个文件结构中?不仅需要epitem一个要素eppoll_entry吗?



但是,我无法完全回答这个问题。据我所知,除非有人打算epoll在一些疯狂的循环中使用实例,否则列表epi->pwqlist将只包含一个元素struct eppoll_entry,并且epi->nwait对于大多数文件来说可能是1



好消息是周围的歧义epi->pwqlist不会以任何方式影响我下面将要讨论的内容。即,我们将讨论Linux如何通知epoll被监视文件发生事件的实例



还记得我们在上一节中讨论的内容吗?这是关于epoll追加wait_queue_t到目标文件的等待列表(至wait_queue_head_t)的内容。尽管wait_queue_t最常用作恢复进程的机制,但它实际上只是一种结构,它存储指向当Linux决定从wait_queue_t附加到的队列中恢复进程的函数调用的指针wait_queue_head_t。在此功能epoll可以决定如何处理恢复信号,但epoll无需恢复任何过程!正如您稍后将看到的那样,通常在调用ep_poll_callback()简历什么也没有发生。



我想还值得注意的是,其中使用的流程恢复机制poll()完全取决于实现。对于TCP套接字文件,等待队列头是sk_wq存储在结构中的成员sock。这也解释了需要使用回调ep_ptable_queue_proc()来处理等待队列。由于在针对不同文件的队列的实现中,队列的头部可能出现在完全不同的位置,因此我们无法找到所需的值wait_queue_head_t不使用回调。



何时才恢复sk_wq结构中的工作sock?事实证明,Linux套接字系统遵循与VFS相同的“ OO”设计原则。该结构sock在文件的第2312行上声明以下挂钩net/core/sock.c



void sock_init_data(struct socket *sock, struct sock *sk)
{
  //  ...
  sk->sk_data_ready  =   sock_def_readable;
  sk->sk_write_space =  sock_def_write_space;
  //  ...
}


Bsock_def_readable()sock_def_write_space()调用是wake_up_interruptible_sync_poll()出于(struct sock)->sk_wq功能回调的目的,可再生过程中的工作。



什么时候会sk->sk_data_ready()会叫sk->sk_write_space()这取决于实现方式。让我们以TCP套接字为例。sk->sk_data_ready()当TCP连接完成三向握手过程时,或者在接收到某个TCP套接字的缓冲区时,将在中断处理程序的后半部分调用该函数sk->sk_write_space()当缓冲区状态从full变为时,将调用该函数available如果您在分析以下主题(尤其是有关前置触发的主题)时牢记这一点,这些主题将显得更加有趣。



结果



这是有关实现的系列文章中的第二篇文章的总结epoll下次,epoll让我们讨论一下它在套接字进程恢复队列中注册的回调中到底做了什么



您使用过epoll吗?










All Articles