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
,则该函数立即终止errno
于ENOSPC
。
然后,它
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_table
是poll()
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()
成员并对其进行写入。_qproc
ep_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
,因为此实体用于解决以下任务:
- 监视事件与特定文件的监视。
- 如果有这种需要,请恢复其他流程的工作。
然后,它将
ep_ptable_queue_proc()
附加pwq->wait
到目标文件的等待队列(whead
)。该函数还将struct eppoll_entry
从struct epitem
(epi->pwqlist
)添加到链接列表,并增加epi->nwait
表示列表长度的值epi->pwqlist
。
在这里,我有一个问题。为什么
epoll
使用链表将结构存储eppoll_entry
在epitem
单个文件结构中?不仅需要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;
// ...
}
B
sock_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吗?