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 -x
imprime 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):
- Exécutez le programme externe
cat
avec l'argument tmp
.
- Ouvert
tmp
à la lecture.
- 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:
- Redirige la sortie standard vers
tmp
, tronquant le fichier dans le processus.
- Exécutez le programme externe
head
avec l'argument -1
.
- 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 tmp
et de lire tmp
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:
{ 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 cat
fin 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 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.