“fork()”之后的 printf 异常

Posted

技术标签:

【中文标题】“fork()”之后的 printf 异常【英文标题】:printf anomaly after "fork()" 【发布时间】: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!

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

【问题讨论】:

【参考方案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 缓冲区。


按要求逐行分析(大括号等被删除 - 前导空格被标记编辑器删除):

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

分析:

    将“你好,我的 pid 是 1234”复制到标准输出缓冲区。因为末尾没有换行符,并且输出在行缓冲模式(或全缓冲模式)下运行,所以终端上不会出现任何内容。 为我们提供了两个独立的进程,在标准输出缓冲区中使用完全相同的材料。 孩子有pid == 0并执行第4行和第5行;父级的 pid 具有非零值(这两个进程之间的少数区别之一 - 来自 getpid()getppid() 的返回值是另外两个)。 将换行符和“I was forked! :D”添加到子级的输出缓冲区。第一行输出出现在终端上;其余部分保存在缓冲区中,因为输出是行缓冲的。 一切停止 3 秒。在此之后,孩子通过main结束时的return正常退出。此时,标准输出缓冲区中的剩余数据被刷新。由于没有换行符,这会将输出位置留在行尾。 父母来了。 父母等待孩子完成死亡。 父级添加了一个换行符并且“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 或只是返回(这将刷新任何缓冲输出)。或者通过在分叉前冲洗来完全避免这个问题。

【讨论】:

以上是关于“fork()”之后的 printf 异常的主要内容,如果未能解决你的问题,请参考以下文章

计算机系统篇之异常控制流:利用 fork 和 execve 实现一个简易的 shell 程序

计算机系统篇之异常控制流:利用 fork 和 execve 实现一个简易的 shell 程序

fork与printf缓冲问题

系统调用fork()在powerpc上的源码分析

系统调用fork()在powerpc上的源码分析

深入理解计算机系统 第八章 异常控制流(2)