输入/输出传输管道

7
这个问题源于我尝试实现以下指令:

Linux Pipes as Input and Output

How to send a simple string between two programs using pipes?

http://tldp.org/LDP/lpg/node11.html

我的问题类似于这个问题:Linux Pipes as Input and Output,但更具体。

本质上,我正在尝试替换:

/directory/program < input.txt > output.txt

使用C++中的管道来避免使用硬盘。以下是我的代码:

//LET THE PLUMBING BEGIN 
int fd_p2c[2], fd_pFc[2], bytes_read;
    // "p2c" = pipe_to_child, "pFc" = pipe_from_child (see above link)
pid_t childpid;
char readbuffer[80];
string program_name;// <---- includes program name + full path
string gulp_command;// <---- includes my line-by-line stdin for program execution
string receive_output = "";

pipe(fd_p2c);//create pipe-to-child
pipe(fd_pFc);//create pipe-from-child
childpid = fork();//create fork

if (childpid < 0)
{
    cout << "Fork failed" << endl;
    exit(-1);
}
else if (childpid == 0)
{
    dup2(0,fd_p2c[0]);//close stdout & make read end of p2c into stdout
    close(fd_p2c[0]);//close read end of p2c
    close(fd_p2c[1]);//close write end of p2c
    dup2(1,fd_pFc[1]);//close stdin & make read end of pFc into stdin
    close(fd_pFc[1]);//close write end of pFc
    close(fd_pFc[0]);//close read end of pFc

    //Execute the required program
    execl(program_name.c_str(),program_name.c_str(),(char *) 0);
    exit(0);
}
else
{
    close(fd_p2c[0]);//close read end of p2c
    close(fd_pFc[1]);//close write end of pFc

    //"Loop" - send all data to child on write end of p2c
    write(fd_p2c[1], gulp_command.c_str(), (strlen(gulp_command.c_str())));
    close(fd_p2c[1]);//close write end of p2c

    //Loop - receive all data to child on read end of pFc
    while (1)
    {        
        bytes_read = read(fd_pFc[0], readbuffer, sizeof(readbuffer));

        if (bytes_read <= 0)//if nothing read from buffer...
            break;//...break loop

        receive_output += readbuffer;//append data to string
    }
    close(fd_pFc[0]);//close read end of pFc
}

我非常确定上述字符串已经被正确初始化。但是,有两件事情发生了,让我感到很困惑:
(1) 我执行的程序报告说“输入文件为空”。既然我没有使用 "<" 调用程序,它就不应该期望有输入文件。相反,它应该期望键盘输入。此外,它应该读取 "gulp_command" 中包含的文本。
(2) 程序的报告(通过标准输出提供)出现在终端上。这很奇怪,因为这个管道的目的是将 stdout 传输到我的字符串 "receive_output" 中。但是由于它出现在屏幕上,这表明信息没有正确地通过管道传递到变量中。如果我在 if 语句的末尾实现以下内容,
cout << receive_output << endl;

我得到的是空字符串,好像什么都没有。非常感谢您能给予的任何帮助!

编辑:澄清

我的程序目前使用文本文件与另一个程序通信。我的程序写入一个文本文件(例如input.txt),该文件由外部程序读取。然后该程序生成output.txt,由我的程序读取。因此它看起来像这样:

my code -> input.txt -> program -> output.txt -> my code

因此,我的代码目前使用:
system("program < input.txt > output.txt");

我希望使用管道来替换这个过程。我想将我的输入作为标准输入传递给程序,并将程序的标准输出读入到一个字符串中。


你的起始命题不够清晰。你说你想用管道替换 /directory/program <input.txt >output.txt,以避免文件系统访问。但是你需要多个进程才能使用管道。虽然你可以在单个进程中使用管道,但通常没有意义。因此,你可能会使用 /directory/program1 <input.txt | /directory/program2 >output.txt;这是有意义的(之前你可能已经使用了 /directory/program1 <input.txt >intermediate.txt; /directory/program2 <intermediate.txt >output.txt)。请澄清你的意图。 - Jonathan Leffler
好的,我编辑了问题。 - Eric Inclan
作为额外的澄清,我的目标基本上与您之前回答过的问题相同:stackoverflow.com/questions/1734932/…(顺便说一句,您的回答非常好)。 - Eric Inclan
@EricInclan 如果我想要为两个子进程做同样的事情,也就是在两个子进程之间发送和接收字符串,而忽略父进程,我们应该如何运行这段代码? - Mohsin
@Mohsin 我以前从未尝试过这种情况。不幸的是,我现在无法给你提供指导。 - Eric Inclan
3个回答

