La sortie de substitution de processus est hors de l'ordre

16

le

echo one; echo two > >(cat); echo three; 

donne une sortie inattendue.

Je lis ceci: Comment la substitution de processus est-elle implémentée dans bash? et de nombreux autres articles sur la substitution de processus sur Internet, mais je ne comprends pas pourquoi il se comporte de cette façon.

Production attendue:

one
two
three

Sortie réelle:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

De plus, ces deux commandes devraient être équivalentes de mon point de vue, mais elles ne le font pas:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

Pourquoi je pense, ils devraient être les mêmes? Parce que les deux connectent la seqsortie à l' catentrée via le canal anonyme - Wikipedia, Substitution de processus .

Question: Pourquoi il se comporte de cette façon? Où est mon erreur? La réponse complète est souhaitée (avec explication de la façon dont bashcela se fait sous le capot).

MiniMax
la source
2
Même si ce n'est pas si clair à première vue, c'est en fait un double de bash attendre la substitution de processus en processus même si la commande n'est pas valide
Stéphane Chazelas
2
En fait, il serait préférable que cette autre question soit marquée comme double de celle-ci, car celle-ci est plus pertinente. C'est pourquoi j'ai copié ma réponse là-bas.
Stéphane Chazelas

Réponses:

21

Oui, bashcomme dans ksh(d'où vient la fonctionnalité), les processus à l'intérieur de la substitution de processus ne sont pas attendus (avant d'exécuter la commande suivante dans le script).

pour un <(...), c'est généralement bien comme dans:

cmd1 <(cmd2)

le shell attendra cmd1et cmd1attendra généralement cmd2en raison de sa lecture jusqu'à la fin du fichier sur le canal qui est substitué, et cette fin de fichier se produit généralement lors de la cmd2mort. C'est la même raison plusieurs obus (non bash) ne prennent pas la peine d' attendre pour cmd2en cmd2 | cmd1.

Car cmd1 >(cmd2), cependant, ce n'est généralement pas le cas, car c'est plus cmd2qui attend généralement cmd1là-bas, donc il sortira généralement après.

C'est fixé dans zshqui attend cmd2là (mais pas si vous écrivez comme cmd1 > >(cmd2)et cmd1n'est pas BuiltIn, utilisez {cmd1} > >(cmd2)plutôt comme documenté ).

kshn'attend pas par défaut, mais vous permet de l'attendre avec la fonction waitintégrée (cela rend également le pid disponible dans $!, bien que cela n'aide pas si vous le faites cmd1 >(cmd2) >(cmd3))

rc(avec la cmd1 >{cmd2}syntaxe), identique à ce que kshvous pouvez obtenir les pids de tous les processus d'arrière-plan avec $apids.

es(également avec cmd1 >{cmd2}) attend cmd2comme dans zsh, et attend également les redirections cmd2en <{cmd2}cours.

bashne rend pas disponible le pid de cmd2(ou plus exactement du sous-shell car il s'exécute cmd2dans un processus enfant de ce sous-shell même s'il s'agit de la dernière commande) $!, mais ne vous laisse pas l'attendre.

Si vous devez utiliser bash, vous pouvez contourner le problème en utilisant une commande qui attendra les deux commandes avec:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

Cela fait les deux cmd1et cmd2ouvre leur fd 3 à un tuyau. catattendra la fin du fichier à l'autre extrémité, donc ne sortira généralement que lorsque les deux cmd1et cmd2seront morts. Et le shell attendra cette catcommande. Vous pouvez voir cela comme un filet pour attraper la fin de tous les processus en arrière-plan (vous pouvez l'utiliser pour d'autres choses démarrées en arrière-plan comme avec &, les coprocs ou même les commandes qui se mettent en arrière-plan à condition de ne pas fermer tous leurs descripteurs de fichiers comme le font généralement les démons ).

Notez que grâce à ce processus de sous-shell gaspillé mentionné ci-dessus, il fonctionne même si cmd2ferme son fd 3 (les commandes ne font généralement pas cela, mais certains aiment sudoou sshfont). Les futures versions de bashpourraient éventuellement faire l'optimisation comme dans d'autres shells. Ensuite, vous auriez besoin de quelque chose comme:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

Pour vous assurer qu'il y a encore un processus shell supplémentaire avec ce fd 3 open en attente de cette sudocommande.

Notez que catcela ne lira rien (puisque les processus n'écrivent pas sur leur fd 3). Il est juste là pour la synchronisation. Il ne fera qu'un seul read()appel système qui reviendra sans rien à la fin.

Vous pouvez réellement éviter de courir caten utilisant une substitution de commande pour effectuer la synchronisation des canaux:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

Cette fois, c'est le shell au lieu de catcela qui lit le tuyau dont l'autre extrémité est ouverte sur le fd 3 de cmd1et cmd2. Nous utilisons une affectation de variable afin que l'état de sortie de cmd1soit disponible dans $?.

Ou vous pouvez faire la substitution de processus à la main, puis vous pouvez même utiliser votre système shcar cela deviendrait la syntaxe standard du shell:

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

mais notez comme indiqué précédemment que toutes les shimplémentations n'attendraient pas cmd1après la cmd2fin (bien que ce soit mieux que l'inverse). Cette fois, $?contient le statut de sortie de cmd2; cependant bashet zshrendre cmd1le statut de sortie de disponible dans ${PIPESTATUS[0]}et $pipestatus[1]respectivement (voir également l' pipefailoption dans quelques coquilles afin de $?pouvoir signaler la défaillance de composants de tuyaux autres que le dernier)

Notez qu'il yasha des problèmes similaires avec sa fonction de redirection de processus . cmd1 >(cmd2)serait écrit cmd1 /dev/fd/3 3>(cmd2)là-bas. Mais cmd2n'est pas attendu et vous ne pouvez pas non waitplus l'attendre et son pid n'est pas non plus disponible dans la $!variable. Vous utiliseriez les mêmes contournements que pour bash.

Stéphane Chazelas
la source
Tout d'abord, j'ai essayé echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;, puis je l'ai simplifié echo one; echo two > >(cat) | cat; echo three;et il affiche également les valeurs dans le bon ordre. Toutes ces manipulations de descripteurs 3>&1 >&4 4>&-sont-elles nécessaires? De plus, je ne comprends pas >&4 4>&- nous sommes redirigés stdoutvers le quatrième fd, puis fermons le quatrième fd, puis utilisons-le à nouveau 4>&1. Pourquoi en avait-il besoin et comment cela fonctionne-t-il? Peut-être, je devrais créer une nouvelle question sur ce sujet?
MiniMax
1
@MiniMax, mais là, vous affectez la sortie standard de cmd1et cmd2, le point avec la petite danse avec le descripteur de fichier est de restaurer les originaux et d'utiliser uniquement le tuyau supplémentaire pour l' attente au lieu de canaliser également la sortie des commandes.
Stéphane Chazelas
@MiniMax Il m'a fallu un certain temps pour comprendre que je n'avais pas mis les tuyaux à un niveau aussi bas auparavant. Le plus à droite 4>&1crée un descripteur de fichier (fd) 4 pour la liste de commandes des accolades externes et le rend égal à la sortie standard des accolades externes. Les accolades intérieures ont stdin / stdout / stderr configuré automatiquement pour se connecter aux accolades extérieures. Cependant, 3>&1fd 3 se connecte au stdin des accolades externes. >&4connecte la sortie standard des accolades intérieures aux accolades extérieures fd 4 (celle que nous avons créée auparavant). 4>&-ferme fd 4 des accolades intérieures (puisque la sortie standard des accolades intérieures est déjà connectée à la fd 4 des accolades).
Nicholas Pipitone
@MiniMax La partie déroutante était la partie de droite à gauche, elle 4>&1est exécutée en premier, avant les autres redirections, donc vous ne "réutilisez 4>&1" plus. Dans l'ensemble, les accolades internes envoient des données à leur sortie standard, qui a été remplacée par le fd 4 qui leur a été donné. Le fd 4 donné aux accolades intérieures est le fd 4 des accolades extérieures, qui est égal à la sortie d'origine des accolades extérieures.
Nicholas Pipitone
Bash donne l'impression que 4>5"4 va à 5", mais vraiment "fd 4 est remplacé par fd 5". Et avant l'exécution, les fd 0/1/2 sont automatiquement connectés (avec tout fd de la coque externe), et vous pouvez les remplacer comme vous le souhaitez. C'est du moins mon interprétation de la documentation bash. Si vous avez compris quelque chose sur ce , LMK.
Nicholas Pipitone
4

Vous pouvez diriger la deuxième commande vers une autre cat, qui attendra la fermeture de son canal d'entrée. Ex:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

Court et simple.

==========

Aussi simple que cela puisse paraître, il se passe beaucoup de choses dans les coulisses. Vous pouvez ignorer le reste de la réponse si vous ne souhaitez pas savoir comment cela fonctionne.

Lorsque c'est le cas echo two > >(cat); echo three, >(cat)est bifurqué par le shell interactif et s'exécute indépendamment de echo two. Ainsi, se echo twotermine, puis echo threes'exécute, mais avant les >(cat)finitions. Lorsque bashobtient des données à partir du >(cat)moment où il ne s'y attendait pas (quelques millisecondes plus tard), il vous donne cette situation semblable à une invite où vous devez appuyer sur la nouvelle ligne pour revenir au terminal (comme si un autre utilisateur mesgvous avait édité).

Cependant, étant donné echo two > >(cat) | cat; echo three, deux sous-coquilles sont générées (selon la documentation du |symbole).

Un sous-shell nommé A est pour echo two > >(cat), et un sous-shell nommé B est pour cat. A est automatiquement connecté à B (la sortie standard de A est la sortie standard de B). Ensuite, echo twoet >(cat)commencez à exécuter. >(cat)stdout de est défini sur stdout de A, qui est égal à stdin de B. Après avoir echo twoterminé, A sort, fermant sa sortie standard. Cependant, >(cat)tient toujours la référence au stdin de B. Le second catstdin tient le stdin de B, et cela catne sortira pas tant qu'il n'aura pas vu d'EOF. Un EOF n'est donné que lorsque personne n'a plus le fichier ouvert en mode écriture, donc >(cat)stdout bloque le second cat. B attend cette seconde cat. Depuis sa echo twosortie, >(cat)obtient finalement un EOF, donc>(cat)vide son tampon et quitte. Personne ne tient catplus le stdin de B / seconde , donc le second catlit un EOF (B ne lit pas du tout son stdin, il s'en fiche). Cet EOF amène le second catà vider son tampon, à fermer sa sortie standard et à sortir, puis B se termine parce qu'il est catsorti et B attend cat.

Une mise en garde est que bash génère également un sous-shell pour >(cat)! Pour cette raison, vous verrez que

echo two > >(sleep 5) | cat; echo three

attendra encore 5 secondes avant de s'exécuter echo three, même s'il sleep 5ne tient pas le stdin de B. C'est parce qu'un sous-shell caché que C a généré >(sleep 5)attend sleepet que C contient le stdin de B. Vous pouvez voir comment

echo two > >(exec sleep 5) | cat; echo three

N'attendra pas cependant, car il sleepne contient pas le stdin de B, et il n'y a pas de sous-shell fantôme C qui détient le stdin de B (l'exécutif forcera le sommeil à remplacer C, au lieu de bifurquer et de faire attendre C sleep). Indépendamment de cette mise en garde,

echo two > >(exec cat) | cat; echo three

exécutera toujours correctement les fonctions dans l'ordre, comme décrit précédemment.

Nicholas Pipitone
la source
Comme indiqué dans la conversion avec @MiniMax dans les commentaires de ma réponse, cela a cependant l'inconvénient d'affecter la sortie standard de la commande et signifie que la sortie doit être lue et écrite une fois de plus.
Stéphane Chazelas
L'explication n'est pas exacte. An'attend pas l' catapparition >(cat). Comme je le mentionne dans ma réponse, la raison pour laquelle les echo two > >(sleep 5 &>/dev/null) | cat; echo threesorties threeaprès 5 secondes sont dues au fait que les versions actuelles de bashgaspillent un processus shell supplémentaire >(sleep 5)qui attend sleepet que ce processus a toujours une sortie standard pipequi empêche la seconde catde se terminer. Si vous le remplacez par echo two > >(exec sleep 5 &>/dev/null) | cat; echo threepour éliminer ce processus supplémentaire, vous constaterez qu'il revient immédiatement.
Stéphane Chazelas
Cela fait un sous-shell imbriqué? J'ai essayé d'examiner l'implémentation de bash pour le comprendre, je suis presque sûr que echo two > >(sleep 5 &>/dev/null)le minimum obtient son propre sous-shell. Est-ce un détail d'implémentation non documenté qui provoque également l' sleep 5obtention de son propre sous-shell? S'il est documenté, ce serait un moyen légitime de le faire avec moins de caractères (à moins qu'il n'y ait une boucle serrée, je ne pense pas que quiconque remarquera des problèmes de performances avec un sous-shell ou un chat) `. Si ce n'est pas documenté, alors rip, un bon hack, ne fonctionnera pas sur les futures versions.
Nicholas Pipitone
$(...), <(...)impliquent en effet un sous-shell, mais ksh93 ou zsh exécuterait la dernière commande dans ce sous-shell dans le même processus, bashce qui n'est pas la raison pour laquelle il y a encore un autre processus qui maintient le tuyau ouvert pendant qu'il sleepexécute un qui ne maintient pas le tuyau ouvert. Les futures versions de bashpourraient implémenter une optimisation similaire.
Stéphane Chazelas
1
@ StéphaneChazelas J'ai mis à jour ma réponse et je pense que l'explication actuelle de la version courte est correcte, mais vous semblez connaître les détails d'implémentation des shells afin que vous puissiez vérifier. Je pense que cette solution devrait être utilisée par opposition à la danse des descripteurs de fichiers, car même sous exec, cela fonctionne comme prévu.
Nicholas Pipitone