Почему программа с fork () иногда выводит свой вывод несколько раз?

50

В программе 1 Hello worldпечатается только один раз, но когда я удаляю \nи запускаю его (программа 2), вывод печатается 8 раз. Может кто-нибудь, пожалуйста, объясните мне значение \nздесь и как это влияет на fork()?

Программа 1

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...\n");
    fork();
    fork();
    fork();
}

Выход 1:

hello world... 

Программа 2

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...");
    fork();
    fork();
    fork();
}

Выход 2:

hello world... hello world...hello world...hello world...hello world...hello world...hello world...hello world...
lmaololrofl
источник
10
Попробуйте запустить программу 1 с выводом в файл ( ./prog1 > prog1.out) или канал ( ./prog1 | cat). Приготовьтесь к тому, что ваш разум будет взорван :-) ⁠
G-Man говорит «Восстановить Монику»
Соответствующий вопрос Q + A, охватывающий еще один вариант этой проблемы: система C («bash») игнорирует стандартный ввод
Майкл Гомер
13
Это собрало несколько близких голосов, поэтому комментарий к этому: вопросы по "UNIX C API и Системные интерфейсы" явно разрешены . Проблемы с буферизацией часто встречаются и в сценариях оболочки, и fork()в некоторой степени специфичны и для Unix, поэтому может показаться, что это довольно актуально для unix.SE.
ilkkachu
@ilkkachu на самом деле, если вы прочтете эту ссылку и нажмете мета-вопрос, на который она ссылается, это очень ясно говорит о том, что это не по теме. Просто потому, что что-то есть C, а Unix имеет C, не делает это по теме.
Патрик
@ Патрик, собственно, я и сделал. И я все еще думаю, что это соответствует условию «в пределах разумного», но, конечно, это только я.
ilkkachu

Ответы:

93

При выводе в стандартный вывод с использованием функции библиотеки C. printf()вывод обычно буферизуется. Буфер не очищается до тех пор, пока вы не выведете новую строку, не вызовете fflush(stdout)или не закроете программу (но не через вызов _exit()). Стандартный поток вывода по умолчанию буферизуется таким образом, когда он подключен к TTY.

Когда вы разветвляете процесс в «Программе 2», дочерние процессы наследуют каждую часть родительского процесса, включая неразбавленный выходной буфер. Это эффективно копирует невыполненный буфер в каждый дочерний процесс.

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

Это восемь, потому что в каждом fork()вы получаете вдвое больше процессов, чем до fork()(так как они безусловны), и у вас есть три из них (2 3 = 8).

Кусалананда
источник
14
Связанный: вы можете закончить mainс _exit(0)просто сделать систему выхода вызова без промывочных буферов, а затем он будет напечатан в ноль раз без перевода строки. ( Реализация Системного вызова выхода () и как прийти _exit (0) (выход от системного вызова) позволяет мне получать любой контент стандартного вывода? ). Или вы можете перенаправить Program1 в catфайл или перенаправить его в файл и посмотреть, как он будет напечатан 8 раз. (stdout по умолчанию полностью буферизован, когда это не TTY). Или добавьте fflush(stdout)к делу без новой строки до 2-го fork()...
Питер Кордес
17

Это никак не влияет на вилку.

В первом случае вы получите 8 процессов, которым нечего писать, поскольку выходной буфер уже очищен (из-за \n).

Во втором случае у вас все еще есть 8 процессов, каждый с буфером, содержащим «Hello world ...», и буфер записывается в конце процесса.

edc65
источник
12

@Kusalananda объяснил, почему вывод повторяется . Если вам интересно, почему вывод повторяется 8 раз, а не только 4 раза (базовая программа + 3 вилки):

