“fork()”之后的printf异常

时间:2010-03-27 19:37:15

标签: c linux unix printf fork

操作系统: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!

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

3 个答案:

答案 0 :(得分:81)

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

当程序输出到终端(屏幕)时,它是行缓冲的。当程序的输出进入管道时,它是完全缓冲的。您可以通过标准C函数setvbuf()_IOFBF(完全缓冲),_IOLBF(行缓冲)和_IONBF(无缓冲)模式来控制缓冲模式。< / p>

您可以通过将程序的输出汇总到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 );
  9. 分析:

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

答案 1 :(得分:25)

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

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

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

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

答案 2 :(得分:5)

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

通常,孩子仅用于调用exec*函数。由于这取代了完整的子进程映像(包括任何缓冲区),因此技术上不需要fflush,如果这真的是你要在孩子身上做的那一切。但是,如果可能存在缓冲数据,那么您应该注意如何处理exec失败。特别是,避免使用任何stdio函数将错误打印到stdout或stderr(write没问题),然后调用_exit(或_Exit)而不是调用exit或刚刚返回(将刷新任何缓冲的输出)。或者在分叉之前通过冲洗来完全避免这个问题。