ทำไมบางครั้งโปรแกรมด้วย 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 + A ที่เกี่ยวข้องซึ่งครอบคลุมตัวแปรอื่นของปัญหานี้: ระบบ C (“ bash”) จะไม่สนใจ stdin
Michael Homer

13
นี้ได้รวบรวมคะแนนโหวตใกล้บางอย่างเพื่อให้ความคิดเห็นในที่: คำถามเกี่ยวกับ "ยูนิกซ์ C API และระบบการเชื่อมต่อ" จะได้รับอนุญาตอย่างชัดเจน ปัญหาการบัฟเฟอร์นั้นเป็นเรื่องที่พบได้ทั่วไปในเชลล์สคริปต์และfork()ก็ค่อนข้างเฉพาะยูนิกซ์ด้วยเช่นกันดังนั้นจึงดูเหมือนว่านี่เป็นหัวข้อสำหรับ unix
ilkkachu

@ilkkachu จริง ๆ แล้วถ้าคุณอ่านลิงค์นั้นและคลิกคำถามเมตาที่อ้างถึงมันจะสะกดอย่างชัดเจนว่านี่เป็นหัวข้อที่ไม่อยู่ เพียงเพราะบางสิ่งเป็น C และ unix มี C ไม่ได้ทำให้มันอยู่ในหัวข้อ
แพทริค

@ แพทริคจริงฉันทำ และฉันก็ยังคิดว่ามันเหมาะกับประโยค "ในเหตุผล" แต่แน่นอนว่าเป็นเพียงฉัน
ilkkachu

คำตอบ:


93

เมื่อเอาต์พุตไปยังเอาต์พุตมาตรฐานโดยใช้printf()ฟังก์ชันของไลบรารี C เอาต์พุตมักจะถูกบัฟเฟอร์ บัฟเฟอร์จะไม่ถูกล้างจนกว่าคุณจะออกบรรทัดใหม่เรียกfflush(stdout)หรือออกจากโปรแกรม (ไม่ผ่านการเรียก_exit()แม้ว่า) สตรีมเอาต์พุตมาตรฐานจะเป็นค่าเริ่มต้นของบรรทัดที่บัฟเฟอร์ในลักษณะนี้เมื่อเชื่อมต่อกับ TTY

เมื่อคุณแยกกระบวนการใน "โปรแกรม 2" กระบวนการลูกจะสืบทอดทุกส่วนของกระบวนการหลักรวมถึงบัฟเฟอร์เอาต์พุตที่ไม่ได้ฟลัช สิ่งนี้คัดลอกบัฟเฟอร์ที่ไม่ได้ฟลัชอย่างมีประสิทธิภาพไปยังแต่ละกระบวนการลูก

เมื่อกระบวนการสิ้นสุดลงบัฟเฟอร์จะถูกลบทิ้ง คุณเริ่มต้นกระบวนการทั้งหมดแปดกระบวนการ (รวมถึงกระบวนการดั้งเดิม) และบัฟเฟอร์ที่ไม่ได้ฟลัชจะถูกล้างข้อมูลเมื่อสิ้นสุดกระบวนการแต่ละกระบวนการ

มันเป็นแปดเพราะแต่ละครั้งที่fork()คุณได้รับจำนวนของกระบวนการที่คุณมีก่อนหน้านี้fork()(เนื่องจากพวกมันไม่มีเงื่อนไข) และคุณมีสามอย่างนี้ (2 3 = 8)


14
ที่เกี่ยวข้อง: คุณสามารถลงท้ายmainด้วย_exit(0)เพียงแค่โทรออกจากระบบโดยไม่ต้องล้างข้อมูลบัฟเฟอร์และจากนั้นมันจะถูกพิมพ์ครั้งศูนย์โดยไม่ต้องขึ้นบรรทัดใหม่ ( การใช้ Syscall ของ exit ()และ_exit (0) (ออกจาก syscall) เป็นอย่างไรทำให้ฉันไม่ได้รับเนื้อหา stdout ใด ๆ ) หรือคุณสามารถไพพ์ Program1 ไปที่catหรือเปลี่ยนเส้นทางไปยังไฟล์และดูว่ามันถูกพิมพ์ 8 ครั้ง (stdout ถูกบัฟเฟอร์เต็มโดยค่าเริ่มต้นเมื่อไม่ใช่ TTY) หรือเพิ่มfflush(stdout)การกรณีที่ไม่มีการขึ้นบรรทัดใหม่ก่อนที่ 2 fork()...
ปีเตอร์ Cordes

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
นี่เป็นพื้นฐานของการแยก
Prvt_Yadav

3
@Debian_yadav คงเห็นได้ชัดก็ต่อเมื่อคุณคุ้นเคยกับความหมายของมัน เช่นเดียวกับการล้างบัฟเฟอร์stdioเช่น
roaima

2
@Debian_yadav: en.wikipedia.org/wiki/False_consensus_effect - ทำไมเราควรถามคำถามหากทุกคนรู้ทุกอย่าง?
Honza Zidek

