Tại sao một chương trình với fork () đôi khi in đầu ra của nó nhiều lần?


50

Trong Chương trình 1 Hello worldđược in chỉ một lần, nhưng khi tôi gỡ bỏ \nvà chạy nó (Chương trình 2), đầu ra được in 8 lần. Ai đó có thể vui lòng giải thích cho tôi tầm quan trọng của \nở đây và nó ảnh hưởng đến như thế nào fork()?

Chương trình 1

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

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

Đầu ra 1:

hello world... 

Chương trình 2

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

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

Đầu ra 2:

hello world... hello world...hello world...hello world...hello world...hello world...hello world...hello world...

10
Hãy thử chạy Chương trình 1 với đầu ra cho tệp ( ./prog1 > prog1.out) hoặc ống ( ./prog1 | cat). Chuẩn bị để có tâm trí của bạn thổi. :-)
G-Man nói 'Phục hồi Monica'

Q + A có liên quan bao gồm một biến thể khác của vấn đề này: Hệ thống C (Nhật ký bash) bỏ qua stdin
Michael Homer

13
Điều này đã thu thập được một số phiếu bầu gần, vì vậy một nhận xét về điều đó: các câu hỏi về "UNIX C API và Giao diện hệ thống" được cho phép rõ ràng . Các vấn đề về bộ đệm là một cuộc gặp gỡ phổ biến cũng có trong các tập lệnh shell và fork()cũng hơi cụ thể, vì vậy có vẻ như đây là chủ đề khá đơn giản đối với unix.SE.
ilkkachu

@ilkkachu thực sự, nếu bạn đọc liên kết đó và nhấp vào câu hỏi meta mà nó đề cập, nó nói rất rõ rằng đây là chủ đề không phù hợp. Chỉ vì một cái gì đó là C và unix có C, không làm cho nó trở thành chủ đề.
Patrick

@Patrick, thực sự, tôi đã làm. Và tôi vẫn nghĩ rằng nó phù hợp với điều khoản "trong lý do", nhưng tất nhiên đó chỉ là tôi.
ilkkachu

Câu trả lời:


93

Khi xuất ra đầu ra tiêu chuẩn bằng printf()chức năng của thư viện C , đầu ra thường được đệm. Bộ đệm không được xóa cho đến khi bạn xuất một dòng mới, gọi fflush(stdout)hoặc thoát khỏi chương trình (không thông qua gọi _exit()). Luồng đầu ra tiêu chuẩn theo mặc định được đệm theo cách này khi được kết nối với TTY.

Khi bạn rẽ nhánh tiến trình trong "Chương trình 2", các tiến trình con sẽ kế thừa mọi phần của tiến trình cha, bao gồm cả bộ đệm đầu ra không được quét. Điều này sao chép một cách hiệu quả bộ đệm không bị xóa cho mỗi tiến trình con.

Khi quá trình kết thúc, bộ đệm được tuôn ra. Bạn bắt đầu tổng cộng tám quy trình (bao gồm cả quy trình ban đầu) và bộ đệm không được quét sẽ bị xóa khi kết thúc mỗi quy trình riêng lẻ.

Đó là tám vì mỗi lần fork()bạn nhận được gấp đôi số tiến trình bạn đã có trước fork()(vì chúng là vô điều kiện) và bạn có ba trong số các quy trình này (2 3 = 8).


14
Liên quan: bạn có thể kết thúc mainbằng _exit(0)việc chỉ thực hiện một cuộc gọi hệ thống thoát mà không xóa bộ đệm, và sau đó nó sẽ được in 0 lần mà không có dòng mới. ( Việc thực hiện tòa nhà của lối ra ()Làm thế nào đến _exit (0) (thoát khỏi tòa nhà chọc trời ) ngăn tôi nhận bất kỳ nội dung xuất sắc nào? ). Hoặc bạn có thể dẫn chương trình1 vào cathoặc chuyển hướng nó đến một tệp và xem nó được in 8 lần. (thiết bị xuất chuẩn được đệm đầy đủ theo mặc định khi nó không phải là TTY). Hoặc thêm một fflush(stdout)trường hợp không có dòng mới trước lần thứ 2 fork()...
Peter Cordes

17

Nó không ảnh hưởng đến ngã ba theo bất kỳ cách nào.

Trong trường hợp đầu tiên, bạn kết thúc với 8 quy trình không có gì để ghi, vì bộ đệm đầu ra đã bị xóa (do \n).

Trong trường hợp thứ hai, bạn vẫn có 8 tiến trình, mỗi tiến trình có một bộ đệm chứa "Hello world ..." và bộ đệm được ghi ở cuối quá trình.


12

@Kusalananda giải thích tại sao đầu ra được lặp lại . Nếu bạn tò mò tại sao đầu ra được lặp lại 8 lần và không chỉ 4 lần (chương trình cơ sở + 3 dĩa):

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
đây là cơ bản của ngã ba
Prvt_Yadav

3
@Debian_yadav có lẽ chỉ rõ ràng nếu bạn quen với ý nghĩa của nó. Ví dụ như xả bộ đệm stdio chẳng hạn.
roaima

