Вимкніть буферизацію в трубі


395

У мене є сценарій, який викликає дві команди:

long_running_command | print_progress

У long_running_commandдрукує прогрес , але я нещасна з ним. Я використовую, print_progressщоб зробити це більш приємним (а саме, я надрукую прогрес в одному рядку).

Проблема: Підключення труби до stdout також активує 4K буфер, до приємної програми друку нічого не виходить ... нічого ... нічого ... ціла партія ... :)

Як я можу відключити 4K буфер для long_running_command(ні, у мене немає джерела)?


1
Отже, коли ви запускаєте long_running_command без трубопроводів, ви можете бачити оновлення прогресу належним чином, але при трубопроводі вони буферуються?

1
Так, саме так і відбувається.
Аарон Дігулла

20
Нездатність простого способу управління буферизацією є проблемою протягом десятиліть. Наприклад, див: marc.info/?l=glibc-bug&m=98313957306297&w=4 , який основно каже : «Я не можу бути arsed робити це і ось деякі просторікування , щоб виправдати свою позицію»


1
Це насправді stdio, а не труба, яка викликає затримку під час очікування достатньої кількості даних. Труби мають ємність, але, як тільки є якісь дані, записані в трубу, вона негайно готова для читання на іншому кінці.
Сем Уоткінс

Відповіді:


254

Ви можете використовувати unbufferкоманду (яка входить до складу expectпакету), наприклад

unbuffer long_running_command | print_progress

unbufferпідключається до long_running_commandпсевдотерміналу (pty), що змушує систему трактувати це як інтерактивний процес, тому не використовуючи буферизацію 4-кіБ в трубопроводі, що є ймовірною причиною затримки.

Для більш довгих конвеєрів, можливо, вам доведеться розблокувати кожну команду (крім останньої), наприклад

unbuffer x | unbuffer -p y | z

3
Насправді, використання pty для підключення до інтерактивних процесів справді очікується загалом.

15
Під час конвеєрних викликів на випаковувач слід використовувати аргумент -p, щоб сканер зчитувався з stdin.

26
Примітка. У системах debian це називається expect_unbufferі є в expect-devпакеті, а не в expectпакеті
bdonlan

4
@bdonlan: Принаймні, на Ubuntu (на основі debian) expect-devнадає і те, unbufferі expect_unbuffer(перше є символьним посиланням на останнє). Посилання доступні з expect 5.44.1.14-1(2009).
jfs

1
Примітка. У системах Ubuntu 14.04.x ​​він також знаходиться в пакеті очікування-розробника.
Олександр Мазель

462

Ще одним способом шкіри цього кота є використання stdbufпрограми, яка є частиною GNU Coreutils (FreeBSD також має свою власну).

stdbuf -i0 -o0 -e0 command

Це повністю вимикає буферизацію для введення, виводу та помилки. Для деяких застосувань буферизація ліній може бути більш підходящою з міркувань продуктивності:

stdbuf -oL -eL command

Зауважте, що він працює лише для stdioбуферизації ( printf(), fputs()...) для динамічно пов'язаних додатків, і лише якщо ця програма не налаштовує буферизацію своїх стандартних потоків самостійно, хоча це повинно охоплювати більшість програм.


6
"unbuffer" потрібно встановити в Ubuntu, який знаходиться всередині пакета: очікуємо-dev, який становить 2 Мб ...
lepe

2
Це чудово працює при встановленні програми raspbian за замовчуванням для розблокування журналу. Я знайшов sudo stdbuff … commandтвори, хоча stdbuff … sudo commandне став.
natevw

20
@qdii stdbufне працює tee, тому що teeперезаписує параметри за замовчуванням stdbuf. Дивіться сторінку керівництва від stdbuf.
квітня

5
@lepe Bizarrely, unbuffer має залежність від x11 та tcl / tk, це означає, що він фактично потребує> 80 Мб, якщо ви встановлюєте його на сервер без них.
jpatokal

10
@qdii stdbufвикористовує LD_PRELOADмеханізм для вставки власної динамічно завантаженої бібліотеки libstdbuf.so. Це означає, що він не працюватиме з такими видами виконуваних файлів: із встановленими можливостями setuid або файлів, статично пов'язаними, не використовуючи стандартних libc. У цих випадках краще використовувати рішення з unbuffer/ script/ socat. Дивіться також stdbuf із налаштуваннями / можливостями .
пабук

75

Ще один спосіб увімкнути вихідний режим буферизації ліній для використання long_running_command- це використовувати scriptкоманду, яка виконує ваш текст long_running_commandу псевдотерміналі (pty).

