Toda la verdad sobre linux epoll

Bueno, o casi todo ...



Creo que el problema en Internet moderno es el exceso de información de diferente calidad. Encontrar material sobre un tema de interés no es un problema; el problema es distinguir el material bueno del material malo si tiene poca experiencia en este campo. Observo una imagen cuando hay mucha información general "en la parte superior" (casi al nivel de una enumeración simple), muy pocos artículos detallados y ningún artículo de transición de simple a complejo. Sin embargo, es el conocimiento de las características de un mecanismo particular que nos permite tomar una decisión informada durante el desarrollo.


En el artículo intentaré revelar cuál es la diferencia fundamental entre epoll y otros mecanismos, qué lo hace único, así como citar los artículos que solo necesita leer para comprender mejor las posibilidades y los problemas de epoll .


Cualquiera puede empuñar un hacha, pero se necesita un verdadero guerrero para hacerla cantar melodía.

Supongo que el lector está familiarizado con epoll , al menos lea la página del manual. Se ha escrito lo suficiente sobre epoll , poll , select para que todos los que se desarrollan bajo Linux hayan escuchado al menos una vez.


Mucha fd


Cuando la gente habla sobre epoll, básicamente escucho la tesis de que su "rendimiento es mejor cuando hay muchos descriptores de archivos".


Solo quiero hacer una pregunta: ¿cuánto es cuánto? ¿Cuántas conexiones se necesitan y, lo que es más importante, bajo qué condiciones comenzará Epoll a proporcionar ganancias de rendimiento tangibles?


Para aquellos que estudiaron epoll (hay bastante material, incluidos artículos científicos), la respuesta es obvia: es mejor si y solo si el número de compuestos "esperando un evento" excede significativamente el número de "listos para el procesamiento". La marca de la cantidad, cuando la ganancia se vuelve tan significativa que simplemente no hay orina para ignorar este hecho, se consideran 10k compuestos [4].


La suposición de que la mayoría de las conexiones estarán pendientes proviene de la lógica de sonido y la supervisión de la carga de los servidores que están en uso activo.


Si el número de compuestos activos se esfuerza por el número total, no habrá ganancia no habrá ganancia significativa, se debe a una ganancia significativa solo porque epoll devuelve solo descriptores que requieren atención, y la encuesta devuelve todos los descriptores que se agregaron para observación.


Obviamente, en el último caso, pasamos tiempo recorriendo todos los descriptores + gastos generales de copiar una matriz de eventos del núcleo.


De hecho, en la medición de rendimiento inicial, que se adjuntó al parche [9], este punto no se enfatiza y solo se puede adivinar por la presencia de la utilidad deadcon mencionada en el artículo (desafortunadamente, el código de utilidad pipetest.c se pierde). Por otro lado, en otras fuentes [6, 8] es muy difícil no darse cuenta, porque este hecho prácticamente se destaca.


La pregunta surge de inmediato, pero ¿qué pasa ahora si no se planea dar servicio a tantos descriptores de archivos epoll, por así decirlo , y no es necesario?


A pesar de que Epoll fue creado originalmente específicamente para tales situaciones [5, 8, 9], esto está lejos de ser la única diferencia entre Epoll .


EPOLLET


Para empezar, veremos la diferencia entre los disparadores activados por el borde y los activados por nivel. Hay una muy buena declaración sobre este tema en el artículo Interrupciones activadas por el nivel disparado por el borde contra el nivel - Venkatesh Yadav :


Interrupción en el nivel, es como un niño. Si el bebé está llorando, debe renunciar a todo lo que hizo y correr hacia el bebé para alimentarlo. Luego vuelves a poner al bebé en la cuna. Si vuelve a llorar, no lo dejarás en ningún lado, pero intentarás calmarlo. Y mientras el niño está llorando, no lo dejará por un momento, y volverá a trabajar solo cuando se calme. Pero digamos que salimos al jardín (interrupción apagada) cuando el niño comenzó a llorar, luego, cuando regresó a casa (interrupción encendida), lo primero que debe hacer es ir a ver al niño. Pero nunca sabrás que estaba llorando mientras estabas en el jardín.

La interrupción en el frente es como una niñera electrónica para padres sordos. Tan pronto como el niño comienza a llorar en el dispositivo, se enciende una luz roja y se ilumina hasta que presiona el botón. Incluso si el niño comenzó a llorar, pero se detuvo rápidamente y se durmió, sabrá que estaba llorando. Pero si él comenzó a llorar y usted presionó el botón (confirmación de interrupción), la luz no se encenderá incluso si continúa llorando. El nivel de sonido en la habitación debería bajar y luego volver a subir para que se encienda la luz.

Si el epoll (así como sondeo / selección ) se desbloquea en el comportamiento activado por nivel si el controlador está en el estado especificado y se considerará activo hasta que se borre este estado, entonces el disparo por borde se desbloquea solo cambiando el estado ordenado dado actual.


Esto le permite manejar el evento más tarde, y no inmediatamente después de la recepción (casi una analogía directa con la mitad superior y la mitad inferior del controlador de interrupciones).


Ejemplo específico con epoll:


Nivel disparado


  • asa añadida a epoll con bandera EPOLLIN
  • epoll_wait () bloquea mientras espera un evento
  • escribir en el descriptor de archivo 19 bytes
  • epoll_wait () se desbloquea con el evento EPOLLIN
  • no hacemos nada con los datos que vinieron
  • epoll_wait () se desbloquea nuevamente con el evento EPOLLIN

Y esto continuará hasta que contemos o restablezcamos completamente los datos del descriptor.


Edge activado


  • asa añadida a epoll con banderas EPOLLIN | EPOLLET
  • epoll_wait () bloquea mientras espera un evento
  • escribir en el descriptor de archivo 19 bytes
  • epoll_wait () se desbloquea con el evento EPOLLIN
  • no hacemos nada con los datos que vinieron
  • epoll_wait () está bloqueado esperando un nuevo evento
  • escribe otros 19 bytes en el descriptor de archivo
  • epoll_wait () se desbloquea con el nuevo evento EPOLLIN
  • epoll_wait () está bloqueado esperando un nuevo evento

ejemplo simple: epollet_socket.c


Este mecanismo está diseñado para evitar la devolución de epoll_wait () debido a un evento que ya se está procesando.


Si, en el caso del nivel, al llamar a epoll_wait (), el kernel verifica si fd está en este estado, entonces edge omite esta verificación e inmediatamente pone el proceso de llamada en estado de suspensión.


EPOLLET es lo que hace que epoll O (1) sea un multiplexor para eventos.


Es necesario aclarar sobre EAGAIN y EPOLLET : la recomendación con EAGAIN es no tratar el flujo de bytes, el peligro en este último caso surge solo si no leyó el descriptor hasta el final y no llegaron nuevos datos. Luego, la cola colgará en el descriptor, pero no recibirá una nueva notificación. Con accept (), la situación es simplemente diferente, allí debe continuar hasta que accept () devuelva EAGAIN , solo en este caso se garantiza la operación correcta.


// TCP socket (byte stream) //  fd    EPOLLIN      int len = read(fd, buffer, BUFFER_LEN); if(len < BUFFER_LEN) { //   } else { //         //  -       epoll_wait, //      } 

  // accept //  listenfd    EPOLLIN      event.events = EPOLLIN | EPOLLERR; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event); sleep(5); //       >1  //   while(epoll_wait()) { newfd = accept(listenfd, ...); //      //        //  epoll_wait    listenfd    } //   while(epoll_wait()) { while((newfd = accept(...)) > 0) { //  -  } if(newfd == -1 && errno = EAGAIN) { //       //       } } 

Con esta propiedad, solo el hambre es suficiente:


  • los paquetes llegan al descriptor
  • leer paquetes en el búfer
  • otro paquete viene
  • leer paquetes en el búfer
  • viene una pequeña porción
  • ...

Por lo tanto , no recibiremos EAGAIN pronto, pero es posible que no lo recibamos en absoluto.


Por lo tanto, otros descriptores de archivos no reciben tiempo para el procesamiento, y estamos ocupados leyendo pequeñas porciones de datos que llegan constantemente.


atronador nerd rebaño


Para ir al último indicador, debe comprender por qué se creó realmente y uno de los problemas que surgieron para los desarrolladores con la evolución de la tecnología y el software.


Problema de rebaño atronador


Problema del rebaño de truenos