2
@Debian_yadav: en.wikipedia.org/wiki/False_consinating_effect - tại sao chúng ta nên đặt câu hỏi nếu mọi người biết tất cả mọi thứ?
Honza Zidek

8
@Debian_yadav Tôi không thể đọc được suy nghĩ của OP nên tôi không biết. Dù sao, stackexchange là nơi mà những người khác cũng tìm kiếm kiến ​​thức và tôi nghĩ câu trả lời của tôi có thể là một sự bổ sung hữu ích cho câu trả lời hay của Kicesandra. Câu trả lời của tôi thêm một cái gì đó (cơ bản nhưng hữu ích), so với câu trả lời của edc65 chỉ lặp lại những gì K Formulairaza đã nói 2 giờ trước anh ta.
Honza Zidek

2
Đây chỉ là một nhận xét ngắn cho một câu trả lời, không phải là một câu trả lời thực tế. Câu hỏi hỏi về "nhiều lần" chứ không phải tại sao chính xác là 8.
đường ống

3

Bối cảnh quan trọng ở đây là stdoutbắt buộc phải được đệm theo tiêu chuẩn như thiết lập mặc định.

Điều này gây ra một \nđể tuôn ra đầu ra.

Vì ví dụ thứ hai không chứa dòng mới, đầu ra không bị xóa và khi fork()sao chép toàn bộ quá trình, nó cũng sao chép trạng thái của stdoutbộ đệm.

Bây giờ, các fork()cuộc gọi này trong ví dụ của bạn tạo tổng cộng 8 quy trình - tất cả chúng đều có một bản sao trạng thái của stdoutbộ đệm.

Theo định nghĩa, tất cả các quy trình này gọi exit()khi trở về main()exit()các cuộc gọi fflush()theo sau fclose()trên tất cả các luồng stdio hoạt động . Điều này bao gồm stdoutvà kết quả là, bạn thấy cùng một nội dung tám lần.

Đó là một cách thực hành tốt để gọi fflush()tất cả các luồng có đầu ra đang chờ xử lý trước khi gọi fork()hoặc để cho đứa trẻ rẽ nhánh gọi một cách rõ ràng _exit()rằng chỉ thoát khỏi quy trình mà không xóa các luồng stdio.

Lưu ý rằng việc gọi exec()không xóa bộ đệm stdio, vì vậy không cần quan tâm đến bộ đệm stdio nếu bạn (sau khi gọi fork()) gọi exec()và (nếu không) gọi _exit().

BTW: Để hiểu rằng bộ đệm sai có thể gây ra, đây là một lỗi cũ trong Linux đã được sửa gần đây:

Tiêu chuẩn này đòi hỏi stderrphải được giải mã theo mặc định, nhưng Linux đã bỏ qua điều này và tạo ra stderrdòng đệm và (thậm chí tệ hơn) được đệm hoàn toàn trong trường hợp stderr được chuyển hướng qua một đường ống. Vì vậy, các chương trình được viết cho UNIX đã thực hiện công cụ đầu ra mà không có dòng mới quá muộn trên Linux.

Xem bình luận dưới đây, nó dường như được sửa chữa ngay bây giờ.

Đây là những gì tôi làm để giải quyết vấn đề Linux này:

    /* 
     * 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); 

Mã này không gây hại cho các nền tảng khác vì việc gọi fflush()một luồng vừa bị xóa là không có.


2
Không, thiết bị xuất chuẩn bắt buộc phải được đệm hoàn toàn trừ khi đó là một thiết bị tương tác trong trường hợp không xác định được, nhưng trong thực tế, sau đó nó được đệm theo dòng. stderr được yêu cầu để không được đệm hoàn toàn. Xem pubs.opengroup.org/onlinepub/9699919799.2018edition/fifts/ mẹo
Stéphane Chazelas

Trang người đàn ông của tôi setbuf(), trên Debian ( trang trên man7.org trông tương tự ), nói rằng " Stderr luồng lỗi tiêu chuẩn luôn luôn không được xử lý theo mặc định." và một thử nghiệm đơn giản dường như hành động theo cách đó, bất kể đầu ra đi vào một tập tin, một đường ống hoặc một thiết bị đầu cuối. Bạn có bất kỳ tài liệu tham khảo cho phiên bản của thư viện C sẽ làm gì khác không?
ilkkachu

4
Linux là một kernel, bộ đệm stdio là một tính năng người dùng, kernel không liên quan ở đó. Có một số triển khai libc có sẵn cho các nhân Linux, phổ biến nhất trong các hệ thống kiểu máy chủ / máy trạm là triển khai GNU, trong đó stdout được đệm đầy đủ (bộ đệm nếu tty) và stderr không được đệm.
Stéphane Chazelas

1
@schily, chỉ là bài kiểm tra tôi đã chạy: paste.dy.fi/xk4 . Tôi cũng nhận được kết quả tương tự với một hệ thống lỗi thời khủng khiếp.
ilkkachu

1
@schily Điều đó không đúng. Ví dụ, tôi đang viết bình luận này bằng cách sử dụng Alpine Linux, sử dụng musl thay thế.
NieDzejkob
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.