Вопрос или проблема
Сначала я хочу записать стандартный вывод программы (фактически rsync, или diff, или другие, проиллюстрированные функцией prog()
ниже), стандартную ошибку и комбинированный вывод и ошибку (т.е. stdall
) в отдельные файлы.
(eval
нужен в моем реальном сценарии.)
#!/bin/sh
prog() {
echo 1
echo 2 >> /dev/stderr
echo 3
echo 4 >> /dev/stderr
}
rm -f std*.output
{
{
{
echo Going to run prog
eval ' prog'
} | tee -a stdout.output
} 3>&1 1>&2 2>&3 | tee -a stderr.output
} 2>&1 | tee -a stdall.output
echo ---------------------
tail -vn +1 -- *
Вывод:
2
4
Going to run prog
1
3
---------------------
==> stdall.output <==
2
4
Going to run prog
1
3
==> stderr.output <==
2
4
==> stdout.output <==
Going to run prog
1
3
Обратите внимание, что как в консольном выводе, так и в файле stdall.output строки находятся в беспорядке. stderr
(2, 4) выводится перед stdout
(сообщение журнала, 1, 3).
Мы можем упростить вышеуказанный сценарий до просто | cat -
, который также может иллюстрировать ту же проблему.
#!/bin/sh
prog() {
echo 1
echo 2 >> /dev/stderr
echo 3
echo 4 >> /dev/stderr
}
{
echo Going to run prog
eval ' prog'
} | cat -
Вывод: (обратите внимание, что он в беспорядке.)
2
4
Going to run prog
1
3
Между прочим, я обнаружил, что добавление | cat -
к сообщению статуса и добавление пустой стандартной ошибки в конце могут вернуть вывод в правильный порядок.
#!/bin/sh
prog() {
echo 1
echo 2 >> /dev/stderr
echo 3
echo 4 >> /dev/stderr
}
{
echo Going to run prog | cat - # исправление, не знаю почему
eval ' prog'
printf '' >> /dev/stderr # исправление, не знаю почему
} | cat -
Вывод в правильном порядке:
Going to run prog
1
2
3
4
Почему, когда используется пайп, стандартный вывод и стандартная ошибка оказались в беспорядке в первую очередь? Как это работает? Как правильно избежать беспорядка стандартного вывода и стандартной ошибки, когда используется пайп? Моя попытка выше – это просто непроверенная проба и ошибка. Почему исполнение исправило проблему?
Как только стандартный вывод и стандартная ошибка становятся двумя разными пайпами, нет способа узнать порядок, в котором prog
запишет сообщения в каждую из них.
Тем не менее, вы можете сделать что-то вроде:
strace -qqqfo >(
perl -ne 'print pack("H*", substr($_, 10, 48) =~ s/ //gr) if /^ \"https://unix.stackexchange.com/" > both
) -P out -P err -e write=all -e %desc -a0 -s0 prog > out 2> err
Где стандартный вывод prog идет в out
, его стандартная ошибка идет в err
, и strace
отслеживает записи, которые делаются в файлы out
и err
(которые будут отслеживаться в порядке, в котором их делает prog
), которые мы получаем perl
для декодирования в both
.
Это предполагает, что prog
не записывает в свои файлы stdout/stderr через splice()
или mmap()
, где не будет ничего для strace
, чтобы увидеть. Если он lseeks()
в этих файлах, это не будет отражено в both
. Я только проверил, что это работает с процессами, которые используют системный вызов write()
для записи в stdout/stderr, я не уверен, работает ли это также с send()
, writev()
, pwrite()
… но это было бы гораздо реже для записи в stdout/stderr.
Ответ или решение
Как избежать неупорядоченности stdout и stderr при использовании трубок
При работе с процессами в Unix-подобных операционных системах существует потенциальная проблема, связанная с тем, что стандартный вывод (stdout) и стандартный поток ошибок (stderr) могут быть выведены в неверном порядке. Это может вызвать путаницу при анализе логов, особенно когда важно идентифицировать ошибки, сопоставив их с соответствующим выводом. В данной статье мы рассмотрим причины возникновения этой проблемы и предложим решения для ее предотвращения.
Причины возникновения проблемы
Причина неупорядоченности связана с тем, как операционная система управляет потоками stdout и stderr. Оба потока являются отдельными прикладными интерфейсами (API) и, когда процесс генерирует вывод, он может записывать данные в stdout и stderr в произвольном порядке. В результате, когда потоки объединяются через трубы или перенаправления, порядок может быть нарушен.
Когда вы запускаете команды в скрипте, stdout и stderr могут быть буферизированы независимо. Это означает, что сообщение, отправленное в stderr, может быть выведено на экран (или в файл) до сообщения, отправленного в stdout, даже если оно было вызвано позже. Кроме того, использование таких конструкций как eval
может добавить уровень сложности, поскольку это может изменять порядок операций в зависимости от особенностей реализации обработки команды.
Решения для сохранения порядка вывода
-
Использование
stdbuf
илиunbuffer
:
Вы можете отключить буферизацию вывода, что позволит добиться надежного порядка сообщений. Например:stdbuf -oL -eL prog
-
Объединение потоков:
Если вы не нуждаетесь в отдельном выводе для stderr, можно комбинацией потоков решить проблему:prog 2>&1 | tee output.log
Это перенаправит стандартный поток ошибок на стандартный вывод и сохранит порядок.
-
Использование
script
:
Утилитаscript
может быть использована для записи сессий подразумеваемую как в stdout, так и в stderr:script -c "prog" output.log
-
Синхронизация потоков:
В порой сложных сценариях может потребоваться ручная синхронизация вывода. Например:{ { echo "Сообщение"; } >&1 { echo "Ошибка" >&2; } } | cat
-
Вставка задержек:
Простой, но неэффективный способ — это вставка задержек (sleep
) в вывод. Это может помочь на стадии отладки, но не рекомендуется для продуктивного использования. -
Использование временных файлов для очередности записи:
Вывод можно записать в временные файлы и затем объединить их в правильном порядке. Однако, это потребует больше ресурсов и времени.
Пример исправленного скрипта
Вот как можно переписать ваш пример, чтобы сохранить правильный порядок вывода:
#!/bin/sh
prog() {
echo 1
echo 2 >&2
echo 3
echo 4 >&2
}
{
echo "Going to run prog"
eval 'prog'
} 2>&1 | tee output.log
Заключение
Вопрос о порядке вывода stdout и stderr является распространенной проблемой при разработке скриптов в Unix-подобных системах. Понимание особенностей буферизации и управления потоками является ключевым для их решения. Используйте предложенные методы и инструменты, чтобы убедиться, что порядок вывода остается последовательным и понятным для анализа.