Désactiver la mise en mémoire tampon dans le tuyau

396

J'ai un script qui appelle deux commandes:

long_running_command | print_progress

Les long_running_commandempreintes sont un progrès, mais je ne suis pas satisfait. J'utilise print_progresspour le rendre plus agréable (à savoir, j'imprime la progression en une seule ligne).

Le problème: La connexion d’un tuyau à stdout active également un tampon 4K, le programme d’impression n’obtenant rien… rien… rien… beaucoup … :)

Comment puis-je désactiver le tampon 4K pour le long_running_command(non, je n'ai pas la source)?

Aaron Digulla
la source
1
Ainsi, lorsque vous exécutez long_running_command sans tuyauterie, vous pouvez voir les mises à jour de progression correctement, mais lorsque les tuyauteries sont mises en mémoire tampon?
1
Oui, c'est exactement ce qui se passe.
Aaron Digulla
21
L’incapacité de trouver un moyen simple de contrôler la mise en mémoire tampon est un problème depuis des décennies. Par exemple, voir: marc.info/?l=glibc-bug&m=98313957306297&w=4 qui dit en gros "je ne peux pas être arsé pour faire cela et voici quelques pièges pour justifier ma position"
1
Ce n’est pas le canal qui cause un retard dans l’attente de données suffisantes. Les tuyaux ont une capacité, mais dès qu’une donnée est écrite dans le tuyau, il est immédiatement prêt à être lu à l’autre bout.
Sam Watkins

Réponses:

254

Vous pouvez utiliser la unbuffercommande (qui fait partie du expectpaquet), par exemple

unbuffer long_running_command | print_progress

unbufferse connecte à long_running_commandvia un pseudoterminal (pty), ce qui fait que le système le traite comme un processus interactif; par conséquent, n'utilisez pas la mise en mémoire tampon de 4 kio dans le pipeline qui est la cause probable du retard.

Pour les pipelines plus longs, vous devrez peut-être débuffer chaque commande (sauf la dernière), par exemple

unbuffer x | unbuffer -p y | z
Stephen Kitt
la source
3
En fait, l’utilisation d’un pty pour se connecter à des processus interactifs est vraie en général.
15
Lors du traitement en pipeline des appels vers la mémoire tampon, vous devez utiliser l'argument -p pour que la mémoire tampon mate à partir de stdin.
26
Remarque: sur les systèmes Debian, cela s'appelle expect_unbufferet se trouve dans le expect-devpaquet, pas dans le expectpaquet
bdonlan le
4
@bdonlan: au moins sur Ubuntu (basé sur Debian), expect-devfournit les deux unbufferet expect_unbuffer(le premier est un lien symbolique vers le second). Les liens sont disponibles depuis expect 5.44.1.14-1(2009).
Jfs
1
Remarque: sur les systèmes Ubuntu 14.04.x, cela se trouve également dans le paquet expect-dev.
Alexandre Mazel
463

Une autre façon de peauner ce chat est d'utiliser le stdbufprogramme, qui fait partie de GNU Coreutils (FreeBSD en possède également un).

stdbuf -i0 -o0 -e0 command

Cela désactive complètement la mise en mémoire tampon pour les entrées, les sorties et les erreurs. Pour certaines applications, la mise en mémoire tampon des lignes peut être plus appropriée pour des raisons de performances:

stdbuf -oL -eL command

Notez que cela ne fonctionne que pour la stdiomise en mémoire tampon ( printf(), fputs()...) pour les applications liées dynamiquement, et uniquement si cette application ne règle pas la mise en mémoire tampon de ses flux standard par elle-même, bien que cela devrait couvrir la plupart des applications.

a3nm
la source
6
"unbuffer" doit être installé dans Ubuntu, qui est dans le paquet: expect-dev qui fait 2MB ...
lepe
2
Cela fonctionne très bien sur l’installation par défaut de Raspbian pour la journalisation sans tampon. J'ai trouvé des sudo stdbuff … commandœuvres bien stdbuff … sudo commandque non.
mardi
20
@qdii stdbufne fonctionne pas avec tee, car teeécrase les valeurs par défaut définies par stdbuf. Voir la page de manuel de stdbuf.
Ceving
5
@lepe Bizarrely, unbuffer a des dépendances sur x11 et tcl / tk, ce qui signifie qu'il a réellement besoin de plus de 80 Mo si vous l'installez sur un serveur qui n'en contient pas.
Jpatokal
10
@qdii stdbufutilise un LD_PRELOADmécanisme pour insérer sa propre bibliothèque chargée dynamiquement libstdbuf.so. Cela signifie qu'il ne fonctionnera pas avec les types de fichiers exécutables suivants: avec setuid ou les capacités de fichier définies, liées statiquement, sans utiliser la bibliothèque standard. Dans ces cas, il est préférable d'utiliser les solutions avec unbuffer/ script/ socat. Voir aussi stdbuf avec setuid / capacités .
pabouk
75

Une autre façon d'activer le mode de sortie avec mise en mémoire tampon de lignes long_running_commandconsiste à utiliser la scriptcommande qui exécute votre long_running_commandpseudo-terminal (pty).

script -q /dev/null long_running_command | print_progress      # FreeBSD, Mac OS X
script -c "long_running_command" /dev/null | print_progress    # Linux
chad
la source
15
+1 joli tour, puisqu'il scripts'agit d'une commande si ancienne, elle devrait être disponible sur toutes les plates-formes de type Unix.
Aaron Digulla
5
vous avez aussi besoin -qsous Linux:script -q -c 'long_running_command' /dev/null | print_progress
jfs
1
Il semble que le script se lit à partir de stdin, ce qui rend impossible l’exécution d’une telle tâche long_running_commanden arrière-plan, du moins lorsqu’il est lancé depuis un terminal interactif. Pour contourner le problème, j'ai pu rediriger stdin /dev/null, car mon long_running_commandne l'utilise pas stdin.
haridsv
1
Fonctionne même sur Android.
not2qubit
3
Un inconvénient important: ctrl-z ne fonctionne plus (je ne peux pas suspendre le script). Cela peut être corrigé, par exemple: echo | script sudo -c / usr / local / bin / ec2-snapshot-all / dev / null | ts, si cela ne vous dérange pas de ne pas pouvoir interagir avec le programme.
rlpowell
66

Pour grep, sedet awkvous pouvez forcer la sortie à la ligne tamponne. Vous pouvez utiliser:

grep --line-buffered

Force la sortie à être mise en mémoire tampon. Par défaut, la sortie est mise en mémoire tampon de ligne lorsque la sortie standard est un terminal et la mise en mémoire tampon de bloc autrement.

sed -u

Rendre la ligne de sortie en mémoire tampon.

Voir cette page pour plus d'informations: http://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html

Yaneku
la source
51

S'il y a un problème avec la libc qui modifie sa mise en tampon / vidage lorsque la sortie ne va pas à un terminal, vous devriez essayer socat . Vous pouvez créer un flux bidirectionnel entre presque n'importe quel type de mécanisme d'E / S. Un de ceux-ci est un programme forké qui parle à un pseudo tty.

 socat EXEC:long_running_command,pty,ctty STDIO 

Ce qu'il fait est

  • créer un pseudo tty
  • fork long_running_command avec le côté esclave du pty comme stdin / stdout
  • établir un flux bidirectionnel entre le côté maître du pty et la deuxième adresse (ici c'est STDIO)

Si cela vous donne le même résultat que long_running_command, alors vous pouvez continuer avec un tuyau.

Edit: Wow Vous n'avez pas vu la réponse du tampon! Eh bien, socat est de toute façon un excellent outil, je pourrais donc laisser cette réponse

Shodanex
la source
1
... et je ne savais pas que socat - ressemble un peu à Netcat mais peut-être plus encore. ;) Merci et +1.
3
J'utiliserais socat -u exec:long_running_command,pty,end-close -ici
Stéphane Chazelas
20

Vous pouvez utiliser

long_running_command 1>&2 |& print_progress

Le problème est que libc mettra le tampon de ligne lorsque stdout à l'écran et le tampon complet lorsque stdout dans un fichier. Mais pas de tampon pour stderr.

Je ne pense pas que ce soit le problème avec le tampon de pipe, tout repose sur la politique de tampon de libc.

Wang HongQin
la source
Vous avez raison; Ma question est toujours la suivante: comment puis-je influencer la politique de tampon de libc sans recompiler?
Aaron Digulla
@ StéphaneChazelas fd1 sera redirigé vers stderr
Wang HongQin
@ StéphaneChazelas Je ne comprends pas votre argument. plz faire un test, cela fonctionne
Wang HongQin
3
Bien, ce qui se passe, c’est que c’est avec les deux zsh(d’où |&vient de adapté de csh) et bash, lorsque vous le faites cmd1 >&2 |& cmd2, les deux fd 1 et 2 sont connectés à la sortie externe. Cela empêche donc la mise en mémoire tampon lorsque cette sortie externe est un terminal, mais uniquement parce que la sortie ne passe pas par le canal (elle print_progressn'imprime donc rien). Donc, c'est la même chose que long_running_command & print_progress(sauf que print_progress stdin est un tuyau qui n'a pas d'écrivain). Vous pouvez vérifier avec ls -l /proc/self/fd >&2 |& catcomparé à ls -l /proc/self/fd |& cat.
Stéphane Chazelas
3
C'est parce que |&c'est court 2>&1 |, littéralement. Ainsi l' cmd1 |& cmd2est cmd1 1>&2 2>&1 | cmd2. Ainsi, les deux signaux 1 et 2 finissent par être connectés au stderr d'origine, et rien ne reste à écrire sur le tuyau. ( s/outer stdout/outer stderr/gdans mon commentaire précédent).
Stéphane Chazelas
11

Auparavant, et probablement encore, lorsque la sortie standard est écrite dans un terminal, la ligne est mise en mémoire tampon par défaut. Lorsqu'une nouvelle ligne est écrite, la ligne est écrite dans le terminal. Lorsque la sortie standard est envoyée à un canal, celle-ci est entièrement mise en mémoire tampon. Ainsi, les données ne sont envoyées qu'au processus suivant du pipeline lorsque le tampon d'E / S standard est rempli.

C'est la source du problème. Je ne sais pas si vous pouvez faire grand chose pour résoudre ce problème sans modifier le programme écrit dans le tuyau. Vous pouvez utiliser la setvbuf()fonction avec l' _IOLBFindicateur pour passer inconditionnellement stdouten mode de mise en tampon de ligne. Mais je ne vois pas de moyen facile d'appliquer cela à un programme. Ou bien le programme peut le faire fflush()aux moments appropriés (après chaque ligne de sortie), mais le même commentaire s’applique.

Je suppose que si vous remplaciez le canal par un pseudo-terminal, la bibliothèque d'E / S standard penserait que la sortie est un terminal (car il s'agit d'un type de terminal) et alignera automatiquement le tampon en ligne. C'est une façon complexe de traiter les choses, cependant.

Jonathan Leffler
la source
7

Je sais que c’est une vieille question et que de nombreuses réponses ont déjà été apportées, mais si vous souhaitez éviter le problème de la mémoire tampon, essayez quelque chose du genre:

stdbuf -oL tail -f /var/log/messages | tee -a /home/your_user_here/logs.txt

Cela affichera les journaux en temps réel, les enregistrera également dans le logs.txtfichier et le tampon n’affectera plus la tail -fcommande.

Marin Nedea
la source
4
Cela ressemble à la deuxième réponse: - /
Aaron Digulla
2
stdbuf est inclus dans gnu coreutils (j'ai vérifié la dernière version 8.25). vérifié cela fonctionne sur un linux embarqué.
Zhaorufei
D'après la documentation de stdbuf,NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for example) then that will override corresponding changes by 'stdbuf'.
shrewmouse
6

Je ne pense pas que le problème est avec le tuyau. Il semble que votre processus de longue durée ne vide pas suffisamment son propre tampon. Changer la taille de la mémoire tampon du tuyau serait un hack pour le contourner, mais je ne pense pas que ce soit possible sans reconstruire le noyau - ce que vous ne voudriez pas faire comme un hack, car il affectera probablement beaucoup d'autres processus.


la source
18
La cause principale est que la libc bascule vers la mémoire tampon de 4k si la sortie standard n'est pas un terminal.
Aaron Digulla
5
C'est très intéressant ! parce que le tuyau ne provoque pas de tampon. Ils fournissent une mise en mémoire tampon, mais si vous lisez à partir d'un canal, vous obtenez toutes les données disponibles, vous n'avez pas besoin d'attendre un tampon dans le canal. Donc, le coupable serait la mise en mémoire tampon stdio dans l'application.
3

Selon ce post , vous pouvez essayer de réduire le tuyau ulimit à un seul bloc de 512 octets. Cela ne désactivera certainement pas la mise en mémoire tampon, mais bon, 512 octets est bien moins que 4K: 3

RAKK
la source
3

Dans la même veine que la réponse de chad , vous pouvez écrire un petit script comme celui-ci:

# save as ~/bin/scriptee, or so
script -q /dev/null sh -c 'exec cat > /dev/null'

Ensuite, utilisez cette scripteecommande en remplacement de tee.

my-long-running-command | scriptee

Hélas, je n'arrive pas à faire en sorte qu'une telle version fonctionne parfaitement sous Linux. Cela semble donc être limité aux Unix de type BSD.

Sous Linux, c'est proche, mais vous ne recevez pas votre invite quand elle se termine (jusqu'à ce que vous appuyiez sur entrée, etc.) ...

script -q -c 'cat > /proc/self/fd/1' /dev/null
jwd
la source
Pourquoi ça marche? Est-ce que "script" désactive la mise en mémoire tampon?
Aaron Digulla
@Aaron Digulla: scriptémule un terminal, alors oui, je pense qu'il désactive la mise en mémoire tampon. Il renvoie également chaque caractère qui lui catest envoyé - c’est pourquoi il est envoyé /dev/nulldans l’exemple. En ce qui concerne le programme en cours d'exécution script, il s'agit d'une session interactive. Je crois que cela ressemble à expectcet égard, mais fait scriptprobablement partie de votre système de base.
Jwd
La raison pour laquelle j'utilise teeest d'envoyer une copie du flux dans un fichier. Où le fichier est-il spécifié scriptee?
Bruno Bronosky le
@BrunoBronosky: Vous avez raison, c'est un mauvais nom pour ce programme. Ce n'est pas vraiment une opération de «tee». Il s’agit simplement de désactiver la mise en mémoire tampon de la sortie, conformément à la question initiale. Peut-être que cela devrait s'appeler "scriptcat" (bien que ce ne soit pas une concaténation non plus ...). Quoi qu’il en soit, vous pouvez remplacer la catcommande par tee myfile.txtet obtenir l’effet souhaité.
jwd
2

J'ai trouvé cette solution intelligente: (echo -e "cmd 1\ncmd 2" && cat) | ./shell_executable

Cela fait l'affaire. catlira des entrées supplémentaires (jusqu’à EOF) et les transmettra au tube après que le echoa placé ses arguments dans le flux d’entrée de shell_executable.

jaggedsoft
la source
2
En fait, catne voit pas la sortie de la echo; vous exécutez simplement deux commandes dans un sous-shell et la sortie des deux est envoyée dans le canal. La deuxième commande du sous-shell ('cat') lit à partir du parent / stdin externe, c’est pourquoi cela fonctionne.
Aaron Digulla
0

Selon cela, la taille de la mémoire tampon du tube semble être définie dans le noyau et vous obligerait à recompiler votre noyau pour le modifier.


la source
7
Je crois que c'est un tampon différent.
Samuel Edwin Ward