Des tuyaux «qui fuient» sous Linux

12

Supposons que vous ayez un pipeline comme celui-ci:

$ a | b

Si barrête le traitement stdin, après un certain temps, le tuyau se remplit et écrit, de aà sa sortie standard, se bloquera (jusqu'à ce que le btraitement recommence ou qu'il meure).

Si je voulais éviter cela, je pourrais être tenté d'utiliser un tuyau plus gros (ou, plus simplement buffer(1)) comme ceci:

$ a | buffer | b

Cela me ferait simplement gagner plus de temps, mais afinirait par s'arrêter.

Ce que j'aimerais avoir (pour un scénario très spécifique que j'aborde), c'est d'avoir un tuyau "qui fuit" qui, une fois plein, laisserait tomber certaines données (idéalement, ligne par ligne) du tampon pour laisser acontinuer traitement (comme vous pouvez probablement l'imaginer, les données qui circulent dans le tuyau sont extensibles, c'est-à-dire que le traitement des données best moins important que de apouvoir fonctionner sans blocage).

Pour résumer, j'aimerais avoir quelque chose comme un tampon délimité et qui fuit:

$ a | leakybuffer | b

Je pourrais probablement l'implémenter assez facilement dans n'importe quelle langue, je me demandais juste s'il y avait quelque chose de "prêt à l'emploi" (ou quelque chose comme un bash one-liner) qui me manque.

Remarque: dans les exemples, j'utilise des tuyaux normaux, mais la question s'applique également aux tuyaux nommés


Bien que j'aie attribué la réponse ci-dessous, j'ai également décidé d'implémenter la commande leakybuffer car la solution simple ci-dessous avait certaines limites: https://github.com/CAFxX/leakybuffer

CAFxX
la source
Les pipes nommées se remplissent-elles vraiment? J'aurais pensé que les tuyaux nommés sont la solution à cela, mais je ne pouvais pas dire avec certitude.
Wildcard
3
Les canaux nommés ont (par défaut) la même capacité que les canaux non nommés, AFAIK
CAFxX

Réponses:

14

Le moyen le plus simple serait de passer par un programme qui définit une sortie non bloquante. Voici un simple perl oneliner (que vous pouvez enregistrer en tant que leakybuffer ) qui le fait:

donc votre a | bdevient:

a | perl -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { print }' | b

ce qui est fait est de lire l'entrée et d'écrire sur la sortie (comme cat(1)) mais la sortie n'est pas bloquante - ce qui signifie que si l'écriture échoue, elle renverra une erreur et perdra des données, mais le processus se poursuivra avec la ligne d'entrée suivante car nous ignorons commodément le Erreur. Le processus est en quelque sorte mis en mémoire tampon comme vous le souhaitez, mais voir la mise en garde ci-dessous.

vous pouvez tester avec par exemple:

seq 1 500000 | perl -w -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { print }' | \
    while read a; do echo $a; done > output

vous obtiendrez un outputfichier avec des lignes perdues (la sortie exacte dépend de la vitesse de votre shell, etc.) comme ceci:

12768
12769
12770
12771
12772
12773
127775610
75611
75612
75613

vous voyez où le shell a perdu des lignes après 12773, mais aussi une anomalie - le perl n'avait pas assez de tampon pour 12774\nmais il l'a fait 1277donc il a écrit juste cela - et donc le numéro suivant 75610ne commence pas au début de la ligne, ce qui le rend peu laid.

Cela pourrait être amélioré en ayant perl détecter quand l'écriture n'a pas complètement réussi, puis essayer plus tard de vider le reste de la ligne tout en ignorant les nouvelles lignes qui arrivent, mais cela compliquerait beaucoup plus le script perl, donc est laissé comme exercice pour le lecteur intéressé :)

Mise à jour (pour les fichiers binaires): Si vous ne traitez pas les lignes terminées par la nouvelle ligne (comme les fichiers journaux ou similaires), vous devez modifier légèrement la commande, ou perl consommera de grandes quantités de mémoire (selon la fréquence à laquelle les caractères de nouvelle ligne apparaissent dans votre entrée):

perl -w -MFcntl -e 'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (read STDIN, $_, 4096) { print }' 

cela fonctionnera également correctement pour les fichiers binaires (sans consommer de mémoire supplémentaire).

Update2 - sortie de fichier texte plus agréable: éviter les tampons de sortie ( syswriteau lieu de print):

seq 1 500000 | perl -w -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { syswrite STDOUT,$_ }' | \
    while read a; do echo $a; done > output

semble résoudre les problèmes avec les "lignes fusionnées" pour moi:

12766
12767
12768
16384
16385
16386

(Remarque: on peut vérifier sur quelles lignes la sortie a été coupée avec: perl -ne '$c++; next if $c==$_; print "$c $_"; $c=$_' outputoneliner)

Matija Nalis
la source
J'adore le Oneliner: je ne suis pas un expert en Perl, si quelqu'un pouvait suggérer les améliorations ci-dessus, ce serait génial
CAFxX
1
Cela semble fonctionner dans une certaine mesure . Mais pendant que je regarde ma commande perl -w -MFcntl -e 'fcntl STDOUT,F_SETFL,O_WRONLY|O_NONBLOCK; while (<STDIN>) { print }' | aplay -t raw -f dat --buffer-size=16000, Perl semble allouer continuellement plus de mémoire jusqu'à ce qu'il soit tué par le gestionnaire OOM.
Ponkadoodle
@Wallacoloo merci d'avoir souligné cela, mon cas était en train de diffuser des fichiers journaux ... Voir la réponse mise à jour pour un léger changement nécessaire pour prendre en charge les fichiers binaires.
Matija Nalis
Voir aussi GNU ddde dd oflag=nonblock status=none.
Stéphane Chazelas
1
Désolé, encore une fois, les écritures de moins de PIPE_BUF octets (4096 sous Linux, requis pour être au moins 512 par POSIX) sont garanties atomiques, $| = 1et votre syswrite()approche empêche donc les écritures courtes tant que les lignes sont raisonnablement courtes.
Stéphane Chazelas