аномалія printf після “fork ()”


77

ОС: Linux, мова: чиста C

Я рухаюся вперед у вивченні програмування на C в цілому та програмування на C під UNIX в окремому випадку.

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

Код

Вихідні дані

Чому другий рядок "Привіт" трапився у виході дитини?

Так, саме те, що надрукував батько, коли він починався, разом із батьківським pid.

Але! Якщо ми розмістимо \nсимвол у кінці кожного рядка, ми отримаємо очікуваний результат:

Вихід :

Чому так трапляється? Це правильна поведінка, чи це помилка?

Відповіді:


91

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

Коли вихідні дані вашої програми надходять на термінал (екран), вона буферизується. Коли вихідні дані вашої програми надходять у конвеєр, вона повністю буферизується. Ви можете керувати режимом буферизації за допомогою функції Standard 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. Копіює "Привіт, мій pid 1234" у буфер для стандартного виводу. Оскільки в кінці немає нового рядка, а вихід працює в буферному режимі (або повнобуферному режимі), на терміналі нічого не відображається.
  2. Дає нам два окремі процеси, з абсолютно однаковим матеріалом у буфері stdout.
  3. Дитина має pid == 0і виконує рядки 4 і 5; у батьків є ненульове значення для pid(одна з небагатьох відмінностей між двома процесами - повертаються значення з getpid()і getppid()ще два).
  4. Додає новий рядок та "Мені було роздвоєно!: D" до вихідного буфера дочірньої організації. Перший рядок виводу з'являється на терміналі; решта утримується в буфері, оскільки висновок буферизується рядком.
  5. Все зупиняється на 3 секунди. Після цього дитина нормально виходить через повернення в кінці основного. У цей момент залишкові дані в буфері stdout видаляються. Це залишає вихідне положення в кінці рядка, оскільки нового рядка немає.
  6. Сюди приходить батько.
  7. Батьки чекають, поки дитина закінчить вмирати.
  8. Батько додає новий рядок і "1345 був роздвоєний!" до вихідного буфера. Новий рядок видаляє повідомлення "Привіт" на вихід після неповного рядка, згенерованого дочірнім об'єктом.

Тепер батько зазвичай виходить через повернення в кінці основного, а залишкові дані стираються; оскільки в кінці все ще немає нового рядка, позиція курсору стоїть після знака оклику, і на цьому ж рядку з'являється підказка оболонки.

Я бачу:

Номери PID різні, але загальний вигляд чіткий. Додавання нових рядків до кінцяprintf() висловлень (що дуже швидко стає стандартною практикою) значно змінює результат:

Тепер я отримую:

Зверніть увагу, що коли вихідні дані надходять на термінал, вони буферизуються, тому рядок "Привіт" з'являється перед, fork()і там була лише одна копія. Коли вихід виводиться по трубопроводі cat, він повністю буферизується, тому ніщо не з'являється перед fork()і обидва процеси мають у буфері рядку "Hello", який потрібно очистити.


Добре. Я зрозумів. Але я все ще не можу пояснити собі, чому "буферне сміття" з'являється в кінці нещодавно надрукованого рядка у виході дитини? Але почекайте, зараз я сумніваюся, що це насправді висновок ДИТИНИ .. о, не могли б ви пояснити, чому результат виглядає ТОЧНО (новий рядок ДО старого) так, крок за кроком, тому я був би дуже вдячний В будь-якому випадку, дякую Вам!
печеніе

ДУЖЕ вражаюче пояснення! Щиро дякую, нарешті я це зрозумів чітко! PS: Раніше я проголосував за вас, а зараз ще раз тупо натиснув на стрілку вгору, тож голосування зникло. Але я не можу дати вам ще раз через "відповідь застаріла" :( PPS: Я дав вам голос за інше питання. І ще раз дякую!
pechenie

27

Причиною тому є те, що без \nкінця рядка форматування значення не відразу друкується на екрані. Натомість він буферизується в процесі. Це означає, що він фактично друкується лише після операції вилки, отже, ви отримуєте його двічі.

Додавання \n хоча змушує буфер змиватися і виводитися на екран. Це відбувається перед вилкою, і тому друкується лише один раз.

Ви можете змусити це статися, використовуючи fflushметод. Наприклад


1
fflush(stdout);Здається, тут є більш правильна відповідь imo.
оксагаст

5

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

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

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