“ fork()”之后的printf异常


77

操作系统:Linux,语言:纯C

我正在学习一般的C编程,在特殊情况下学习UNIX下的C编程。

printf()使用fork()调用后,我检测到该函数的奇怪行为(对我而言)。

#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!

为什么第二个“ Hello”字符串出现在孩子的输出中?

是的,这恰恰是父母在开始时打印的内容,并带有父母的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!

为什么会发生?这是正确的行为还是错误?

Answers:


91

我注意到这<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. 为我们提供了两个单独的过程,在stdout缓冲区中使用完全相同的材料。
  3. 孩子有pid == 0并执行第4和5行;父对象的值为非零pid(两个进程之间的少数差异之一-从getpid()和返回的值getppid()是另外两个)。
  4. 将一个换行符和“我被分叉!:D”添加到子级的输出缓冲区。输出的第一行出现在终端上;其余的保留在缓冲区中,因为输出是行缓冲的。
  5. 一切暂停3秒钟。此后,孩子通常会通过main的返回处正常退出。此时,标准输出缓冲区中的剩余数据将被清除。由于没有换行符,因此将输出位置留在行尾。
  6. 父母来了。
  7. 父母等待孩子完成死亡。
  8. 父母添加了一个换行符,并且“派生了1345!” 到输出缓冲区。在子代生成的行不完整之后,换行符将“ Hello”消息刷新到输出。

父级现在通常通过main的结尾处的返回退出,并且清除了剩余数据;由于末尾仍然没有换行符,因此光标位置在感叹号之后,并且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”行之前,什么都没有出现。


好,我知道了。但是我仍然不能自己解释为什么“缓冲区垃圾”出现在孩子的输出中新打印的行的末尾?但是,等等,现在我怀疑这确实是CHILD的输出。.哦,您能一步一步解释为什么输出看起来像这样(在旧字符串之前是新字符串),所以,我将不胜感激。不管怎样,谢谢你!
pechenie

非常令人印象深刻的解释!非常感谢,终于我明白了!PS:以前我为您投票,但现在我又一次愚蠢地单击了“向上箭头”,因此投票消失了。但由于“答案太旧”,我不能再给您它了:( PPS:我对其他问题投了赞成票。再次感谢您!
pechenie 2010年

27

原因是没有\n格式字符串的末尾,该值不会立即显示在屏幕上。而是在进程中对其进行缓冲。这意味着直到分叉操作之后才实际打印它,因此将其打印两次。

添加\nough强制将缓冲区刷新并输出到屏幕。这发生在叉子之前,因此只能打印一次。

您可以使用该fflush方法强制执行此操作。例如

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

1
fflush(stdout);似乎是imo更正确的答案。
oxagast

5

fork()有效地创建过程的副本。如果在调用之前fork(),它具有已缓冲的数据,则父级和子级将具有相同的缓冲数据。它们中的每一个下次执行某些操作来刷新其缓冲区时(例如在终端输出的情况下打印换行符),除了该进程产生的任何新输出之外,您还将看到该缓冲的输出。因此,如果要在父级和子级中都使用stdio,则应fflush在分叉之前确保没有缓冲数据。

通常,子级仅用于调用exec*函数。由于这将替换整个子进程映像(包括所有缓冲区),因此从技术上讲,fflush如果这确实是您要在子进程中完成的全部工作,则无需这样做。但是,如果可能存在缓冲的数据,则应谨慎处理exec故障。特别是,请避免使用任何stdio函数(write可以)将错误输出到stdout或stderr ,然后调用_exit(或_Exit)而不是调用exit或仅返回(这将刷新任何缓冲的输出)。或通过在分叉之前冲洗完全避免此问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.