Atomicidad de `write (2)` a un sistema de archivos local

Aparentemente POSIX dice que

Ya sea un descriptor de archivo o una secuencia se llama “manejar” en la descripción de archivo abierto a la que se refiere; una descripción de archivo abierto puede tener varios identificadores. […] Toda la actividad de la aplicación que afecte al desplazamiento del archivo en el primer identificador se suspenderá hasta que vuelva a ser el manejador del archivo activo. […] Las manijas no necesitan estar en el mismo proceso para que se apliquen estas reglas. – POSIX.1-2008

y

Si dos hilos cada llamada [la función write ()], cada llamada verá todos los efectos especificados de la otra llamada, o ninguno de ellos. – POSIX.1-2008

Mi comprensión de esto es que cuando el primer proceso emite un write(handle, data1, size1) y el segundo proceso write(handle, data2, size2) , las escrituras pueden ocurrir en cualquier orden pero los data1 y data2 deben ser prístinos y contiguo

Pero ejecutar el siguiente código me da resultados inesperados.

 #include  #include  #include  #include  #include  #include  #include  die(char *s) { perror(s); abort(); } main() { unsigned char buffer[3]; char *filename = "/tmp/atomic-write.log"; int fd, i, j; pid_t pid; unlink(filename); /* XXX Adding O_APPEND to the flags cures it. Why? */ fd = open(filename, O_CREAT|O_WRONLY/*|O_APPEND*/, 0644); if (fd < 0) die("open failed"); for (i = 0; i < 10; i++) { pid = fork(); if (pid < 0) die("fork failed"); else if (! pid) { j = 3 + i % (sizeof(buffer) - 2); memset(buffer, i % 26 + 'A', sizeof(buffer)); buffer[0] = '-'; buffer[j - 1] = '\n'; for (i = 0; i < 1000; i++) if (write(fd, buffer, j) != j) die("write failed"); exit(0); } } while (wait(NULL) != -1) /* NOOP */; exit(0); } 

Intenté ejecutar esto en Linux y Mac OS X 10.7.4 y al usar grep -a '^[^-]\|^..*-' /tmp/atomic-write.log muestra que algunas escrituras no son contiguas o se superponen ( Linux) o simplemente corrupto (Mac OS X).

Agregar el indicador O_APPEND en la llamada open(2) corrige este problema. Bien, pero no entiendo por qué. POSIX dice

O_APPEND Si se establece, el desplazamiento del archivo se establecerá al final del archivo antes de cada escritura.

pero este no es el problema aquí. Mi progtwig de ejemplo nunca hace lseek(2) pero comparte la misma descripción de archivo y, por lo tanto, el mismo desplazamiento de archivo.

Ya leí preguntas similares sobre Stackoverflow pero todavía no responden completamente a mi pregunta.

Atomic write on file from two process no aborda específicamente el caso en el que los procesos comparten la misma descripción de archivo (a diferencia del mismo archivo).

¿Cómo se determina programáticamente si la llamada al sistema “write” es atómica en un archivo en particular? dice que

La llamada de write , tal como se define en POSIX, no tiene ninguna garantía de atomicidad.

Pero como se citó anteriormente , sí tiene algo. Y lo que es más, O_APPEND parece desencadenar esta garantía de atomicidad, aunque me parece que esta garantía debería estar presente incluso sin O_APPEND .

