简体   繁体   English

与子进程stdout / stdin通信

[英]Communicate with child process stdout/stdin

I am trying to communicate with a process (that itself writes to stdin and stdout to interact in a terminal with a user) and read it's stdin and write to it's stdout in C. 我正在尝试与进程通信(它本身写入stdin和stdout以在终端中与用户进行交互)并读取它的stdin并在C中写入它的stdout。

Hence I try to substitute a shell user programmatically. 因此,我尝试以编程方式替换shell用户。 A methapohrical example: Imagine I want to use VIM in C for some reason. 一个美国式的例子:想象一下,我想出于某种原因在C中使用VIM。 Then I also need to write commands (stdout) and read stuff from the editor (stdin). 然后我还需要编写命令(stdout)并从编辑器(stdin)读取东西。

Initially I thought this might be a trivial task, but it seems like there's no standard approach. 最初我认为这可能是一项微不足道的任务,但似乎没有标准方法。 int system(const char *command); just executes a command and sets the commands stdin/stdout to the one of the calling process. 只执行一个命令并将命令stdin / stdout设置为一个调用进程。

Because this leads nowhere, I looked at FILE *popen(const char *command, const char *type); 因为这无处可去,我看了FILE *popen(const char *command, const char *type); but the manual pages state that: 但手册页说明:

Since a pipe is by definition unidirectional, the type argument may specify only reading or writing, not both; 由于管道根据定义是单向的,因此类型参数可以仅指定读取或写入,而不是两者; the resulting stream is correspondingly read-only or write-only. 结果流相应地是只读的或只写的。

and its implication: 及其含义:

The return value from popen() is a normal standard I/O stream in all respects save that it must be closed with pclose() rather than fclose(3). popen()的返回值在所有方面都是普通的标准I / O流,除非必须用pclose()而不是fclose(3)来关闭它。 Writing to such a stream writes to the standard input of the command; 写入这样的流写入命令的标准输入; the command's standard output is the same as that of the process that called popen() , unless this is altered by the command itself. 命令的标准输出 与调用popen()的进程相同 ,除非命令本身改变了它。 Conversely, reading from a "popened" stream reads the command's standard output , and the command's standard input is the same as that of the process that called popen() . 相反, 从“popened”流读取读取命令的标准输出命令的标准输入与调用popen()的进程相同

Hence it wouldn't be completely impossible to use popen(), but it appears to me very inelegant, because I would have to parse the stdout of the calling process (the code that called popen()) in order to parse data sent from the popened command (when using popen type 'w'). 因此,使用popen()并不是完全不可能,但在我看来它非常不优雅,因为我必须解析调用进程的stdout(调用popen()的代码)以解析从popened命令(当使用popen类型'w'时)。

Conversely, when popen is called with type 'r', I would need to write to the calling's process stdin, in order to write data to the popened command. 相反,当使用类型'r'调用popen时,我需要写入调用的进程stdin,以便将数据写入popened命令。 It's not even clear to me whether both these processes receive the same data in the stdin in this case... 在这种情况下,我甚至不清楚这两个进程是否在stdin中接收相同的数据......

I just need to control stdin and stdout of a program. 我只需要控制程序的stdin和stdout。 I mean can't there be a function like: 我的意思是不能有这样的功能:

stdin_of_process, stdout_of_process = real_popen("/path/to/bin", "rw")
// write some data to the process stdin
write("hello", stdin_of_process)
// read the response of the process
read(stdout_of_process)

So my first question : What is the best way to implement the upper functionality? 所以我的第一个问题是 :实现上层功能的最佳方法是什么?

Currently I am trying the following approach to communicate with another process: 目前我正在尝试以下方法与另一个进程通信:

  1. Set up two pipes with int pipe(int fildes[2]); int pipe(int fildes[2]);设置两个管道int pipe(int fildes[2]); . One pipe to read the stdout of the process, the other pipe to write to the stdin of the process. 一个管道读取进程的stdout,另一个管道写入进程的stdin。
  2. Fork. 叉子。
  3. Execute the process that I want to communicate with in the forked child process using int execvp(const char *file, char *const argv[]); 使用int execvp(const char *file, char *const argv[]);执行我想在forked子进程中与之通信的进程int execvp(const char *file, char *const argv[]); .
  4. Communicate with the child using the two pipes in the original process. 在原始过程中使用两个管道与孩子沟通。

That's easy said bot not so trivially implemented (At least for me). 这很容易说机器人不是那么简单(至少对我来说)。 I oddly managed to do so in one case, but when I tried to understand what I am doing with a simpler example, I fail. 我奇怪地设法在一个案例中这样做,但当我试图通过一个更简单的例子来理解我在做什么时,我失败了。 Here is my current problem: 这是我目前的问题:

