2010-03-27 37 views
59

操作系統:Linux,語言:純Cprintf的異常「fork()的」

我在一個特殊的情況下UNIX下學習一般C編程和C編程前進。

在使用fork()調用之後,我檢測到printf()函數的一個奇怪的(對我來說)行爲。

代碼

#include <stdio.h> 
#include <system.h> 

int main() 
{ 
    int pid; 
    printf("Hello, my pid is %d", getpid()); 

    pid = fork(); 
    if(pid == 0) 
    { 
      printf("\nI was forked! :D"); 
      sleep(3); 
    } 
    else 
    { 
      waitpid(pid, NULL, 0); 
      printf("\n%d was forked!", pid); 
    } 
    return 0; 
} 

輸出

Hello, my pid is 1111 
I was forked! :DHello, my pid is 1111 
2222 was forked! 

爲什麼第二個 「你好」 字符串出現在孩子的輸出?

是的,這正是母公司開始時打印的父母的pid

但是!如果我們在每個字符串的結尾放置\n字符,我們得到了預期的輸出:

#include <stdio.h> 
#include <system.h> 

int main() 
{ 
    int pid; 
    printf("Hello, my pid is %d\n", getpid()); // SIC!! 

    pid = fork(); 
    if(pid == 0) 
    { 
      printf("I was forked! :D"); // removed the '\n', no matter 
      sleep(3); 
    } 
    else 
    { 
      waitpid(pid, NULL, 0); 
      printf("\n%d was forked!", pid); 
    } 
    return 0; 
} 

輸出

Hello, my pid is 1111 
I was forked! :D 
2222 was forked! 

它爲什麼會發生?這是正確的行爲,還是它的錯誤?

回答

75

我注意到<system.h>是一個非標準的標題;我用<unistd.h>替換它,代碼編譯乾淨。

當你的程序輸出到終端(屏幕)時,它是行緩衝的。當你的程序輸出到一個管道時,它被完全緩衝。您可以通過標準C函數setvbuf()_IOFBF(完全緩衝),_IOLBF(行緩衝)和_IONBF(無緩衝)模式來控制緩衝模式。

你可以在你修改的程序中通過將你的程序的輸出輸出到cat來證明這一點。即使使用printf()字符串末尾的換行符,您也會看到雙重信息。如果您直接將其發送到終端,那麼您將只看到一個信息。

故事的寓意是要小心撥打fflush(0);在分叉前清空所有I/O緩衝區。


線,由線分析,按要求(括號等去除 - 通過標記編輯器刪除前導空格):

  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. 爲我們提供了兩個單獨的進程,標準輸出緩衝區中的資料完全相同。
  3. 孩子有pid == 0並執行第4行和第5行;父母的pid(兩個過程之間的少數差異之一 - getpid()getppid()的返回值是兩個以上)具有非零值。
  4. 向孩子的輸出緩衝區添加一個換行符和「I was forkked!:D」。輸出的第一行出現在終端上;由於輸出是行緩衝的,其餘的都保存在緩衝區中。
  5. 一切都停止3秒鐘。在此之後,孩子通過主結束時的返回正常退出。此時,stdout緩衝區中的殘留數據將被刷新。由於沒有換行符,因此這會將輸出位置留在行尾。
  6. 父母來到這裏。
  7. 父母等待孩子完成死亡。
  8. 父母添加換行符,「1345被分叉了!」到輸出緩衝區。在由孩子生成的不完整行之後,換行符將'Hello'消息刷新到輸出。

現在父母通過主結束處的返回正常退出,並且殘留數據被刷新;因爲最後還沒有換行符,光標位置在感嘆號後面,並且shell提示符出現在同一行上。

我看到的是:

Osiris-2 JL: ./xx 
Hello, my pid is 37290 
I was forked! :DHello, my pid is 37290 
37291 was forked!Osiris-2 JL: 
Osiris-2 JL: 

的PID號不同 - 但整體外觀爲無色。添加新行到printf()語句(這很快成爲標準的做法)的結束改變了輸出了很多:

#include <stdio.h> 
#include <unistd.h> 

int main() 
{ 
    int pid; 
    printf("Hello, my pid is %d\n", getpid()); 

    pid = fork(); 
    if(pid == 0) 
     printf("I was forked! :D %d\n", getpid()); 
    else 
    { 
     waitpid(pid, NULL, 0); 
     printf("%d was forked!\n", pid); 
    } 
    return 0; 
} 

我現在得到:

Osiris-2 JL: ./xx 
Hello, my pid is 37589 
I was forked! :D 37590 
37590 was forked! 
Osiris-2 JL: ./xx | cat 
Hello, my pid is 37594 
I was forked! :D 37596 
Hello, my pid is 37594 
37596 was forked! 
Osiris-2 JL: 

注意,當輸出達到終端,它是行緩衝的,所以'Hello'行出現在fork()之前,並且只有一個副本。當輸出連接到cat時,它是完全緩衝的,因此在fork()之前沒有任何內容出現,並且兩個進程都在緩衝區中有'Hello'行被刷新。

+0

好吧,我明白了。但是我仍然無法向自己解釋爲什麼「緩衝垃圾」出現在孩子輸出中新打印行的末尾? 但是等等,現在我懷疑它確實是CHILD的輸出。哦,你能解釋爲什麼輸出看起來像這樣,一步一步地完成(新字符串),所以我會非常感激。 無論如何謝謝你! – pechenie 2010-03-28 06:40:09

+0

非常令人印象深刻的解釋!非常感謝,終於明白了! P.S .:我先前給你投了一個票,現在我再次愚蠢地點擊了「向上箭頭」,所以投票消失了。但我不能再給你了,因爲「答案太舊了」:( P.P.S .:我給了你一個其他問題的投票,並且再次感謝你! – pechenie 2010-03-28 17:43:25

22

之所以這樣,是因爲如果在格式字符串末尾沒有\n,則該值不會立即顯示在屏幕上。相反,它在過程中被緩衝。這意味着它在fork操作之後纔會實際打印,因此您將打印兩次。

雖然強制緩衝區被刷新並輸出到屏幕,但添加\n。這發生在叉子之前,因此只打印一次。

您可以使用fflush方法強制執行此操作。例如

printf("Hello, my pid is %d", getpid()); 
fflush(stdout); 
+0

謝謝你的回答! – pechenie 2010-03-28 06:40:29

+0

'fflush(stdout);'這裏似乎是更正確的答案。 – 2017-09-29 06:52:19

5

fork()有效地創建了一個過程的副本。如果在調用fork()之前,它的數據已被緩衝,則父級和子級都將具有相同的緩衝數據。下一次他們每個人都會執行一些操作來清空其緩衝區(例如在終端輸出的情況下打印新行),除了該進程產生的任何新輸出外,您還將看到緩衝輸出。因此,如果你要在父母和孩子中使用stdio,那麼你應該在分岔之前fflush,以確保沒有緩衝數據。

通常,孩子通常只用於調用exec*函數。因爲這代替了完整的子進程映像(包括任何緩衝區),所以在技術上不需要fflush,如果這真的是你要在孩子身上做的所有事情。但是,如果可能存在緩衝數據,則應該小心如何處理exec失敗。特別是,避免使用任何stdio函數(write ok)將錯誤打印到stdout或stderr,然後調用_exit(或_Exit)而不是調用exit或僅返回(它將刷新任何緩衝的輸出)。或者在分叉之前通過沖洗來完全避免這個問題。