c - 如何正确关闭未使用的管道?
问题描述
我正在实现一个支持管道的简化外壳。下面显示的部分代码运行良好,但我不确定它为什么有效。
主文件
#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);
在父进程中解决了所有问题?是不是说,真正重要的是管道的写端,而我们不必关心读端是否显式关闭?
此外,为什么我不需要关闭子进程中的任何内容并且它仍然有效?
解决方案
会发生意外!当没有充分的理由让他们可靠地这样做时,事情有时似乎会奏效。如果您没有正确关闭所有未使用的管道描述符,则不能保证多级管道可以正常工作,即使它恰好适合您。特别是,您没有在子进程中关闭足够的文件描述符。您应该关闭所有管道的所有未使用端。
这是我在其他答案中包含的“经验法则”。
经验法则:如果您
将管道的一端连接到标准输入或标准输出,请
尽快dup2()
关闭返回的两个原始文件描述符
。pipe()
特别是,您应该在使用任何
exec*()
函数系列之前关闭它们。
如果您使用 或 或 复制描述符,该
规则也dup()
适用
。fcntl()
F_DUPFD
F_DUPFD_CLOEXEC
如果父进程不会通过管道与其任何子进程通信,它必须确保它足够早地关闭管道的两端(例如,在等待之前),以便其子进程可以在读取时接收 EOF 指示(或获取 SIGPIPE信号或写入错误),而不是无限期地阻塞。即使父级使用管道而不使用dup2()
,它通常也应该至少关闭管道的一端——程序在单个管道的两端读取和写入的情况极为罕见。
请注意,选项O_CLOEXEC
to
open()
和选项 to也可以作为讨论的因素。FD_CLOEXEC
F_DUPFD_CLOEXEC
fcntl()
如果您使用
posix_spawn()
及其广泛的支持函数系列(总共 21 个函数),您将需要查看如何在生成的进程中关闭文件描述符(posix_spawn_file_actions_addclose()
等)。
请注意,
出于各种原因,使用dup2(a, b)
比使用更安全。close(b); dup(a);
一个是,如果您想强制文件描述符大于通常的数字,dup2()
这是唯一明智的方法。另一个是 ifa
与b
(例如 both 0
)相同,则正确处理它(在复制之前dup2()
它不会关闭),而单独的并可怕地失败。这是一个不太可能但并非不可能的情况。b
a
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 — 添加管道作为一个问题,并提供一个答案来说明如何做到这一点。这不是唯一的方法,我不相信这是最好的方法,但它确实有效。
在你的代码中整理出来作为一个练习——我现在需要完成一些工作。但这应该会为您指明正确的方向。
请注意,由于您的父 shell 创建了所有子进程,因此waitpid()
代码并不理想。您将累积僵尸进程。您需要考虑一个循环来收集任何死去的孩子,可能WNOHANG
作为第三个参数的一部分,这样当没有僵尸时,shell 可以继续。当您在后台管道等中运行进程时,这变得更加重要。
推荐阅读
- react-native - react-native 上传图片 react-native-fetch-blob
- mysql - phpmyadmin 与 azure 中国
- kdb - 如何在 kdb 中使用类型“c”和“C”
- python - 在 Pandas Dataframe 中访问感兴趣的行之前和之后的行
- c++ - 这是我的 .h 文件的一些错误,当我在其中包含我的类模板时显示 [Error] unterminated #ifndef
- pycharm - 如何使用 Pycharm 找出谁在特定行上进行了最后一次更改
- angularjs - 无法在 AngularJs 中获取选定的单选按钮值
- r - RMarkdown 图以 101% 的宽度和高度插入 Word
- openshift - 小贩数据访问错误
- jenkins - Jenkins + (yarn or npm) - 如何从命令行设置内部版本号