C语言 “fork()”后的printf异常

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2530663/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 04:58:34  来源:igfitidea点击:

printf anomaly after "fork()"

clinuxunixprintffork

提问by pechenie

OS: Linux, Language: pure C

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

I'm moving forward in learning C programming in general, and C programming under UNIX in a special case.

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

I detected a strange (for me) behaviour of the printf()function after using a fork()call.

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

Code

代码

#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;
}

Output

输出

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

Why did the second "Hello" string occur in the child's output?

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

Yes, it is exactly what the parent printed when it started, with the parent's pid.

是的,这正是父级在开始时打印的内容,带有父级的pid.

But! If we place a \ncharacter at the end of each string we get the expected output:

但!如果我们\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;
}

Output:

输出

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

Why does it happen? Is it correct behaviour, or is it a bug?

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

回答by Jonathan Leffler

I note that <system.h>is a non-standard header; I replaced it with <unistd.h>and the code compiled cleanly.

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

When the output of your program is going to a terminal (screen), it is line buffered. When the output of your program goes to a pipe, it is fully buffered. You can control the buffering mode by the Standard C function setvbuf()and the _IOFBF(full buffering), _IOLBF(line buffering) and _IONBF(no buffering) modes.

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

You could demonstrate this in your revised program by piping the output of your program to, say, cat. Even with the newlines at the end of the printf()strings, you would see the double information. If you send it direct to the terminal, then you will see just the one lot of information.

您可以通过将程序的输出通过管道传输到例如cat. 即使在printf()字符串末尾有换行符,您也会看到双重信息。如果您将其直接发送到终端,那么您只会看到一大堆信息。

The moral of the story is to be careful to call fflush(0);to empty all I/O buffers before forking.

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



Line-by-line analysis, as requested (braces etc removed - and leading spaces removed by markup editor):

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

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

The analysis:

分析:

  1. Copies "Hello, my pid is 1234" into the buffer for standard output. Because there is no newline at the end and the output is running in line-buffered mode (or full-buffered mode), nothing appears on the terminal.
  2. Gives us two separate processes, with exactly the same material in the stdout buffer.
  3. The child has pid == 0and executes lines 4 and 5; the parent has a non-zero value for pid(one of the few differences between the two processes - return values from getpid()and getppid()are two more).
  4. Adds a newline and "I was forked! :D" to the output buffer of the child. The first line of output appears on the terminal; the rest is held in the buffer since the output is line buffered.
  5. Everything halts for 3 seconds. After this, the child exits normally through the return at the end of main. At that point, the residual data in the stdout buffer is flushed. This leaves the output position at the end of a line since there is no newline.
  6. The parent comes here.
  7. The parent waits for the child to finish dying.
  8. The parent adds a newline and "1345 was forked!" to the output buffer. The newline flushes the 'Hello' message to the output, after the incomplete line generated by the child.
  1. 将“你好,我的 pid 是 1234”复制到标准输出的缓冲区中。因为末尾没有换行符并且输出以行缓冲模式(或全缓冲模式)运行,所以终端上没有任何显示。
  2. 为我们提供了两个独立的进程,在标准输出缓冲区中使用完全相同的材料。
  3. 孩子拥有pid == 0并执行第 4 行和第 5 行;父级具有非零值pid(两个进程之间的少数差异之一 - 来自getpid()和 的返回值getppid()还有两个)。
  4. 添加一个换行符和“我被分叉了!:D”到孩子的输出缓冲区。第一行输出出现在终端上;其余部分保存在缓冲区中,因为输出是行缓冲的。
  5. 一切都停止了 3 秒钟。在此之后,子进程通过 main 末尾的 return 正常退出。此时,stdout 缓冲区中的剩余数据将被刷新。这将输出位置留在行尾,因为没有换行符。
  6. 家长来了。
  7. 父母等待孩子完成死亡。
  8. 父级添加了一个换行符,“1345 被分叉了!” 到输出缓冲区。在孩子生成的不完整行之后,换行符将“Hello”消息刷新到输出。

The parent now exits normally through the return at the end of main, and the residual data is flushed; since there still isn't a newline at the end, the cursor position is after the exclamation mark, and the shell prompt appears on the same line.

parent 现在通过main 末尾的return 正常退出,刷新剩余数据;由于末尾仍然没有换行符,光标位置在感叹号之后,并且 shell 提示出现在同一行。

What I see is:

我看到的是:

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: 

The PID numbers are different - but the overall appearance is clear. Adding newlines to the end of the printf()statements (which becomes standard practice very quickly) alters the output a lot:

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

I now get:

我现在得到:

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:

Notice that when the output goes to the terminal, it is line-buffered, so the 'Hello' line appears before the fork()and there was just the one copy. When the output is piped to cat, it is fully-buffered, so nothing appears before the fork()and both processes have the 'Hello' line in the buffer to be flushed.

请注意,当输出到达终端时,它是行缓冲的,因此“Hello”行出现在 之前,fork()并且只有一个副本。当输出通过管道传输到 时cat,它是完全缓冲的,因此在fork()和 两个进程在缓冲区中刷新“Hello”行之前不会出现任何内容。

回答by JaredPar

The reason why is that without the \nat the end of the format string the value is not immediately printed to the screen. Instead it is buffered within the process. This means it is not actually printed until after the fork operation hence you get it printed twice.

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

Adding the \nthough forces the buffer to be flushed and outputted to the screen. This happens before the fork and hence is only printed once.

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

You can force this to occur by using the fflushmethod. For example

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

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

回答by mark4o

fork()effectively creates a copy of the process. If, before calling fork(), it had data that was buffered, both the parent and child will have the same buffered data. The next time that each of them does something to flush its buffer (such as printing a newline in the case of terminal output) you will see that buffered output in addition to any new output produced by that process. So if you are going to use stdio in both the parent and child then you should fflushbefore forking, to ensure that there is no buffered data.

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

Often, the child is used only to call an exec*function. Since that replaces the complete child process image (including any buffers) there is technically no need to fflushif that is really all that you are going to do in the child. However, if there may be buffered data then you should be careful in how an exec failure is handled. In particular, avoid printing the error to stdout or stderr using any stdio function (writeis ok), and then call _exit(or _Exit) rather than calling exitor just returning (which will flush any buffered output). Or avoid the issue altogether by flushing before forking.

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