如何正确关闭不用的管道?

How to correctly close unused pipes?

我正在实施一个支持管道的简化 shell。 下面显示的我的部分代码运行良好,但我不确定它为什么有效。

main.cpp

#include <iostream>
#include <string>
#include <queue>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include "include/command.h"

using namespace std;

int main()
{
    string rawCommand;
    IndividualCommand tempCommand = {};

    int pipeFD[2] = {PIPE_IN, PIPE_OUT};
    int firstPipeRead, firstPipeWrite, secondPipeRead, secondPipeWrite;

    while (true)
    {
        cout << "% ";
        getline(cin, rawCommand);

        if (rawCommand == "exit")
            break;

        Command *command = new Command(rawCommand);
        deque<IndividualCommand> commandQueue = command->parse();

        delete command;

        while (!commandQueue.empty())
        {
            tempCommand = commandQueue.front();
            commandQueue.pop_front();

            firstPipeRead = secondPipeRead;
            firstPipeWrite = secondPipeWrite;

            if (tempCommand.outputStream == PIPE_OUT)
            {
                pipe(pipeFD);
                secondPipeRead = pipeFD[0];
                secondPipeWrite = pipeFD[1];
            }

            pid_t child_pid;
            child_pid = fork();

            int status;

            // child process
            if (child_pid == 0)
            {
                if (tempCommand.redirectToFile != "")
                {
                    int fd = open(tempCommand.redirectToFile.c_str(), O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
                    dup2(fd, STDOUT_FILENO);
                    close(fd);
                }

                if (tempCommand.inputStream == PIPE_IN)
                {
                    close(firstPipeWrite);
                    dup2(firstPipeRead, STDIN_FILENO);
                    close(firstPipeRead);
                }
                if (tempCommand.outputStream == PIPE_OUT)
                {
                    close(secondPipeRead);
                    dup2(secondPipeWrite, STDOUT_FILENO);
                    close(secondPipeWrite);
                }

                if (tempCommand.argument != "")
                    execl(tempCommand.executable.c_str(), tempCommand.executable.c_str(), tempCommand.argument.c_str(), NULL);
                else
                    execl(tempCommand.executable.c_str(), tempCommand.executable.c_str(), NULL);
            }
            else
            {
                close(secondPipeWrite);
                if (commandQueue.empty())
                    waitpid(child_pid, &status, 0);
            }
        }
    }

    return 0;
}

command.h

#ifndef COMMAND_H
#define COMMAND_H

#include <string>
#include <queue>
#include <sstream>
#include <unistd.h>
using namespace std;

#define PIPE_IN  0x100000
#define PIPE_OUT 0x100001

struct IndividualCommand
{
    string executable = "";
    string argument = "";
    string redirectToFile = "";
    int inputStream = STDIN_FILENO;
    int outputStream = STDOUT_FILENO;
    int errorStream = STDERR_FILENO;
};

class Command
{
private:
    string rawCommand, tempString;
    queue<string> splittedCommand;
    deque<IndividualCommand> commandQueue;
    stringstream commandStream;
    IndividualCommand tempCommand;
    bool isExecutableName;

public:
    Command(string rawCommand);
    deque<IndividualCommand> parse();
};

#endif

command.cpp

#include "include/command.h"

Command::Command(string rawCommand)
{
    this->rawCommand = rawCommand;
    isExecutableName = true;
}

deque<IndividualCommand>  Command::parse()
{
    commandStream << rawCommand;

    while (!commandStream.eof())
    {
        commandStream >> tempString;
        splittedCommand.push(tempString);
    }

    while (!splittedCommand.empty())
    {
        tempString = splittedCommand.front();
        splittedCommand.pop();

        if (isExecutableName)
        {
            tempCommand.executable = tempString;
            isExecutableName = false;

            if (!commandQueue.empty() && commandQueue.back().outputStream == PIPE_OUT)
                tempCommand.inputStream = PIPE_IN;
        }
        else
        {
            // normal pipe
            if (tempString == "|")
            {
                tempCommand.outputStream = PIPE_OUT;
                isExecutableName = true;
                commandQueue.push_back(tempCommand);
                tempCommand = {};
            }
            // redirect to file
            else if (tempString == ">")
            {
                tempCommand.redirectToFile = splittedCommand.front();
                splittedCommand.pop();
            }
            // argv
            else
                tempCommand.argument = tempString;
        }

        if (splittedCommand.empty())
        {
            commandQueue.push_back(tempCommand);
            tempCommand = {};
        }
    }

    return commandQueue;
}

所以基本上通信是在两个子进程之间建立的,而不是在子进程和父进程之间。 (我正在使用第一和第二个管道来避免在遇到类似“ls | cat |cat”的情况时连续调用 pipe() 覆盖 FD)。

shell原来卡死是因为写端没有关闭,导致读端阻塞。我已经尝试关闭两个子进程中的所有内容,但没有任何改变。

我的问题是为什么close(secondPipeWrite);在父进程中解决了一切?这是否意味着真正重要的是管道的写端,我们不必关心读端是否显式关闭?

此外,为什么我不需要关闭子进程中的任何东西,它仍然有效?

会发生意外!当没有充分的理由让他们可靠地这样做时,事情有时似乎会奏效。如果您没有正确关闭所有未使用的管道描述符,则无法保证 multi-stage 管道正常工作,即使它恰好适合您。尤其是在 child 进程中,您没有关闭足够多的文件描述符。您应该关闭所有管道的所有未使用端。

这是我在其他答案中包含的 'Rule of Thumb'。


经验法则:如果你 dup2() 管道的一端连接到标准输入或标准输出,同时关闭 返回的原始文件描述符 pipe() 尽早。 特别是,您应该在使用任何 exec*() 函数族。

如果您使用以下任一方式复制描述符,则该规则也适用 dup() 或者 fcntl() F_DUPFDF_DUPFD_CLOEXEC.


如果 parent 进程不会通过以下方式与它的任何 children 通信 管道,它必须确保尽早关闭管道的两端 足够(例如,在等待之前)以便其 children 可以接收 EOF 指示读取(或获取 SIGPIPE 信号或写入错误 写),而不是无限期地阻塞。 即使 parent 使用管道而不使用 dup2(),它也应该 通常至少关闭管道的一端——这种情况极为罕见 在单个管道的两端读写的程序。

请注意 O_CLOEXEC 选项 open(), 并且 fcntl()FD_CLOEXECF_DUPFD_CLOEXEC 选项也可以考虑因素 进入这个讨论。

如果你使用 posix_spawn() 及其广泛的支持功能系列(总共 21 个功能), 您将需要查看如何在生成的进程中关闭文件描述符 (posix_spawn_file_actions_addclose(), 等)。

请注意,使用 dup2(a, b) 比使用 close(b); dup(a); 更安全 由于各种原因。 一个是如果你想强制文件描述符大于 通常的数字,dup2() 是唯一明智的方法。 另一个是如果 ab 相同(例如两者都是 0),则 dup2() 正确处理它(它在复制 a 之前不会关闭 b) 而单独的 close()dup() 则非常失败。 这种情况不太可能,但并非不可能。


请注意,如果错误的进程保持管道描述符打开,它会阻止进程检测到 EOF。如果管道中的最后一个进程打开了管道的写入端,其中一个进程(可能是它自己)正在读取直到该管道的读取端出现 EOF,则该进程将永远不会获得 EOF。

审查 C++ 代码

总的来说,你的代码很好。我的默认编译选项选择了 close(firstPipeWrite)close(firstPipeRead) 对未初始化变量进行操作的两个问题;它们被视为错误,因为我编译时使用:

c++ -O3 -g -std=c++11 -Wall -Wextra -Werror -c -o main.o main.cpp

但仅此而已 — 这是非常出色的工作。

但是,这些错误也指出了您的问题所在。

假设您有一个命令输入,需要两个管道(P1 和 P2)和三个进程(或命令,C1、C2、C3),例如:

who | grep -v root | sort

您希望命令设置如下:

  • C1: who — 创建 P1;标准输入 = 标准输入,标准输出 = P1[W]
  • C2: grep — 创建 P2;标准输入 = P1[R],标准输出 = P2[W]
  • C3: sort — 不创建管道;标准输入 = P2[R],标准输出 = stdout

PN[R]表示法表示管道N的读描述符等

一个更精细的管道,例如 who | awk '{print }' | sort | uniq -c | sort -n,有 5 个命令和 4 个管道是类似的:它只是有更多的进程 CN(N = 2、3、4)创建 PN 和 运行,标准输入来自 P(N-1)[R],标准输出为 PN[W]。

一个two-command管道当然只有一个管道,结构:

  • C1 — 创建 P1;标准输入 = 标准输入,标准输出 = P1[W]
  • C2 — 不创建管道;标准输入 = P1[R],标准输出 = stdout

而 one-command(退化)管道当然有零个管道,结构:

  • C1 — 不创建管道;标准输入 = stdin,标准输出 = stdout

请注意,您需要知道您正在处理的命令是第一个、最后一个还是在管道的中间——为每个命令完成的管道工作是不同的。另外,如果你有一个 multi-command 管道(三个或更多命令),你可以在一段时间后关闭旧管道;他们将不再需要。所以在处理 C3 时,P1 的两端都可以永久关闭;他们不会再被引用。您需要当前进程的输入管道和输出管道;任何旧管道都可以通过协调管道的过程关闭。

您需要决定哪个进程正在协调管道。在某些方面,最简单的方法是让原始 (parent) shell 进程启动所有 sub-processes、left-to-right — 这就是您正在做的 —但这绝不是唯一的方法。

随着 shell 进程启动 child 进程,shell 最终关闭它打开的所有管道的所有描述符是至关重要的,这样 child 进程可以检测到 EOF。这必须在等待任何 children 之前完成。事实上,管道中的所有进程都必须在 parent 能够等待其中任何一个之前启动——这些进程必须同时 运行,一般来说,否则,中间的管道可能会填满向上,阻塞了整个管道。

我将向您指出 C Minishell — Adding Pipelines 作为一个问题和一个显示如何做的答案。这不是唯一的方法,我不认为这是最好的方法,但它确实有效。

在您的代码中解决这个问题留作练习 — 我现在需要完成一些工作。但这应该会为您提供正确方向的有力指示。

请注意,由于您的 parent shell 创建了所有 sub-processes,因此 waitpid() 代码并不理想。您将有越来越多的僵尸进程。你需要考虑一个循环来收集任何死去的 children,可能将 WNOHANG 作为第三个参数的一部分,这样当没有僵尸时,shell 可以继续。当您 运行 在后台管道中处理等时,这变得更加重要