【问题标题】:printf anomaly after "fork()"“fork()”之后的 printf 异常
【发布时间】:2011-02-01 14:33:09
【问题描述】:

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

我正在学习一般的 C 编程,以及特殊情况下的 UNIX 下的 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!

为什么子输出中出现第二个“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 fork


    【解决方案1】:

    原因是格式字符串末尾没有\n,值不会立即打印到屏幕上。相反,它在进程中缓冲。这意味着它实际上是在 fork 操作之后才被打印出来的,因此你会打印两次。

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

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

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

    【讨论】:

    • fflush(stdout); 似乎是这里更正确的答案。
    【解决方案2】:

    我注意到&lt;system.h&gt; 是一个非标准的标头;我将其替换为&lt;unistd.h&gt;,代码编译干净。

    当你的程序输出到终端(屏幕)时,它是行缓冲的。当程序的输出进入管道时,它是完全缓冲的。您可以通过标准 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 forked! :D”添加到子级的输出缓冲区。第一行输出出现在终端上;其余部分保存在缓冲区中,因为输出是行缓冲的。
    5. 一切停止 3 秒。在此之后,孩子通过main结束时的return正常退出。此时,标准输出缓冲区中的剩余数据被刷新。由于没有换行符,这会将输出位置留在行尾。
    6. 父母来了。
    7. 父母等待孩子完成死亡。
    8. 父级添加了一个换行符并且“1345 被分叉了!”到输出缓冲区。在子级生成的不完整行之后,换行符将“Hello”消息刷新到输出。

    parent现在通过main结束时的return正常退出,残留数据被flush;由于末尾仍然没有换行符,所以光标位置在感叹号之后,并且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 的输出。哦,你能解释一下为什么输出看起来完全一样(旧字符串之前的新字符串),一步一步,所以我将非常感激。还是谢谢你!
    • 非常令人印象深刻的解释!非常感谢,终于明白了! P.S.:我之前给你投了票,现在又傻傻的点了“向上箭头”,所以投票就消失了。但我不能再给你一次,因为“答案太老了”:( P.P.S.:我在其他问题上给了你一票。再次感谢你!
    【解决方案3】:

    fork() 有效地创建了进程的副本。如果在调用fork() 之前,它有缓冲的数据,则父级和子级都将拥有相同的缓冲数据。下一次他们每个人都做了一些事情来刷新它的缓冲区(例如在终端输出的情况下打印一个换行符),除了该进程产生的任何新输出之外,您还将看到缓冲的输出。所以如果你要在父子节点都使用stdio,那么你应该在fork之前fflush,以确保没有缓冲数据。

    通常,子级仅用于调用exec* 函数。由于它替换了完整的子进程映像(包括任何缓冲区),因此从技术上讲不需要fflush,如果这真的是您要在孩子身上做的所有事情。但是,如果可能存在缓冲数据,那么您应该小心处理执行失败的方式。特别是,避免使用任何 stdio 函数(write 可以)将错误打印到 stdout 或 stderr,然后调用 _exit(或 _Exit)而不是调用 exit 或只是返回(这将刷新任何缓冲输出)。或者通过在分叉前冲洗来完全避免这个问题。

    【讨论】:

      猜你喜欢
      • 2018-12-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多