Implementación de Epoll, parte 2

Al publicar la traducción del primer artículo de la serie de implementación epoll, realizamos una encuesta sobre la viabilidad de continuar con la traducción del ciclo. Más del 90% de los encuestados estaban a favor de traducir el resto de los artículos. Por eso, hoy publicamos una traducción del segundo material de este ciclo.







Función ep_insert ()



Una función ep_insert()es una de las funciones más importantes de una implementación epoll. Comprender cómo funciona es extremadamente importante para comprender exactamente cómo epollobtiene información sobre nuevos eventos de los archivos que está viendo.



La declaración ep_insert()se puede encontrar en la línea 1267 del archivo fs/eventpoll.c. Veamos algunos fragmentos de código para esta función:



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


En este fragmento de código, la función ep_insert()primero verifica si el número total de archivos que el usuario actual está viendo es menor que el valor especificado en /proc/sys/fs/epoll/max_user_watches. Si user_watches >= max_user_watches, entonces la función termina inmediatamente con el errnoestablecido en ENOSPC.



Luego ep_insert()asigna memoria usando el mecanismo de administración de memoria de bloque del kernel de Linux:



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


Si la función pudo asignar suficiente memoria para struct epitem, se realizará el siguiente proceso de inicialización:



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


Después de eso, ep_insert()intentará registrar la devolución de llamada en el descriptor de archivo. Pero antes de que podamos hablar de ello, debemos familiarizarnos con algunas estructuras de datos importantes.



Framework poll_tablees una entidad importante utilizada por una implementación de poll()VFS. (Entiendo que esto puede ser confuso, pero aquí me gustaría explicar que la función poll()que mencioné aquí es una implementación de una operación de archivo poll(), no una llamada al sistema poll()). Ella se anuncia en include/linux/poll.h:



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


Una entidad poll_queue_procrepresenta un tipo de función de devolución de llamada que se ve así:



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


Un miembro de una _keymesa en poll_tablerealidad no es lo que parece ser. Es decir, a pesar de que el nombre sugiere una determinada "clave", de _keyhecho, se almacenan las máscaras de los eventos que nos interesan. En la implementación, se epoll _keyestablece en ~0(complemento a 0). Esto significa que epollbusca recibir información sobre eventos de cualquier tipo. Esto tiene sentido, ya que las aplicaciones de espacio de usuario pueden cambiar la máscara de eventos en cualquier momento utilizando epoll_ctl(), aceptando todos los eventos del VFS y luego filtrándolos en la implementación epoll, lo que facilita las cosas.



Para facilitar la restauración de la poll_queue_procestructura original epitem, epollutiliza una estructura simple llamadaep_pqueueque sirve como contenedor poll_tablecon un puntero a la estructura correspondiente epitem(archivo fs/eventpoll.c, línea 243):



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


Luego se ep_insert()inicializa struct ep_pqueue. El siguiente código primero escribe en un miembro de epiestructura un ep_pqueuepuntero a una estructura epitemcorrespondiente al archivo que estamos intentando agregar, y luego escribe ep_ptable_queue_proc()en un miembro de _qprocestructura ep_pqueuey _keyescribe en él ~0.



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


Luego ep_insert()llamará ep_item_poll(epi, &epq.pt);, lo que resultará en una llamada a la implementación poll()asociada con el archivo.



Echemos un vistazo a un ejemplo que utiliza la implementación de la poll()pila TCP de Linux y descubramos qué hace exactamente esa implementación poll_table.



Una función tcp_poll()es una implementación poll()para sockets TCP. Su código se puede encontrar en el archivo net/ipv4/tcp.c, en la línea 436. Aquí hay un fragmento de este 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);

  //  
}


La función tcp_poll()llama sock_poll_wait(), pasando, como segundo argumento, sk_sleep(sk)y como tercero - wait(esta es la tcp_poll()tabla pasada previamente a la función poll_table).



¿Qué es sk_sleep()? Resulta que esto es solo un captador para acceder a la cola de espera de eventos para una estructura en particular sock(archivo include/net/sock.h, línea 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;
}


¿Qué sock_poll_wait()va a hacer con la cola de espera de eventos? Resulta que esta función realizará una simple verificación y luego llamará poll_wait()con los mismos parámetros. La función luego poll_wait()llamará a la devolución de llamada que especificamos y le pasará una cola de eventos en espera (archivo include/linux/poll.h, línea 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);
}


En el caso de la epollentidad, _qprocserá una función ep_ptable_queue_proc()declarada en el archivo fs/eventpoll.cde la línea 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;
  }
}


Primero, ep_ptable_queue_proc()intenta restaurar la estructura epitemque corresponde al archivo de la cola de espera con la que estamos trabajando. Dado que epollutiliza una estructura contenedora ep_pqueue, restaurar epitemdesde un puntero poll_tablees una operación simple de puntero.



