Отключить буферизацию в трубе


396

У меня есть скрипт, который вызывает две команды:

long_running_command | print_progress

В long_running_commandпечатает прогресс , но я несчастна с ним. Я использую, print_progressчтобы сделать его более приятным (а именно, я печатаю прогресс в одной строке).

Проблема: подключение канала к stdout также активирует буфер 4K, к хорошей программе печати ничего не получается ... ничего ... ничего ... много ... :)

Как я могу отключить 4K буфер для long_running_command(нет, у меня нет источника)?


1
Таким образом, когда вы запускаете long_running_command без конвейера, вы можете видеть обновления прогресса правильно, но при конвейере они буферизируются?

1
Да, именно так и происходит.
Аарон Дигулла

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


1
На самом деле это не канал, который вызывает задержку при ожидании достаточного количества данных. Каналы действительно имеют емкость, но как только в канал будут записаны какие-либо данные, они сразу готовы к чтению на другом конце.
Сэм Уоткинс

Ответы:


254

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

unbuffer long_running_command | print_progress

unbufferподключается long_running_commandчерез псевдотерминал (pty), что заставляет систему рассматривать его как интерактивный процесс, поэтому не использует буферизацию в 4 КБ в конвейере, которая является вероятной причиной задержки.

Для более длинных конвейеров вам, возможно, придется снять буфер каждой команды (кроме последней), например

unbuffer x | unbuffer -p y | z

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

15
При конвейерной обработке вызовов unbuffer вы должны использовать аргумент -p, чтобы unbuffer считывал из 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 ​​он также входит в пакет ожидаемого разработчика.
Александр Мазель

463

Еще один способ избавиться от этой кошки - использовать 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 или file, статически связанными, без использования стандартной libc. В этих случаях лучше использовать решения с unbuffer/ script/ socat. Смотрите также stdbuf с setuid / functions .
Пабук

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в фоновом режиме, по крайней мере, при запуске из интерактивного терминала. Чтобы обойти это, я смог перенаправить стандартный ввод /dev/null, так как мой long_running_commandне использует stdin.
Харидсв

1
Даже работает на Android.
not2qubit

3
Один существенный недостаток: Ctrl-Z больше не работает (т.е. я не могу приостановить скрипт). Это можно исправить, например: echo | скрипт sudo -c / usr / local / bin / ec2-snapshot-all / dev / null | если вы не против того, чтобы не иметь возможности взаимодействовать с программой.
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 на экран, и полный буфер при выводе stdout в файл. Но нет буфера для stderr.

Я не думаю, что это проблема с буфером буфера, все дело в политике буфера libc.


Вы правы; мой вопрос все еще: как я могу влиять на политику буфера libc без перекомпиляции?
Аарон Дигулла

@ StéphaneChazelas fd1 будет перенаправлен на stderr
Ван Хунцинь

@ StéphaneChazelas Я не понимаю твою точку зрения. Пожалуйста, сделайте тест, это работает
Ван HongQin

3
Хорошо, что происходит с обоими zsh(откуда |&взято адаптировано из csh) и bash, когда вы это делаете cmd1 >&2 |& cmd2, оба fd 1 и 2 подключаются к внешнему стандартному выводу. Таким образом, он работает для предотвращения буферизации, когда этот внешний stdout является терминалом, но только потому, что вывод не проходит через канал (поэтому print_progressничего не печатает). Так что это тоже самое long_running_command & print_progress(за исключением того, что stdin print_progress - это канал, у которого нет записывающего устройства). Вы можете проверить 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.
Жаоруфеи

Из документации 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

Согласно этому посту , вы можете попытаться уменьшить ulimit канала до одного 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 это близко, но вы не получите свое приглашение после его завершения (пока вы не нажмете ввод и т. Д.) ...

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

Почему это работает? «Сценарий» отключает буферизацию?
Аарон Дигулла

@ Аарон Дигулла: 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') читает из родительского / внешнего стандартного ввода, поэтому она работает.
Аарон Дигулла

0

В соответствии с этим размер буфера канала, по-видимому, установлен в ядре и потребует от вас перекомпиляции ядра для изменения.


7
Я считаю, что это другой буфер.
Сэмюэль Эдвин Уорд
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.