【问题标题】:Extra data printed out after call to fork() [duplicate]调用 fork() 后打印出额外数据 [重复]
【发布时间】:2017-02-14 01:49:25
【问题描述】:

我正在我的 Mac 上运行以下我用 c 编写的代码。

#include <unistd.h>
#include <stdio.h>  

int main(int argc, const char * argv[]) {

    int i = 0;
    printf("Starting pid:%d\n", getpid());
    while(i++<3){
        int ret = fork();
        printf("hello(%d)%d, %d ", i, getpid(), ret);
        if(!ret){
            printf("\n");
            return 0;}
    }
}

我的目标是总共打印 6 个打印件 - 3 个来自原始父级(打印在“起始 pid”中的 pid),另外 1 个来自 3 个生成的子级中的每个。相反,我收到的是:

Starting pid:7998
hello(1)8009, 0 
hello(1)7998, 8009 hello(2)8010, 0 
hello(1)7998, 8009 hello(2)7998, 8010 hello(3)7998, 8011 hello(1)7998, 8009 hello(2)7998, 8010 hello(3)8011, 0 

请注意,原始父级 (7998) 打印了 6 次(i==1 时打印 3 次,i==2 时打印两次,i==3 时打印一次)。

为什么会发生这种情况?为什么我没有收到来自父母的 3 张打印件(当 i==1、i==2、i==3 时一次)。如果我们从父级的角度来看 - 有一个 while 循环,我们应该只在里面点击 print 语句 3 次。

也许那个 fork 是异步的还是什么?我错过了什么?

【问题讨论】:

    标签: c multiprocessing fork


    【解决方案1】:

    当您fork 一个进程时,父进程中的内存在子进程中被复制。这包括stdout 缓冲区。

    从父循环的第二次迭代开始,缓冲区中有hello(1)7998, 8009。当第二个孩子分叉时,此文本仍在该孩子的缓冲区中。然后第二个孩子打印hello(2)8010, 0,然后是换行符。换行符刷新了孩子的缓冲区,所以第二个孩子打印了这个:

    hello(1)7998, 8009 hello(2)8010, 0 
    

    在 fork 之后,父级打印 hello(2)7998, 8010 并将其添加到输出缓冲区。然后在第三次迭代中,第三个孩子继承这个并打印:

    hello(1)7998, 8009 hello(2)7998, 8010 hello(3)8011, 0
    

    然后父级打印hello(3)7998, 8011 再次添加到缓冲区。然后parent然后退出循环并退出,之后buffer被刷新,parent输出:

    hello(1)7998, 8009 hello(2)7998, 8010 hello(3)7998, 8011
    

    要纠正这个问题,父进程需要在分叉之前清除输出缓冲区。

    这可以通过在调用printf 之后调用fflush(stdout) 来完成,或者在printf 的末尾添加一个换行符(\n)。

    编辑:

    作为更通用的解决方案,调用fflush(NULL) 将刷新所有打开的输出流。

    【讨论】:

    • 优秀。缓冲区内容的重复是关键。
    • 哇..简直哇。
    • fflush(NULL) 刷新所有打开的输出文件。
    • 等一下:如果我移除了叉子,那么父级会打印 3 次正确吗?基本上,父母会忽略缓冲区中的内容。那么,既然孩子是父母的精确副本,为什么孩子不会像父母那样“忽略”缓冲区呢? (这让我觉得“忽略”缓冲区是在进程之外完成的——对吗?)
    • 仅供参考,在大多数 *nix 系统上 stderr 默认情况下是无缓冲的。每当我可能有并发打印时,我通常更喜欢使用fprintf(stderr, ...) 以使输出不那么混乱。
    【解决方案2】:

    我认为您所看到的只是我将最终写入到stdout 的进程本地内容缓冲区正在复制到fork,因此内容在分叉之前排队然后由多个进程独立输出。

    分叉正在做你认为的事情,衡量工具有缺陷。

    我修改了您的代码以添加刷新:

        int ret = fork();
        printf("hello(%d)%d, %d ", i, getpid(), ret);
        fflush(stdout);
    

    这确保了printf 的结果不会位于进程本地缓冲区中。然后将其直接写入stdout,然后清空本地缓冲区。然后输出变成:

    Starting pid:41731
    hello(1)41731, 41732 hello(2)41731, 41733 hello(1)41732, 0 
    hello(3)41731, 41734 hello(2)41733, 0 
    hello(3)41734, 0 
    

    即预期的六个电话。

    (我在 Mac 上使用 CodeRunner 对此进行了测试;它还显示了表面上没有 flush 的九个结果,因此我确信我复制了初始条件)。

    【讨论】:

    • 完美!谢谢!冲洗在做什么?
    • @JohnBollinger 同时使用冲洗确实解决了它,这很奇怪
    • 只是确保该进程正在将它拥有的所有内容写入标准输出,而不是可能将其保存在内部缓存中。我敢打赌,当您fork 时,缓存正在被复制,所以许多hello(1)7998s 实际上并不是来自进程7998,它们只是被进程7998 排队,然后排队的文本被复制过来。由于fflush 清除了缓冲区,所以没有什么可复制的。 (编辑:并修改了答案,因为这是一个相当令人信服的理由)
    【解决方案3】:

    Fork 将进程一分为二。在每个子进程中。在 3 次分叉后,你最终会得到 8 个进程(除非你做一些有条件的事情)

    【讨论】:

    • 我立即杀死子进程——它们不会分叉。
    • 嗯,现在你提到这是一件有趣的事情......
    • 我看到其中一个孩子产生了一个子孩子。
    • 这是一个误导性的描述。当它成功时,fork() 产生 一个 子进程,而不是两个,并且父进程继续运行。该进程与两个子进程之间的几个区别都相当重要。
    猜你喜欢
    • 2015-08-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-10
    • 1970-01-01
    • 1970-01-01
    • 2020-07-04
    相关资源
    最近更新 更多