Что мешает чередованию stdout / stderr?

13

Скажем, я запускаю несколько процессов:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

Я запускаю приведенный выше скрипт так:

foobarbaz | cat

насколько я могу судить, когда любой из процессов записывает в stdout / stderr, их вывод никогда не перемежается - каждая строка stdio кажется атомарной. Как это работает? Какая утилита контролирует, насколько каждая строка атомарна?

Александр Миллс
источник
3
Сколько данных выводит ваша команда? Попробуйте заставить их вывести несколько килобайт.
Кусалананда
Вы имеете в виду, где одна из команд выводит несколько килобайт перед новой строкой?
Александр Миллс
Нет, как-то так: unix.stackexchange.com/a/452762/70524
Муру

Ответы:

22

Они чередуются! Вы пробовали только короткие пакеты вывода, которые остаются неразделенными, но на практике трудно гарантировать, что какой-либо конкретный вывод останется неразделенным.

Буферизация вывода

Это зависит от того, как программы буферизуют свой вывод. Библиотека stdio, которую большинство программ используют при написании, использует буферы для повышения эффективности вывода. Вместо вывода данных, как только программа вызывает библиотечную функцию для записи в файл, функция сохраняет эти данные в буфере и фактически выводит данные только после заполнения буфера. Это означает, что вывод осуществляется партиями. Точнее, есть три режима вывода:

  • Небуферизованный: данные записываются немедленно, без использования буфера. Это может быть медленным, если программа записывает свои выходные данные небольшими частями, например, символ за символом. Это режим по умолчанию для стандартной ошибки.
  • Полностью буферизован: данные записываются только тогда, когда буфер заполнен. Это режим по умолчанию при записи в канал или обычный файл, кроме как с помощью stderr.
  • Строковая буферизация: данные записываются после каждой новой строки или когда буфер заполнен. Это режим по умолчанию при записи в терминал, кроме как с помощью stderr.

Программы могут перепрограммировать каждый файл, чтобы вести себя по-разному, и могут явно очистить буфер. Буфер очищается автоматически, когда программа закрывает файл или завершает работу в обычном режиме.

Если все программы, которые пишут в один и тот же канал, либо используют режим буферизации строки, либо используют режим без буферизации и записывают каждую строку одним вызовом функции вывода, и если строки достаточно короткие, чтобы записать их в один фрагмент, то вывод будет чередование целых строк. Но если одна из программ использует полностью буферизованный режим или строки слишком длинные, вы увидите смешанные строки.

Вот пример, где я чередую вывод двух программ. Я использовал GNU coreutils в Linux; разные версии этих утилит могут вести себя по-разному.

  • yes aaaaпишет aaaaнавсегда в том, что по существу эквивалентно режиму с линейной буферизацией. yesУтилита на самом деле пишет несколько строк в то время, но каждый раз , когда он испускает выход, выход представляет собой целое число строк.
  • echo bbbb; done | grep bпишет bbbbнавсегда в режиме полной буферизации. Он использует размер буфера 8192, и каждая строка имеет длину 5 байт. Поскольку 5 не делит 8192, границы между записями вообще не находятся на границе строк.

Давайте разберем их вместе.

$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba
bbaaaa
bbbbaaaa
baaaa
bbbaaaa
bbaaaa
bbbaaaa
ab
bbbbaaa

Как видите, да иногда прерывается grep и наоборот. Только около 0,001% линий были прерваны, но это случилось. Выходные данные рандомизированы, поэтому количество прерываний будет меняться, но я видел по крайней мере несколько прерываний каждый раз. Если бы строки были длиннее, была бы более высокая доля прерванных линий, поскольку вероятность прерывания увеличивается с уменьшением количества строк в буфере.

Есть несколько способов настроить выходную буферизацию . Основными из них являются:

  • Отключите буферизацию в программах, использующих библиотеку stdio, без изменения настроек по умолчанию с помощью программы, stdbuf -o0найденной в GNU coreutils и некоторых других системах, таких как FreeBSD. Вы также можете переключиться на буферизацию строки с помощью stdbuf -oL.
  • Переключитесь на буферизацию строки, направив вывод программы через терминал, созданный специально для этой цели с помощью unbuffer. Некоторые программы могут вести себя по-другому, например, grepпо умолчанию используют цвета, если их выводом является терминал.
  • Сконфигурируйте программу, например, перейдя --line-bufferedв GNU grep.

Давайте посмотрим на фрагмент выше, на этот раз с буферизацией строки с обеих сторон.

