¿Qué sucede detrás de las cortinas durante la E/S de disco?

8 minutos de lectura

avatar de usuario
arrozal

Cuando busco alguna posición en un archivo y escribo una pequeña cantidad de datos (20 bytes), ¿qué sucede detrás de escena?

Mi entendimiento

Que yo sepa, la unidad de datos más pequeña que se puede escribir o leer desde un disco es un sector (tradicionalmente 512 bytes, pero ese estándar ahora está cambiando). Eso significa que para escribir 20 bytes necesito leer un sector completo, modificar algo en la memoria y volver a escribirlo en el disco.

Esto es lo que espero que suceda en la E/S sin búfer. También espero que la E/S almacenada en búfer haga más o menos lo mismo, pero tenga cuidado con su caché. Así que habría pensado que si elimino la localidad por la ventana haciendo búsquedas y escrituras aleatorias, tanto la E/S con búfer como la sin búfer deberían tener un rendimiento similar… tal vez con un búfer saliendo un poco mejor.

Por otra parte, sé que es una locura que las E/S almacenadas en búfer solo búferen un sector, por lo que también podría esperar que funcione terriblemente.

Mi aplicación

Estoy almacenando valores recopilados por un controlador de dispositivo SCADA que recibe telemetría remota para más de cien mil puntos. Hay datos adicionales en el archivo, de modo que cada registro tiene 40 bytes, pero solo se deben escribir 20 bytes durante una actualización.

Punto de referencia previo a la implementación

Para comprobar que no necesito idear una solución con un exceso de ingeniería brillante, realicé una prueba con unos pocos millones de registros aleatorios escritos en un archivo que podría contener un total de 200 000 registros. Cada prueba genera el generador de números aleatorios con el mismo valor para ser justos. Primero borro el archivo y lo relleno hasta la longitud total (alrededor de 7,6 megas), luego hago un bucle unos cuantos millones de veces, pasando un desplazamiento de archivo aleatorio y algunos datos a una de las dos funciones de prueba:

void WriteOldSchool( void *context, long offset, Data *data )
{
    int fd = (int)context;
    lseek( fd, offset, SEEK_SET );
    write( fd, (void*)data, sizeof(Data) );
}

void WriteStandard( void *context, long offset, Data *data )
{
    FILE *fp = (FILE*)context;
    fseek( fp, offset, SEEK_SET );
    fwrite( (void*)data, sizeof(Data), 1, fp );
    fflush(fp);
}

¿Quizás sin sorpresas?

los OldSchool método salió en la parte superior – por mucho. Fue más de 6 veces más rápido (1,48 millones frente a 232000 registros por segundo). Para asegurarme de que no me había topado con el almacenamiento en caché de hardware, amplié el tamaño de mi base de datos a 20 millones de registros (tamaño de archivo de 763 megas) y obtuve los mismos resultados.

Antes de señalar la llamada obvia a fflush, déjame decirte que quitarlo no tuvo ningún efecto. Me imagino que esto se debe a que el caché debe comprometerse cuando busco lo suficientemente lejos, que es lo que hago la mayor parte del tiempo.

Entonces, ¿qué está pasando?

Me parece que la E/S almacenada en búfer debe estar leyendo (y posiblemente escribiendo todo) una gran parte del archivo cada vez que intento escribir. Debido a que casi nunca aprovecho su caché, esto es extremadamente derrochador.

Además (y no conozco los detalles del almacenamiento en caché de hardware en el disco), si la E/S almacenada en búfer intenta escribir un montón de sectores cuando cambio solo uno, eso reduciría la efectividad del caché de hardware.

¿Hay algún experto en discos que pueda comentar y explicar esto mejor que mis hallazgos experimentales? =)

  • Supongo que fseek provoca un flujo (por lo tanto, la falta de diferencia) porque el búfer debe vaciarse antes de poder moverse.

    – dave

    1 de noviembre de 2012 a las 5:21

  • La lectura, escritura será del tamaño de bloque definido por el sistema de archivos. En Linux ext4, el valor predeterminado es 4 KB. Del mismo modo, la memoria caché de archivos (archivos almacenados en búfer) funciona en la unidad PAGE_SIZE, de nuevo probablemente 4 KB.

    – Rohan

    1 de noviembre de 2012 a las 5:34

  • Puede deshabilitar el almacenamiento en búfer a nivel de stdio llamando setbuf(fp, NULL); después de abrir fp.

    – café

    1 de noviembre de 2012 a las 6:45

  • Aparte, sizeof(Data) es el tamaño del puntero, no el tamaño de los datos.

    – janneb

    1 de noviembre de 2012 a las 7:36

  • @janneb Vaya, lo siento, en realidad se compiló como un proyecto de C++, pero dado que se trata esencialmente de funciones de C, lo etiqueté como tal. Usé la sintaxis de C++ para el tamaño de.

    – arroz

    1 de noviembre de 2012 a las 8:38

De hecho, al menos en mi sistema con GNU libc, parece que stdio está leyendo bloques de 4 kB antes de volver a escribir la parte modificada. Me parece falso, pero imagino que alguien pensó que era una buena idea en ese momento.

