Почему перенаправление вывода программы приводит к тому, что вывод ее подпроцессов выходит из строя?

В моей программе на C, работающей в Linux, которая создает подпроцессы с помощью system(), я заметил, что когда я перенаправлял stdout в канал или в файл, вывод подпроцессов был отправлен раньше, чем вывод буферизованных функций ввода-вывода, таких как printf(). Когда stdout оставался для перехода к терминалу, вывод был в ожидаемом порядке. Я упростил программу до следующего примера:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    printf("1. output from printf()\n");
    system("echo '2. output from a command called using system()'");

    return EXIT_SUCCESS;
}

Ожидаемый результат при переходе stdout в терминал:

$ ./iobuffer
1. output from printf()
2. output from a command called using system()

Вывод не по порядку, когда stdout перенаправляется в канал или файл:

$ ./iobuffer | cat
2. output from a command called using system()
1. output from printf()

person pabouk    schedule 03.07.2013    source источник
comment
возможный дубликат Почему стандартный вывод требует явного сброса при перенаправлении на файл?   -  person Martijn Pieters    schedule 03.07.2013
comment
К сожалению, я не нашел аналогичный вопрос Почему стандартный вывод нужен явный сброс при перенаправлении в файл? хотя я пробовал несколько поисков. Я столкнулся с проблемой недавно, и я уже знал ответ, когда писал вопрос. Я думал, что этот вопрос отсутствует здесь. Возможно, это все еще полезно, так как мои поиски не сработали :) У меня был подготовлен ответ, но я не знал, что мне не разрешат сразу ответить на мой вопрос :(   -  person pabouk    schedule 03.07.2013
comment
На странице «Задать вопрос» есть флажок, который также открывает текстовое поле ответа. Затем вы можете одновременно опубликовать вопрос и ответ. Если вы публикуете просто вопрос, вам придется немного подождать.   -  person Martijn Pieters    schedule 03.07.2013


Ответы (2)


Терминал обычно использует линейную буферизацию, а конвейер использует блочную буферизацию.

Это означает, что ваш вызов printf, включающий новую строку, заполнит буфер строки, вызывая сброс. При перенаправлении сброс не происходит, пока программа не завершится.

echo, с другой стороны, всегда сбрасывает буфер, в который он записывает, когда это завершается.

С линейной буферизацией (терминальный вывод) порядок следующий:

  • printf() печатает строку с новой строкой, буфер сбрасывается, вы видите, что 1. output from printf() печатается.
  • echo записывает вывод, выходит, очищает буфер, вы видите 2. output from a command called using system() напечатанным.

При блочной буферизации порядок следующий:

  • printf() печатает строку с новой строкой, не полностью заполняя буфер.
  • echo записывает вывод, выходит, очищает буфер, вы видите 2. output from a command called using system() напечатанным.
  • Ваша программа завершает работу, очищает свой блочный буфер, вы видите, что печатается 1. output from printf().

Ваши варианты: использовать явный сброс с помощью fflush() или установить буферизацию на stdout явно с помощью setvbuf().

person Martijn Pieters    schedule 03.07.2013
comment
Спасибо за ваш невероятно быстрый ответ. На самом деле я уже знал ответ, но мне не разрешили добавить его сразу. Стоит ли добавлять мой ответ полным или просто информацией, которая не отражена в вашем ответе? --- К вашему ответу: Есть небольшая ошибка - буферизация строк может быть установлена ​​только с setvbuf(), а не setbuf(). Также я не понимаю, как fprintf() может обойти буферизацию. Насколько я знаю, он использует точно такую ​​же буферизацию, что и printf(). - person pabouk; 03.07.2013
comment
Я удалил претензию fprintf(), я неверно истолковал другую часть информации. Поправил вызов setvbuf() (там опечатка). Вы всегда можете добавить свой собственный ответ и позволить будущим посетителям этой страницы решить путем голосования, какую информацию они считают полезной. - person Martijn Pieters; 03.07.2013

Этот ответ дополняет ответ Мартина Питерса. Ссылки на источники, описывающие режимы буферизации потоков по умолчанию, приведены в конце.

Решения

Вот три основных решения с объяснением:

a) Очищать буферы перед вызовом подпроцесса. Этот вариант может быть значительно более эффективным, когда вы выполняете много вывода из основной программы и всего несколько вызовов подпроцесса.

printf("1. output from printf()\n");
fflush(stdout);
system("echo '2. output from a command called using system()'");

б) Изменить буферизацию stdout на линейную буферизацию (или небуферизацию) для всей программы. Эта опция представляет собой небольшое изменение в программе, так как вы вызываете sevbuf() только в начале и остальная часть программы остается прежней.

if(setvbuf(stdin, NULL, _IOLBF, BUFSIZ))
    err(EXIT_FAILURE, NULL);
printf("1. output from printf()\n");
system("echo '2. output from a command called using system()'");

Изменить:
c) Изменить буферизацию stdout на линейную буферизацию (или небуферизацию) для всей программы с помощью внешней утилиты . Эта опция никак не меняет программу, поэтому вам не нужно перекомпилировать или даже иметь исходники программы. Вы просто вызываете программу с помощью утилиты stdbuf.

$ stdbuf -oL ./iobuffer | cat
1. output from printf()
2. output from a command called using system()

Ссылки — описание причин изменения режима буферизации.

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

Справочное руководство по библиотеке GNU C
http://www.gnu.org/software/libc/manual/html_node/Buffering-Concepts.html#Buffering-Concepts

Недавно открытые потоки обычно полностью буферизуются, за одним исключением: поток, подключенный к интерактивному устройству, такому как терминал, изначально буферизуется строкой.

Справочные страницы Linux: стандартный ввод (3)
http://linux.die.net/man/3/stdin

Поток stderr не буферизован. Поток stdout буферизуется строкой, когда он указывает на терминал. Частичные строки не будут отображаться до тех пор, пока не будут вызваны fflush(3) или exit(3), или пока не будет напечатана новая строка. Это может привести к неожиданным результатам, особенно при выводе отладочной информации. Режим буферизации стандартных потоков (или любого другого потока) можно изменить с помощью вызова setbuf(3) или setvbuf(3).

Также упоминается буферизация драйвера терминала.

Проект комитета C11 ISO/IEC 9899:201x — 12 апреля 2011 г.; 7.21.3 Файлы, стр. 301
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf

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

Открытая группа: системные интерфейсы и заголовки, выпуск 4, версия 2; 2.4 Стандартные потоки ввода-вывода, стр. 32
https://www2.opengroup.org/ogsys/catalog/C435 (для загрузки необходима бесплатная регистрация)

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

Также есть очень интересная глава 2.4.1 "Взаимодействие файловых дескрипторов и стандартных потоков ввода-вывода" о комбинировании буферизованного и небуферизованного ввода-вывода, которая в некоторой степени относится к вызовам подпроцессов.

person pabouk    schedule 04.07.2013