script -q /dev/null long_running_command | print_progress      # FreeBSD, Mac OS X
script -c "long_running_command" /dev/null | print_progress    # Linux

15
+1 хороший трюк, оскільки scriptце така стара команда, вона повинна бути доступна на всіх платформах, схожих на Unix.
Аарон Дігулла

5
вам також потрібно -qв Linux:script -q -c 'long_running_command' /dev/null | print_progress
jfs

1
Схоже, що сценарій читає з stdin, що робить неможливим запуск такого long_running_commandу фоновому режимі, принаймні при запуску з інтерактивного терміналу. Щоб вирішити, я зміг переадресувати stdin /dev/null, оскільки мій long_running_commandне використовує stdin.
haridsv

1
Навіть працює на Android.
not2qubit

3
Один істотний недолік: ctrl-z більше не працює (тобто я не можу призупинити сценарій). Це можна зафіксувати, наприклад: echo | сценарій sudo -c / usr / local / bin / ec2-знімок-all / dev / null | ts, якщо ви не заперечуєте, що не можете взаємодіяти з програмою.
rlpowell

66

Для grep, sedі awkви можете змусити вихід бути буферизованим рядком. Ви можете використовувати:

grep --line-buffered

Виведення сили для буферизації ліній. За замовчуванням вихідний сигнал буферний, коли стандартний вихід є терміналом, а блок - буферний інший.

sed -u

Зробіть буферну лінію вихідної лінії.

Дивіться цю сторінку для отримання додаткової інформації: http://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html


51

Якщо проблема є в тому, що libc змінює буферизацію / промивання, коли вихід не надходить на термінал, слід спробувати socat . Ви можете створити двосторонній потік між практично будь-яким механізмом вводу / виводу. Один з них - це роздвоєна програма, яка виступає перед псевдо.

 socat EXEC:long_running_command,pty,ctty STDIO 

Що це робить

  • створити псевдо tty
  • fork long_running_command з підлеглою стороною pty як stdin / stdout
  • встановити двонаправлений потік між основною стороною pty та другою адресою (ось це STDIO)

Якщо це дає такий самий вихід, як long_running_commandі тоді, ви можете продовжити трубу.

Редагувати: Вау Не бачив відповіді про розблокування! Ну, socat - це все-таки чудовий інструмент, тому я можу просто залишити цю відповідь


1
... а я не знав про socat - схоже, начебто netcat, можливо, тим більше. ;) Дякую та +1.

3
Я б socat -u exec:long_running_command,pty,end-close -тут застосував
Стефан Шазелас

20

Можна використовувати

long_running_command 1>&2 |& print_progress

Проблема полягає в тому, що libc буде лінійним буфером при stdout на екран, а full-буфером при stdout у файл. Але не буфер для stderr.

Я не думаю, що це проблема в буфері труби, це все в буферній політиці libc.


Ти правий; моє запитання все ще залишається: Як я можу вплинути на буферну політику libc, не переробляючи її?
Аарон Дігулла

@ StéphaneChazelas fd1 буде переспрямований на stderr
Ван HongQin

@ StéphaneChazelas я не розумію вашої точки зору. плз зробити тест, він працює
Ван HongQin

3
Добре, що відбувається - це те, що і з обох zsh(звідки |&походить з адаптованого з csh), і bash, коли ви робите cmd1 >&2 |& cmd2, обидва fd 1 і 2 підключені до зовнішньої версії stdout. Таким чином, це працює у запобіганні буферизації, коли зовнішня вивірка є терміналом, але тільки тому, що вихід не проходить через трубу (тому print_progressнічого не друкується). Отже, це те саме, що long_running_command & print_progress(крім того, що print_progress stdin - це труба, яка не має запису). Ви можете підтвердити ls -l /proc/self/fd >&2 |& catпорівняно з ls -l /proc/self/fd |& cat.
Стефан Шазелас

3
Це тому |&, що коротко 2>&1 |, буквально. Так і cmd1 |& cmd2є cmd1 1>&2 2>&1 | cmd2. Отже, і fd 1 і 2 в кінцевому підключенні підключені до вихідного stderr, і нічого не залишається записати на трубу. ( s/outer stdout/outer stderr/gу моєму попередньому коментарі).
Стефан Шазелас

11

Раніше це було, і, мабуть, все ще так, що коли стандартний вихід записується в термінал, він буферизується за замовчуванням - коли пишеться новий рядок, рядок записується до терміналу. Коли стандартний вихід надсилається в трубу, він повністю буферизується - тому дані надсилаються до наступного процесу в трубопроводі, коли заповнений стандартний буфер вводу / виводу.

