如何解决使用管道在同一进程上执行多个shell命令时读取死锁
我正在制作一个需要在同一个 bash shell 实例中运行多个命令的 C++ 程序。 我需要这个,因为一些命令正在设置一个 bash 变量,该变量需要被后续的命令。
我使用 pipes 制作文件描述符,然后使用 read 和 write 读取和写入,这些管道的另一端连接到使用fork。
当命令不返回输出时会出现问题,例如设置 bash 变量。 在下面的代码中,读取将永远挂在命令编号 2 上。我一直在寻找大约几天,似乎没有办法检测命令何时完成运行而无需在某处关闭管道。我相信如果我关闭管道,我将无法重新打开它,这意味着我需要制作一个没有加载变量的新 bash shell。
此外,我无法确定哪些命令不会返回输出,因为此代码将从 Web 服务器获取它需要运行的命令,并希望避免将命令与“&&”连接起来以进行粒度错误报告。
#include <unistd.h>
#include <fcntl.h>
#include <cstdlib>
#include <string>
#include <iostream>
using namespace std;
int main(int argc,char *argv[])
{
int inPipeFD[2];
int outPipeFD[2];
// Create a read and write pipe for communication with the child process
pipe(inPipeFD);
pipe(outPipeFD);
// Set the read pipe to be blocking
fcntl(inPipeFD[0],F_SETFL,fcntl(inPipeFD[0],F_GETFL) & ~O_NONBLOCK);
fcntl(inPipeFD[1],fcntl(inPipeFD[1],F_GETFL) & ~O_NONBLOCK);
// Create a child to run the job commands in
int pid = fork();
if(pid == 0) // Child
{
// Close STDIN and replace it with outPipeFD read end
dup2(outPipeFD[0],STDIN_FILENO);
// Close STDOUT and replace it with inPipe read end
dup2(inPipeFD[1],STDOUT_FILENO);
system("/bin/bash");
}
else // Parent
{
// Close the read end of the write pipe
close(outPipeFD[0]);
// Close the write end of the read pipe
close(inPipeFD[1]);
}
// Command 1
char buf[256];
string command = "echo test\n";
write(outPipeFD[1],command.c_str(),command.length());
read(inPipeFD[0],buf,sizeof(buf));
cout << buf << endl;
// Command 2
char buf2[256];
command = "var=worked\n";
write(outPipeFD[1],buf2,sizeof(buf2));
cout << buf2 << endl;
// Command 3
char buf3[256];
command = "echo $var\n";
write(outPipeFD[1],buf3,sizeof(buf3));
cout << buf3 << endl;
}
有没有办法在不关闭管道的情况下检测孩子的命令已经完成?
解决方法
一种解决方案是将 bash
设置为交互模式,以 system("/bin/bash -i");
开头,并将提示设置为最后一个命令的退出代码。
首先,一个方便的函数,让读写更简单:
std::string command(int write_fd,int read_fd,std::string cmd) {
write(write_fd,cmd.c_str(),cmd.size());
cmd.resize(1024); // turn cmd into a buffer
auto len = read(read_fd,cmd.data(),cmd.size());
if(len == -1) len = 0;
cmd.resize(static_cast<std::size_t>(len));
return cmd;
}
然后在您的父进程中:
sleep(1); // ugly way to make reasonably sure the child has started bash
int& out = outPipeFD[1]; // for convenience
int& in = inPipeFD[0]; // for convenience
// first,set the prompt
std::cout << command(out,in,"export PS1='$?\\n'\n") << '\n';
// then all these will print something
std::cout << command(out,"echo test\n") << '\n';
std::cout << command(out,"var=worked\n") << '\n';
std::cout << command(out,"echo $var\n") << '\n';
通过这种方式,您将始终可以阅读一些内容 - 您还可以使用它来验证命令是否正确执行。
如果您的 bash
需要一个处于 -i
(交互)模式的真实终端,我们必须在没有它的情况下进行。思路:
- 为每个发送的命令添加
echo $?
+ 一个分隔符 - 将管道设置为非阻塞模式,以便能够捕获杂项。糟糕的情况,例如发送了命令
exit
。 - 阅读直到找到分隔符或发生错误。
为了使分隔符难以猜测(为了不能轻易强制读取不同步),我将为每个命令生成一个新的分隔符。
这里有一个例子,展示了这些想法和内联注释的情况:
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <random>
#include <sstream>
#include <utility>
#include <vector>
// a function to generate a random string to use as a delimiter
std::string generate_delimiter() {
thread_local std::mt19937 prng(std::random_device{}());
thread_local std::uniform_int_distribution dist('a','z');
thread_local auto gen = [&]() { return dist(prng); };
std::string delimiter(128,0);
std::generate(delimiter.begin(),delimiter.end(),gen);
return delimiter;
}
// custom exit codes for the command function
enum exit_status_t {
ES_WRITE_FAILED = 256,ES_READ_FAILED,ES_EXIT_STATUS_NOT_FOUND
};
// a function executing a command and returning the output and exit code
std::pair<std::vector<std::string>,exit_status_t> command(int write_fd,std::string cmd) {
constexpr size_t BufSize = 1024;
// a string that is unlikely to show up in the output:
const std::string delim = generate_delimiter() + "\n";
cmd += "\necho -e $?\"\\n\"" + delim; // add echoing of status code
auto len = write(write_fd,cmd.size()); // send the commands
if(len <= 0) return {{},ES_WRITE_FAILED}; // couldn't write,return
cmd.resize(0); // use cmd to collect all read data
std::string buffer(BufSize,0);
// a loop to extract all data until the delimiter is found
fd_set read_set{};
FD_SET(read_fd,&read_set);
while(true) {
// wait until something happens on the pipe
select(read_fd + 1,&read_set,nullptr,nullptr);
if((len = read(read_fd,buffer.data(),buffer.size())) <= 0) {
// Failed reading - pipe probably closed on the other side.
// Add a custom exit code and the delimiter and break out.
cmd += "\n" + std::to_string(ES_READ_FAILED) + "\n" + delim;
break;
}
// append what was read to cmd
cmd.append(buffer.begin(),buffer.begin() + len);
// break out of the loop if we got the delimiter
if(cmd.size() >= delim.size() &&
cmd.substr(cmd.size() - delim.size()) == delim)
{
break;
}
}
cmd.resize(cmd.size() - delim.size()); // remove the delimiter
// put what was read in an istringstream for parsing
std::istringstream is(cmd);
// extract line by line
std::vector<std::string> output;
while(std::getline(is,cmd)) {
output.push_back(cmd);
}
// extract the exit code at the last line
exit_status_t retval = ES_EXIT_STATUS_NOT_FOUND;
if(not output.empty()) { // should never be empty but ...
retval = static_cast<exit_status_t>(std::stoi(output.back(),nullptr));
output.resize(output.size() - 1);
}
return {output,retval}; // return the pair
}
测试驱动程序:
int main() {
int inPipeFD[2];
int outPipeFD[2];
// Create a read and write pipe for communication with the child process
pipe(inPipeFD);
pipe(outPipeFD);
// Set the read pipe to be non-blocking
fcntl(inPipeFD[0],F_SETFL,fcntl(inPipeFD[0],F_GETFL) | O_NONBLOCK);
fcntl(inPipeFD[1],fcntl(inPipeFD[1],F_GETFL) | O_NONBLOCK);
// Create a child to run the job commands in
int pid = fork();
if(pid == 0) // Child
{
// Close STDIN and replace it with outPipeFD read end
dup2(outPipeFD[0],STDIN_FILENO);
close(outPipeFD[0]); // not needed anymore
// Close STDOUT and replace it with inPipe read end
dup2(inPipeFD[1],STDOUT_FILENO);
close(inPipeFD[1]); // not needed anymore
// execl() is cleaner than system() since it replaces the process
// completely. Use /bin/sh instead if you'd like.
execl("/bin/bash","bash",nullptr);
return 1; // to not run the parent code in case execl fails
}
// Parent
// Close the read end of the write pipe
close(outPipeFD[0]);
// Close the write end of the read pipe
close(inPipeFD[1]);
sleep(1);
int& out = outPipeFD[1]; // for convenience
int& in = inPipeFD[0]; // for convenience
// a list of commands,including an erroneous command(foobar) + exit
for(std::string cmd : {"echo test","var=worked","echo $var","foobar","exit"})
{
std::cout << "EXECUTING COMMAND: " << cmd << '\n';
auto [output,exit_status] = command(out,cmd);
// print what was returned
for(auto str : output) std::cout << str << '\n';
std::cout << "(exit status=" << exit_status << ")\n";
}
}
可能的输出:
EXECUTING COMMAND: echo test
test
(exit status=0)
EXECUTING COMMAND: var=worked
(exit status=0)
EXECUTING COMMAND: echo $var
worked
(exit status=0)
EXECUTING COMMAND: foobar
bash: line 7: foobar: command not found
(exit status=127)
EXECUTING COMMAND: exit
(exit status=257)
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。