在C++中,当cin是一个bash heredoc时调用fork会导致重复的输入片段。

11

我正在用C++实现一个类似于shell的程序。它有一个循环,从cin读取数据,然后fork出子进程并等待其返回结果。

如果输入是交互式的或者从另一个程序进行了管道传输,那么这个程序可以正常运行。但是,当输入是一个bash heredoc时,程序会重新读取部分输入(有时无限期地重复读取)。

我知道子进程会继承父进程的文件描述符,包括共享文件偏移量。但是,这个例子中的子进程没有从cin读取任何内容,所以我认为它不应该接触偏移量。我对这种情况感到困惑。


test.cpp:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char **argv)
{
    std::string line;
    while (std::getline(std::cin, line)) {
        pid_t pid = fork();
        if (pid == 0) { // child
            break; // exit immediately
        }
        else if (pid > 0) { // parent
            waitpid(pid, nullptr, 0);
        }
        else { // error
            perror("fork");
        }

        std::cout << getpid() << ": " << line << "\n";
    }
    return 0;
}

我按照以下方式编译:

g++ test.cpp -std=c++11

然后我用以下命令运行它:

./a.out <<EOF
hello world
goodbye world
EOF

输出:

7754: hello world
7754: goodbye world
7754: goodbye world

如果我在输入命令中添加第三行foo bar,程序就会陷入无限循环:

13080: hello world
13080: goodbye world
13080: foo bar
13080: o world
13080: goodbye world
13080: foo bar
13080: o world
[...]

版本信息:

  • Linux内核: 4.4.0-51-generic
  • Ubuntu系统: 16.04.1 LTS (xenial)
  • Bash shell: GNU bash, version 4.3.46(1)-release (x86_64-pc-linux-gnu)
  • GCC编译器: g++ (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609

1
如果在开头执行 std::ios::sync_with_stdio(false); 并在写入 stdout 后显式刷新会发生什么?(例如将 '\n' 更改为 std::endl - user1084944
3
通过跟踪子进程,我可以清楚地看到正在发生的事情。子进程在退出之前会向后查找文件描述符0,这会影响父进程。不幸的是,我不知道C库为什么会这样做,所以我不会将详细信息发布为答案。这也会发生在显式使用exit(0)但不会在_exit(0)时发生。 - Sam Varshavchik
1
这里的答案可能会有用:http://stackoverflow.com/questions/33899548/file-pointers-after-returning-from-a-forked-child-process - Erix
1
我无法复现这个问题,但是如果子进程没有执行 exec 函数,你绝对应该在 fork 出的子进程中使用 _exitquick_exit 函数。父进程会构建 cout 缓冲区状态并且子进程会继承它。如果子进程正常退出,他们将尝试刷新自己拷贝的 cout 缓冲区,而此时父进程正在刷新缓冲区。如果发生这种情况,你将在输出中获得重复项。 - Petr Skocik
1
据我所知,无法倒回stdin被认为是glibc的一个bug。 - n. m.
显示剩余8条评论
1个回答

2

我能够重现这个问题,不仅使用heredoc,还使用标准的文件重定向。

以下是我使用的测试脚本。在第一和第二种情况下,我都得到了输入的第二行的重复。

./a.out < Input.txt
echo

cat Input.txt | ./a.out
echo

./a.out <<EOF
hello world
goodbye world
EOF

在子进程退出之前关闭stdin似乎可以解决这两个问题。
#include <iostream>
#include <sstream>
#include <unistd.h>
#include <sys/wait.h>
#include <limits>

int main(int argc, char **argv)
{
    std::string line;
    while (std::getline(std::cin, line)) {
        pid_t pid = fork();
        if (pid == 0) { // child
            close(STDIN_FILENO);
            break; // exit after first closing stdin
        }
        else if (pid > 0) { // parent
            waitpid(pid, nullptr, 0);
        }
        else { // error
            perror("fork");
        }

        std::cout << getpid() << ": " << line << "\n";
    }
    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接