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 -f
et ltrace -f
utilisez-les pour attacher à chaque processus enfant. (Sous Linux, il s'agit du même ptrace
appel 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 open
et openat
appels 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 open
appels 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/bin
et le /usr/lib
dé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/true
ou /bin/ls
parfois), 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.
racechecker
lui-même devrait être écrit en C, pas en shell, mais pourrait peut-être utiliser strace
le 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.
racechecker
tout 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 .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
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 -x
imprime chaque commande lors de son exécution):Jouez avec les variations jusqu'à ce que vous soyez à l'aise avec ce que vous observez.
Étant donné la commande composée
le processus de gauche effectue les opérations suivantes (je n'ai répertorié que les étapes pertinentes pour mon explication):
cat
avec l'argumenttmp
.tmp
à la lecture.Le processus de droite fait ce qui suit:
tmp
, tronquant le fichier dans le processus.head
avec l'argument-1
.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
tmp
et de liretmp
en 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/bash
par 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:
ou
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
cat
fin de la lecture, vous devez soit tamponner complètement les données en mémoire, par exempleou é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.
La collection moreutils comprend un programme qui fait exactement cela, appelé
sponge
.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
cat
lecture 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.la source
strace
(c.-à-d. Linuxptrace
) pour faire en sorte que lesopen
appels 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.