“fork()”之后的 printf 异常

printf anomaly after "fork()"

提问人:pechenie 提问时间:3/28/2010 最后编辑:tompechenie 更新时间:12/13/2018 访问量:36169

问:

操作系统: Linux, 语言: pure 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!

为什么会这样?这是正确的行为,还是一个错误?

C Linux UNIX Printf 分叉

评论


答:

28赞 JaredPar 3/28/2010 #1

原因是如果没有格式字符串的末尾,该值不会立即打印到屏幕上。相反,它在进程中缓冲。这意味着它直到分叉操作之后才真正打印出来,因此您可以打印两次。\n

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

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

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

评论

1赞 oxagast 9/29/2017
fflush(stdout);这里似乎是更正确的答案imo。
103赞 Jonathan Leffler 3/28/2010 #2

我注意到这是一个非标准的标题;我用它替换了它,并且代码编译干净。<system.h><unistd.h>

当程序的输出进入终端(屏幕)时,它是行缓冲的。当程序的输出进入管道时,它是完全缓冲的。您可以通过标准 C 函数和(完全缓冲)、(线路缓冲)和(无缓冲)模式来控制缓冲模式。setvbuf()_IOFBF_IOLBF_IONBF

您可以在修改后的程序中演示这一点,方法是将程序的输出通过管道传输到 .即使字符串末尾有换行符,您也会看到双重信息。如果您将其直接发送到终端,那么您只会看到很多信息。catprintf()

这个故事的寓意是在分叉之前小心调用清空所有 I/O 缓冲区。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. 将“Hello, my pid is 1234”复制到标准输出的缓冲区中。由于末尾没有换行符,并且输出在行缓冲模式(或全缓冲模式)下运行,因此终端上不会显示任何内容。
  2. 为我们提供了两个独立的过程,在 stdout 缓冲区中使用完全相同的材料。
  3. 孩子拥有并执行第 4 行和第 5 行;父级具有非零值 for(两个进程之间的少数差异之一 - 返回值 from 和 是另外两个)。pid == 0pidgetpid()getppid()
  4. 将换行符和“I was forked! :D”添加到子项的输出缓冲区。第一行输出出现在终端上;其余部分保存在缓冲器中,因为输出是行缓冲的。
  5. 一切都停止了 3 秒钟。在此之后,孩子通过主线末尾的回车正常退出。此时,将刷新 stdout 缓冲区中的残余数据。这会将输出位置保留在行的末尾,因为没有换行符。
  6. 父母来到这里。
  7. 父母等待孩子完成死亡。
  8. 父级将换行符和“1345 was forked!”添加到输出缓冲区。换行符将“Hello”消息刷新到输出,在子项生成的不完整行之后。

父级现在通过 main 末尾的 return 正常退出,剩余数据被刷新;由于末尾仍然没有换行符,因此光标位置位于感叹号之后,并且 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”行出现在 和 之前,并且只有一个副本。当输出通过管道传递到 时,它是完全缓冲的,因此在 和 两个进程在要刷新的缓冲区中都有 'Hello' 行之前,不会出现任何内容。fork()catfork()

评论

0赞 pechenie 3/28/2010
好的,我明白了。但我仍然无法向自己解释为什么“缓冲区垃圾”会出现在孩子输出中新打印的行的末尾?但是等等,现在我怀疑这真的是 CHILD 的输出。.哦,你能解释一下为什么输出看起来完全一样(新字符串在旧字符串之前)是这样的,一步一步,所以我将不胜感激。无论如何,谢谢你!
1赞 pechenie 3/29/2010
非常令人印象深刻的解释!非常感谢,终于我明白了!P.S.:我之前给你投了一票,现在我又傻傻地点击了“向上箭头”,所以投票消失了。但我不能再给你一次,因为“答案太老了”:(P.P.S.:我在另一个问题上投了你一票。再次感谢你!
7赞 mark4o 3/28/2010 #3

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

通常,子项仅用于调用函数。由于它替换了完整的子进程映像(包括任何缓冲区),因此从技术上讲,如果这真的是您要在子进程中执行的全部操作,则无需这样做。但是,如果可能存在缓冲数据,则应小心处理 exec 故障。特别是,避免使用任何 stdio 函数( 可以)将错误打印到 stdout 或 stderr,然后调用 (或 ) 而不是调用或仅返回(这将刷新任何缓冲输出)。或者通过在分叉前冲洗来完全避免这个问题。exec*fflushwrite_exit_Exitexit