threads thread programming program multithreaded multi library example concurrent c file-io posix multiprocessing

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



thread library c (4)

Alguna interpretación errónea de lo que los mandatos estándar aquí vienen del uso de procesos frente a subprocesos, y lo que eso significa para la situación de "manejo" de la que está hablando. En particular, te perdiste esta parte:

Los manejadores pueden crearse o destruirse mediante una acción explícita del usuario, sin afectar la descripción del archivo abierto subyacente. Algunas de las formas de crearlos incluyen fcntl (), dup (), fdopen (), fileno () y fork() . Se pueden destruir al menos fclose (), close () y las funciones exec. [...] Tenga en cuenta que después de una bifurcación (), existen dos controladores donde existía uno antes.

de la sección de especificaciones POSIX usted cita arriba. La referencia a "crear [manejadores usando] fork " no se desarrolla más adelante en esta sección, pero la especificación para fork() agrega un pequeño detalle:

El proceso hijo tendrá su propia copia de los descriptores de archivos de los padres. Cada uno de los descriptores de archivo 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 de los padres.
  • las copias del niño se refieren a la misma "cosa" a la que los padres pueden acceder a través de dichos archivos
  • Las descripciones de archivo y las descripciones de archivo 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 que " fork() crea [...] identificadores": se crean como copias y, por lo tanto, a partir de ese momento, se separan y ya no se actualizan en Lockstep.

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

En un archivo normal u otro archivo capaz de buscar, la escritura real de los datos procederá de la posición en el archivo indicado por el desplazamiento del archivo asociado con las fildes. Antes de la devolución exitosa desde write (), el desplazamiento del archivo se incrementará por el número de bytes realmente escritos.

Esto significa que mientras todos comienzan la escritura en el mismo desplazamiento (porque la copia de fd se inicializó como tal) podrían, incluso si tienen éxito, escribir cantidades diferentes (no hay ninguna garantía por parte del estándar de que una solicitud de escritura de N bytes escriba exactamente N bytes; puede tener éxito para cualquier cosa 0 <= actual <= N ), y debido a que el orden de las escrituras no se especificó, todo el programa de ejemplo anterior por lo tanto 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 haya incrementado atómicamente (solo una vez), ni tampoco que la escritura real de los datos suceda de una manera atómica.

Sin embargo, una cosa está garantizada: nunca debe 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 eso ... si los resultados finales no pueden explicarse mediante el reordenamiento de partes de las escrituras.

El uso de O_APPEND soluciona esto, porque al usar eso, de nuevo, ver 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 de archivos intermedia entre el cambio del desplazamiento del archivo y la operación de escritura.

que es el comportamiento de serialización "previo a" / "sin intervención" que usted busca.

El uso de subprocesos cambiaría el comportamiento parcialmente, porque los subprocesos, al crearse, no reciben copias de los descriptores / controladores de archivos, sino que operan en uno real (compartido). Los hilos no (necesariamente) todos comienzan a escribir en el mismo desplazamiento. Pero la opción para el éxito parcial de la escritura todavía significa que puede ver el intercalado de maneras que no quiera ver. Sin embargo, posiblemente todavía sería totalmente conforme a las normas.

Moraleja : No cuente con que un estándar POSIX / UNIX sea restrictivo por defecto . Las especificaciones se relajan deliberadamente en el caso común y requieren que usted, como programador, sea ​​explícito acerca de su intención.

Aparentemente POSIX dice que

Un descriptor de archivo o una secuencia se llama un "identificador" en la descripción de archivo abierto a la que hace referencia; Una descripción de archivo abierto puede tener varios manejadores. [...] Toda la actividad de la aplicación que afecte al desplazamiento de archivo en el primer identificador se suspenderá hasta que se convierta nuevamente en el identificador de archivo activo. [...] Los manejadores no necesitan estar en el mismo proceso para que estas reglas se apliquen. - POSIX.1-2008

y

Si dos hilos en 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 una write(handle, data1, size1) y el segundo proceso emite 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 <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <sys/wait.h> 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 usar grep -a ''^[^-]/|^..*-'' /tmp/atomic-write.log muestra que algunas escrituras no son contiguas o se superponen ( Linux) o llano corrupto (Mac OS X).

Agregar el indicador O_APPEND en la llamada a open(2) soluciona 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 programa de ejemplo nunca hace lseek(2) pero comparte la misma descripción de archivo y, por lo tanto, el mismo desplazamiento de archivo.

Ya he leído preguntas similares en Stackoverflow pero todavía no responden completamente mi pregunta.

La escritura atómica en el archivo de dos procesos no aborda específicamente el caso en el que los procesos comparten la misma descripción del archivo (a diferencia del mismo archivo).