Después de eso, ep_ptable_queue_proc()simplemente asigna tanta memoria como sea necesaria para struct eppoll_entry. Esta estructura actúa como un "pegamento" entre la cola de espera del archivo que se está viendo y la estructura correspondiente epitempara ese archivo. Es epollextremadamente importante saber dónde está el encabezado de la cola de espera para el archivo que se está viendo. De epolllo contrario, no podrá cancelar el registro de la cola de espera más tarde. Estructuraeppoll_entrytambién incluye una cola de espera ( pwq->wait) con una función de reanudación de proceso proporcionada ep_poll_callback(). Quizás pwq->waitesta sea la parte más importante de toda la implementación epoll, ya que esta entidad se utiliza para resolver las siguientes tareas:



  1. Monitoreo de eventos que ocurren con un archivo específico monitoreado.
  2. Reanudar el trabajo de otros procesos en caso de que surja tal necesidad.


Luego se ep_ptable_queue_proc()adjuntará pwq->waita la cola de espera del archivo de destino ( whead). La función también se agregará struct eppoll_entrya la lista vinculada de struct epitem( epi->pwqlist) e incrementará el valor que epi->nwaitrepresenta la longitud de la lista epi->pwqlist.



Y aquí tengo una pregunta. ¿Por qué epollutilizar una lista vinculada para almacenar una estructura eppoll_entrydentro de una epitemúnica estructura de archivo? ¿No epitemse eppoll_entrynecesita solo un elemento ?



Realmente no puedo responder esta pregunta exactamente. Por lo que puedo decir, a menos que alguien vaya a usar instancias epollen algunos bucles locos, la lista epi->pwqlistsolo contendrá un elemento struct eppoll_entry, yepi->nwaitpara la mayoría de los archivos es probable que lo sea 1.



Lo bueno es que las ambigüedades en torno epi->pwqlistno afectan de ninguna manera lo que voy a comentar a continuación. Es decir, hablaremos sobre cómo Linux notifica las instancias epollde eventos que ocurren en los archivos que se monitorean.



¿Recuerdas lo que hablamos en la sección anterior? Se trataba de lo que se epollagrega wait_queue_ta la lista de espera del archivo de destino (a wait_queue_head_t). Aunque wait_queue_tse usa más comúnmente como un mecanismo para reanudar procesos, esencialmente es solo una estructura que almacena un puntero a una función que se llamará cuando Linux decida reanudar procesos desde la cola wait_queue_tadjunta wait_queue_head_t. En esta funciónepollpuede decidir qué hacer con la señal de reanudación, ¡pero epollno es necesario reanudar ningún proceso! Como verá más adelante, normalmente ep_poll_callback()no sucede nada cuando llama a resume.



Supongo que también vale la pena señalar que el mecanismo de reanudación del proceso utilizado poll()depende completamente de la implementación. En el caso de los archivos de socket TCP, el encabezado de la cola de espera es un miembro sk_wqalmacenado en la estructura sock. Esto también explica la necesidad de utilizar una devolución ep_ptable_queue_proc()de llamada para trabajar con la cola de espera. Dado que en las implementaciones de la cola para diferentes archivos, el encabezado de la cola puede aparecer en lugares completamente diferentes, no tenemos forma de encontrar el valor que necesitamos.wait_queue_head_tsin usar una devolución de llamada.



¿Cuándo exactamente se lleva a cabo la reanudación del trabajo sk_wqen la estructura sock? Resulta que el sistema de sockets Linux sigue los mismos principios de diseño "OO" que VFS. La estructura sockdeclara los siguientes ganchos en la línea 2312 del archivo 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()y sock_def_write_space()la llamada es wake_up_interruptible_sync_poll()para (struct sock)->sk_wqel trabajo de proceso renovable de devolución de llamada de función.



¿Cuándo será sk->sk_data_ready()y será llamado sk->sk_write_space()? Depende de la implementación. Tomemos como ejemplo los sockets TCP. La función sk->sk_data_ready()se llamará en la segunda mitad del manejador de interrupciones cuando la conexión TCP complete el procedimiento de protocolo de enlace de tres vías, o cuando se reciba un búfer para un determinado socket TCP. La función sk->sk_write_space()se llamará cuando el estado del búfer cambie de fulla available. Si tiene esto en cuenta al analizar los siguientes temas, especialmente el de activación frontal, estos temas se verán más interesantes.



Salir



Con esto concluye el segundo artículo de una serie de artículos sobre implementación epoll. La próxima vez, epollhablemos de qué hace exactamente en la devolución de llamada registrada en la cola de reanudación del proceso de socket.



¿Ha utilizado epoll?










All Articles