pipe, {liste; } ne fonctionne qu'avec certains programmes

13

Besoin d'explications de la part des utilisateurs expérimentés pour un tel comportement imprévisible:

ps -eF | { head -n 1;grep worker; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
root       441     2  0     0     0   2 paź15 ?       00:00:00 [kworker/2:1H]

tout semble ok alors

ls -la / | { head -n 1;grep sbin; }

affiche uniquement la sortie de head

... J'ai réfléchi stdout 2>&1et ne fonctionne pas non plus pour moi c'est bizarre, des explications ou suggérer comment le gérer?

ast
la source
1
Le dernier devrait tout imprimer. Le headet grepne faites rien là-bas.
jordanm
Oui, tu as raison. Mais au lieu de cela, pourquoi ps -eF fonctionne alors que ls -la / not?
ast

Réponses:

9

J'ai fait des recherches sur l'utilisation straceet cela semble être dû à la façon dont le programme sur le côté gauche du pipeline écrit sur le terminal. Lorsque la lscommande est exécutée, elle écrit toutes les données en un seul write(). Cela fait headconsommer tout stdin.

D'un autre côté, psécrit les données par lots, donc seul le premier write()est consommé par head, puis il existe. Les appels ultérieurs à write()iront au grepprocessus nouvellement généré .

Cela signifie que cela ne fonctionnerait pas si le processus pour lequel vous essayez de grepne se produisait pas dans le premier write(), car il grepne voit pas toutes les données (il voit encore moins que les données moins la première ligne).

Voici un exemple de tentative de grep pour le pid 1 sur mon système:

$ ps -eF | { head -n2; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
root         1     0  0  1697  3768   2 Oct03 ?        00:00:03 /lib/systemd/systemd
$ ps -eF | grep '/lib/systemd/systemd$'
root         1     0  0  1697  3768   2 Oct03 ?        00:00:03 /lib/systemd/systemd
$ ps -eF | { head -n1; grep '/lib/systemd/systemd$'; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD

Votre ps -eFexemple ne fonctionne que par hasard.

jordanm
la source
grande et complète expalnation merci beaucoup
ast
1
En fait, c'est plus une condition de course. C'est juste qu'il est plus lent d'effectuer plusieurs write()appels. Si l' appel headétait lent à exécuter son read()appel (de sorte que le tampon de canal contenait toutes les données), il présenterait le même comportement à la fois sur lset ps.
Patrick
6

Cela est dû à la mise en mémoire tampon dans la glibc. Dans le cas de lsla sortie est dans un tampon interne et en tant que tel est passé juste à head. Pour le ps -eF, la sortie est plus grande et donc une fois headterminée, ce qui suit grepobtient les parties restantes (mais pas la totalité) de la sortie de ps.

Vous pouvez vous en débarrasser en décompressant le tube - par exemple avec sed -u(je ne suis pas sûr que ce ne soit pas une extension GNU):

$ ls -al / | sed -u "#" | { head -n 1; grep bin; }
total 76
drwxr-xr-x   2 root root  4096 Oct  2 21:52 bin
drwxr-xr-x   2 root root  8192 Oct  3 01:54 sbin
peterph
la source
4

Ce qui se passe, c'est qu'il head -n 1lit plus d'une ligne. Pour un débit optimal, head lit des morceaux d'octets, il peut donc lire 1024 octets à la fois, puis rechercher dans ces octets le premier saut de ligne. Étant donné que le saut de ligne peut se produire au milieu de ces 1024 octets, le reste des données est perdu. Il ne peut pas être remis sur le tuyau. Ainsi, le prochain processus qui s'exécute n'obtient que 1025 octets et plus.

Votre première commande réussit parce que le kworkerprocessus est après ce premier morceau qui headlit.

Pour que cela fonctionne, il headfaudrait lire 1 caractère à la fois. Mais c'est extrêmement lent, donc ce n'est pas le cas.
La seule façon de faire quelque chose comme ça efficacement est d'avoir un seul processus à la fois "head" et "grep".

Voici deux façons de procéder:

echo -e '1\n2\n3\n4\n5' | perl -ne 'print if $i++ == 0 || /4/'

ou

echo -e '1\n2\n3\n4\n5' | awk '{if (NR == 1 || /4/) print }'

Il y a beaucoup plus...

Patrick
la source
oui, je connaissais 'way of awk' pour gérer cette tâche, mais je me demandais pourquoi le comportement était si imprévisible avec {list; }. Merci d'avoir clarifié comment cela fonctionne. Je suis impressionné par toutes les réponses ci - dessus
ast
2

Si vous ne voulez que la première ligne ou deux, le type d'astuce suivant fonctionne et évite les problèmes de mise en mémoire tampon causés par l'utilisation de deux commandes différentes pour lire le flux de sortie:

$ ps -eF   | { IFS= read -r x ; echo "$x" ; grep worker; }
$ ls -la / | { IFS= read -r x ; echo "$x" ; grep sbin; }

Le readest intégré au shell et ne consomme pas tout un tampon d'entrée juste pour sortir la ligne, donc l'utilisation readlaisse tout le reste de la sortie pour la commande suivante.

Si vous souhaitez accentuer les problèmes de mise en mémoire tampon illustrés par vos exemples qui utilisent deux commandes différentes, ajoutez-y un sleeppour éliminer les problèmes de synchronisation et autorisez la commande de gauche à générer toutes ses sorties avant que les commandes de droite n'essaient de lire il:

$ ps -eF   | { sleep 5 ; head -n 1 ; grep worker; }
$ ls -la / | { sleep 5 ; head -n 1 ; grep sbin; }

Maintenant, les deux exemples ci-dessus échouent de la même manière - le headlit un tampon entier de la sortie juste pour produire la seule ligne, et ce tampon n'est pas disponible pour les suivants grep.

Vous pouvez voir le problème de mise en mémoire tampon encore plus clairement en utilisant quelques exemples qui numérotent les lignes de sortie afin que vous puissiez savoir quelles lignes sont manquantes:

$ ps -eF          | cat -n | { sleep 5 ; head -n 1 ; head ; }
$ ls -la /usr/bin | cat -n | { sleep 5 ; head -n 1 ; head ; }

Un moyen simple de voir le problème de mise en mémoire tampon est d'utiliser sequn outil qui génère une liste de nombres. Nous pouvons facilement identifier les numéros manquants:

$ seq 1 100000    | { sleep 5 ; head -n 1 ; head ; }
1

1861
1862
1863
1864
1865
1866
1867
1868
1869

Ma solution astucieuse utilisant le shell pour lire et faire écho à la première ligne fonctionne correctement même avec le retard de sommeil ajouté:

$ seq 1 100000 | { sleep 5 ; IFS= read -r x ; echo "$x" ; head ; }
1
2
3
4
5
6
7
8
9
10
11

Ci-dessous est un exemple complet montrant les headproblèmes de mise en mémoire tampon, montrant comment headconsomme une mémoire tampon entière de la sortie juste pour produire ses cinq lignes à chaque fois. Ce tampon consommé n'est pas disponible pour la headcommande suivante de la séquence:

$ seq 1 100000 | { sleep 5 ; head -5 ; head -5 ; head -5 ; head -5 ; }
1
2
3
4
5

1861
1862
1863
1864
499
3500
3501
3502
3503
7
5138
5139
5140
5141

En regardant le nombre 1861ci-dessus, nous pouvons calculer la taille du tampon utilisé headen comptant la seqsortie de 1à 1860:

$ seq 1 1860 | wc -c
8193

Nous voyons que cela headmet en mémoire tampon en lisant un 8 Ko complet (8 * 1024 octets) de la sortie du tuyau à la fois, même pour produire seulement quelques lignes de sa propre sortie.

Ian D. Allen
la source