аномалия printf после "fork ()" - PullRequest
62 голосов
/ 27 марта 2010

ОС: Linux, язык: чистый C

Я продолжаю изучать программирование на С в целом и программирование на С под UNIX в особом случае.

Я обнаружил странное (для меня) поведение функции printf() после использования вызова fork().

Код

#include <stdio.h>
#include <system.h>

int main()
{
    int pid;
    printf( "Hello, my pid is %d", getpid() );

    pid = fork();
    if( pid == 0 )
    {
            printf( "\nI was forked! :D" );
            sleep( 3 );
    }
    else
    {
            waitpid( pid, NULL, 0 );
            printf( "\n%d was forked!", pid );
    }
    return 0;
}

выход

Hello, my pid is 1111
I was forked! :DHello, my pid is 1111
2222 was forked!

Почему вторая строка "Hello" появилась в выходных данных дочернего элемента?

Да, именно то, что родитель напечатал при запуске, с pid.

.

Но! Если мы поместим символ \n в конце каждой строки, мы получим ожидаемый результат:

#include <stdio.h>
#include <system.h>

int main()
{
    int pid;
    printf( "Hello, my pid is %d\n", getpid() ); // SIC!!

    pid = fork();
    if( pid == 0 )
    {
            printf( "I was forked! :D" ); // removed the '\n', no matter
            sleep( 3 );
    }
    else
    {
            waitpid( pid, NULL, 0 );
            printf( "\n%d was forked!", pid );
    }
    return 0;
}

выход

Hello, my pid is 1111
I was forked! :D
2222 was forked!

Почему это происходит? Это правильное поведение или это ошибка?

Ответы [ 3 ]

81 голосов
/ 27 марта 2010

Замечу, что <system.h> - нестандартный заголовок; Я заменил его на <unistd.h> и код скомпилирован чисто.

Когда выходные данные вашей программы поступают на терминал (экран), они буферизуются. Когда вывод вашей программы поступает в канал, он полностью буферизуется. Вы можете управлять режимом буферизации с помощью стандартных функций C setvbuf() и _IOFBF (полная буферизация), _IOLBF (линейная буферизация) и _IONBF (без буферизации).

Вы могли бы продемонстрировать это в своей пересмотренной программе, отправив вывод вашей программы, скажем, на cat. Даже с символами перевода строки в конце строки printf() вы увидите двойную информацию. Если вы отправите его прямо в терминал, то увидите только одну партию информации.

Мораль этой истории - быть осторожным, чтобы вызвать fflush(0);, чтобы очистить все буферы ввода / вывода перед разветвлением.


Строчный анализ в соответствии с запросом (фигурные скобки и т. Д. Удалены, а ведущие пробелы удалены редактором разметки):

  1. printf( "Hello, my pid is %d", getpid() );
  2. pid = fork();
  3. if( pid == 0 )
  4. printf( "\nI was forked! :D" );
  5. sleep( 3 );
  6. else
  7. waitpid( pid, NULL, 0 );
  8. printf( "\n%d was forked!", pid );

Анализ:

  1. Копирует "Hello, my pid 1234" в буфер для стандартного вывода. Поскольку в конце нет новой строки и вывод работает в режиме буферизации строки (или режиме полной буферизации), на терминале ничего не появляется.
  2. Дает нам два отдельных процесса с абсолютно одинаковым материалом в буфере stdout.
  3. Ребенок имеет pid == 0 и выполняет строки 4 и 5; родительский элемент имеет ненулевое значение для pid (одно из немногих различий между двумя процессами - возвращаемые значения из getpid() и getppid() - еще два).
  4. Добавляет перевод строки и «меня разветвляют!: D» в выходной буфер дочернего элемента. Первая строка вывода появляется на терминале; остальное хранится в буфере, так как вывод буферизуется в строке.
  5. Все останавливается на 3 секунды. После этого ребенок выходит нормально через возврат в конце main. В этот момент остаточные данные в буфере stdout сбрасываются. Это оставляет позицию вывода в конце строки, так как нет новой строки.
  6. Родитель приходит сюда.
  7. Родитель ждет, когда ребенок умрет.
  8. Родитель добавляет новую строку и "1345 был разветвлен!" в выходной буфер. Новая строка сбрасывает сообщение «Hello» на выход после неполной строки, сгенерированной дочерним элементом.

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

То, что я вижу:

Osiris-2 JL: ./xx
Hello, my pid is 37290
I was forked! :DHello, my pid is 37290
37291 was forked!Osiris-2 JL: 
Osiris-2 JL: 

Номера PID разные, но общий вид ясен. Добавление новых строк в конце операторов printf() (что очень быстро становится стандартной практикой) сильно меняет вывод:

#include <stdio.h>
#include <unistd.h>

int main()
{
    int pid;
    printf( "Hello, my pid is %d\n", getpid() );

    pid = fork();
    if( pid == 0 )
        printf( "I was forked! :D %d\n", getpid() );
    else
    {
        waitpid( pid, NULL, 0 );
        printf( "%d was forked!\n", pid );
    }
    return 0;
}

Теперь я получаю:

Osiris-2 JL: ./xx
Hello, my pid is 37589
I was forked! :D 37590
37590 was forked!
Osiris-2 JL: ./xx | cat
Hello, my pid is 37594
I was forked! :D 37596
Hello, my pid is 37594
37596 was forked!
Osiris-2 JL:

Обратите внимание, что когда вывод поступает на терминал, он буферизуется строкой, поэтому строка «Hello» появляется перед fork(), и была только одна копия. Когда вывод передается по каналу cat, он полностью буферизуется, поэтому перед fork() ничего не появляется, и оба процесса имеют строку «Hello» в буфере для очистки.

25 голосов
/ 27 марта 2010

Причина в том, что без \n в конце строки формата значение не сразу выводится на экран. Вместо этого он буферизуется в процессе. Это означает, что он на самом деле не печатается до окончания операции разветвления, поэтому вы получаете его дважды.

Добавление \n заставляет буфер очищаться и выводиться на экран. Это происходит до разветвления и, следовательно, печатается только один раз.

Вы можете заставить это произойти, используя метод fflush. Например

printf( "Hello, my pid is %d", getpid() );
fflush(stdout);
5 голосов
/ 28 марта 2010

fork() эффективно создает копию процесса. Если перед вызовом fork() у него были данные, которые были буферизованы, у родительского и дочернего объектов будут одинаковые буферизованные данные. В следующий раз, когда каждый из них сделает что-то для очистки своего буфера (например, напечатает новую строку в случае вывода на терминал), вы увидите этот буферизованный вывод в дополнение к любому новому выводу, созданному этим процессом. Так что, если вы собираетесь использовать stdio как в родительском, так и в дочернем процессах, вам нужно fflush перед разветвлением, чтобы гарантировать отсутствие буферизованных данных.

Часто дочерний процесс используется только для вызова функции exec*. Поскольку он заменяет полный образ дочернего процесса (включая любые буферы), технически нет необходимости fflush, если это действительно все, что вы собираетесь делать в дочернем процессе. Однако, если могут быть буферизованные данные, вы должны быть осторожны в том, как обрабатывается ошибка exec. В частности, избегайте вывода ошибки в stdout или stderr с использованием любой функции stdio (write в порядке), а затем вызывайте _exit (или _Exit) вместо вызова exit или просто возврата (что очистит все буферизованные данные). выход). Или вообще избежите проблемы, промыв перед разветвлением.

...