Lo verifiqué escribiendo un programa trivial en C para abrir un archivo, escribir una pequeña cantidad de datos una vez y salir; luego lo ejecutó bajo strace, para ver qué llamadas al sistema activó realmente. Escribiendo en un desplazamiento de 10000, vi estas llamadas al sistema:

lseek(3, 8192, SEEK_SET)                = 8192
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1808) = 1808
write(3, "hello", 5)                    = 5

Parece que querrá quedarse con la E/S estilo Unix de bajo nivel para este proyecto, ¿eh?

  • 4kB es probablemente el tamaño de bloque de su sistema de archivos (creo que es predeterminado en ext4). Podrías cambiar eso si quieres.

    – dave

    1 de noviembre de 2012 a las 5:20

  • @dave, es cierto que el tamaño del bloque del sistema de archivos era de 4kB en esta prueba, y también lo es el tamaño de página de mi memoria, pero no le pedí a libc que leyera nada. En este caso, no hay ninguna razón para hacerlo. usando la materia prima lseek/write syscalls funciona bien sin copias adicionales de la memoria del usuario del núcleo.

    – Jamey Sharp

    1 de noviembre de 2012 a las 5:27

  • Tengo una teoría de por qué está haciendo esa lectura: dado que el tamaño de bloque del fs es 4kB, la única forma de garantizar que otro proceso no haya modificado el archivo es leerlo y luego volver a escribirlo. El uso de write omite esta verificación.

    – dave

    1 de noviembre de 2012 a las 5:33


  • @dave: Huh, esa es una teoría interesante. Sin embargo, no lo entiendo: los ciclos de lectura-modificación-escritura introducen más carreras de datos, no menos. Las llamadas al sistema subyacentes proporcionan un cierto grado de atomicidad que solo el kernel puede ofrecer.

    – Jamey Sharp

    1 de noviembre de 2012 a las 5:37

  • @paddy: Oh, esto es totalmente discutir lo que sucede en el espacio de usuario. El núcleo todavía tiene que leer todo el bloque del disco, modificarlo con sus nuevas escrituras y (algún tiempo después) volver a escribir los cambios. Sin embargo, el kernel puede hacerlo de manera eficiente y segura, mientras que el espacio de usuario generalmente no puede hacerlo.

    – Jamey Sharp

    1 de noviembre de 2012 a las 19:43

avatar de usuario
bdonlan

Las funciones de la biblioteca estándar de C realizan almacenamiento en búfer adicional y, por lo general, están optimizadas para lecturas de transmisión, en lugar de E/S aleatorias. en mi sistema, No observo las lecturas espurias que vio Jamey Sharp Solo veo lecturas falsas cuando el desplazamiento no está alineado con el tamaño de una página; podría ser que la biblioteca C siempre intente mantener su búfer IO alineado a 4kb o algo así.

En su caso, si está haciendo muchas lecturas y escrituras aleatorias en un conjunto de datos razonablemente pequeño, es probable que le sirva mejor usar pread/pwrite para evitar tener que buscar llamadas al sistema, o simplemente mmaping el conjunto de datos y escribirlo en la memoria (probablemente sea el más rápido, si su conjunto de datos cabe en la memoria).

  • Intente buscar un desplazamiento que no sea un múltiplo de 4kB u otros tamaños de bloque probables; es por eso que usé un desplazamiento de 10,000. Sospecho que verás la misma lectura espuria que yo hice.

    – Jamey Sharp

    1 de noviembre de 2012 a las 7:45

  • gracias no sabia pread/pwrite. De hecho, estoy en una plataforma Windows (había etiquetado como VS-2010, pero eso se eliminó; supongo que debería haberlo dicho en mi pregunta). Encontré esto (stackoverflow.com/questions/766477/…) que sugiere que la E/S asíncrona con el CreateFile La API podría ser el camino a seguir. Algo de lo que siempre he rehuido. El rendimiento de lseek + write es más que adecuado para mí… Con la admisión de que hice esto en un sistema con una configuración RAID-0 de 2 unidades.

    – arroz

    1 de noviembre de 2012 a las 9:01

  • @paddy, la E/S asíncrona es uno de los pocos lugares en los que tengo envidia de Windows. (Linux es terrible en eso; prácticamente los únicos casos que funcionan son los que Oracle necesita). Si lseek + write es lo suficientemente rápido para ti, definitivamente lo haría, pero vale la pena considerar AIO si chocas contra una pared.

    – Jamey Sharp

    1 de noviembre de 2012 a las 19:47

  • @JameySharp Tendré un hilo separado cuyo único propósito es procesar una cola de trabajo que contiene todos los mensajes que se escribirán en el disco. En ese sentido, no estoy seguro de qué ventaja proporcionaría la E/S asíncrona. Creo que todavía haré algunas pruebas (pero tal vez después de que termine el trabajo), al menos me obligará a aprender algo nuevo.

    – arroz

    1 de noviembre de 2012 a las 21:13

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad