Implementação de Epoll, parte 2

Ao publicar a tradução do primeiro artigo da série de implementação 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 epollobté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 errnoconjunto 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_procrepresenta 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 _keymesa poll_tablenão é realmente o que parece ser. Ou seja, apesar do nome sugerir uma certa "chave", de _keyfato, as máscaras dos eventos que nos interessam estão armazenadas. Na implementação, é epoll _keydefinido como ~0(complemento a 0). Isso significa que ele epollbusca 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_procestrutura original epitem, epollutiliza uma estrutura simples chamadaep_pqueueque serve como um wrapper poll_tablecom 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 epiestrutura um ep_pqueueponteiro para uma estrutura epitemcorrespondente ao arquivo que estamos tentando adicionar e, em seguida, grava ep_ptable_queue_proc()em um membro da _qprocestrutura ep_pqueuee _keygrava 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 epollentidade, _qprocserá uma função ep_ptable_queue_proc()declarada no arquivo fs/eventpoll.cna 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 epitemque corresponde ao arquivo da fila de espera com a qual estamos trabalhando. Visto que epollusa uma estrutura de wrapper ep_pqueue, restaurar a epitempartir 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 epitempara esse arquivo. É epollextremamente importante saber onde está o início da fila de espera para o arquivo que está sendo monitorado. Caso contrário, epollnão será possível cancelar o registro da fila de espera posteriormente. Estruturaeppoll_entrytambém inclui uma fila wait ( pwq->wait) com uma função de retomada do processo fornecida ep_poll_callback(). Talvez pwq->waitesta seja a parte mais importante de toda a implementação epoll, uma vez que esta entidade é utilizada para resolver as seguintes tarefas:



  1. Monitore eventos que ocorrem com um arquivo específico sendo monitorado.
  2. 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->nwaitrepresenta o comprimento da lista epi->pwqlist.



E aqui eu tenho uma pergunta. Por que epollusar uma lista vinculada para armazenar uma estrutura eppoll_entryem uma epitemúnica estrutura de arquivo? Não é necessário epitemapenas 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 epollem alguns loops malucos, a lista epi->pwqlistconterá apenas um elemento struct eppoll_entry, eepi->nwaitpara a maioria dos arquivos é provável que seja 1.



O bom é que as ambigüidades ao redor epi->pwqlistnão afetam de forma alguma o que falarei a seguir. A saber, falaremos sobre como o Linux notifica as instâncias epollde eventos que ocorrem nos arquivos monitorados.



Lembra do que falamos na seção anterior? Tratava-se do que é epolladicionado wait_queue_tà lista de espera do arquivo de destino (para wait_queue_head_t). Embora wait_queue_tmais 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_tanexada wait_queue_head_t. Nesta funçãoepollpode decidir o que fazer com o sinal de retomada, mas epollnã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_wqarmazenado 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_tsem usar um retorno de chamada.



Quando exatamente é realizada a retomada das obras sk_wqna estrutura sock? Acontece que o sistema de soquetes do Linux segue os mesmos princípios de design "OO" do VFS. A estrutura sockdeclara 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_wqobjetivo 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 fullpara 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, epollvamos falar sobre o que exatamente ele faz no retorno de chamada registrado na fila de retomada de processos de soquete.



Você já usou epoll?










All Articles