I have two programs. 我有两个程序。 The first just writes a incremented number every 100 ms to it's stdout: 第一个只是每100毫秒写一个递增的数字到它的标准输出:

#include <unistd.h>
#include <time.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

void sleepMs(uint32_t ms) {
    struct timespec ts;
    ts.tv_sec = 0 + (ms / 1000);
    ts.tv_nsec = 1000 * 1000 * (ms % 1000);
    nanosleep(&ts, NULL);
}

int main(int argc, char *argv[]) {
    long int cnt = 0;
    char buf[0x10] = {0};

    while (1) {
        sleepMs(100);
        sprintf(buf, "%ld\n", ++cnt);
        if (write(STDOUT_FILENO, buf, strlen(buf)) == -1)
            perror("write");
    }
}

Now the second program is supposed to read the stdout of the first program (Please keep in my mind that I eventually want to read AND write with a process, so a technical correct solution to use popen() for the upper use case might be right in this specific case, because I simplified my experiments to just capture the stdout of the bottom program). 现在第二个程序应该读取第一个程序的stdout(请记住我最终想要读取和写入一个进程,因此对于上层用例使用popen()的技术正确解决方案可能是正确的在这个特定的情况下,因为我简化了我的实验,只是捕获了底部程序的标准输出。 I expect from the bottom program that it reads whatever data the upper program writes to stdout. 我希望从底层程序中读取上层程序写入stdout的任何数据。 But it does not read anything. 但它没有读任何东西。 Where could be the reason? 可能是哪里的原因? (second question). (第二个问题)。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <time.h>

void sleepMs(uint32_t ms) {
    struct timespec ts;
    ts.tv_sec = 0 + (ms / 1000);
    ts.tv_nsec = 1000 * 1000 * (ms % 1000);
    nanosleep(&ts, NULL);
}

int main() {
    int pipe_fds[2];
    int n;
    char buf[0x100] = {0};
    pid_t pid;

    pipe(pipe_fds);

    char *cmd[] = {"/path/to/program/above", NULL};

    if ((pid = fork()) == 0) { /* child */
        dup2(pipe_fds[1], 1); // set stdout of the process to the write end of the pipe
        execvp(cmd[0], cmd); // execute the program.
        fflush(stdout);
        perror(cmd[0]); // only reached in case of error
        exit(0);
    } else if (pid == -1) { /* failed */
        perror("fork");
        exit(1);
    } else { /* parent */

        while (1) {
            sleepMs(500); // Wait a bit to let the child program run a little
            printf("Trying to read\n");
            if ((n = read(pipe_fds[0], buf, 0x100)) >= 0) { // Try to read stdout of the child process from the read end of the pipe
                buf[n] = 0; /* terminate the string */
                fprintf(stderr, "Got: %s", buf); // this should print "1 2 3 4 5 6 7 8 9 10 ..."
            } else {
                fprintf(stderr, "read failed\n");
                perror("read");
            }
        }
    }
}

Here is a (C++11-flavored) complete example. 是一个(C ++ 11风格)完整的例子。

For many practical purposes, however, the Expect library could probably be a good choice (check out the code in the example subdirectory of its source distribution). 但是,出于许多实际目的, Expect库可能是一个不错的选择(查看其源代码分发的example子目录中的代码)。

You've got the right idea, and I don't have time to analyze all of your code to point out the specific problem, but I do want to point out a few things that you may have overlooked on how programs and terminals work. 你有正确的想法,我没有时间分析你的所有代码来指出具体的问题,但我想指出一些你可能忽略的关于程序和终端如何工作的事情。

The idea of a terminal as a "file" is naivé. 终端作为“文件”的想法是天真的。 Programs like vi use a library (ncurses) to send special control characters (and change terminal device driver settings). 像vi这样的程序使用库(ncurses)来发送特殊控制字符(并更改终端设备驱动程序设置)。 For example, vi puts the terminal device driver itself into a mode where it can read a character at a time, among other things. 例如,vi将终端设备驱动程序本身置于可以一次读取字符的模式中。

It is very non-trivial to "control" a program like vi this way. 以这种方式“控制”像vi这样的程序是非常重要的。

On your simplified experiment... 关于你的简化实验......

Your buffer is one byte too small. 你的缓冲区是一个字节太小。 Also, be aware IO is sometimes line buffered. 另外,请注意IO有时是行缓冲的。 So, you might try making sure the newline is getting transferred (use printf instead of sprintf/strlen/write...you hooked the stdout up to your pipe already), otherwise you might not see data until a newline is hit. 所以,你可能会尝试确保转换换行符(使用printf而不是sprintf / strlen / write ...你已经将stdout连接到你的管道),否则你可能看不到数据,直到命令换行。 I don't remember pipe being line buffered, but it is worth a shot. 我不记得管道是行缓冲的,但值得一试。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM