Atomicité de `write (2)` sur un système de fichiers local

Apparemment, POSIX déclare que

Un descripteur de fichier ou un stream est appelé “descripteur” dans la description de fichier ouverte à laquelle il fait référence. une description de fichier ouvert peut avoir plusieurs descripteurs. […] Toute activité de l’application affectant le décalage de fichier sur le premier descripteur doit être suspendue jusqu’à ce qu’elle redevienne le descripteur de fichier actif. […] Les descripteurs n’ont pas besoin d’être dans le même processus pour que ces règles s’appliquent. – POSIX.1-2008

et

Si deux threads appellent chacun [la fonction write ()], chaque appel verra tous les effets spécifiés de l’autre appel ou aucun d’entre eux. – POSIX.1-2008

write(handle, data1, size1) j’ai bien compris, lorsque le premier processus émet une write(handle, data1, size1) et que le second processus write(handle, data1, size1) problème d’ write(handle, data2, size2) , l’écriture peut avoir lieu dans n’importe quel ordre, mais que les data1 et data2 doivent être toutes deux vierges. et contiguë.

Mais exécuter le code suivant me donne des résultats inattendus.

 #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); } 

J’ai essayé de l’exécuter sous Linux et Mac OS X 10.7.4 et d’utiliser grep -a '^[^-]\|^..*-' /tmp/atomic-write.log montre que certaines écritures ne sont pas contiguës ou se chevauchent ( Linux) ou brutalement corrompu (Mac OS X).

L’ajout de l’indicateur O_APPEND à l’appel open(2) corrige ce problème. Bien, mais je ne comprends pas pourquoi. POSIX dit

O_APPEND S’il est défini, le décalage de fichier doit être défini à la fin du fichier avant chaque écriture.

mais ce n’est pas le problème ici. Mon exemple de programme ne fonctionne jamais avec lseek(2) mais partage la même description de fichier et donc le même décalage de fichier.

J’ai déjà lu des questions similaires sur Stackoverflow mais elles ne répondent toujours pas pleinement à ma question.

L’écriture atomique sur un fichier à partir de deux processus n’aborde pas spécifiquement le cas où les processus partagent la même description de fichier (par opposition au même fichier).

Comment déterminer par programme si un appel système «en écriture» est atomique sur un fichier particulier? dit ça

L’appel d’ write tel que défini dans POSIX n’a ​​aucune garantie d’atomicité.

Mais comme cité ci-dessus, il en a. De plus, O_APPEND semble déclencher cette garantie d’atomicité, bien qu’il me semble que cette garantie devrait être présente même sans O_APPEND .

Pouvez-vous expliquer plus loin ce comportement?

man 2 write sur mon système résume bien:

Notez que tous les systèmes de fichiers ne sont pas conformes à POSIX.

Voici une citation d’une discussion récente sur la liste de diffusion ext4 :

Actuellement, les lectures / écritures simultanées ne concernent que des pages individuelles, mais ne font pas partie de l’appel système. Cela peut amener read() à renvoyer des données mélangées provenant de plusieurs écritures différentes, ce qui, selon moi, n’est pas une bonne approche. Nous pourrions faire valoir que les applications faisant cela sont défectueuses, mais en réalité, nous pouvons facilement le faire au niveau du système de fichiers sans problèmes de performances significatifs, afin que nous puissions être cohérents. POSIX le mentionne également et le système de fichiers XFS possède déjà cette fonctionnalité.

Ceci indique clairement que ext4 – pour ne citer qu’un système de fichiers moderne – n’est pas conforme à POSIX.1-2008 à cet égard.

Une certaine interprétation erronée de ce que les mandats standard font ici vient de l’utilisation de processus par rapport à des threads, et de ce que cela signifie pour la situation de “traitement” dont vous parlez. En particulier, vous avez manqué cette partie:

Les descripteurs peuvent être créés ou détruits par une action explicite de l’utilisateur, sans affecter la description du fichier ouvert sous-jacent. Fcntl (), dup (), fdopen (), fileno () et fork() quelques-uns des moyens de les créer . Ils peuvent être détruits par au moins fclose (), close () et les fonctions exec. […] Notez qu’après un fork (), il existe deux descripteurs là où il en existait un auparavant.

de la section de spécifications POSIX que vous citez ci-dessus. La référence à “create [handles using]] fork ” ne sera pas développée plus avant dans cette section, mais la spécification de fork() ajoute un petit détail:

Le processus fils doit avoir sa propre copie des descripteurs de fichier du parent. Chacun des descripteurs de fichier de l’enfant doit faire référence à la même description de fichier ouverte avec le descripteur de fichier correspondant du parent.

Les bits pertinents ici sont:

  • l’enfant a des copies des descripteurs de fichier du parent
  • les copies de l’enfant font référence à la même “chose” à laquelle le parent peut accéder via ledit fds
  • Les descriptions de fichier et les descriptions de fichier ne sont pas la même chose; en particulier, un descripteur de fichier est un descripteur au sens précédent.

