epoll
, realizamos uma pesquisa sobre a viabilidade de continuar a tradução do ciclo. Mais de 90% dos participantes da pesquisa foram a favor da tradução do restante dos artigos. Por isso, hoje publicamos uma tradução do segundo material deste ciclo.

Função Ep_insert ()
Uma função
ep_insert()
é uma das funções mais importantes em uma implementação epoll
. Entender como funciona é extremamente importante para entender como exatamente ele epoll
obtém informações sobre novos eventos dos arquivos que está assistindo.
A declaração
ep_insert()
pode ser encontrada na linha 1267 do arquivo fs/eventpoll.c
. Vejamos alguns trechos de código para esta função:
user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
return -ENOSPC;
Neste trecho de código, a função
ep_insert()
primeiro verifica se o número total de arquivos que o usuário atual está assistindo não é maior do que o valor especificado em /proc/sys/fs/epoll/max_user_watches
. Se user_watches >= max_user_watches
, então a função termina imediatamente com o errno
conjunto para ENOSPC
.
Em seguida, ele
ep_insert()
aloca memória usando o mecanismo de gerenciamento de memória slab do kernel Linux:
if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
return -ENOMEM;
Se a função conseguiu alocar memória suficiente para
struct epitem
, o seguinte processo de inicialização será executado:
/* ... */
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;
Depois disso, ele
ep_insert()
tentará registrar o callback no descritor de arquivo. Mas antes de falarmos sobre isso, precisamos nos familiarizar com algumas estruturas de dados importantes.
A estrutura
poll_table
é uma entidade importante usada por uma implementação poll()
VFS. (Eu entendo que isso pode ser confuso, mas aqui eu gostaria de explicar que a função poll()
que mencionei aqui é uma implementação de uma operação de arquivo poll()
, não uma chamada de sistema poll()
). Ela é anunciada em include/linux/poll.h
:
typedef struct poll_table_struct {
poll_queue_proc _qproc;
unsigned long _key;
} poll_table;
Uma entidade
poll_queue_proc
representa um tipo de função de retorno de chamada semelhante a esta:
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
Um membro de uma
_key
mesa poll_table
não é realmente o que parece ser. Ou seja, apesar do nome sugerir uma certa "chave", de _key
fato, as máscaras dos eventos que nos interessam estão armazenadas. Na implementação, é epoll
_key
definido como ~0
(complemento a 0). Isso significa que ele epoll
busca receber informações sobre eventos de qualquer natureza. Isso faz sentido, pois os aplicativos do espaço do usuário podem alterar a máscara de evento a qualquer momento usando epoll_ctl()
, aceitando todos os eventos do VFS e, em seguida, filtrando-os na implementação epoll
, o que torna as coisas mais fáceis.
Para facilitar a restauração da
poll_queue_proc
estrutura original epitem
, epoll
utiliza uma estrutura simples chamadaep_pqueue
que serve como um wrapper poll_table
com um ponteiro para a estrutura correspondente epitem
(arquivo fs/eventpoll.c
, linha 243):
/* -, */
struct ep_pqueue {
poll_table pt;
struct epitem *epi;
};
Em seguida, ele
ep_insert()
inicializa struct ep_pqueue
. O código a seguir primeiro grava em um membro da epi
estrutura um ep_pqueue
ponteiro para uma estrutura epitem
correspondente ao arquivo que estamos tentando adicionar e, em seguida, grava ep_ptable_queue_proc()
em um membro da _qproc
estrutura ep_pqueue
e _key
grava nele ~0
.
/* */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
Em seguida
ep_insert()
, ele chamará ep_item_poll(epi, &epq.pt);
, o que resultará em uma chamada para a implementação poll()
associada ao arquivo.
Vamos dar uma olhada em um exemplo que usa a implementação da
poll()
pilha TCP do Linux e entender o que exatamente essa implementação faz poll_table
.
Uma função
tcp_poll()
é uma implementação poll()
para sockets TCP. Seu código pode ser encontrado no arquivo net/ipv4/tcp.c
, na linha 436. Aqui está um trecho deste código:
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);
//
}
A função
tcp_poll()
chama sock_poll_wait()
, passando, como o segundo argumento sk_sleep(sk)
e como o terceiro - wait
(esta é a tcp_poll()
tabela passada anteriormente para a função poll_table
).
O que é isso
sk_sleep()
? Acontece que este é apenas um getter para acessar a fila de espera de eventos para uma estrutura particular sock
(arquivo include/net/sock.h
, linha 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;
}
O que
sock_poll_wait()
vai fazer com a fila de espera de eventos? Acontece que esta função irá realizar uma verificação simples e, em seguida, chamar poll_wait()
com os mesmos parâmetros. A função poll_wait()
irá então chamar o retorno de chamada que especificamos e passar para ele uma fila de espera de eventos (arquivo include/linux/poll.h
, linha 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);
}
No caso da
epoll
entidade, _qproc
será uma função ep_ptable_queue_proc()
declarada no arquivo fs/eventpoll.c
na linha 1091.
/*
* - ,
* , .
*/
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;
}
}
Primeiro, ele
ep_ptable_queue_proc()
tenta restaurar a estrutura epitem
que corresponde ao arquivo da fila de espera com a qual estamos trabalhando. Visto que epoll
usa uma estrutura de wrapper ep_pqueue
, restaurar a epitem
partir de um ponteiro poll_table
é uma operação simples de ponteiro.
Depois disso, ele
ep_ptable_queue_proc()
apenas aloca a quantidade de memória necessária para struct eppoll_entry
. Essa estrutura atua como uma "cola" entre a fila de espera do arquivo que está sendo monitorado e a estrutura correspondente epitem
para esse arquivo. É epoll
extremamente importante saber onde está o início da fila de espera para o arquivo que está sendo monitorado. Caso contrário, epoll
não será possível cancelar o registro da fila de espera posteriormente. Estruturaeppoll_entry
também inclui uma fila wait ( pwq->wait
) com uma função de retomada do processo fornecida ep_poll_callback()
. Talvez pwq->wait
esta seja a parte mais importante de toda a implementação epoll
, uma vez que esta entidade é utilizada para resolver as seguintes tarefas:
- Monitore eventos que ocorrem com um arquivo específico sendo monitorado.
- Retomar o trabalho de outros processos caso surja tal necessidade.
Em seguida, ele será
ep_ptable_queue_proc()
anexado pwq->wait
à fila de espera do arquivo de destino ( whead
). A função também adicionará struct eppoll_entry
à lista vinculada de struct epitem
( epi->pwqlist
) e incrementará o valor que epi->nwait
representa o comprimento da lista epi->pwqlist
.
E aqui eu tenho uma pergunta. Por que
epoll
usar uma lista vinculada para armazenar uma estrutura eppoll_entry
em uma epitem
única estrutura de arquivo? Não é necessário epitem
apenas um elemento eppoll_entry
?
Eu realmente não posso responder a essa pergunta exatamente. Tanto quanto eu posso dizer, a menos que alguém vá usar instâncias
epoll
em alguns loops malucos, a lista epi->pwqlist
conterá apenas um elemento struct eppoll_entry
, eepi->nwait
para a maioria dos arquivos é provável que seja 1
.
O bom é que as ambigüidades ao redor
epi->pwqlist
não afetam de forma alguma o que falarei a seguir. A saber, falaremos sobre como o Linux notifica as instâncias epoll
de eventos que ocorrem nos arquivos monitorados.
Lembra do que falamos na seção anterior? Tratava-se do que é
epoll
adicionado wait_queue_t
à lista de espera do arquivo de destino (para wait_queue_head_t
). Embora wait_queue_t
mais comumente usado como um mecanismo para retomar processos, é essencialmente apenas uma estrutura que armazena um ponteiro para uma função que será chamada quando o Linux decidir retomar processos da fila wait_queue_t
anexada wait_queue_head_t
. Nesta funçãoepoll
pode decidir o que fazer com o sinal de retomada, mas epoll
não há necessidade de retomar nenhum processo! Como você verá mais tarde, geralmente ep_poll_callback()
nada acontece quando você liga para retomar.
Suponho que também seja importante notar que o mecanismo de retomada do processo usado no
poll()
é totalmente dependente da implementação. No caso de arquivos de soquete TCP, a cabeça da fila de espera é um membro sk_wq
armazenado na estrutura sock
. Isso também explica a necessidade de usar um retorno ep_ptable_queue_proc()
de chamada para trabalhar com a fila de espera. Já que em implementações da fila para arquivos diferentes, o cabeçalho da fila pode aparecer em lugares completamente diferentes, não temos como encontrar o valor que precisamoswait_queue_head_t
sem usar um retorno de chamada.
Quando exatamente é realizada a retomada das obras
sk_wq
na estrutura sock
? Acontece que o sistema de soquetes do Linux segue os mesmos princípios de design "OO" do VFS. A estrutura sock
declara os seguintes ganchos na linha 2312 do arquivo 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()
e sock_def_write_space()
a chamada tem wake_up_interruptible_sync_poll()
como (struct sock)->sk_wq
objetivo o retorno de chamada de função, trabalho de processo renovável.
Quando será
sk->sk_data_ready()
e será chamado sk->sk_write_space()
? Depende da implementação. Vamos tomar os soquetes TCP como exemplo. A função sk->sk_data_ready()
será chamada na segunda metade do manipulador de interrupção quando a conexão TCP completar o procedimento de handshake de três vias ou quando um buffer for recebido para um determinado soquete TCP. A função sk->sk_write_space()
será chamada quando o estado do buffer mudar de full
para available
. Se você manter isso em mente ao analisar os tópicos a seguir, especialmente aquele sobre o acionamento frontal, esses tópicos parecerão mais interessantes.
Resultado
Isso conclui o segundo artigo de uma série de artigos sobre implementação
epoll
. Na próxima vez, epoll
vamos falar sobre o que exatamente ele faz no retorno de chamada registrado na fila de retomada de processos de soquete.
Você já usou epoll?

