Как избежать несоответствия stdout и stderr при использовании конвейера?

Вопрос или проблема

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

Решения для сохранения порядка вывода

  1. Использование stdbuf или unbuffer:
    Вы можете отключить буферизацию вывода, что позволит добиться надежного порядка сообщений. Например:

    stdbuf -oL -eL prog
  2. Объединение потоков:
    Если вы не нуждаетесь в отдельном выводе для stderr, можно комбинацией потоков решить проблему:

    prog 2>&1 | tee output.log

    Это перенаправит стандартный поток ошибок на стандартный вывод и сохранит порядок.

  3. Использование script:
    Утилита script может быть использована для записи сессий подразумеваемую как в stdout, так и в stderr:

    script -c "prog" output.log
  4. Синхронизация потоков:
    В порой сложных сценариях может потребоваться ручная синхронизация вывода. Например:

    {
       { echo "Сообщение"; } >&1
       { echo "Ошибка" >&2; } 
    } | cat
  5. Вставка задержек:
    Простой, но неэффективный способ — это вставка задержек (sleep) в вывод. Это может помочь на стадии отладки, но не рекомендуется для продуктивного использования.

  6. Использование временных файлов для очередности записи:
    Вывод можно записать в временные файлы и затем объединить их в правильном порядке. Однако, это потребует больше ресурсов и времени.

Пример исправленного скрипта

Вот как можно переписать ваш пример, чтобы сохранить правильный порядок вывода:

#!/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-подобных системах. Понимание особенностей буферизации и управления потоками является ключевым для их решения. Используйте предложенные методы и инструменты, чтобы убедиться, что порядок вывода остается последовательным и понятным для анализа.

Оцените материал
Добавить комментарий

Капча загружается...