¿Puedes explicar más este comportamiento?

    man 2 write en mi sistema lo resume muy bien:

    Tenga en cuenta que no todos los sistemas de archivos cumplen con POSIX.

    Aquí hay una cita de una discusión reciente en la lista de correo ext4 :

    Actualmente, las lecturas y escrituras concurrentes son atómicas solo en páginas individuales, sin embargo, no están en la llamada al sistema. Esto puede hacer que read() devuelva datos mezclados de varias escrituras diferentes, lo que no creo que sea un buen enfoque. Podríamos argumentar que la aplicación que hace esto está rota, pero en realidad esto es algo que podemos hacer fácilmente en el nivel del sistema de archivos sin problemas significativos de rendimiento, por lo que podemos ser coherentes. También POSIX menciona esto también y el sistema de archivos XFS ya tiene esta característica.

    Esta es una clara indicación de que ext4 – por nombrar solo un sistema de archivos moderno – no se ajusta a POSIX.1-2008 a este respecto.

    Alguna interpretación errónea de lo que los mandatos estándar aquí provienen del uso de procesos vs. hilos, y lo que eso significa para la situación de “manejo” de la que está hablando. En particular, te perdiste esta parte:

    Los identificadores se pueden crear o destruir mediante acciones explícitas del usuario, sin afectar la descripción subyacente del archivo abierto. Algunas de las formas de crearlos incluyen fcntl (), dup (), fdopen (), fileno () y fork() . Pueden ser destruidos por al menos fclose (), close () y las funciones de exec. […] Tenga en cuenta que después de una bifurcación (), existen dos controles donde uno existía antes.

    de la sección de especificaciones POSIX que cita más arriba. La referencia a “crear [manipula usando” fork “] no se detalla más en esta sección, pero la especificación para fork() agrega un pequeño detalle:

    El proceso secundario tendrá su propia copia de los descriptores de archivos del padre. Cada uno de los descriptores de archivos del niño se referirá a la misma descripción de archivo abierto con el descriptor de archivo correspondiente del padre.

    Los bits relevantes aquí son:

    • el niño tiene copias de los descriptores de archivos del padre
    • las copias del niño se refieren a la misma “cosa” a la que el padre puede acceder a través de dicho fds
    • los archivos descriptivos y los archivos descriptivos no son lo mismo; en particular, un descriptor de archivo es un identificador en el sentido anterior.

    Esto es a lo que se refiere la primera cita cuando dice ” fork() crea […] identificadores”: se crean como copias y, por lo tanto, a partir de ese punto, se separan y ya no se actualizan en lockings.

    En su progtwig de ejemplo, cada proceso hijo obtiene su propia copia que comienza en el mismo estado, pero después del acto de copiar, estos descriptores de archivo / controladores se han convertido en instancias independientes , y por lo tanto las escrituras compiten entre sí. Esto es perfectamente aceptable con respecto al estándar, porque write() solo garantiza:

    En un archivo regular u otro archivo capaz de buscar, la escritura real de datos procederá desde la posición en el archivo indicado por el desplazamiento del archivo asociado con los filtros. Antes del retorno exitoso desde write (), el desplazamiento del archivo se incrementará en la cantidad de bytes realmente escritos.

    Esto significa que mientras todos comienzan la escritura con el mismo desplazamiento (porque la copia fd se inicializó como tal) podrían, incluso si son exitosos, escribir cantidades diferentes (la norma no garantiza que una solicitud de escritura de N bytes escriba exactamente N bytes; puede tener éxito para cualquier cosa 0 < = real < = N ), y debido a que el ordenamiento de las escrituras no está especificado, todo el progtwig de ejemplo anterior tiene resultados no especificados. Incluso si se escribe la cantidad total solicitada, todo el estándar anterior dice que el desplazamiento del archivo se incrementa ; no dice que se incrementó atómicamente (una sola vez), ni dice que la escritura real de datos ocurrirá de forma atómica.

    Sin embargo, una cosa está garantizada: nunca debería ver nada en el archivo que no haya estado allí antes de ninguna de las escrituras o que no haya provenido de ninguno de los datos escritos por ninguna de las escrituras. Si lo hace, eso sería corrupción y un error en la implementación del sistema de archivos. Lo que ha observado anteriormente podría ser que ... si los resultados finales no pueden explicarse reordenando partes de las escrituras.

    El uso de O_APPEND corrige esto, porque al usar eso, de nuevo - see write() , hace:

    Si se establece el indicador O_APPEND de los indicadores de estado del archivo, el desplazamiento del archivo se establecerá al final del archivo antes de cada escritura y no se producirá ninguna operación de modificación del archivo intermedio entre el cambio del desplazamiento del archivo y la operación de escritura.

    que es el comportamiento de serialización "anterior a" / "no intervenir" que busca.

    El uso de subprocesos cambiaría parcialmente el comportamiento, porque los subprocesos, al crearlos, no reciben copias de los descriptores / identificadores de archivo, sino que operan en el real (compartido). Los hilos no (necesariamente) comenzarán a escribir con el mismo desplazamiento. Pero la opción de éxito de escritura parcial todavía significa que es posible que vea el intercalado de formas que quizás no desee ver. Sin embargo, posiblemente todavía sea totalmente conforme a los estándares.

    Moraleja : no cuente con que un estándar POSIX / UNIX sea restrictivo de forma predeterminada . Las especificaciones se relajan deliberadamente en el caso común y requieren que el progtwigdor sea ​​explícito sobre su intención.

    Editar: actualizado en agosto de 2017 con los últimos cambios en los comportamientos del sistema operativo.

    En primer lugar, O_APPEND o el equivalente FILE_APPEND_DATA en Windows significa que los incrementos de la extensión máxima del archivo (“longitud” del archivo) son atómicos en los escritores simultáneos. Esto está garantizado por POSIX, y Linux, FreeBSD, OS X y Windows lo implementan correctamente. Samba también lo implementa correctamente, NFS antes de v5 no, ya que carece de la capacidad de formato de cable para anexarse ​​atómicamente. Por lo tanto, si abre su archivo con solo anexar, las escrituras concurrentes no se romperán entre sí en ningún sistema operativo importante a menos que se trate de NFS.

    Sin embargo, esto no dice nada sobre si las lecturas verán alguna vez una escritura fragmentada, y en eso POSIX dice lo siguiente acerca de la atomicidad de read () y write () en los archivos normales:

    Todas las siguientes funciones serán atómicas entre sí en los efectos especificados en POSIX.1-2008 cuando operan en archivos regulares o enlaces simbólicos … [muchas funciones] … leer () … escribir ( ) … Si cada uno de los dos hilos llama a una de estas funciones, cada llamada verá todos los efectos especificados de la otra llamada o ninguna de ellas. [Fuente]

    y

    Las escrituras se pueden serializar con respecto a otras lecturas y escrituras. Si puede demostrarse (por algún medio) que una lectura () de datos de archivo ocurre después de una escritura () de los datos, debe reflejar dicha escritura (), incluso si las llamadas se realizan mediante procesos diferentes. [Fuente]

    pero a la inversa

    Este volumen de POSIX.1-2008 no especifica el comportamiento de las escrituras simultáneas en un archivo de múltiples procesos. Las aplicaciones deben usar alguna forma de control de concurrencia. [Fuente]

    Una interpretación segura de estos tres requisitos sugeriría que todas las escrituras superpuestas en una extensión en el mismo archivo deben ser serializadas una con respecto a la otra y que las escrituras desgarradas nunca aparezcan para los lectores.

    Una interpretación menos segura, pero aún permitida, podría ser que las lecturas y escrituras solo se serializan entre hilos dentro del mismo proceso, y entre procesos las escrituras se serializan con respecto a las lecturas solamente (es decir, hay orden de E / S secuencialmente consistente entre hilos en un proceso, pero entre procesos i / o es solo adquirir-liberar).

    Entonces, ¿cómo funcionan el sistema operativo y los sistemas de archivos populares en esto? Como autor de Boost.AFIO propuesto, un sistema de archivos asíncrono y una biblioteca de archivos i / o C ++, decidí escribir un probador empírico. Los resultados son los siguientes para muchos hilos en un solo proceso.


    No O_DIRECT / FILE_FLAG_NO_BUFFERING:

    Microsoft Windows 10 con NTFS: actualizar atomicity = 1 byte hasta e incluyendo 10.0.10240, desde 10.0.14393 al menos 1Mb, probablemente infinito según la especificación POSIX.

    Linux 4.2.6 con ext4: actualización atomicidad = 1 byte

    FreeBSD 10.2 con ZFS: actualizar atomicidad = al menos 1Mb, probablemente infinito según la especificación POSIX.

    O_DIRECT / FILE_FLAG_NO_BUFFERING:

    Microsoft Windows 10 con NTFS: actualizar atomicity = hasta e incluyendo 10.0.10240 hasta 4096 bytes solo si la página está alineada, de lo contrario 512 bytes si FILE_FLAG_WRITE_THROUGH está desactivado, sino 64 bytes. Tenga en cuenta que esta atomicidad es probablemente una característica de PCIe DMA en lugar de diseñada en. Desde 10.0.14393, al menos 1Mb, probablemente infinito según la especificación POSIX.

    Linux 4.2.6 con ext4: atomicidad de actualización = al menos 1Mb, probablemente infinito según la especificación POSIX. Tenga en cuenta que Linuxes anteriores con ext4 definitivamente no excedieron 4096 bytes, XFS ciertamente solía tener un locking personalizado, pero parece que Linux reciente finalmente ha resuelto este problema en ext4.

    FreeBSD 10.2 con ZFS: actualizar atomicidad = al menos 1Mb, probablemente infinito según la especificación POSIX.


    En resumen, FreeBSD con ZFS y Windows muy reciente con NTFS cumplen con POSIX. Un Linux muy reciente con ext4 es POSIX que se conforma solo con O_DIRECT.

    Puede ver los resultados de la prueba empírica en bruto en https://github.com/ned14/afio/tree/master/programs/fs-probe . Tenga en cuenta que probamos las desviaciones rotas solo en múltiplos de 512 bytes, por lo que no puedo decir si una actualización parcial de un sector de 512 bytes se desgarraría durante el ciclo de lectura, modificación y escritura.

    Está malinterpretando la primera parte de la especificación que citó:

    Ya sea un descriptor de archivo o una secuencia se llama “manejar” en la descripción de archivo abierto a la que se refiere; una descripción de archivo abierto puede tener varios identificadores. […] Toda la actividad de la aplicación que afecte al desplazamiento del archivo en el primer identificador se suspenderá hasta que vuelva a ser el manejador del archivo activo. […] Las manijas no necesitan estar en el mismo proceso para que se apliquen estas reglas.

    Esto no impone ningún requisito sobre la implementación para manejar el acceso concurrente. En cambio, impone requisitos a una aplicación para no hacer acceso simultáneo, incluso desde diferentes procesos, si desea un ordenamiento bien definido de la salida y los efectos secundarios.

    La única vez que se garantiza la atomicidad es para las tuberías cuando el tamaño de escritura se ajusta en PIPE_BUF .

    Por cierto, incluso si la llamada a write fuera atómica para archivos ordinarios, excepto en el caso de escrituras en tuberías que caben en PIPE_BUF , la write siempre puede regresar con una escritura parcial (es decir, haber escrito menos del número solicitado de bytes). Esta escritura más pequeña que la solicitada sería atómica, pero no ayudaría en absoluto a la situación con respecto a la atomicidad de toda la operación (su aplicación tendría que volver a llamar la write para finalizar).