如何正确关闭不用的管道?
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_DUPFD
或 F_DUPFD_CLOEXEC
.
如果 parent 进程不会通过以下方式与它的任何 children 通信
管道,它必须确保尽早关闭管道的两端
足够(例如,在等待之前)以便其 children 可以接收
EOF 指示读取(或获取 SIGPIPE 信号或写入错误
写),而不是无限期地阻塞。
即使 parent 使用管道而不使用 dup2()
,它也应该
通常至少关闭管道的一端——这种情况极为罕见
在单个管道的两端读写的程序。
请注意 O_CLOEXEC
选项
open()
,
并且 fcntl()
的 FD_CLOEXEC
和 F_DUPFD_CLOEXEC
选项也可以考虑因素
进入这个讨论。
如果你使用
posix_spawn()
及其广泛的支持功能系列(总共 21 个功能),
您将需要查看如何在生成的进程中关闭文件描述符
(posix_spawn_file_actions_addclose()
,
等)。
请注意,使用 dup2(a, b)
比使用 close(b); dup(a);
更安全
由于各种原因。
一个是如果你想强制文件描述符大于
通常的数字,dup2()
是唯一明智的方法。
另一个是如果 a
与 b
相同(例如两者都是 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 可以继续。当您 运行 在后台管道中处理等时,这变得更加重要
我正在实施一个支持管道的简化 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_DUPFD
或 F_DUPFD_CLOEXEC
.
如果 parent 进程不会通过以下方式与它的任何 children 通信
管道,它必须确保尽早关闭管道的两端
足够(例如,在等待之前)以便其 children 可以接收
EOF 指示读取(或获取 SIGPIPE 信号或写入错误
写),而不是无限期地阻塞。
即使 parent 使用管道而不使用 dup2()
,它也应该
通常至少关闭管道的一端——这种情况极为罕见
在单个管道的两端读写的程序。
请注意 O_CLOEXEC
选项
open()
,
并且 fcntl()
的 FD_CLOEXEC
和 F_DUPFD_CLOEXEC
选项也可以考虑因素
进入这个讨论。
如果你使用
posix_spawn()
及其广泛的支持功能系列(总共 21 个功能),
您将需要查看如何在生成的进程中关闭文件描述符
(posix_spawn_file_actions_addclose()
,
等)。
请注意,使用 dup2(a, b)
比使用 close(b); dup(a);
更安全
由于各种原因。
一个是如果你想强制文件描述符大于
通常的数字,dup2()
是唯一明智的方法。
另一个是如果 a
与 b
相同(例如两者都是 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 可以继续。当您 运行 在后台管道中处理等时,这变得更加重要