Printf аномалия после "fork()"

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

Я продвигаюсь вперед в обучении программированию на языке C в целом и программировании на 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!

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

Ответ 1

Я отмечаю, что <system.h> является нестандартным заголовком; Я заменил его на <unistd.h>, и код был скомпилирован.

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

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

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


Поэтапный анализ по запросу (скобки и т.д. удалены - и ведущие пробелы удалены редактором разметки):

  • 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 );

Анализ:

  • Копии "Hello, my pid 1234" в буфер для стандартного вывода. Поскольку в конце нет новой строки, а выход работает в режиме с буферизацией в линии (или режиме с полной буферизацией), на терминале ничего не появляется.
  • Предоставляет нам два отдельных процесса с точно таким же материалом в буфере stdout.
  • Ребенок имеет pid == 0 и выполняет строки 4 и 5; у родителя есть ненулевое значение для pid (одно из немногих различий между двумя процессами - возвращаемые значения из getpid() и getppid() - еще два).
  • Добавляет новую строку и "Я был разветвлен!: D" в выходной буфер дочернего элемента. Первая строка вывода появляется на терминале; остальное удерживается в буфере, так как вывод буферизируется по строке.
  • Все останавливается на 3 секунды. После этого ребенок обычно выезжает через возвращение в конце основного. В этот момент остаточные данные в буфере stdout очищаются. Это оставляет выходное положение в конце строки, поскольку нет новой строки.
  • Здесь приходит родитель.
  • Родитель ждет, пока ребенок закончит умирание.
  • Родитель добавляет новую строку и "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:

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

Ответ 2

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

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

Вы можете заставить это произойти с помощью метода fflush. Например

printf( "Hello, my pid is %d", getpid() );
fflush(stdout);

Ответ 3

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

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

Ответ 4

У меня та же проблема: enter image description here

Это дает мне: enter image description here

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

Когда форк запускается, у меня теперь есть 2 процесса, которые выполняются: 1- parent 2- child, он буферизует строку родителя, затем child, и затем child, умирает, возвращается к parent и только тогда показывает\п.

Мой ожидаемый результат будет состоять в том, чтобы фактически показать \n как часть строки... и иметь две отдельные строки со строкой

>string1 \n
>string2 \n
>

и я получаю это:

>string1 string2 \n
>

Я пытался с fflush, но тот же вывод, что и раньше...

enter image description here

>"Show me the new line before the pid child dies Show me the new line before the pid child dies"
>

(как видно из предыдущих комментариев, fflush будет пытаться выдвинуть его, но \n будет напечатан только один раз, и я хочу, чтобы он печатался дважды, как часть строки, а не только один раз как конец моего " flush/vector с процессами и т.д. "- не знаю, как это назвать, просто строка, в которой есть \n, но здесь \n работает как часть работы с форком и будет последней вещью, которую он выполняет, к сожалению...)

Как мне сделать так, чтобы я получил следующий вывод:

>"Show me the new line before the pid child dies \n"
> Show me the new line before the pid child dies \n"
>