【问题标题】:Read deadlock when using pipes to execute multiple shell commands on the same process使用管道在同一进程上执行多个shell命令时读取死锁
【发布时间】:2021-01-22 15:05:22
【问题描述】:

我正在制作一个需要在同一个 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], F_SETFL, 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], command.c_str(), command.length());
    read(inPipeFD[0], buf2, sizeof(buf2));
    cout << buf2 << endl;

    // Command 3
    char buf3[256];
    command = "echo $var\n";
    write(outPipeFD[1], command.c_str(), command.length());
    read(inPipeFD[0], buf3, sizeof(buf3));
    cout << buf3 << endl;
}

有没有一种方法可以检测到孩子的命令已经完成而无需关闭管道?

【问题讨论】:

  • 这一切都必须手动完成吗(fork/exec 等)?如果不是,我会推荐 boost.process 并使用异步 IO 功能。

标签: c++ linux shell pipe


【解决方案1】:

一种解决方案是将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, in, "echo test\n") << '\n';
    std::cout << command(out, in, "var=worked\n") << '\n';
    std::cout << command(out, in, "echo $var\n") << '\n';

这样你总是有一些东西要读——你也可以用它来验证命令是否正确执行。


如果您的bash 需要-i(nteractive) 模式下的真实终端,我们必须在没有它的情况下这样做。想法:

  • 为每个发送的命令添加echo $? + 分隔符
  • 将管道设置为非阻塞模式,以便能够捕获杂项。糟糕的情况,例如发送命令exit。
  • 读取直到找到分隔符或发生错误。

为了使分隔符难以猜测(为了不能轻易地强制读取不同步),我会为每个命令生成一个新的分隔符。

以下是使用内联 cmets 实现这些想法的示例:

#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,
                                                           int read_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.c_str(), 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, 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], F_SETFL, 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, in, 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)

【讨论】:

  • 感谢您的回答,相信这会解决我们的问题!我确实还有一个问题想与您分享,与此有关。在运行system("/bin/bash -i") 并尝试导出该新变量时,我收到以下两个错误:bash: cannot set terminal process group (122365): Inappropriate ioctl for device bash: no job control in this shell 这是否意味着我需要将 shell 链接到某种系统设备以实际处理发送给它的命令?
  • @D.Share 嗯,看来你的bash 需要一个真正的tty 连接到 I/O,所以任何一个人都必须作弊并编造一些代码来假装它实际上是tty 或 -I 的想法很糟糕。我可以想出一个替代解决方案,但我认为我需要将管道设置为非阻塞模式。我会努力的。
  • @D.Share 我添加了一个替代方案。我试图避免使管道不阻塞,但发现很难让它处理所有奇怪的情况。希望这个对你有用。
  • @D.Share 不客气!不过要注意一点。如果该功能的用户启动也打印到屏幕的后台进程,这将不起作用。在这种情况下,您也可以将wait 添加到命令列表中。
  • @D.Share 两者。如果使用command(out, in, "program &amp;");,它可能会被wait 保存,但在command(out, in, "program_that_starts_subprocesses_that_outlives_parent"); 的情况下,它可能是不可能的。您不仅需要扫描最后一件事read,还需要到处寻找分隔符。定界符也可能与后台程序的输出交错,从而破坏了找到它的可能性。棘手的情况。
猜你喜欢
  • 1970-01-01
  • 2017-05-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-01
相关资源
最近更新 更多