Comment faire en sorte que la lecture et l'écriture du même fichier dans le même pipeline «échouent» toujours?

9

Disons que j'ai le script suivant:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

Sur la ligne clé, je lis et écris le même fichier tmpqui échoue parfois.

(Je l'ai lu à cause des conditions de concurrence parce que les processus dans le pipeline sont exécutés en parallèle, ce que je ne comprends pas pourquoi - chacun headdoit prendre les données de la précédente, n'est-ce pas? Ce n'est PAS ma principale question, mais vous pouvez aussi y répondre.)

Lorsque j'exécute le script, il génère environ 200 lignes. Existe-t-il un moyen de forcer ce script à toujours générer 0 ligne (donc la redirection d'E / S vers tmpest toujours préparée en premier et donc les données sont toujours détruites)? Pour être clair, je veux dire changer les paramètres du système, pas ce script.

Merci pour vos idées.

karlosss
la source

Réponses:

2

La réponse de Gilles explique les conditions de course. Je vais juste répondre à cette partie:

Existe-t-il un moyen de forcer ce script à afficher toujours 0 ligne (de sorte que la redirection d'E / S vers tmp soit toujours préparée en premier et que les données soient toujours détruites)? Pour être clair, je veux dire changer les paramètres du système

IDK si un outil pour cela existe déjà, mais j'ai une idée de comment on pourrait l'implémenter. (Mais notez ce ne serait pas toujours 0 lignes, juste un testeur utile que les prises courses simples comme celui - ci facilement, et quelques courses plus compliquées. Voir le commentaire de @Gilles .) Il ne serait pas garantie qu'un script était sûr , mais peut être un outil utile dans les tests, semblable à tester un programme multithread sur différents CPU, y compris des CPU non x86 faiblement ordonnés comme ARM.

Vous l'exécuteriez comme racechecker bash foo.sh

Utilisez les mêmes fonctions de traçage / interception d'appels système strace -fet ltrace -futilisez-les pour attacher à chaque processus enfant. (Sous Linux, il s'agit du même ptraceappel système utilisé par GDB et d'autres débogueurs pour définir des points d'arrêt, une seule étape et modifier la mémoire / les registres d'un autre processus.)

Instrument les openet openatappels système: lorsque tout processus en cours d' exécution sous cet outil fait un un open(2)appel système (ou openat) avec O_RDONLY, peut - être pour le sommeil 1/2 ou 1 seconde. Laissez les autres openappels système (en particulier ceux inclus O_TRUNC) s'exécuter sans délai.

Cela devrait permettre à l'auteur de gagner la course dans presque toutes les conditions de course, à moins que la charge du système ne soit également élevée, ou que ce ne soit une condition de course compliquée où la troncature ne se produise qu'après une autre lecture. Ainsi, une variation aléatoire dont open()s (et peut-être read()s ou écrit) sont retardés augmenterait la puissance de détection de cet outil, mais bien sûr sans tester pendant une durée infinie avec un simulateur de retard qui couvrira éventuellement toutes les situations possibles que vous pouvez rencontrer dans dans le monde réel, vous ne pouvez pas être sûr que vos scripts sont exempts de races à moins de les lire attentivement et de prouver qu'ils ne le sont pas.


Vous en auriez probablement besoin pour mettre sur liste blanche (sans retarder open) les fichiers /usr/binet le /usr/libdémarrage du processus ne prend donc pas une éternité. (La liaison dynamique au cours de l'exécution doit concerner open()plusieurs fichiers (regardez strace -eopen /bin/trueou /bin/lsparfois), bien que si le shell parent lui-même effectue la troncature, ce sera correct. Mais il sera toujours bon pour cet outil de ne pas ralentir excessivement les scripts).

Ou peut-être mettre en liste blanche chaque fichier que le processus appelant n'a pas l'autorisation de tronquer en premier lieu. c'est-à-dire que le processus de traçage peut effectuer un access(2)appel système avant de suspendre réellement le processus qui voulait open()un fichier.


racecheckerlui-même devrait être écrit en C, pas en shell, mais pourrait peut-être utiliser stracele code de comme point de départ et pourrait ne pas prendre beaucoup de travail à implémenter.

Vous pourriez peut-être obtenir les mêmes fonctionnalités avec un système de fichiers FUSE . Il y a probablement un exemple FUSE d'un système de fichiers purement passthrough, vous pouvez donc ajouter des vérifications à la open()fonction dans celle qui la fait dormir pour les ouvertures en lecture seule mais laisser la troncature se produire immédiatement.

Peter Cordes
la source
Votre idée de vérificateur de course ne fonctionne pas vraiment. Tout d'abord, il y a le problème que les délais d'attente ne sont pas fiables: un jour, l'autre gars prendra plus de temps que prévu (c'est un problème classique avec les scripts de construction ou de test, qui semblent fonctionner pendant un certain temps, puis échouer de manière difficile à déboguer lorsque la charge de travail augmente et que beaucoup de choses s'exécutent en parallèle). Mais au-delà de cela, à quelle ouverture allez-vous ajouter un retard? Afin de détecter quelque chose d'intéressant, vous devez effectuer de nombreuses exécutions avec différents modèles de retard et comparer leurs résultats.
Gilles 'SO- arrête d'être méchant'
@ Gilles: C'est vrai, tout retard raisonnablement court ne garantit pas que le tronqué gagnera la course (sur une machine lourdement chargée comme vous le signalez). L'idée ici est que vous l'utilisez pour tester votre script plusieurs fois, pas que vous l'utilisez racecheckertout le temps. Et vous voudriez probablement que le temps de veille ouvert pour la lecture soit configurable pour le bénéfice des personnes sur des machines très chargées qui souhaitent le régler plus haut, comme 10 secondes. Ou réglez-le plus bas, comme 0,1 seconde pour les scripts longs ou inefficaces qui rouvrent beaucoup les fichiers .
Peter Cordes
@ Gilles: Excellente idée sur les différents modèles de retard, qui pourraient vous permettre d'attraper plus de courses que les simples trucs dans le même pipeline qui "devraient être évidents (une fois que vous savez comment fonctionnent les shells)" comme le cas de l'OP. Mais "qui s'ouvre?" toute ouverture en lecture seule, avec une liste blanche ou une autre façon de ne pas retarder le démarrage du processus.
Peter Cordes
Je suppose que vous pensez à des races plus complexes avec des emplois d'arrière-plan qui ne sont tronqués qu'après la fin d'un autre processus? Oui, une variation aléatoire pourrait être nécessaire pour attraper cela. Ou peut-être regardez l'arborescence des processus et retardez les lectures "précoces" pour essayer d'inverser l'ordre habituel. Vous pourriez rendre l'outil de plus en plus compliqué pour simuler de plus en plus de possibilités de réorganisation, mais à un moment donné, vous devez toujours concevoir vos programmes correctement si vous faites du multitâche. Les tests automatisés pourraient être utiles pour des scripts plus simples où les problèmes possibles sont plus limités.
Peter Cordes
C'est assez similaire au test de code multi-thread, en particulier les algorithmes sans verrouillage: le raisonnement logique sur la raison pour laquelle il est correct est très important, ainsi que le test, car vous ne pouvez pas compter sur le test sur un ensemble particulier de machines pour produire toutes les réorganisations qui pourraient être un problème si vous n'avez pas fermé toutes les échappatoires. Mais tout comme tester sur une architecture faiblement ordonnée comme ARM ou PowerPC est une bonne idée dans la pratique, tester un script sous un système qui retarde artificiellement les choses peut exposer certaines races, donc c'est mieux que rien. Vous pouvez toujours introduire des bugs qu'il n'attrapera pas!
Peter Cordes
18

Pourquoi il y a une condition de concurrence

Les deux côtés d'un tuyau sont exécutés en parallèle, pas l'un après l'autre. Il existe un moyen très simple de le démontrer: exécutez

time sleep 1 | sleep 1

Cela prend une seconde, pas deux.

Le shell démarre deux processus enfants et attend qu'ils se terminent tous les deux. Ces deux processus s'exécutent en parallèle: la seule raison pour laquelle l'un d'eux se synchroniserait avec l'autre, c'est quand il doit attendre l'autre. Le point de synchronisation le plus courant est lorsque le côté droit bloque en attente de lecture des données sur son entrée standard, et devient débloqué lorsque le côté gauche écrit plus de données. L'inverse peut également se produire, lorsque le côté droit est lent à lire les données et le côté gauche bloque dans son opération d'écriture jusqu'à ce que le côté droit lise plus de données (il y a un tampon dans le tuyau lui-même, géré par le noyau, mais il a une petite taille maximale).

Pour observer un point de synchronisation, observez les commandes suivantes ( sh -ximprime chaque commande lors de son exécution):

time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'

Jouez avec les variations jusqu'à ce que vous soyez à l'aise avec ce que vous observez.

Étant donné la commande composée

cat tmp | head -1 > tmp

le processus de gauche effectue les opérations suivantes (je n'ai répertorié que les étapes pertinentes pour mon explication):

  1. Exécutez le programme externe catavec l'argument tmp.
  2. Ouvert tmpà la lecture.
  3. Bien qu'il n'ait pas atteint la fin du fichier, lisez un morceau du fichier et écrivez-le sur la sortie standard.

Le processus de droite fait ce qui suit:

  1. Redirige la sortie standard vers tmp, tronquant le fichier dans le processus.
  2. Exécutez le programme externe headavec l'argument -1.
  3. Lisez une ligne de l'entrée standard et écrivez-la sur la sortie standard.

Le seul point de synchronisation est que right-3 attend que left-3 ait traité une ligne complète. Il n'y a pas de synchronisation entre gauche-2 et droite-1, ils peuvent donc se produire dans l'un ou l'autre ordre. L'ordre dans lequel ils se produisent n'est pas prévisible: cela dépend de l'architecture du processeur, du shell, du noyau, des cœurs dans lesquels les processus sont programmés, des interruptions que le processeur reçoit à ce moment-là, etc.

Comment changer le comportement

Vous ne pouvez pas modifier le comportement en modifiant un paramètre système. L'ordinateur fait ce que vous lui demandez de faire. Vous lui avez dit de tronquer tmpet de lire tmpen parallèle, donc il fait les deux choses en parallèle.

Ok, il y a un "paramètre système" que vous pouvez changer: vous pouvez le remplacer /bin/bashpar un programme différent qui n'est pas bash. J'espère qu'il va sans dire que ce n'est pas une bonne idée.

Si vous souhaitez que la troncature se produise avant le côté gauche du tuyau, vous devez le placer en dehors du pipeline, par exemple:

{ cat tmp | head -1; } >tmp

ou

( exec >tmp; cat tmp | head -1 )

Je n'ai aucune idée pourquoi vous voudriez ceci cependant. Quel est l'intérêt de lire un fichier que vous savez être vide?

Inversement, si vous souhaitez que la redirection de sortie (y compris la troncature) se produise après la catfin de la lecture, vous devez soit tamponner complètement les données en mémoire, par exemple

line=$(cat tmp | head -1)
printf %s "$line" >tmp

ou écrivez dans un autre fichier, puis déplacez-le en place. Il s'agit généralement de la manière la plus robuste de faire les choses dans les scripts, et présente l'avantage que le fichier est écrit en entier avant d'être visible par le nom d'origine.

cat tmp | head -1 >new && mv new tmp

La collection moreutils comprend un programme qui fait exactement cela, appelé sponge.

cat tmp | head -1 | sponge tmp

Comment détecter le problème automatiquement

Si votre objectif était de prendre des scripts mal écrits et de déterminer automatiquement où ils se cassent, alors désolé, la vie n'est pas si simple. L'analyse du temps d'exécution ne trouvera pas le problème de manière fiable, car la catlecture se termine parfois avant la troncature. L'analyse statique peut en principe le faire; l'exemple simplifié de votre question est détecté par Shellcheck , mais il ne peut pas détecter un problème similaire dans un script plus complexe.

Gilles 'SO- arrête d'être méchant'
la source
C'était mon objectif, déterminer si le script était bien écrit ou non. Si le script pouvait avoir détruit des données de cette façon, je voulais juste qu'il les détruise à chaque fois. Il n'est pas bon d'entendre que cela est presque impossible. Grâce à vous, je sais maintenant quel est le problème et vais essayer de trouver une solution.
karlosss
@karlosss: Hmm, je me demande si vous pourriez utiliser le même système de traçage / interception d'appels système que strace(c.-à-d. Linux ptrace) pour faire en sorte que les openappels système tout -à-lire (dans tous les processus enfants) dorment pendant une demi-seconde, donc lorsque vous courez avec une troncature, la troncature gagnera presque toujours.
Peter Cordes
@PeterCordes je suis un novice en la matière, si vous pouvez trouver un moyen d'y parvenir et l'écrire comme réponse, je l'accepterai.
karlosss
@PeterCordes Vous ne pouvez pas garantir que la troncature gagnera avec un retard. Cela fonctionnera la plupart du temps, mais parfois sur une machine lourdement chargée, votre script échouera de manière plus ou moins mystérieuse.
Gilles 'SO- arrête d'être méchant'
@ Gilles: Discutons de cela sous ma réponse.
Peter Cordes