Al diseñar aplicaciones de red de alto rendimiento con sockets sin bloqueo, es importante decidir qué método de monitoreo de eventos de red usaremos. Hay varios de ellos, y cada uno es bueno y malo a su manera. Elegir el método correcto puede ser crítico para la arquitectura de su aplicación.
En este artículo consideraremos:
- seleccione ()
- encuesta ()
- epoll ()
- liberador
Usando select ()
El viejo, probado a través de los años, trabajador duro select () fue creado en aquellos días cuando los "enchufes" se llamaban "
enchufes Berkeley ". Este método no se incluyó en la primera especificación de esos zócalos Berkeley, ya que en aquellos días todavía no existía el concepto de E / S sin bloqueo. Pero en algún momento de los años 80 apareció, y con ella seleccionó (). Desde entonces, nada ha cambiado significativamente en su interfaz.
Para usar select (), el desarrollador necesita inicializar y llenar varias estructuras fd_set con descriptores y eventos que necesitan ser monitoreados, y luego llamar a select (). Un código típico se parece a esto:
fd_set fd_in, fd_out; struct timeval tv;
Cuando se diseñó select (), nadie esperaba que en el futuro necesitáramos escribir aplicaciones de subprocesos múltiples que sirvan a miles de conexiones. Select () tiene varios inconvenientes importantes que lo hacen poco adecuado para trabajar en dichos sistemas. Los principales son:
- select modifica las estructuras fd_sets que se le pasan, de modo que ninguna de ellas pueda reutilizarse. Incluso si no necesita cambiar nada (por ejemplo, después de recibir un dato, desea obtener más), las estructuras de fd_sets tendrán que reinicializarse. Bueno, o copie desde una copia de seguridad previamente guardada usando FD_COPY. Y esto tendrá que hacerse una y otra vez, antes de cada llamada de selección.
- Para saber exactamente qué descriptor generó el evento, debe sondearlos manualmente con FD_ISSET. Cuando monitorea 2000 descriptores, y el evento ocurrió solo para uno de ellos (que, de acuerdo con la ley de la mezquindad, será el último en la lista), desperdiciará muchos recursos del procesador.
- ¿Acabo de mencionar 2000 descriptores? Me emocioné al respecto. select no es tan compatible. Bueno, al menos en Linux normal, con el núcleo habitual. El número máximo de descriptores observados simultáneamente está limitado por la constante FD_SETSIZE, que es rígidamente igual a 1024 en Linux. Algunos sistemas operativos le permiten implementar un hack al anular el valor FD_SETSIZE antes de incluir el archivo de encabezado sys / select.h, pero este hack no es parte de algún estándar común. El mismo Linux lo ignorará.
- No puede trabajar con descriptores de un conjunto observable de otro hilo. Imagine un hilo ejecutando el código anterior. Entonces comenzó y espera eventos en su select (). Ahora imagine que tiene otro subproceso que supervisa la carga general en el sistema, y ahora decidió que los datos del zócalo sock1 no han llegado durante demasiado tiempo y que era hora de romper la conexión. Dado que este socket se puede reutilizar para servir a nuevos clientes, sería bueno cerrarlo correctamente. Pero el primer hilo es observar este descriptor en este momento. ¿Qué pasará si lo cerramos de todos modos? Ah, la documentación tiene una respuesta a esta pregunta y no le gustará: "Si el identificador observado con select () está cerrado por otro hilo, obtendrá un comportamiento indefinido".
- El mismo problema aparece cuando intenta enviar algunos datos a través de sock1. No enviaremos nada hasta que select finalice su trabajo.
- La elección de eventos que podemos monitorear es bastante limitada. Por ejemplo, para determinar que se ha cerrado un socket remoto, primero debe monitorear los eventos de llegada de datos y, en segundo lugar, intentar leer estos datos (leer devolverá 0 para el socket cerrado). Todavía se puede llamar aceptable cuando se leen datos de un socket (leer 0: el socket está cerrado), pero ¿qué pasa si nuestra tarea actual en este momento es enviar datos a este socket y no es necesario leer datos de él ahora?
- select le impone una carga innecesaria para calcular el "descriptor más grande" y pasarlo como un parámetro separado
Por supuesto, todo lo anterior no es ninguna noticia. Los desarrolladores de sistemas operativos han sido conscientes de estos problemas y muchos de ellos se tuvieron en cuenta al diseñar el método de encuesta. En este punto, usted puede preguntar, ¿por qué estamos estudiando historia antigua ahora, y hay alguna razón hoy para usar el antiguo? Sí, hay dos de esos motivos. No es el hecho de que te sean útiles alguna vez, sino por qué no descubrirlos.
La primera razón es la portabilidad. select () ha estado con nosotros durante un millón de años. No importa lo que le traiga la jungla de las plataformas de hardware y software, si hay una red allí, habrá una selección. Puede que no haya otros métodos, pero select estará casi garantizado. Y no piense que ahora estoy cayendo en la senilidad senil y recuerdo algo como tarjetas perforadas y ENIAC, no. No hay un método de encuesta más moderno
, por ejemplo, en Windows XP . Pero seleccionar es.
La segunda razón es más exótica y está relacionada con el hecho de que select puede (en teoría) trabajar con tiempos de espera del orden de un nanosegundo (si el hardware lo permite), mientras que sondeo y epoll solo admiten una precisión de milisegundos. Esto no debería desempeñar un papel especial en los escritorios comunes (o incluso en los servidores), donde todavía no tiene un temporizador de precisión de nanosegundos de hardware. Pero aún en el mundo hay sistemas en tiempo real que tienen tales temporizadores. Así que te ruego, cuando escribas el firmware de un reactor nuclear o cohete, no seas demasiado vago para medir el tiempo en nanosegundos. Sabes, quiero vivir.
El caso descrito anteriormente es probablemente el único en el que realmente no tiene elección qué usar (solo seleccionar es adecuado). Sin embargo, si está escribiendo una aplicación regular para trabajar en hardware ordinario, y operará con un número adecuado de sockets (decenas, cientos, y no más), entonces la diferencia en el rendimiento de la encuesta y la selección no será notable, por lo que la elección se basará en otros factores.
Encuesta con encuesta ()
poll es un método más nuevo de sondeo de sockets, creado después de que las personas comenzaron a intentar escribir servicios de red grandes y muy cargados. Está diseñado mucho mejor y no sufre la mayoría de los inconvenientes del método seleccionado. En la mayoría de los casos, al escribir aplicaciones modernas, elegirás entre usar poll y epoll / libevent.
Para usar poll, un desarrollador necesita inicializar miembros de la estructura pollfd con descriptores y eventos observables, y luego llamar a poll ().
Un código típico se ve así:
La encuesta se creó para resolver los problemas del método select, veamos cómo resultó:
- No hay límite para el número de descriptores observados; se pueden monitorear más de 1024
- La estructura pollfd no se modifica, lo que hace posible reutilizarla entre llamadas a poll (); solo necesita restablecer el campo revents.
- Los eventos observados están mejor estructurados. Por ejemplo, puede determinar si un cliente remoto está desconectado sin tener que leer los datos del socket.
Ya hablamos sobre las deficiencias del método de encuesta: no está disponible en algunas plataformas, como Windows XP. Desde Vista, existe, pero se llama WSAPoll. El prototipo es el mismo, por lo que para el código independiente de la plataforma puede escribir una anulación, como:
#if defined (WIN32) static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); } #endif
Bueno, la precisión de los tiempos de espera es de 1 ms, lo que rara vez será suficiente. Sin embargo, la encuesta tiene otras desventajas:
- Al igual que con el uso de select, es imposible determinar qué descriptores generaron los eventos sin un pase completo a través de todas las estructuras observadas y verificar los campos revents en ellos. Peor aún, también se implementa en el núcleo del sistema operativo.
- Al igual que con select, no hay forma de cambiar dinámicamente el conjunto de eventos observados
Sin embargo, todo lo anterior puede considerarse relativamente insignificante para la mayoría de las aplicaciones cliente. La excepción es probablemente solo protocolos p2p, donde cada uno de los clientes puede asociarse con miles de otros. Estos problemas pueden ser ignorados incluso por la mayoría de las aplicaciones de servidor. Por lo tanto, la encuesta debe ser su preferencia predeterminada sobre la selección, a menos que una de las dos razones anteriores lo limite.
Mirando hacia el futuro, diré que la encuesta es preferible incluso en comparación con el epoll más moderno (discutido a continuación) en los siguientes casos:
- Desea escribir código multiplataforma (epoll solo está en Linux)
- No necesita monitorear más de 1000 sockets (epoll no le dará nada significativo en este caso)
- Debe supervisar más de 1000 sockets, pero el tiempo de conexión con cada uno de ellos es muy pequeño (en estos casos, el rendimiento de sondeo y epoll estará muy cerca; la ganancia de esperar menos eventos en epoll se tachará por la sobrecarga de agregarlos / eliminarlos)
- Su aplicación no está diseñada para cambiar eventos de un hilo mientras otro los está esperando (o no lo necesita)
Encuesta con epoll ()
epoll es el mejor y más nuevo método para esperar eventos en Linux (y solo en Linux). Bueno, no es que el "más nuevo" sea directo: ha estado en el núcleo desde 2002. Se diferencia de la encuesta y la selección en que proporciona una API para agregar / eliminar / modificar la lista de descriptores y eventos observados.
Usar epoll requiere preparaciones un poco más exhaustivas. El desarrollador debe:
- Cree un descriptor de epoll llamando a epoll_create
- Inicialice la estructura epoll_event con los eventos y punteros necesarios para los contextos de conexión. El "contexto" aquí puede ser cualquier cosa, epoll simplemente pasa ese valor en los eventos devueltos
- Llame a epoll_ctl (... EPOLL_CTL_ADD) para agregar un identificador a la lista de observables
- Llame a epoll_wait () para esperar eventos (indicamos exactamente cuántos eventos queremos recibir a la vez, por ejemplo, 20). A diferencia de los métodos anteriores, obtenemos estos eventos por separado y no en las propiedades de las estructuras de entrada. Si observamos 200 descriptores y 5 de ellos recibieron nuevos datos, epoll_wait devolverá solo 5 eventos. Si ocurren 50 eventos, los primeros 20 nos serán devueltos, y los 30 restantes esperarán la próxima llamada, no se perderán
- Proceso de eventos recibidos. Este será un procesamiento relativamente rápido, porque no miramos esos descriptores donde no pasó nada
Un código típico se ve así:
Comencemos con los defectos de epoll: son obvios por el código. Este método es más difícil de usar, necesita escribir más código, hace más llamadas al sistema.
Las ventajas también son evidentes:
- epoll devuelve una lista de solo aquellos descriptores para los cuales ocurrieron los eventos observados. No necesita mirar a través de miles de estructuras en busca de una, posiblemente aquella en la que funcionó el evento esperado.
- Puede asociar un contexto significativo con cada evento observado. En el ejemplo anterior, usamos un puntero a un objeto de la clase de conexión para esto; esto nos ahorró otra posible búsqueda de una matriz de conexiones.
- Puede agregar o quitar sockets de la lista en cualquier momento. Incluso puede modificar los eventos observados. Todo funcionará correctamente, esto está oficialmente respaldado y documentado.
- Puede iniciar varios subprocesos esperando eventos de la misma cola usando epoll_wait. Algo que de ninguna manera se puede hacer con select / poll.
Pero también debe recordar que epoll no es "una encuesta mejorada". Tiene desventajas en comparación con la encuesta:
- Cambiar los indicadores de evento (por ejemplo, cambiar de LEER a ESCRIBIR) requiere una llamada adicional al sistema epoll_ctl, mientras que para la encuesta simplemente cambia la máscara de bits (completamente en modo de usuario). Cambiar 5,000 sockets de lectura a escritura requerirá 5,000 llamadas al sistema y cambios de contexto para epoll, mientras que para sondeo será una operación trivial en un bucle.
- Para cada nueva conexión, debe llamar a accept () y epoll_ctl () son dos llamadas al sistema. Si usa la encuesta, solo habrá una llamada. Con una vida útil de conexión muy corta, esto puede marcar la diferencia.
- epoll solo está disponible en Linux. Otros sistemas operativos tienen mecanismos similares, pero aún no son completamente idénticos. No podrá escribir código con epoll para que se construya y funcione, por ejemplo, en FreeBSD.
- Escribir código paralelo altamente cargado es difícil. Muchas aplicaciones no necesitan un enfoque tan fundamental, ya que su nivel de carga se procesa fácilmente utilizando métodos más simples.
Por lo tanto, epoll solo debe usarse cuando se cumple todo lo siguiente:
- Su aplicación utiliza un grupo de subprocesos para manejar las conexiones de red. La ganancia de epoll en una aplicación de un solo subproceso será insignificante, y no debe molestarse con la implementación.
- Espera un número relativamente grande de conexiones (de 1000 y superiores). En un pequeño número de sockets observados, epoll no dará un aumento de rendimiento, y si hay literalmente unos pocos sockets, incluso puede disminuir la velocidad.
- Tus conexiones viven relativamente tiempo. En una situación en la que una nueva conexión transfiere solo unos pocos bytes de datos y se cierra allí mismo, la encuesta funcionará más rápido, ya que necesitará hacer menos llamadas al sistema para procesarla.
- Tiene la intención de ejecutar su código en Linux y solo en Linux.
Si uno o más de los ítems fallan, considere usar sondeo o liberador.
liberador
libevent es una biblioteca que envuelve los métodos de sondeo enumerados en este artículo (así como algunos otros) en una API unificada. La ventaja aquí es que una vez que ha escrito el código, puede compilarlo y ejecutarlo en diferentes sistemas operativos. Sin embargo, es importante comprender que libevent es solo un envoltorio, dentro del cual funcionan todos los métodos anteriores, con todas sus ventajas y desventajas. libevent no forzará a select para escuchar más de 1024 sockets, y epoll no modificará la lista de eventos sin una llamada adicional al sistema. Por lo tanto, conocer las tecnologías subyacentes sigue siendo importante.
La necesidad de soportar diferentes métodos de sondeo hace que la API de la biblioteca libevent sea más compleja. Pero aún así, su uso es más fácil que escribir manualmente dos motores de selección de eventos diferentes para, por ejemplo, Linux y FreeBSD (usando epoll y kqueue).
Considere usar libevent cuando combine dos eventos:
- Miraste los métodos de selección y encuesta y definitivamente no funcionaron para ti.
- Necesitas soportar múltiples SO