C’est ce à quoi se réfère la première citation quand il est écrit que ” fork() crée des […] poignées” – elles sont créées en tant que copies et, à partir de ce moment, sont détachées et ne sont plus mises à jour simultanément.

Dans votre exemple de programme, chaque processus enfant reçoit sa propre copie, qui commence au même état, mais après l’acte de copie, ces descripteurs de fichiers / descripteurs sont devenus des instances indépendantes et, par conséquent, les écritures se font concurrence. Ceci est parfaitement acceptable par rapport à la norme, car write() ne garantit que:

Sur un fichier normal ou un autre fichier capable de rechercher, l’écriture effective des données doit commencer à partir de la position dans le fichier indiquée par le décalage de fichier associé à fildes. Avant de renvoyer avec succès write (), le décalage de fichier doit être incrémenté du nombre d’octets réellement écrits.

Cela signifie que même s’ils réussissent, ils commencent tous à écrire avec le même décalage (car la copie fd a été initialisée en tant que telle), mais ils réussissent tous à écrire des montants différents (rien ne garantit que la demande d’écriture de N octets sera écrite) exactement N octets; il peut réussir n’importe quoi 0 <= actuel <= N ) et, étant donné que l'ordre des écritures est non spécifié, l'ensemble du programme d'exemple ci-dessus a donc des résultats non spécifiés. Même si le montant total demandé est écrit, toutes les normes ci-dessus indiquent que le décalage de fichier est incrémenté - il ne dit pas qu'il est incrémenté de manière atomique (une seule fois), ni que l'écriture réelle de données se fera de manière atomique.

Cependant, une chose est garantie: vous ne devriez jamais voir dans le fichier des éléments qui n'y étaient ni avant les écritures, ni qui ne proviennent d'aucune des données écrites par ces écritures. Si vous le faites, ce serait une corruption et un bogue dans l'implémentation du système de fichiers. Ce que vous avez observé ci-dessus pourrait bien être que ... si les résultats finaux ne peuvent pas être expliqués en réorganisant des parties des écritures.

L'utilisation de O_APPEND résout ce problème car, encore une fois - voir write() :

Si l'indicateur O_APPEND des indicateurs d'état du fichier est défini, l'offset de fichier doit être défini à la fin du fichier avant chaque écriture et aucune opération de modification de fichier ne doit intervenir entre la modification de l'offset de fichier et l'opération d'écriture.

qui est le comportement de sérialisation "avant" / "sans intervention" que vous recherchez.

L'utilisation de threads changerait partiellement le comportement - car les threads, lors de leur création, ne reçoivent pas de copies des descripteurs de fichiers / descripteurs de fichiers, mais opèrent sur le nom réel (partagé). Les threads ne commenceraient pas (nécessairement) tous à écrire au même décalage. Mais l'option de réussite partielle en écriture signifie que vous pouvez voir l'entrelacement d'une manière que vous ne voudriez peut-être pas voir. Pourtant, il serait peut-être toujours conforme aux normes.

Morale : Ne comptez pas sur un standard POSIX / UNIX ressortingctif par défaut . Les spécifications sont délibérément assouplies dans le cas courant et vous obligent, en tant que programmeur, à être explicite à propos de votre intention.

Edit: Mise à jour août 2017 avec les dernières modifications des comportements du système d’exploitation.

Premièrement, O_APPEND ou l’équivalent de FILE_APPEND_DATA sous Windows signifie que les incréments de l’étendue maximale du fichier (“longueur”) sont atomiques sous des rédacteurs simultanés. Ceci est garanti par POSIX, et Linux, FreeBSD, OS X et Windows l’implémentent correctement. Samba l’implémente également correctement. NFS avant la v5 ne le laisse pas, car il ne lui manque pas la capacité de format filaire à append de manière atomique. Donc, si vous ouvrez votre fichier avec append-only seulement, les écritures simultanées ne seront pas déchirées les unes par rapport aux autres sur tous les systèmes d’exploitation majeurs, sauf si NFS est impliqué.

Cela ne dit pas si les lectures verront jamais une écriture déchirée, et POSIX dit ce qui suit au sujet de l’atomicité de read () et write () dans des fichiers normaux:

Toutes les fonctions suivantes doivent être atomiques les unes par rapport aux autres dans les effets spécifiés dans POSIX.1-2008 lorsqu’elles fonctionnent sur des fichiers normaux ou des liens symboliques … [nombreuses fonctions] … read () … write ( ) … Si deux threads appellent chacun l’une de ces fonctions, chaque appel verra tous les effets spécifiés de l’autre appel ou aucun d’eux. [La source]

et

Les écritures peuvent être sérialisées par rapport à d’autres lectures et écritures. Si un read () des données de fichier peut être prouvé (par quelque moyen que ce soit) après un write () des données, il doit alors refléter write (), même si les appels sont effectués par des processus différents. [La source]

