Чому програма з 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...

10
Спробуйте запустити програму 1 з виведенням у файл ( ./prog1 > prog1.out) або трубу ( ./prog1 | cat). Підготуйтеся, щоб ваш розум був роздутий. :-) ⁠
G-Man каже: "Відновіть Моніку"

Відповідні питання Q +, що висвітлюють інший варіант цього випуску: система C ("баш") ігнорує stdin
Майкл Гомер

13
Це зібрало кілька близьких голосів, тому коментар до цього: питання щодо "UNIX C API та системних інтерфейсів" явно дозволені . Проблеми з буферизацією є звичайною зустріччю також у сценаріях оболонки, і вона fork()є дещо унікальною, тому, здавалося б, це цілком актуально для unix.SE.
ilkkachu

@ilkkachu насправді, якщо ви читаєте це посилання та натискаєте мета-питання, на яке воно посилається, це дуже чітко визначає, що це поза темою. Тільки тому, що щось є C, а у Unix є C, це не робить це на тему.
Патрік

@Patrick, власне, я і зробив. І я все ще вважаю, що це відповідає умові "всередині розуму", але, звичайно, це тільки я.
ilkkachu

Відповіді:


93

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

Коли ви роздвоюєте процес у "Програмі 2", дочірні процеси успадковують кожну частину батьківського процесу, включаючи незамкнутий вихідний буфер. Це ефективно копіює незапущений буфер до кожного дочірнього процесу.

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

Це вісім, тому що на кожен fork()ви отримуєте вдвічі більше процесів, які ви мали до цього fork()(оскільки вони є безумовними), і у вас є три (2 3 = 8).


14
Пов'язаний: ви можете закінчити mainз _exit(0)просто зробити систему виходу виклику без промивних буферів, а потім він буде надрукований в нуль раз без перекладу рядка. ( Реалізація Syscall реалізації exit () та How come _exit (0) (вихід за допомогою syscall) не дозволяє мені отримувати будь-який вміст stdout? ). Або ви можете вставити Program1 у файл catабо перенаправити його у файл і побачити, як він надрукується 8 разів. (stdout є повним буфером за замовчуванням, коли це не TTY). Або додайте fflush(stdout)до справи, яка не є новою лінією, до 2-го fork()
Пітер Кордес

17

Це ніяк не впливає на вилку.

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

У другому випадку у вас ще є 8 процесів, кожен з яких має буфер, що містить "Hello world ...", а буфер записується в кінці процесу.


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 .
roaima

2
@Debian_yadav: en.wikipedia.org/wiki/False_consensus_effect - навіщо нам задавати питання, якщо всі все знають?
Honza Zidek

8
@Debian_yadav Я не можу прочитати думку ОП, тому не знаю. У будь-якому випадку, stackexchange - це місце, де також інші шукають знання, і я думаю, що моя відповідь може стати корисним доповненням до хорошої відповіді Куласандри. Моя відповідь додає щось (основне, але корисне), порівняно з тим, яке є edc65, яке просто повторює те, що Куласандра сказав за 2 години до нього.
Honza Zidek

2
Це лише короткий коментар до відповіді, а не фактична відповідь. Питання задається питанням "багаторазово", а не чому це саме 8.
труба

3

Тут важливим є той факт, що stdoutдля встановлення за замовчуванням необхідна лінія, яка буде забудована стандартом.

Це призводить \nдо змивання виводу.

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

Тепер ці fork()виклики у вашому прикладі створюють загалом 8 процесів - усі вони мають копію стану stdoutбуфера.

За визначенням, всі ці процеси викликають exit()при поверненні з main()та exit()виклики, fflush()за якими слідують fclose()усі активні потоки stdio . Це включає stdoutі, як результат, ви бачите один і той же вміст вісім разів.

Доцільно зателефонувати fflush()в усі потоки з очікуванням виходу перед викликом fork()або дозволити виклику дочірнього дзвінка чітко _exit()виходити з процесу лише без змивання потоків stdio.

Зауважте, що виклик exec()не змиває буфери stdio, тому добре, щоб ви не піклувалися про буфери stdio, якщо ви (після виклику fork()) дзвоните exec()та (якщо це не вдається) дзвонити _exit().

BTW: Щоб зрозуміти, що неправильне буферизація може спричинити, ось колишня помилка в Linux, яку нещодавно виправили:

Стандарт вимагає stderrрозблокувати за замовчуванням, але Linux проігнорував це і зробив stderrбуферизовану лінію та (ще гірше) повністю буферизовану у випадку, якщо 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()потоку, щойно розмитого, є noop.


2
Ні, stdout не вимагається для повного буферизації, якщо це не інтерактивний пристрій, в цьому випадку він не визначений, але на практиці він тоді буферний. stderr вимагається, щоб він не був повністю буферним. Дивіться pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/…
Stéphane Chazelas

Моя сторінка man для setbuf()Debian ( ця на man7.org виглядає схоже ) зазначає, що "стандартний потік помилок stderr завжди розблокований за замовчуванням". а простий тест, здається, діє таким чином, незалежно від того, чи є вихід у файл, трубу чи термінал. Чи є у вас посилання на те, яка версія бібліотеки С поступила б інакше?
ilkkachu

4
Linux - це ядро, буферування stdio - це функція userland, ядро ​​там не бере участь. Існує ряд реалізацій libc для ядер Linux, найпоширенішим у системах типу сервер / робоча станція є реалізація GNU, з якою stdout буферизована (рядок буферизовано, якщо tty), а stderr - небуферованим.
Стефан Шазелас

1
@schily, просто тест, який я провів: paste.dy.fi/xk4 . Такий же результат я отримав і з жахливо застарілою системою.
ilkkachu

1
@schily Це неправда. Наприклад, я пишу цей коментар, використовуючи Alpine Linux, який використовує musl.
NieDzejkob
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.