{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb

Так что на этот раз да никогда не прерывал grep, но grep иногда прерывал да. Я пойду, почему позже.

Чередование труб

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

Если данных для копирования больше, чем умещается в буфере передачи канала, ядро ​​копирует один буфер за раз. Если несколько программ пишут в один и тот же канал, и первая программа, которую выбирает ядро, хочет написать более одного буфера, тогда нет гарантии, что ядро ​​выберет ту же самую программу во второй раз. Например, если P - размер буфера, он fooхочет записать 2 * P байтов и barхочет записать 3 байта, то одно возможное перемежение - это P байтов из foo, затем 3 байта из barи P байтов из foo.

Возвращаясь к приведенному выше примеру «да + grep», в моей системе yes aaaaпишется столько строк, сколько может поместиться в 8192-байтовый буфер за один раз. Поскольку необходимо записать 5 байтов (4 печатаемых символа и символ новой строки), это означает, что каждый раз записывается 8190 байт. Размер буфера канала составляет 4096 байт. Следовательно, можно получить 4096 байт из yes, затем некоторый вывод из grep, а затем остальную часть записи из yes (8190 - 4096 = 4094 байт). 4096 байт оставляет место для 819 строк с aaaaодинокой a. Следовательно, строка с этим lone aсопровождается одной записью из grep, давая строку с abbbb.

Если вы хотите увидеть подробности происходящего, getconf PIPE_BUF .вам сообщат размер буфера канала в вашей системе, и вы увидите полный список системных вызовов, выполненных каждой программой с

strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba

Как гарантировать чистое чередование строк

Если длина строки меньше размера буфера канала, то буферизация строки гарантирует, что в выводе не будет смешанной строки.

Если длины строк могут быть больше, невозможно избежать произвольного микширования, когда несколько программ записывают в один канал. Чтобы обеспечить разделение, необходимо заставить каждую программу записывать в отдельный канал и использовать программу для объединения строк. Например, GNU Parallel делает это по умолчанию.

Жиль "ТАК - прекрати быть злым"
источник
Интересно, что может быть хорошим способом гарантировать, что все строки были написаны catатомарно, так что процесс cat получает целые строки из foo / bar / baz, но не половину строки от одной и половину строки от другой и т. д. Есть ли что-то, что я могу сделать со скриптом bash?
Александр Миллс
1
Похоже, это относится и к моему случаю, когда у меня были сотни файлов, и awkбыло создано две (или более) строки вывода для одного идентификатора, find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }' но с find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'ним правильно получена только одна строка для каждого идентификатора.
αғsнιη
Чтобы предотвратить любое чередование, я могу сделать это в программном окружении, таком как Node.js, но с bash / shell, не зная, как это сделать.
Александр Миллс
1
@JoL Это из-за заполнения буфера трубы. Я знал, что мне придется написать вторую часть истории ... Готово.
Жиль "ТАК - перестань быть злым"
1
@ OlegzandrDenman TLDR добавил: они чередуются. Причина сложна.
Жиль "ТАК - перестань быть злым"
1

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P рассмотрел это:

GNU xargs поддерживает параллельное выполнение нескольких заданий. -P n, где n - количество заданий, выполняемых параллельно.

seq 100 | xargs -n1 -P10 echo "$a" | grep 5
seq 100 | xargs -n1 -P10 echo "$a" > myoutput.txt

Это будет хорошо работать во многих ситуациях, но имеет обманчивый недостаток: если $ a содержит более ~ 1000 символов, эхо может быть не атомарным (его можно разделить на несколько вызовов write ()), и существует риск, что две строки будет смешанным

$ perl -e 'print "a"x2000, "\n"' > foo
$ strace -e write bash -c 'read -r foo < foo; echo "$foo"' >/dev/null
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 1008) = 1008
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 993) = 993
+++ exited with 0 +++

Очевидно, такая же проблема возникает, если есть несколько вызовов echo или printf:

slowprint() {
  printf 'Start-%s ' "$1"
  sleep "$1"
  printf '%s-End\n' "$1"
}
export -f slowprint
seq 10 | xargs -n1 -I {} -P4 bash -c "slowprint {}"
# Compare to no parallelization
seq 10 | xargs -n1 -I {} bash -c "slowprint {}"
# Be sure to see the warnings in the next Pitfall!

Выходы из параллельных заданий смешиваются вместе, поскольку каждое задание состоит из двух (или более) отдельных вызовов write ().

Если вам нужны несмешанные выходы, поэтому рекомендуется использовать инструмент, который гарантирует сериализацию вывода (такой как GNU Parallel).

Оле Танге
источник
Этот раздел не так. xargs echoне вызывает встроенную функцию echo bash, а echoутилиту from $PATH. И в любом случае я не могу воспроизвести это поведение bash echo с помощью bash 4.4. В Linux запись в канал (не / dev / null) больше 4K не гарантируется как атомарная.
Стефан