linux-kernel sigsegv, - Ecriture de programmes pour faire face aux erreurs d'E / S provoquant des écritures perdues sous Linux




received signal (5)

Utilisez l'indicateur O_SYNC lorsque vous ouvrez le fichier. Il s'assure que les données sont écrites sur le disque.

Si cela ne vous satisfait pas, il n'y aura rien.

TL; DR: Si le noyau Linux perd une écriture d'E / S tamponnée , l'application peut-elle le trouver?

Je sais que vous devez fsync() le fichier (et son répertoire parent) pour la durabilité . La question est de savoir si le noyau perd des tampons encrés qui sont en attente d'écriture en raison d'une erreur d'E / S, comment l'application peut-elle les détecter et les récupérer ou les abandonner?

Pensez aux applications de base de données, etc., où l'ordre des écritures et la durabilité de l'écriture peuvent être cruciaux.

Lost écrit? Comment?

La couche de bloc du noyau Linux peut dans certaines circonstances perdre des requêtes d'E / S tamponnées qui ont été soumises avec succès par write() , pwrite() etc, avec une erreur comme:

Buffer I/O error on device dm-0, logical block 12345
lost page write due to I/O error on dm-0

(Voir end_buffer_write_sync(...) et end_buffer_async_write(...) dans fs/buffer.c ).

Sur les nouveaux noyaux, l'erreur contiendra à la place "lost async page write" , comme:

Buffer I/O error on dev dm-0, logical block 12345, lost async page write

Puisque l'application write() aura déjà retourné sans erreur, il ne semble pas possible de signaler une erreur à l'application.

Les détecter?

Je ne suis pas très familier avec les sources du noyau, mais je pense qu'il définit AS_EIO sur le tampon qui n'a pas pu être écrit s'il fait une écriture asynchrone:

    set_bit(AS_EIO, &page->mapping->flags);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

mais il n'est pas clair pour moi si ou comment l'application peut trouver à ce sujet quand il plus tard fsync() s le fichier pour confirmer qu'il est sur le disque.

Il semble que wait_on_page_writeback_range(...) en mm/filemap.c pourrait être fait par do_sync_mapping_range(...) dans fs/sync.c qui est appelé par sys_sync_file_range(...) . Il renvoie -EIO si un ou plusieurs tampons n'ont pas pu être écrits.

Si, comme je le devine, cela se propage au fsync() de fsync() , alors si l'application panique et se retire si elle reçoit une erreur d'E / S de fsync() et sait comment refaire son travail quand elle est redémarrée, cela devrait être une garantie suffisante?

Il n'y a probablement aucun moyen pour l'application de savoir quels décalages d'octets dans un fichier correspondent aux pages perdues afin de pouvoir les réécrire si elle sait comment, mais si l'application répète tout son travail en attente depuis la dernière fsync() du fichier, et qui réécrit tous les tampons de noyau sales correspondant aux écritures perdues contre le fichier, cela devrait effacer tous les drapeaux d'erreur d'E / S sur les pages perdues et permettre au prochain fsync() de compléter - droit?

Y a-t-il alors d'autres circonstances, inoffensives, où fsync() peut revenir - -EIO où le renflouement et la reprise du travail seraient trop drastiques?

Pourquoi?

Bien sûr, de telles erreurs ne devraient pas se produire. Dans ce cas, l'erreur résultait d'une interaction malheureuse entre les valeurs par défaut du pilote dm-multipath et le code de détection utilisé par le réseau de stockage pour signaler l'échec de l'allocation du stockage avec allocation dynamique. Mais ce n'est pas la seule circonstance où ils peuvent se produire - j'ai également vu des rapports à partir de LVM provisionné par exemple, tel qu'utilisé par libvirt, Docker, et plus encore. Une application critique comme une base de données devrait essayer de faire face à de telles erreurs, plutôt que de continuer aveuglément comme si tout allait bien.

Si le noyau pense qu'il est acceptable de perdre des écritures sans mourir avec une panique du noyau, les applications doivent trouver un moyen de s'en sortir.

L'impact pratique est que j'ai trouvé un cas où un problème de multi-chemin avec un SAN a causé des écritures perdues qui ont abouti à la corruption de la base de données parce que le SGBD ne savait pas que ses écritures avaient échoué. Pas drôle.


Vérifiez la valeur de retour de close. close peut échouer tandis que les écritures tamponnées semblent réussir.


fsync() renvoie -EIO si le noyau a perdu une écriture

(Remarque: la partie antérieure fait référence à des noyaux plus anciens, mis à jour ci-dessous pour refléter les noyaux modernes)

Cela ressemble à une écriture tampon asynchrone dans les end_buffer_async_write(...) définissant un indicateur -EIO sur la page de tampon sale échoué pour le fichier :

set_bit(AS_EIO, &page->mapping->flags);
set_buffer_write_io_error(bh);
clear_buffer_uptodate(bh);
SetPageError(page);

qui est ensuite détecté par wait_on_page_writeback_range(...) comme appelé par do_sync_mapping_range(...) comme appelé par sys_sync_file_range(...) comme appelé par sys_sync_file_range2(...) pour implémenter l'appel de bibliothèque C fsync() .

Mais seulement une fois!

Ce commentaire sur sys_sync_file_range

168  * SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER will detect any
169  * I/O errors or ENOSPC conditions and will return those to the caller, after
170  * clearing the EIO and ENOSPC flags in the address_space.

suggère que lorsque fsync() renvoie -EIO ou (non documenté dans la page de -ENOSPC ) -ENOSPC , il -ENOSPC l'état d'erreur afin qu'un fsync() ultérieur fsync() le succès même si les pages n'ont jamais été écrites.

