"fork()"之后的printf异常

pec*_*nie 66 c unix linux 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;
}
Run Code Online (Sandbox Code Playgroud)

产量

Hello, my pid is 1111
I was forked! :DHello, my pid is 1111
2222 was forked!
Run Code Online (Sandbox Code Playgroud)

为什么第二个"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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

Hello, my pid is 1111
I was forked! :D
2222 was forked!
Run Code Online (Sandbox Code Playgroud)

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

Jon*_*ler 84

我注意到这<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. 将"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"消息刷新到输出.

父节点现在通过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: 
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

我现在得到:

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:
Run Code Online (Sandbox Code Playgroud)

请注意,当输出到达终端时,它是行缓冲的,因此"Hello"行出现在之前,fork()并且只有一个副本.当输出通过管道输出时cat,它是完全缓冲的,因此在此之前没有任何内容出现,fork()并且两个进程都要在刷新缓冲区中使用"Hello"行.


Jar*_*Par 25

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

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

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

printf( "Hello, my pid is %d", getpid() );
fflush(stdout);
Run Code Online (Sandbox Code Playgroud)


mar*_*k4o 5

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

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