首页 > 解决方案 > 如何正确关闭未使用的管道?

问题描述

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

主文件

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

命令.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

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

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

标签: cunixpipesystem-calls

解决方案


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

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


经验法则:如果您 将管道的一端连接到标准输入或标准输出,请 尽快dup2() 关闭返回的两个原始文件描述符 。pipe()特别是,您应该在使用任何 exec*() 函数系列之前关闭它们。

如果您使用 或 或 复制描述符,该 规则也dup() 适用 。fcntl()F_DUPFDF_DUPFD_CLOEXEC


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

请注意,选项O_CLOEXECto open()和选项 to也可以作为讨论的因素。FD_CLOEXECF_DUPFD_CLOEXECfcntl()

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

请注意, 出于各种原因,使用dup2(a, b)比使用更安全。close(b); dup(a);一个是,如果您想强制文件描述符大于通常的数字,dup2()这是唯一明智的方法。另一个是 ifab(例如 both 0)相同,则正确处理它(在复制之前dup2() 它不会关闭),而单独的并可怕地失败。这是一个不太可能但并非不可能的情况。baclose()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 -n5 个命令和 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 — 添加管道作为一个问题,并提供一个答案来说明如何做到这一点。这不是唯一的方法,我不相信这是最好的方法,但它确实有效。

在你的代码中整理出来作为一个练习——我现在需要完成一些工作。但这应该会为您指明正确的方向。

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


推荐阅读