mais inversement:

Ce volume de POSIX.1-2008 ne spécifie pas le comportement des écritures simultanées dans un fichier à partir de plusieurs processus. Les applications doivent utiliser une forme de contrôle de concurrence. [La source]

Une interprétation sûre de ces trois exigences suggérerait que toutes les écritures qui chevauchent une étendue dans le même fichier doivent être sérialisées les unes par rapport aux autres et que les lectures doivent être telles que les écritures déchirées n’apparaissent jamais aux lecteurs.

Une interprétation moins sûre, mais tout de même permise, pourrait consister en une lecture et une écriture uniquement sérialisées les unes avec les autres entre les threads d’un même processus et entre les écritures de processus entre des threads en lecture seule (c’est-à-dire qu’il existe un ordre cohérent des un processus, mais entre les processus, l’entrée / sortie n’est qu’une acquisition / libération).

Alors, comment fonctionnent les systèmes d’exploitation et les systèmes de fichiers populaires? En tant qu’auteur de Boost.AFIO, système de fichiers asynchrone et bibliothèque de fichiers i / o C ++, j’ai décidé d’écrire un testeur empirique. Les résultats sont les suivants pour plusieurs threads dans un processus unique.


Non O_DIRECT / FILE_FLAG_NO_BUFFERING:

Microsoft Windows 10 avec NTFS: mise à jour de l’atomicité = 1 octet jusqu’au 10.0.10240 inclus, à partir du 10.0.14393 au moins 1 Mo, probablement infini selon les spécifications POSIX.

Linux 4.2.6 avec ext4: update atomicity = 1 octet

FreeBSD 10.2 avec ZFS: mise à jour de l’atomicité = au moins 1 Mo, probablement infinie selon les spécifications POSIX.

O_DIRECT / FILE_FLAG_NO_BUFFERING:

Microsoft Windows 10 avec NTFS: mettez à jour atomicity = jusqu’au 10.0.10240 inclus, jusqu’à 4096 octets uniquement si la page est alignée, sinon 512 octets si FILE_FLAG_WRITE_THROUGH est désactivé, sinon 64 octets. Notez que cette atomicité est probablement une caractéristique de DMA PCIe plutôt que conçue. Depuis 10.0.14393, au moins 1 Mo, probablement infini selon les spécifications POSIX.

Linux 4.2.6 avec ext4: update atomicity = au moins 1 Mo, probablement infini selon les spécifications POSIX. Notez que les précédents Linux avec ext4 n’excédaient certainement pas 4096 octets. XFS utilisait certes le locking personnalisé, mais il semble bien que Linux récent ait finalement résolu ce problème dans ext4.

FreeBSD 10.2 avec ZFS: mise à jour de l’atomicité = au moins 1 Mo, probablement infinie selon les spécifications POSIX.


En résumé, FreeBSD avec ZFS et Windows très récent avec NTFS sont conformes à POSIX. Très récent, Linux avec ext4 est POSIX conforme uniquement à O_DIRECT.

Vous pouvez voir les résultats des tests empiriques bruts à l’ adresse https://github.com/ned14/afio/tree/master/programs/fs-probe . Notez que nous testons les décalages déchirés uniquement sur des multiples de 512 octets. Par conséquent, je ne peux pas dire si une mise à jour partielle d’un secteur de 512 octets se déchirerait au cours du cycle lecture-modification-écriture.

Vous interprétez mal la première partie de la spécification que vous avez citée:

Un descripteur de fichier ou un stream est appelé “descripteur” dans la description de fichier ouverte à laquelle il fait référence. une description de fichier ouvert peut avoir plusieurs descripteurs. […] Toute activité de l’application affectant le décalage de fichier sur le premier descripteur doit être suspendue jusqu’à ce qu’elle redevienne le descripteur de fichier actif. […] Les descripteurs n’ont pas besoin d’être dans le même processus pour que ces règles s’appliquent.

Cela n’impose pas à l’implémentation de gérer les access simultanés. Au lieu de cela, il oblige une application à ne pas créer un access simultané, même à partir de processus différents, si vous souhaitez un ordre bien défini de la sortie et des effets secondaires.

Le seul moment où l’atomicité est garantie concerne les tubes lorsque la taille d’écriture correspond à PIPE_BUF .

En passant, même si l’appel à write était atomique pour les fichiers ordinaires, excepté dans le cas d’écritures sur des canaux conformes à PIPE_BUF , write peut toujours être renvoyé avec une écriture partielle (c’est-à-dire après avoir écrit moins que le nombre d’octets demandé). Cette écriture plus petite que celle demandée serait alors atomique, mais cela n’aiderait pas du tout la situation en ce qui concerne l’atsortingcité de l’opération entière (votre application devra rappeler l’ write pour terminer).