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 epoll
obtiene 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 errno
establecido 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_table
es 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_proc
representa 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
_key
mesa en poll_table
realidad no es lo que parece ser. Es decir, a pesar de que el nombre sugiere una determinada "clave", de _key
hecho, se almacenan las máscaras de los eventos que nos interesan. En la implementación, se epoll
_key
establece en ~0
(complemento a 0). Esto significa que epoll
busca 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_proc
estructura original epitem
, epoll
utiliza una estructura simple llamadaep_pqueue
que sirve como contenedor poll_table
con 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 epi
estructura un ep_pqueue
puntero a una estructura epitem
correspondiente al archivo que estamos intentando agregar, y luego escribe ep_ptable_queue_proc()
en un miembro de _qproc
estructura ep_pqueue
y _key
escribe 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
epoll
entidad, _qproc
será una función ep_ptable_queue_proc()
declarada en el archivo fs/eventpoll.c
de 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 epitem
que corresponde al archivo de la cola de espera con la que estamos trabajando. Dado que epoll
utiliza una estructura contenedora ep_pqueue
, restaurar epitem
desde un puntero poll_table
es 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 epitem
para ese archivo. Es epoll
extremadamente importante saber dónde está el encabezado de la cola de espera para el archivo que se está viendo. De epoll
lo contrario, no podrá cancelar el registro de la cola de espera más tarde. Estructuraeppoll_entry
también incluye una cola de espera ( pwq->wait
) con una función de reanudación de proceso proporcionada ep_poll_callback()
. Quizás pwq->wait
esta sea la parte más importante de toda la implementación epoll
, ya que esta entidad se utiliza para resolver las siguientes tareas:
- Monitoreo de eventos que ocurren con un archivo específico monitoreado.
- Reanudar el trabajo de otros procesos en caso de que surja tal necesidad.
Luego se
ep_ptable_queue_proc()
adjuntará pwq->wait
a la cola de espera del archivo de destino ( whead
). La función también se agregará struct eppoll_entry
a la lista vinculada de struct epitem
( epi->pwqlist
) e incrementará el valor que epi->nwait
representa la longitud de la lista epi->pwqlist
.
Y aquí tengo una pregunta. ¿Por qué
epoll
utilizar una lista vinculada para almacenar una estructura eppoll_entry
dentro de una epitem
única estructura de archivo? ¿No epitem
se eppoll_entry
necesita solo un elemento ?
Realmente no puedo responder esta pregunta exactamente. Por lo que puedo decir, a menos que alguien vaya a usar instancias
epoll
en algunos bucles locos, la lista epi->pwqlist
solo contendrá un elemento struct eppoll_entry
, yepi->nwait
para la mayoría de los archivos es probable que lo sea 1
.
Lo bueno es que las ambigüedades en torno
epi->pwqlist
no afectan de ninguna manera lo que voy a comentar a continuación. Es decir, hablaremos sobre cómo Linux notifica las instancias epoll
de eventos que ocurren en los archivos que se monitorean.
¿Recuerdas lo que hablamos en la sección anterior? Se trataba de lo que se
epoll
agrega wait_queue_t
a la lista de espera del archivo de destino (a wait_queue_head_t
). Aunque wait_queue_t
se 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_t
adjunta wait_queue_head_t
. En esta funciónepoll
puede decidir qué hacer con la señal de reanudación, ¡pero epoll
no 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_wq
almacenado 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_t
sin usar una devolución de llamada.
¿Cuándo exactamente se lleva a cabo la reanudación del trabajo
sk_wq
en la estructura sock
? Resulta que el sistema de sockets Linux sigue los mismos principios de diseño "OO" que VFS. La estructura sock
declara 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_wq
el 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 full
a 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, epoll
hablemos de qué hace exactamente en la devolución de llamada registrada en la cola de reanudación del proceso de socket.
¿Ha utilizado epoll?