Effectivement, wait_on_page_writeback_range(...) efface les bits d'erreur quand il les teste :

301         /* Check for outstanding write errors */
302         if (test_and_clear_bit(AS_ENOSPC, &mapping->flags))
303                 ret = -ENOSPC;
304         if (test_and_clear_bit(AS_EIO, &mapping->flags))
305                 ret = -EIO;

Donc, si l'application s'attend à ce qu'elle puisse réessayer fsync() jusqu'à ce qu'elle réussisse et que l'on fsync() que les données sont sur disque, c'est terriblement faux.

Je suis assez sûr que c'est la source de la corruption de données que j'ai trouvé dans le SGBD. Il fsync() et pense que tout ira bien quand il réussit.

Est-ce autorisé?

Les docs POSIX / SuS sur fsync() ne le spécifient pas vraiment:

Si la fonction fsync () échoue, les opérations d'E / S en attente ne sont pas forcément terminées.

La page de fsync() de fsync() pour fsync() ne dit rien de ce qui se passe en cas d'échec.

Donc, il semble que la signification des erreurs fsync() est "ne sais pas ce qui est arrivé à vos écritures, pourrait avoir travaillé ou non, mieux essayer à nouveau pour être sûr".

Noyaux plus récents

Sur 4.9 end_buffer_async_write définit -EIO sur la page, juste via mapping_set_error .

    buffer_io_error(bh, ", lost async page write");
    mapping_set_error(page->mapping, -EIO);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

Du côté de la synchronisation, je pense que c'est similaire, même si la structure est maintenant assez complexe à suivre. filemap_check_errors en mm/filemap.c maintenant:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;

ce qui a beaucoup le même effet. Les vérifications d'erreurs semblent toutes passer par filemap_check_errors qui fait un test-and-clear:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;
    return ret;

J'utilise btrfs sur mon ordinateur portable, mais quand je crée un loopback ext4 pour le tester sur /mnt/tmp et que j'installe une sonde perf:

sudo dd if=/dev/zero of=/tmp/ext bs=1M count=100
sudo mke2fs -j -T ext4 /tmp/ext
sudo mount -o loop /tmp/ext /mnt/tmp

sudo perf probe filemap_check_errors

sudo perf record -g -e probe:end_buffer_async_write -e probe:filemap_check_errors dd if=/dev/zero of=/mnt/tmp/test bs=4k count=1 conv=fsync

Je trouve la pile d'appel suivante dans le perf report -T :

        ---__GI___libc_fsync
           entry_SYSCALL_64_fastpath
           sys_fsync
           do_fsync
           vfs_fsync_range
           ext4_sync_file
           filemap_write_and_wait_range
           filemap_check_errors

Une lecture suggère que oui, les noyaux modernes se comportent de la même manière.

Cela semble signifier que si fsync() (ou probablement write() ou close() ) renvoie -EIO , le fichier est dans un état indéfini entre la dernière fois que vous avez réussi fsync() d ou close() d et ses plus récents write() dix état.

Tester

J'ai implémenté un cas de test pour démontrer ce comportement .

Implications

Un SGBD peut faire face à cela en entrant en récupération après un accident. Comment diable est une application utilisateur normale censée faire face à cela? La page de fsync() ne donne aucun avertissement que cela signifie "fsync-if-you-feel-it-it" et je m'attends à ce que beaucoup d'applications ne se débrouillent pas bien avec ce comportement.

Rapports de bogues

En lire plus

lwn.net a abordé ce sujet dans l'article "Amélioration de la gestion des erreurs de couche de bloc" .

Fil de la liste de diffusion postgresql.org .


Puisque l'application write () aura déjà retourné sans erreur, il ne semble pas possible de signaler une erreur à l'application.

Je ne suis pas d'accord. write peut retourner sans erreur si l'écriture est simplement mise en file d'attente, mais l'erreur sera signalée sur l'opération suivante qui nécessitera l'écriture réelle sur le disque, à la prochaine fsync , éventuellement sur une écriture suivante si le système décide de vider le cache et au moins sur le dernier fichier à proximité.

C'est la raison pour laquelle il est essentiel pour l'application de tester la valeur de retour de close pour détecter d'éventuelles erreurs d'écriture.

Si vous avez vraiment besoin d'être capable de faire un traitement d'erreur intelligent, vous devez supposer que tout ce qui a été écrit depuis la dernière fsync réussie a échoué et que tout au moins quelque chose a échoué.


  1. Oui. Utilisez les modules d'assemblage en ligne ou d'assemblage de liens. La méthode que vous devez utiliser dépend du code d'assemblage dont vous avez besoin. Habituellement, il est bon d'utiliser l'assemblage en ligne pour quelques lignes et de basculer une fois pour séparer les modules d'objet s'il s'agit de plusieurs fonctions.
  2. Certainement, mais parfois c'est nécessaire. L'exemple le plus frappant serait la programmation d'un système d'exploitation.
  3. La plupart des compilateurs optimisent aujourd'hui le code que vous écrivez dans un langage de haut niveau beaucoup mieux que quiconque pourrait écrire du code assembleur. Les gens l'utilisent principalement pour écrire du code qui serait impossible à écrire dans un langage de haut niveau comme C. Si quelqu'un l'utilise pour autre chose, il est soit meilleur en optimisation qu'un compilateur moderne (j'en doute) ou tout simplement stupide , par exemple, il ne sait pas quels drapeaux de compilateur ou attributs de fonction utiliser.




c linux linux-kernel posix