8
@Debian_yadav ฉันไม่สามารถอ่านใจของ OP ดังนั้นฉันไม่รู้ อย่างไรก็ตาม stackexchange เป็นสถานที่ที่คนอื่นค้นหาความรู้และฉันคิดว่าคำตอบของฉันอาจเป็นประโยชน์ในการตอบที่ดีของ Kulasandra คำตอบของฉันเพิ่มบางสิ่ง (พื้นฐาน แต่มีประโยชน์) เมื่อเปรียบเทียบกับ edc65 ซึ่งเพิ่งทำซ้ำสิ่งที่ Kulasandra พูด 2 ชั่วโมงก่อนเขา
Honza Zidek

2
นี่เป็นเพียงความคิดเห็นสั้น ๆ สำหรับคำตอบไม่ใช่คำตอบจริง คำถามถามเกี่ยวกับ "หลาย ๆ ครั้ง" ไม่ใช่สาเหตุที่แน่ชัด 8
ไปป์

3

พื้นหลังที่สำคัญในที่นี้stdoutคือต้องมีการบัฟเฟอร์บรรทัดโดยมาตรฐานเป็นการตั้งค่าเริ่มต้น

สิ่งนี้ทำให้ a \nเพื่อล้างเอาต์พุต

เนื่องจากตัวอย่างที่สองไม่มีการขึ้นบรรทัดใหม่เอาต์พุตจะไม่ถูกลบทิ้งและfork()คัดลอกกระบวนการทั้งหมดจึงคัดลอกสถานะของstdoutบัฟเฟอร์

ในตอนนี้การfork()เรียกเหล่านี้ในตัวอย่างของคุณสร้างกระบวนการทั้งหมด 8 กระบวนการ - ทั้งหมดนั้นมีการคัดลอกสถานะของstdoutบัฟเฟอร์

ตามคำนิยามกระบวนการเหล่านี้จะเรียกใช้exit()เมื่อส่งคืนจากmain()และexit()เรียกfflush()ตามด้วยสตรีมstdio ที่fclose()ใช้งานอยู่ทั้งหมด ซึ่งรวมถึงและเป็นผลให้คุณเห็นเนื้อหาเดียวกันแปดครั้งstdout

เป็นวิธีปฏิบัติที่ดีในการเรียกfflush()ใช้สตรีมทั้งหมดที่มีเอาต์พุตที่ค้างอยู่ก่อนการโทรfork()หรือเพื่อให้เด็กที่ถูกแยกออกมาเรียกอย่างชัดเจน_exit()ว่าจะออกจากกระบวนการโดยไม่ล้างข้อมูลสตรีม stdio

หมายเหตุโทรที่exec()ไม่ได้ล้างบัฟเฟอร์ stdio ดังนั้นจึงมีการตกลงที่จะไม่สนใจเกี่ยวกับบัฟเฟอร์ stdio ถ้าคุณ (หลังจากเรียกfork()) โทรexec()และ (ถ้าที่ล้มเหลว) _exit()โทร

BTW: เพื่อให้เข้าใจว่าการบัฟเฟอร์ผิดอาจทำให้เกิดนี่เป็นข้อผิดพลาดเดิมใน 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()สตรีมที่เพิ่งถูกฟลัชคือ noop


2
ไม่ต้อง stdout ต้องถูกบัฟเฟอร์อย่างสมบูรณ์เว้นแต่จะเป็นอุปกรณ์แบบอินเทอร์แอคทีฟในกรณีที่มันไม่ได้ระบุ แต่ในทางปฏิบัติแล้วจะเป็นบัฟเฟอร์บรรทัด stderr จำเป็นต้องไม่ถูกบัฟเฟอร์อย่างสมบูรณ์ ดูpubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/…
Stéphane Chazelas

หน้าคนของฉันสำหรับsetbuf()Debian ( หน้าหนึ่งบน man7.org มีลักษณะคล้ายกัน ) กล่าวว่า "stderr สตรีมข้อผิดพลาดมาตรฐานมักไม่ได้รับการอัปเดตอยู่เสมอ" และการทดสอบอย่างง่าย ๆ ดูเหมือนว่าจะทำเช่นนั้นโดยไม่คำนึงว่าเอาต์พุตไปที่ไฟล์ไพพ์หรือเทอร์มินัลหรือไม่ คุณมีการอ้างอิงสำหรับไลบรารี่ C รุ่นใดที่จะทำอย่างอื่น?
ilkkachu

4
Linux เป็นเคอร์เนล stdio buffering เป็นคุณสมบัติ userland เคอร์เนลไม่มีส่วนเกี่ยวข้อง มีการใช้ libc จำนวนหนึ่งสำหรับ Linux kernels ส่วนใหญ่ในระบบเซิร์ฟเวอร์ / เวิร์กสเตชันประเภทคือการใช้งานของ GNU ซึ่ง stdout นั้นเต็มไปด้วยบัฟเฟอร์
Stéphane Chazelas

1
@schily เพียงการทดสอบฉันวิ่ง: paste.dy.fi/xk4 ฉันได้ผลลัพธ์เดียวกันกับระบบที่ล้าสมัยอย่างน่ากลัวเช่นกัน
ilkkachu

1
@schily นั่นไม่ใช่ความจริง ตัวอย่างเช่นฉันเขียนความคิดเห็นนี้โดยใช้ Alpine Linux ซึ่งใช้ musl แทน
NieDzejkob
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.