【问题标题】:How to correctly close unused pipes?如何正确关闭未使用的管道?
【发布时间】:2021-01-26 00:46:59
【问题描述】:

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

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); 解决了所有问题?是不是说真正重要的是管道的写端,而我们不必关心读端是否显式关闭?

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

【问题讨论】:

标签: c unix pipe system-calls


【解决方案1】:

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

这是我在其他答案中包含的“经验法则”。


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

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


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

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

如果你使用 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],标准输出 = 标准输出

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

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

一个双命令管道当然只有一个管道,结构如下:

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

当然,单命令(退化)管道有零个管道,结构:

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

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

您需要确定哪个进程正在协调管道。在某些方面,最简单的方法是让原始(父)shell 进程启动所有子进程,从左到右——这就是你正在做的——但这绝不是唯一的方法。

随着 shell 进程启动子进程,shell 最终关闭它打开的所有管道的所有描述符至关重要,这样子进程才能检测到 EOF。这必须在等待任何孩子之前完成。实际上,管道中的所有进程必须在父进程能够等待它们之前启动——这些进程通常必须同时运行,否则中间的管道可能会填满,阻塞整个管道。

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

在您的代码中对此进行排序留作练习——我现在需要完成一些工作。但这应该会为您指明正确的方向。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-09-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-19
    • 1970-01-01
    • 2012-06-29
    相关资源
    最近更新 更多