Imagine una gran cantidad de procesos esperando un evento. Si se produce un evento, se despertará y comenzará la lucha por los recursos, aunque solo se requiere un proceso que se encargará del procesamiento posterior del evento. El resto de los procesos volverá a dormir.

Terminología de TI - Vasily Alekseenko

En este caso, estamos interesados ​​en el problema de accept () y read () distribuidos en streams junto con epoll .


aceptar


En realidad, con una llamada de bloqueo para aceptar (), no ha habido problemas durante mucho tiempo. El núcleo se encargará de que solo se haya desbloqueado un proceso para este evento, y todas las conexiones entrantes se serialicen.


Pero con epoll, tal truco no funcionará. Si hemos escuchado () en un socket sin bloqueo, cuando se establece la conexión, todos epoll_wait () esperarán el evento desde este descriptor.


Por supuesto, accept () solo podrá hacer un hilo, el resto recibirá EAGAIN , pero esto es un desperdicio de recursos.


Además, EPOLLET tampoco nos ayuda, ya que no sabemos exactamente cuántas conexiones hay en la cola de conexiones ( backlog ). Como recordamos, cuando se usa EPOLLET , el procesamiento del socket debe continuar hasta que regrese con el código de error EAGAIN , por lo que existe la posibilidad de que todos los accept () sean procesados ​​por un hilo y el resto no funcione.


Y esto nuevamente nos lleva a una situación en la que la corriente vecina se despertó en vano.


También podemos obtener un tipo diferente de inanición: solo tendremos un subproceso cargado y el resto no recibirá conexiones para el procesamiento.


EPOLLONESHOT


Antes de la versión 4.5, la única forma correcta de procesar el epoll distribuido en un descriptor listen () sin bloqueo con la siguiente llamada accept () era establecer el indicador EPOLLONESHOT , lo que nuevamente nos llevó a aceptar que () solo se procesara en un hilo a la vez.


En resumen: si se usa EPOLLONESHOT, el evento asociado con un descriptor particular se activará solo una vez, después de lo cual es necesario volver a activar las banderas usando epoll_ctl () .


EPOLLEXCLUSIVO


Aquí EPOLLEXCLUSIVE y activado por nivel viene en nuestra ayuda.


EPOLLEXCLUSIVE desbloquea un epoll_wait () pendiente a la vez para un evento.


El esquema es bastante simple (en realidad no):


  • Tenemos N hilos esperando un evento de conexión
  • El primer cliente se conecta con nosotros.
  • El hilo 0 se dispersará y comenzará a procesarse, otros hilos permanecerán bloqueados
  • Un segundo cliente se conecta con nosotros, si el hilo 0 todavía está ocupado con el procesamiento, entonces el hilo 1 está desbloqueado
  • Continuamos más hasta que se agote el grupo de subprocesos (nadie espera un evento en epoll_wait () )
  • Otro cliente se está conectando con nosotros.
  • Y su procesamiento recibirá el primer hilo, que llamará a epoll_wait ()
  • El segundo hilo recibirá el segundo cliente, que llamará a epoll_wait ()

Por lo tanto, todo el mantenimiento se distribuye uniformemente entre los flujos.


 $ ./epollexclusive --help -i, --ip=ADDR specify ip address -p, --port=PORT specify port -n, --threads=NUM specify number of threads to use #    -  n*8 -t, --thunder not adding EPOLLEXCLUSIVE #     thunder herd -h, --help prints this message $ sudo taskset -c 0-7 ./epollexclusive -i 10.56.75.201 -p 40000 -n 8 2>&1 

código de ejemplo: epollexclusive.c (solo funcionará con la versión del kernel de 4.5)


Tenemos un modelo pre-fork en epoll. Este esquema se aplica bien para conexiones TCP de corto tiempo .


leer


Pero con read () en el caso de la transmisión de bytes, EPOLLEXCLUSIVE , como EPOLLET, no nos ayudará.


Por razones obvias, sin EPOLLEXCLUSIVE no podemos usar ningún nivel activado por nivel. Con EPOLLEXCLUSIVE, no todo es mejor, ya que podemos obtener un paquete distribuido en secuencias, además de que llega un orden desconocido de bytes.


Con EPOLLET, la situación es la misma.


