Estaba viendo esta charla sobre la implementación de Async IO en Rust y Carl menciona dos modelos potenciales. Preparación y finalización.
Modelo de preparación:
- le dices al kernel que quieres leer desde un socket
- hacer otras cosas por un tiempo ...
- el núcleo te dice cuando el zócalo está listo
- lees (llena un búfer)
- haz lo que necesites
- liberar el búfer (ocurre automáticamente con Rust)
Modelo de terminación:
- usted asigna un búfer para que se llene el núcleo
- hacer otras cosas por un tiempo ...
- el núcleo te dice cuándo se ha llenado el búfer
- haz lo que necesites con los datos
- liberar el búfer
En el ejemplo de Carl de usar el modelo de preparación, podría iterar sobre los sockets listos para llenar y liberar un búfer global que hace que parezca que usaría mucha menos memoria.
Ahora mis suposiciones:
Debajo del capó (en el espacio del kernel) cuando se dice que un socket está "listo", los datos ya existen. Ha entrado en el zócalo a través de la red (o desde cualquier lugar) y el sistema operativo está reteniendo los datos.
No es que esa asignación de memoria no ocurra mágicamente en el modelo de preparación. Es solo que el sistema operativo lo está abstrayendo de usted. En el modelo de finalización, el sistema operativo le pide que asigne memoria antes de que los datos realmente fluyan y es obvio lo que está sucediendo.
Aquí está mi versión modificada del Modelo de preparación:
- le dices al kernel que quieres leer desde un socket
- hacer otras cosas por un tiempo ...
- ENMIENDA: los datos llegan al sistema operativo (en algún lugar de la memoria del núcleo)
- el núcleo te dice que el zócalo está listo
- lees (llenas otro búfer separado del búfer del núcleo anterior (¿o obtienes un puntero hacia él?))
- haz lo que necesites
- liberar el búfer (ocurre automáticamente con Rust)
/ Mis suposiciones
Me gusta mantener pequeño el programa de espacio de usuario, pero solo quería aclaraciones sobre lo que, en realidad, está sucediendo aquí. No veo que un modelo inherentemente use menos memoria o admita un mayor nivel de IO concurrente. Me encantaría escuchar pensamientos y una explicación más profunda de esto.
Respuestas:
En el modelo de preparación, el consumo de memoria es proporcional a la cantidad de datos no consumidos por la aplicación.
En el modelo de finalización, el consumo de memoria es proporcional a la cantidad de llamadas de socket pendientes.
Si hay muchos sockets que en su mayoría están inactivos, entonces el Modelo de preparación consume menos memoria.
Hay una solución fácil para el Modelo de finalización: Inicie una lectura de 1 byte. Esto consume solo un pequeño búfer. Cuando la lectura se completa, emite otra lectura (tal vez sincrónica) que obtiene el resto de los datos.
En algunos idiomas, el Modelo de finalización es extremadamente sencillo de implementar. Considero que es una buena opción por defecto.
fuente
Pero, ¿qué sucede si ingresan más datos de los que asignó espacio? El núcleo todavía tiene que asignar su propio búfer para no descartar los datos. (Por ejemplo, esta es la razón por la cual funciona el truco de lectura de 1 byte mencionado en la respuesta de usr).
La desventaja es que si bien el Modelo de finalización consume más memoria, también puede (a veces) realizar menos operaciones de copia, ya que mantener el búfer alrededor significa que el hardware puede DMA directamente dentro o fuera de él. También sospecho (pero estoy menos seguro) que el Modelo de finalización tiende a realizar la operación de copia real (cuando existe) en otro hilo, al menos para el IOCP de Windows, mientras que el Modelo de preparación lo hace como parte del bloqueo
read()
owrite()
llamada.fuente