关于“ fork()”之后的c:printf异常

printf anomaly after “fork()”

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

我将继续学习一般的C编程,在特殊情况下将学习UNIX下的C编程。

使用fork()调用后,我检测到printf()函数的奇怪(对我而言)行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <system.h>

int main()
{
    int pid;
    printf("Hello, my pid is %d", getpid() );

    pid = fork();
    if( pid == 0 )
    {
            printf("
I was forked! :D"
);
            sleep( 3 );
    }
    else
    {
            waitpid( pid, NULL, 0 );
            printf("
%d was forked!"
, pid );
    }
    return 0;
}

输出量

1
2
3
Hello, my pid is 1111
I was forked! :DHello, my pid is 1111
2222 was forked!

为什么第二个" Hello"字符串出现在孩子的输出中?

是的,它恰好是父级开始时打印的内容,带有父级的pid

但! 如果我们在每个字符串的末尾放置一个
字符,则会得到预期的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <system.h>

int main()
{
    int pid;
    printf("Hello, my pid is %d
"
, getpid() ); // SIC!!

    pid = fork();
    if( pid == 0 )
    {
            printf("I was forked! :D" ); // removed the '
', no matter
            sleep( 3 );
    }
    else
    {
            waitpid( pid, NULL, 0 );
            printf("
%d was forked!", pid );
    }
    return 0;
}

输出:

1
2
3
Hello, my pid is 1111
I was forked! :D
2222 was forked!

为什么会发生? 这是正确的行为还是错误?


我注意到是一个非标准头文件;我用替换了它,并干净地编译了代码。

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

您可以通过将程序的输出传递到cat来在修订的程序中进行演示。即使在printf()字符串末尾有换行符,您也会看到重复信息。如果直接将其发送到终端,则只会看到很多信息。

这个故事的寓意是要小心在分叉之前调用fflush(0);清空所有I / O缓冲区。

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

  • printf("Hello, my pid is %d", getpid() );
  • pid = fork();
  • if( pid == 0 )
  • printf("
    I was forked! :D" );
  • sleep( 3 );
  • else
  • waitpid( pid, NULL, 0 );
  • printf("
    %d was forked!", pid );
  • 分析:

  • 将"您好,我的pid是1234"复制到缓冲区中以进行标准输出。由于末尾没有换行符,并且输出以行缓冲模式(或全缓冲模式)运行,因此终端上没有任何内容。
  • 为我们提供了两个单独的过程,标准输出缓冲区中的材料完全相同。
  • 子对象具有pid == 0并执行第4行和第5行;父对象的pid值非零(两个进程之间的几个区别之一-getpid()getppid()的返回值是另外两个)。
  • 将一个换行符和"我被分叉!:D"添加到子级的输出缓冲区。输出的第一行出现在终端上;其余的保留在缓冲区中,因为输出是行缓冲的。
  • 一切暂停3秒钟。此后,孩子通常通过main的返回返回。此时,标准输出缓冲区中的剩余数据将被清除。由于没有换行符,因此将输出位置留在行尾。
  • 父母来了。
  • 父母等待孩子完成死亡。
  • 父母添加了一个换行符,并且"派生了1345!"到输出缓冲区。在子代生成的行不完整之后,换行符将" Hello"消息刷新到输出。
  • 父级现在通常通过main的结尾处的返回退出,并且清除了剩余数据;由于末尾仍然没有换行符,因此光标位置在感叹号之后,并且shell提示出现在同一行上。

    我看到的是:

    1
    2
    3
    4
    5
    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()语句的末尾添加换行符(这很快成为标准做法)会极大地改变输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <stdio.h>
    #include <unistd.h>

    int main()
    {
        int pid;
        printf("Hello, my pid is %d
    "
    , getpid() );

        pid = fork();
        if( pid == 0 )
            printf("I was forked! :D %d
    "
    , getpid() );
        else
        {
            waitpid( pid, NULL, 0 );
            printf("%d was forked!
    "
    , pid );
        }
        return 0;
    }

    我现在得到:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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"行。


    原因是在格式字符串的末尾没有
    时,该值不会立即打印到屏幕上。而是在进程内对其进行缓冲。这意味着直到分叉操作之后才实际打印它,因此将其打印两次。

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

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

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


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

    通常,子级仅用于调用exec*函数。由于这将替换整个子进程映像(包括所有缓冲区),因此从技术上讲,如果这确实是您要在子进程中进行的全部操作,则无需fflush。但是,如果可能存在缓冲数据,则应谨慎处理exec故障。特别是,避免使用任何stdio函数(write正常)将错误输出到stdout或stderr,然后调用_exit(或_exit)而不是调用exit或仅返回(这将刷新所有缓冲的缓冲区)输出)。或通过在分叉之前冲洗完全避免此问题。