¿Cómo se determina mediante programación si la llamada al sistema de "escritura" es atómica en un archivo en particular? dice que

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

Pero como se POSIX.1-2008 tiene algunos. Y lo que es más, O_APPEND parece activar 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?


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

Un descriptor de archivo o una secuencia se llama un "identificador" en la descripción de archivo abierto a la que hace referencia; Una descripción de archivo abierto puede tener varios manejadores. [...] Toda la actividad de la aplicación que afecte al desplazamiento de archivo en el primer identificador se suspenderá hasta que se convierta nuevamente en el identificador de archivo activo. [...] Los manejadores no necesitan estar en el mismo proceso para que estas reglas se apliquen.

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

El único momento en que se garantiza la atomicidad es para tuberías cuando el tamaño de escritura se ajusta a PIPE_BUF .

Por cierto, incluso si la llamada a write fuera atómica para archivos normales, excepto en el caso de escrituras en tuberías que encajan en PIPE_BUF , la write siempre se puede devolver con una escritura parcial (es decir, haber escrito menos que el número de bytes solicitado). Esta escritura menor a 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 write para terminar).


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 discussion reciente en la lista de correo ext4 :

Actualmente, las lecturas / 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 realiza esto está dañada, pero en realidad esto es algo que podemos hacer fácilmente a nivel de sistema de archivos sin problemas de rendimiento significativos, 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 , para nombrar solo un sistema de archivos moderno, no cumple con POSIX.1-2008 a este respecto.


Edición: 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 (archivo "longitud") son atómicos bajo escritores concurrentes. Esto está garantizado por POSIX, y Linux, FreeBSD, OS X y Windows lo implementan correctamente. Samba también lo implementa correctamente, NFS antes que v5 no, ya que carece de la capacidad de formato de cable para agregar atómicamente. Por lo tanto, si abre su archivo solo con anexos, las escrituras simultáneas no se desgarrarán entre sí en ningún sistema operativo importante a menos que esté involucrado NFS.

Sin embargo, esto no dice nada sobre si las lecturas verán una escritura desgarrada, y en eso POSIX dice lo siguiente sobre la atomicidad de leer () y escribir () en archivos normales:

Todas las siguientes funciones serán atómicas entre sí en los efectos especificados en POSIX.1-2008 cuando operan en archivos normales o enlaces simbólicos ... [muchas funciones] ... leer () ... escribir ( ) ... Si dos subprocesos llaman a una de estas funciones, cada llamada verá todos los efectos especificados de la otra llamada, o ninguno de ellos. POSIX.1-2008

y

Las escrituras pueden ser serializadas con respecto a otras lecturas y escrituras. Si se puede probar (por cualquier medio) que una lectura () de datos de archivo se produce después de una escritura () de los datos, debe reflejar esa escritura (), incluso si las llamadas se realizan mediante procesos diferentes. write()

pero a la inversa:

Este volumen de POSIX.1-2008 no especifica el comportamiento de las escrituras simultáneas en un archivo desde varios procesos. Las aplicaciones deben utilizar algún tipo de control de concurrencia. write()

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

Una interpretación menos segura, pero aún así permitida, podría ser que las lecturas y las escrituras solo se serializan entre sí dentro de un mismo proceso, y entre las escrituras de los procesos se serializan con respecto a las lecturas solamente (es decir, hay un orden de entrada / salida coherente secuencial entre las hebras en un proceso, pero entre procesos i / o es solo adquisición-liberación).

Entonces, ¿cómo funcionan los sistemas operativos y los archivos populares en esto? Como autor de la propuesta de Boost.AFIO un sistema de archivos asíncrono y una biblioteca de i / o C ++, decidí escribir un comprobador 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: actualice la atomicidad = 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: actualizar atomicidad = 1 byte

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

O_DIRECT / FILE_FLAG_NO_BUFFERING:

Microsoft Windows 10 con NTFS: actualice la atomicidad = 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, si no 64 bytes. Tenga en cuenta que esta atomicidad es probablemente una característica de PCIe DMA en lugar de estar diseñada. Desde 10.0.14393, al menos 1Mb, probablemente infinito según la especificación POSIX.

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

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

Entonces, en resumen, FreeBSD con ZFS y Windows muy reciente con NTFS cumplen con POSIX. Linux muy reciente con ext4 cumple POSIX solo con O_DIRECT.

Puede ver los resultados de las pruebas empíricas sin procesar en https://github.com/ned14/afio/tree/master/programs/fs-probe . Tenga en cuenta que probamos las compensaciones 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 rasgaría durante el ciclo de lectura-modificación-escritura.