Оце джерело неприємностей. Я не впевнений, чи можна багато зробити, щоб виправити це, не змінюючи запис програми в трубу. Ви можете використовувати setvbuf()функцію з _IOLBFпрапором, щоб беззастережно перевести stdoutв режим буферизації рядків. Але я не бачу простий спосіб застосувати це в програмі. Або програма може робити fflush()у відповідних точках (після кожного рядка виводу), але застосовується той самий коментар.

Я припускаю, що якщо ви замінили трубу на псевдотермінал, то стандартна бібліотека вводу / виводу подумала б, що вихід є терміналом (оскільки це тип терміналу) і буде автоматично буферувати рядок. Але це складний спосіб поводження з речами.


7

Я знаю, що це старе питання, і на нього вже було багато відповідей, але якщо ви хочете уникнути проблеми з буфером, просто спробуйте щось на кшталт:

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

Це видасть журнали в режимі реального часу, а також збереже їх у logs.txtфайл, і буфер більше не впливатиме на tail -fкоманду.


4
Це виглядає як друга відповідь: - /
Аарон Дігулла

2
stdbuf включений до gnu coreutils (я перевірив останню версію 8.25). підтверджено це працює на вбудованому Linux.
zhaorufei

З документації stdbuf, NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for example) then that will override corresponding changes by 'stdbuf'.
майстриня

6

Я не думаю, що проблема з трубою. Це здається, що ваш тривалий процес недостатньо часто промиває власний буфер. Зміна розміру буфера труби було б злому, щоб обійти його, але я не думаю, що це неможливо без відновлення ядра - те, що ви б не хотіли робити як хак, оскільки це, ймовірно, аверслі впливає на безліч інших процесів.


18
Першопричиною є те, що libc переходить на 4k буферизацію, якщо stdout не є tty.
Аарон Дігулла

5
Це дуже цікаво! тому що труба не викликає буферизації. Вони забезпечують буферизацію, але якщо ви читаєте з труби, ви отримуєте будь-які доступні дані, вам не доведеться чекати буфера в трубі. Тож винуватцем буде буферизація stdio у додатку.

3

Відповідно до цієї публікації тут , ви можете спробувати зменшити обмеження труби до одного блоку 512 байтів. Це, безумовно, не вимикає буферизацію, але добре, 512 байт набагато менше, ніж 4K: 3


3

Як і у відповідь Чада , ви можете написати такий маленький сценарій:

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

Потім використовуйте цю scripteeкоманду як заміну для tee.

my-long-running-command | scriptee

На жаль, я не можу отримати подібну версію, яка б прекрасно працювала в Linux, тому, здається, обмежена уніксами стилю BSD.

В Linux це близько, але ви не отримаєте своє повернення, коли воно закінчиться (поки ви не натиснете клавішу Enter тощо) ...

script -q -c 'cat > /proc/self/fd/1' /dev/null

Чому це працює? Чи "скрипт" відключає буферизацію?
Аарон Дігулла

@Aaron Digulla: scriptемулює термінал, так що так, я вважаю, що це вимикає буферизацію. Це також повторює кожен персонаж, який йому надіслали - саме тому він і catприводиться /dev/nullу прикладі. Що стосується програми, що працює всередині script, вона розмовляє з інтерактивним сеансом. Я вважаю, що це схоже на expectце в цьому плані, але, scriptймовірно, є частиною вашої базової системи.
jwd

Я використовую причину tee- надіслати копію потоку до файлу. Де файл вказується scriptee?
Бруно Броноський

@BrunoBronosky: Ви праві, це погана назва цієї програми. Це насправді не робиться операція «трійник». Це просто вимкнення буферизації результатів, відповідно до оригінального питання. Можливо, його слід назвати "scriptcat" (хоча це теж не робить конкатенацію ...). Незалежно від того, ви можете замінити catкоманду на tee myfile.txt, і ви повинні отримати потрібний ефект.
jwd

2

Я знайшов це розумне рішення: (echo -e "cmd 1\ncmd 2" && cat) | ./shell_executable

Це робить трюк. catпрочитає додатковий вхід (до EOF) і передасть його в трубу після того, echoяк аргумент введе свої аргументи у вхідний потік shell_executable.


2
Насправді, catне бачить висновок echo; Ви просто запускаєте дві команди в підрозділі, і вихід обох надсилається в трубу. Друга команда в нижній частині ('cat') читає з батьківського / зовнішнього stdin, тому це працює.
Аарон Дігулла

0

Відповідно до цього розмір буфера труби, здається, встановлений в ядрі і вимагає перекомпілювати ваше ядро ​​для зміни.


7
Я вважаю, що це інший буфер.
Семюель Едвін Уорд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.