Y aquí EPOLLONESHOT con reinicialización al finalizar el trabajo será la salida. Entonces, tan pronto como un hilo funcione con este descriptor de archivo y búfer:


  • asa añadida a epoll con banderas EPOLLONESHOT | EPOLLET
  • esperando en epoll_wait ()
  • leer desde el socket al búfer hasta que read () devuelva EAGAIN
  • reinicializar con banderas EPOLLONESHOT | EPOLLET

struct epoll_event


 typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; 

Este artículo es quizás el único en mi artículo mi IMHO personal. La capacidad de usar un puntero o un número es útil. Por ejemplo, usar un puntero cuando usas epoll te permite hacer un truco como este:


 #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) struct epoll_client { /** some usefull associated data...*/ struct epoll_event event; }; struct epoll_client* to_epoll_client(struct epoll_event* event) { return container_of(event, struct epoll_client, event); } struct epoll_client ec; ... epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ec.e); ... epoll_wait (efd, events, 1, -1); struct epoll_client* ec_ = to_epoll_client(events[0].data.ptr); 

Creo que todos saben de dónde vino esta técnica.


Conclusión


Espero que hayamos podido abrir el tema de epoll . Aquellos que quieran usar este mecanismo conscientemente, solo necesitan leer los artículos en la lista de referencias [1, 2, 3, 5].


Con base en este material (o, mejor aún, leyendo cuidadosamente los materiales de las referencias), puede hacer un servidor pre-fork multi-thread (generación avanzada del proceso) sin bloqueo (sin bloqueo) o revisar las estrategias existentes basadas en las propiedades especiales de epoll () ).


epoll es uno de los mecanismos únicos que las personas que han elegido sus rutas de programación de Linux necesitan saber, ya que brindan una gran ventaja sobre otros sistemas operativos) y, tal vez, abandonarán la plataforma cruzada para este caso particular (déjelo funcionar solo en Linux pero lo hará bien).


Razonamiento sobre la "especificidad" del problema


Antes de que alguien hable sobre la especificidad de estos indicadores y patrones de uso, quiero hacer una pregunta:


"¿Pero no es nada que estemos tratando de discutir la especificidad para el mecanismo que se creó para tareas específicas inicialmente [9, 11]? ¿O incluso damos servicio a conexiones de 1k es una tarea diaria para un programador?"


No entiendo el concepto de "especificidad de tarea", me recuerda todo tipo de gritos sobre la utilidad y la inutilidad de las diversas disciplinas que se enseñan. Permitiéndonos razonar de esta manera, nos arrogamos el derecho de decidir por otros qué información es útil para ellos y cuál es inútil, mientras que, no importa, no participar en el proceso educativo en su conjunto.


Para los escépticos, un par de enlaces:


Aumento del rendimiento con SO_REUSEPORT en NGINX 1.9.1 - VBart
Aprendiendo de Unicornio: la manada de truenos accept () no es un problema - Chris Siebenmann
Serializing accept (), AKA Thundering Herd, AKA the Zeeg Problem - Roberto De Ioris
¿Cómo interactúa el modo EPOLLEXCLUSIVE de epoll con la activación de nivel?


Referencias


  1. Select está fundamentalmente roto - Marek
  2. Epoll está fundamentalmente roto 1/2 - Marek
  3. Epoll está fundamentalmente roto 2/2 - Marek
  4. El problema C10K - Dan Kegel
  5. Encuesta vs Epoll, una vez más - Jacques Mattheij
  6. epoll - Facilidad de notificación de eventos de E / S - The Mann
  7. El método para la locura de Epoll - Cindy Sridharan

Puntos de referencia


  1. https://www.kernel.org/doc/ols/2004/ols2004v1-pages-215-226.pdf
  2. http://lse.sourceforge.net/epoll/index.html
  3. https://mvitolin.wordpress.com/2015/12/05/endurox-testing-epollexclusive-flag/

La evolución de epoll


  1. https://lwn.net/Articles/13918/
  2. https://lwn.net/Articles/520012/
  3. https://lwn.net/Articles/520198/
  4. https://lwn.net/Articles/542629/
  5. https://lwn.net/Articles/633422/
  6. https://lwn.net/Articles/637435/

Postdata


¡Muchas gracias a Sergey ( dlinyj ) y Peter Ovchenkov por sus valiosos debates, comentarios y ayuda!

Source: https://habr.com/ru/post/es416669/


All Articles