与子进程 stdout/stdin 通信

Nik*_*her 6 c linux ipc stdout pipe

我正在尝试与一个进程通信(该进程本身写入标准输入和标准输出以在终端中与用户交互)并读取它的标准输入并写入它在 C 中的标准输出。

因此我尝试以编程方式替换 shell 用户。一个隐喻的例子:假设我出于某种原因想在 C 中使用 VIM。然后我还需要编写命令(stdout)并从编辑器(stdin)读取内容。

最初我认为这可能是一项微不足道的任务,但似乎没有标准方法。int system(const char *command);只是执行一个命令并将命令 stdin/stdout 设置为调用进程之一。

因为这没有任何结果,所以我查看了FILE *popen(const char *command, const char *type);但手册页指出:

由于管道根据定义是单向的,因此类型参数只能指定读或写,而不是两者;结果流相应地是只读或只写的。

及其含义:

popen() 的返回值在所有方面都是一个普通的标准 I/O 流,除了它必须用 pclose() 而不是 fclose(3) 关闭。 写入这样的流会写入命令的标准输入;命令的标准输出与调用 popen() 的进程的标准输出相同,除非命令本身对此进行了更改。相反,从“popened”流中读取会读取命令的标准输出,并且命令的标准输入与调用 popen() 的进程的标准输入相同

因此,使用 popen() 并不是完全不可能,但在我看来非常不优雅,因为我必须解析调用进程的标准输出(调用 popen() 的代码)才能解析从popened 命令(使用 popen 时输入“w”)。

相反,当使用类型“r”调用 popen 时,我需要写入调用进程的 stdin,以便将数据写入 popened 命令。在这种情况下,我什至不清楚这两个进程是否在标准输入中接收到相同的数据......

我只需要控制程序的标准输入和标准输出。我的意思是不能有这样的功能:

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)
Run Code Online (Sandbox Code Playgroud)

所以我的第一个问题:实现上层功能的最佳方式是什么?

目前我正在尝试以下方法与另一个进程进行通信:

  1. 设置两个管道int pipe(int fildes[2]);。一个管道用于读取进程的标准输出,另一个管道用于写入进程的标准输入。
  2. 叉。
  3. 使用 执行我想在分叉子进程中与之通信的进程int execvp(const char *file, char *const argv[]);
  4. 使用原进程中的两个管道与子进程进行通信。

这很容易说,机器人的实现并不那么简单(至少对我来说)。在一种情况下,我奇怪地设法做到了这一点,但是当我试图用一个更简单的例子来理解我在做什么时,我失败了。这是我当前的问题:

我有两个程序。第一个只是每 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,第二个程序应该读取第一个程序的标准输出(请记住,我最终想要使用进程进行读取和写入,因此在上层用例中使用 popen() 的技术正确解决方案可能是正确的在这种特定情况下,因为我简化了实验以仅捕获底部程序的标准输出)。我期望从底层程序中读取上层程序写入标准输出的任何数据。但它什么也没读。可能是什么原因呢?(第二个问题)。

#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");
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Ton*_* K. 2

你的想法是正确的,我没有时间分析你的所有代码来指出具体问题,但我确实想指出一些你可能忽略的关于程序和终端的事情工作。

\n\n

将终端作为“文件”的想法是 naiv\xc3\xa9。像 vi 这样的程序使用库 (ncurses) 发送特殊控制字符(并更改终端设备驱动程序设置)。例如,vi 将终端设备驱动程序本身设置为一次可以读取一个字符的模式等。

\n\n

以这种方式“控制”像 vi 这样的程序是非常不简单的。

\n\n

在你的简化实验中......

\n\n

您的缓冲区太小了一个字节。另外,请注意 IO 有时是行缓冲的。因此,您可以尝试确保换行符被传输(使用 printf 而不是 sprintf/strlen/write ...您已经将标准输出连接到管道),否则您可能在遇到换行符之前看不到数据。我不记得管道是行缓冲的,但值得一试。

\n

  • 你已经发现了。Pipe(2) 系统调用。就是这样,没有别的了。您创建两对...一对用于从进程中读取,一对用于写入进程。使用 dup2 将每对的一端代替 fd 0 和 1(如果您关心错误,则可能是 2)。 (2认同)