8

您的主要问题是dup2()函数的参数顺序错误。正确的使用方式应该是:

dup2(fd_p2c[0], 0);   // Duplicate read end of pipe to standard input
dup2(fd_pFc[1], 1);   // Duplicate write end of pipe to standard output

我误读了你的内容,认为一切正常,直到在设置代码上加入错误检查并从 dup2() 的调用中得到意外值时才发现问题所在。当出现问题时,请插入之前忽略的错误检查。

您还没有确保从子进程读取的数据以空值结尾; 这段代码解决了这个问题。

使用诊断功能的可工作代码,以最简单的“cat”命令为例:

#include <unistd.h>
#include <string>
#include <iostream>
using namespace std;

int main()
{
    int fd_p2c[2], fd_c2p[2], bytes_read;
    pid_t childpid;
    char readbuffer[80];
    string program_name = "/bin/cat";
    string gulp_command = "this is the command data sent to the child cat (kitten?)";
    string receive_output = "";

    if (pipe(fd_p2c) != 0 || pipe(fd_c2p) != 0)
    {
        cerr << "Failed to pipe\n";
        exit(1);
    }
    childpid = fork();

    if (childpid < 0)
    {
        cout << "Fork failed" << endl;
        exit(-1);
    }
    else if (childpid == 0)
    {
        if (dup2(fd_p2c[0], 0) != 0 ||
            close(fd_p2c[0]) != 0 ||
            close(fd_p2c[1]) != 0)
        {
            cerr << "Child: failed to set up standard input\n";
            exit(1);
        }
        if (dup2(fd_c2p[1], 1) != 1 ||
            close(fd_c2p[1]) != 0 ||
            close(fd_c2p[0]) != 0)
        {
            cerr << "Child: failed to set up standard output\n";
            exit(1);
        }

        execl(program_name.c_str(), program_name.c_str(), (char *) 0);
        cerr << "Failed to execute " << program_name << endl;
        exit(1);
    }
    else
    {
        close(fd_p2c[0]);
        close(fd_c2p[1]);

        cout << "Writing to child: <<" << gulp_command << ">>" << endl;
        int nbytes = gulp_command.length();
        if (write(fd_p2c[1], gulp_command.c_str(), nbytes) != nbytes)
        {
            cerr << "Parent: short write to child\n";
            exit(1);
        }
        close(fd_p2c[1]);

        while (1)
        {
            bytes_read = read(fd_c2p[0], readbuffer, sizeof(readbuffer)-1);

            if (bytes_read <= 0)
                break;

            readbuffer[bytes_read] = '\0';
            receive_output += readbuffer;
        }
        close(fd_c2p[0]);
        cout << "From child: <<" << receive_output << ">>" << endl;
    }
    return 0;
}

样本输出:

Writing to child: <<this is the command data sent to the child cat (kitten?)>>
From child: <<this is the command data sent to the child cat (kitten?)>>

请注意,您需要小心确保您的代码不会死锁。如果您有一个严格同步的协议(所以父进程写入消息并在锁定步骤中读取响应),那么您应该没问题,但是如果父进程试图写入一个太大而无法适合管道到子进程,而子进程正在尝试写入一个太大而无法适合管道返回到父进程的消息,则每个进程都将被阻止写入等待另一个进程读取。

1
这个很好地解决了问题。感谢您的帮助,以及提醒我关于死锁代码的问题。我不认为我在这个程序中会遇到这个问题,但知道这点还是很好的。 - Eric Inclan
如果我想要为两个子进程做同样的事情,也就是在两个子进程之间发送和接收字符串,而忽略父进程,那么我们该如何运行这段代码呢? - Mohsin
1
@Mohsin:这在一定程度上取决于子进程之间的通信方式,以及父进程在启动两个子进程后要执行什么操作。有多种处理方法。其中一种选择是,在执行上述任何代码之前,父进程简单地进行分叉,而来自该分叉的父进程可以退出、等待或继续其他工作,而子进程则处理上述代码。另一种选择是,父进程设置两个管道,进行两次分叉,每个子进程解决自己的管道问题,而父进程关闭两个管道的四个文件描述符并继续自己的工作。 - Jonathan Leffler
是的,我确切地按照你建议的第二种方法做了,我只是简单地创建了一个管道并调用了两个fork,一个用于child_a,另一个用于child_b。如果pid_child_a为零,则它会写入管道,否则如果pid_child_b为零,则它会从缓冲区读取并打印值,问题是当我第一次运行它时,它可以正常运行,但第二次运行时就不会打印任何内容。有时候当我清理项目或进行一些编辑后,它会再次给出正确的输出,但然后再次开始打印什么都没有,以此类推。 - Mohsin
而且,如果我在调试模式下运行它,每次都会得到正确的输出。 - Mohsin
@Mohsin:我无法从评论中模糊的描述中调试您的代码。如果您可以将其制作成一个MCVE([MCVE]),那么您可以在这里提出有关它的问题 - 否则请查看我的个人资料,但它仍应尽可能接近MCVE。如果您确保每个变量都得到适当的初始化并确保每个系统调用成功,我不确定为什么会出现不稳定的行为。在调试的第一步中,请确保检查您的系统调用。 - Jonathan Leffler

1
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>
#include <iostream>
using namespace std;
int main() {
    int i, status, len;
    char str[10];
    mknod("pipe", S_IFIFO | S_IRUSR | S_IWUSR, 0); //create named pipe
    pid_t pid = fork(); // create new process
    /* Process A */
    if (pid == 0) {
        int myPipe = open("pipe", O_WRONLY); // returns a file descriptor for the pipe
        cout << "\nThis is process A having PID= " << getpid(); //Get pid of process A
        cout << "\nEnter the string: ";
        cin >> str;
        len = strlen(str);
        write(myPipe, str, len); //Process A write to the named pipe
        cout << "Process A sent " << str;
        close(myPipe); //closes the file descriptor fields.
        }
    /* Process B */
        else {
        int myPipe = open("pipe", O_RDONLY); //Open the pipe and returns file descriptor
        char buffer[21];
        int pid_child;
        pid_child = wait(&status); //wait until any one child process terminates
        int length = read(myPipe, buffer, 20); //reads up to size bytes from pipe with descriptor fields, store results
    //  in buffer;
        cout<< "\n\nThis is process B having PID= " << getpid();//Get pid of process B
        buffer[length] = '\0';
        cout << "\nProcess B received " << buffer;
        i = 0;
        //Reverse the string
        for (length = length - 1; length >= 0; length--)
        str[i++] = buffer[length];
        str[i] = '\0';
        cout << "\nRevers of string is " << str;
        close(myPipe);
        }
    unlink("pipe");
return 0;
}

1
似乎你正在寻找 协同处理。你可以使用 C/C++ 编程,但由于它们已经在 (bash) shell 中可用,因此使用 shell 更容易,对吧?
首先使用 coproc 内建命令启动外部程序:
coproc external_program
< p > coproc 命令在后台启动程序,并将与其通信的文件描述符存储在一个 shell 变量数组中。现在,您只需要连接到这些文件描述符,就可以启动您的程序了:

your_program <&${COPROC[0]} >&${COPROC[1]}

在我的程序中,我会反复调用 external_program(数千次),每次使用不同的输入。这似乎像是一个一次性的 bash 脚本,在启动时执行,对吗?我不太了解文件描述符,但如果我想使用它,这是否意味着我必须写入到驱动器上的文件中,还是文件描述符可以指向代码中的字符串? - Eric Inclan
1
文件描述符在这种情况下连接到管道而不是文件。此外,您也可以运行数千次bash脚本。 - Joni

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接