int main()
{
    printf("hello world...");
    fork(); // here it creates a copy of itself --> 2 instances
    fork(); // each of the 2 instances creates another copy of itself --> 4 instances
    fork(); // each of the 4 instances creates another copy of itself --> 8 instances
}
Хонза Зидек
источник
2
это
основа
3
@Debian_yadav, вероятно, очевидно, только если вы знакомы с его последствиями. Как , например, очистка буферов stdio .
Ройма
2
@Debian_yadav: en.wikipedia.org/wiki/False_consensus_effect - зачем нам задавать вопросы, если все знают все?
Хонза Зидек
8
@Debian_yadav Я не могу прочитать мысли ОП, поэтому я не знаю. В любом случае, stackexchange - это место, где другие тоже ищут знания, и я думаю, что мой ответ может быть полезным дополнением к хорошему ответу Куласандры. Мой ответ добавляет кое-что (простое, но полезное) по сравнению с тем, что в edc65 повторяет то, что Куласандра сказал за 2 часа до него.
Хонза Зидек
2
Это всего лишь короткий комментарий к ответу, а не фактический ответ. Вопрос о «многократном» вопросе не в том, почему именно 8
труба
3

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

Это приводит \nк сбросу вывода.

Поскольку второй пример не содержит символ новой строки, выходные данные не сбрасываются и, поскольку fork()копирует весь процесс, он также копирует состояние stdoutбуфера.

Теперь эти fork()вызовы в вашем примере создают в общей сложности 8 процессов - все они с копией состояния stdoutбуфера.

По определению, все эти процессы вызывают exit()при возврате из, main()а затем exit()вызовы во всех активных потоках stdio . Это включает в себя, и в результате вы видите один и тот же контент восемь раз.fflush()fclose()stdout

Хорошей практикой является вызов fflush()всех потоков с ожидающим выводом перед вызовом fork()или явное разрешение разветвленного дочернего вызова, _exit()который только завершает процесс, не сбрасывая потоки stdio.

Обратите внимание, что вызов exec()не очищает буферы stdio, поэтому нормально не заботиться о буферах stdio, если вы (после вызова fork()) вызываете exec()и (если это не удается) вызываете _exit().

КСТАТИ: Чтобы понять, что неправильная буферизация может вызвать, вот старая ошибка в Linux, которая была недавно исправлена:

Стандарт требует stderrотмены stderrбуферизации по умолчанию, но Linux проигнорировал это и сделал буферизацию строки и (что еще хуже) полную буферизацию в случае, если stderr был перенаправлен через канал. Так что программы, написанные для UNIX, выводили вещи без перевода строки в Linux слишком поздно.

См. Комментарий ниже, кажется, сейчас исправлено.

Вот что я делаю, чтобы обойти эту проблему Linux:

    /* 
     * Linux comes with a broken libc that makes "stderr" buffered even 
     * though POSIX requires "stderr" to be never "fully buffered". 
     * As a result, we would get garbled output once our fork()d child 
     * calls exit(). We work around the Linux bug by calling fflush() 
     * before fork()ing. 
     */ 
    fflush(stderr); 

Этот код не наносит вреда на других платформах, так как вызов fflush()только что очищенного потока является пустым занятием.

Шили
источник
2
Нет, стандартный вывод должен быть полностью буферизован, если только он не является интерактивным устройством, и в этом случае он не указан, но на практике он затем буферизуется с помощью строки. Требуется, чтобы stderr не был полностью буферизован. См. Pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/…
Стефан
Моя страница руководства для setbuf()Debian ( похожая на man7.org выглядит аналогично ) гласит: «Стандартный поток ошибок stderr по умолчанию всегда небуферизован». и простой тест, кажется, действует таким образом, независимо от того, идет ли вывод в файл, канал или терминал. Есть ли у вас какие-либо ссылки на то, что версия библиотеки C будет делать иначе?
ilkkachu
4
Linux - это ядро, буферизация stdio - это пользовательская функция, ядро ​​там не задействовано. Существует несколько реализаций libc, доступных для ядер Linux, наиболее распространенной в системах типа сервер / рабочая станция является реализация GNU, с которой stdout является полной буферизацией (строка буферизована, если tty), а stderr не буферизована.
Стефан Шазелас
1
@schily, только тест, который я провел: paste.dy.fi/xk4 . Я получил тот же результат с ужасно устаревшей системой тоже.
ilkkachu
1
@schily Это не правда. Например, я пишу этот комментарий, используя Alpine Linux, который использует musl.
NieDzejkob