【问题标题】:Writing and reading to child process via pipes doesn't work通过管道写入和读取子进程不起作用
【发布时间】:2012-11-30 17:04:49
【问题描述】:

作为 Unix 编程练习,我编写了一个程序,它创建两个管道,分叉一个子节点,然后通过管道向子节点发送和接收一些文本。如果在子进程中我使用函数filter 中的代码读取和写入数据,它就可以工作。但是,如果孩子试图将管道重定向到它的标准输入和标准输出(使用dup2)并执行(使用execlptr 实用程序,那么它不起作用,它会卡在某个地方。此代码在filter2 函数中。问题是,为什么?代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>

void err_sys(const char* x) { perror(x); exit(1); } 

void upper(char *s) { while((*s = toupper(*s))) ++s; }

void filter(int input, int output)
{   
    char buff[1024];
    bzero(buff, sizeof(buff));
    size_t n = read(input, buff, sizeof(buff));

    printf("process %ld: got '%s'\n", (long) getpid(), buff);

    upper(buff);
    write(output, buff, strlen(buff));
}   

void filter2(int input, int output)
{   
    if (dup2(input, 0) != 0) err_sys("dup2(input, 0)");
    if (dup2(output, 1) != 1) err_sys("dup2(output, 1)");
    execlp("/usr/bin/tr", "tr", "[a-z]", "[A-Z]" , (char*)0);
}   

int main(int argc, char** argv) 
{   
    int pipe1[2];
    int pipe2[2];
    if (pipe(pipe1) < 0) err_sys("pipe1");
    if (pipe(pipe2) < 0) err_sys("pipe2");

    pid_t pid;
    if ((pid = fork()) < 0) err_sys("fork");
    else if (pid > 0)
    {   
        close(pipe1[0]);
        close(pipe2[1]);
        char* s = "Hello there, can you please uppercase this and send it back to me? Thank you!";
        write(pipe1[1], s, strlen(s));

        char buff[1024];
        bzero(buff, sizeof(buff));
        size_t n = read(pipe2[0], buff, sizeof(buff));
        pid_t mypid = getpid();
        printf("process %ld: got '%s'\n", (long) mypid, buff);
    } else
    {   // Child.
        close(pipe1[1]);
        close(pipe2[0]);

        filter(pipe1[0], pipe2[1]); 
        //filter2(pipe1[0], pipe2[1]);  // FIXME: This doesn't work
    }   
    return 0;
} 

【问题讨论】:

  • 请定义“不起作用”。
  • 试试char argv[] = {"tr", "[a-z]", "[A-Z]", NULL}; execvp(*argv, argv);
  • @NikolaiNFetissov tr 进程在 read(0, ...,正如我在 strace 的输出中看到的那样被阻止
  • @SergeyL。使用execvp 而不是execlp 并没有改变任何东西,我只是检查了一下。

标签: c unix exec posix pipe


【解决方案1】:

你在 main 中的父进程需要一个小改动:

/* Was: */
char* s = "Hello there, can you please uppercase this and send it back to me? Thank you!";
write(pipe1[1], s, strlen(s));
/* add: */
close(pipe1[1]);

其他人提到过缓冲,但这并不是真正的缓冲问题。这是关于进程间通信的。

管道被称为“管道”而不是“传送带”是有原因的。与传送带不同,管道不保留包装边界。管道只是一个字节流; write 将一堆字节转储到流中,但没有标记它已经这样做的事实。因此,您的代码可能完全相同:

    write(pipe1[1], s, strlen(s)/2);
    write(pipe1[1], s + strlen(s)/2,
                    strlen(s+strlen(s)/2));

writes 的任何其他组合。接收端将只读取方便数量的字节(即方便它),并处理它们。它可能会这样做:

     read(stdin, buffer, BUFSIZ);

在读取 BUFSIZ 字节或到达 EOF 之前不会返回。由于您无法进入阅读过程的系统调用并追溯更改阅读的长度,因此您可以让阅读过程实际完成其工作的唯一方法是安排它获得 EOF 指示,并且您可以这样做就是关闭管道。因此我在上面的解决方案。

这并不总是很方便,因为它不可能将两个连续的请求放入一个流中。在两个进程之间建立通信会涉及很多开销(特别是如果服务器进程需要重新启动)。如果您想“管道”请求(以便在每个请求结束时发送响应),您需要设计一个清楚地指示“包边界”的通信协议;请求之间的划分。换句话说,您需要使用管道实现自己的传送带。

一个通信协议需要两端的支持;你不能只从客户端实现它。因此,您将无法让tr 理解任意协议;它只是做它做的事情(当它觉得有足够的字节来打扰发送时,读取 EOF 并写入翻译的字节)。所以如果你想玩弄这个想法,你需要编写客户端和服务器进程。

可用的最简单的包协议可能是 Daniel Bernstein 的netstrings。该链接包含实际代码,非常简单,但基本思想是这样的:发送字符串的方法是将其长度作为十进制数发送,后跟冒号 (:),后跟长度中承诺的字节数。编写者在发送之前需要知道要发送多少字节;读者需要读到':'(djb 使用scanf 来做到这一点,这表明scanf 的一个经常被低估的功能);一旦它知道请求中有多少字节,它就可以阻止读取该字节数。这是一个在双方都实现的简单协议,因此可以进行简单的实践练习。

HTTP 使用类似但更复杂的协议(并且与所有不必要的复杂协议一样,结果是由于误解而导致互操作性错误很常见),但本质上是相同的:发送方需要指出多长时间消息(或消息的正文,在 HTTP 的情况下)是,它使用 Content-Length: 标头。但是,由于在发送所有字节之前知道要发送多少字节并不总是很方便,因此 HTTP 允许“分块”编码(用不同的标头表示);在这种情况下,每个块都包含一个长度(以十六进制表示),然后是 \r\n,然后是正文,然后是 \r\n,然后是......好吧,你可以阅读 RFC 了解混乱的细节。这里的问题包括一些客户端只发送\n 而不是\r\n,并且如何处理尾随\r\n 有点模棱两可。正如 djb 所指出的,Netstrings 会简单得多。

除非您想使用完整的 HTTP 客户端/服务器库,否则实现进程间通信的更实用的替代方案是 Google 的开源 protobuf 包。对于一个较早的并且在我看来技术上更优越的解决方案,不幸的是它没有一套方便的开源工具,是ASN.1(但不要马上进入那个站点;它很大)。

【讨论】:

  • 嘿,谢谢。但是......你认为你所描述的任何东西都被例如期望程序使用了吗?我还没有时间正确分析 expect 的来源,但我认为它对伪电传打字机(pty)做了一些事情。
  • @piokuc,不,期望一次只读取一个字符,并且依赖于 ptys 一次读取一个字符的事实。我想这绝对是最简单的通信协议,但远非最有效的。一个类似的协议是所谓的“一次一行”,但它简化为一次一个字符,因为你真的必须检查每个字符以查看它是否是行尾。
  • @piokuc,顺便说一下,我很确定tr 只是使用标准 C 库进行 I/O,从标准输入一次读取一个字符,一次写入一个字符到标准输出。如果 stdout 是 tty(幸运的是,它包括 pty,因为我们中很少有人仍然拥有真正的串行控制台,但不包括管道或套接字),则标准库行缓冲区输出。
  • 一定有办法将管道或套接字包裹在 pty 中,不是吗?
  • @piokuc,不是真的。 pty 是非常奇怪的野兽,它有效地包装了内核控制台(曾经是串行控制台的东西)。当您远程登录(或 ssh)到主机上的登录进程时,远端有一个 pty。如果您从 shell 进行 telnet,则本地也有一个 pty。这使得套接字成为 pty 之间透明的面向字节的连接,但它并没有以任何方式包装套接字。
【解决方案2】:

这里最可能的问题是stdinstdout 流默认情况下都是行缓冲,所以tr 进程正在工作,只是没有得到它的输入/没有刷新流入管道。尝试向子进程发送更多输入,您会看到它做出响应,但是...

  • 小心字符串零终止符 - 现在您正在打印从管道读取的字节,该管道可能不是正确的 C 样式零终止字符串,
  • 检查所有系统调用的返回值,如write(2)
  • 避免竞争条件 - 目前您的父子节点都被阻塞等待输入,您可能希望切换到非阻塞模式并使用 select(2) 进行 IO 多路复用。

【讨论】:

  • 谢谢。关于行缓冲,请注意我没有使用任何高级 C 函数进行读/写,而是使用低级函数(读和写)。我只是将程序更改为在消息末尾添加几个 '\n' ,但这没有帮助。我会看看发送更多文本是否会使trread 完成。
【解决方案3】:

tr 正在阻塞读取,因为它使用缓冲输入。

如果您不想写更多内容,只需在写完后(以及在阅读之前)关闭管道。

【讨论】:

  • 我宁愿在完成写入之前从子进程开始读取。假设它是一个交互式程序,就像一个脚本语言解释器。有可能吗?
  • 当然,您需要一些方法来禁用缓冲以进行交互使用。例如,参见this question(推荐 expect 的 unbuffer 命令,或 GNU stdbuf ...您将使用它们来包装 tr 子进程)
  • 谢谢,我会调查缓冲的东西。
【解决方案4】:

write(pipe1[1], s, strlen(s)); 不写入 NUL 字符,但这对于 while((*s = toupper(*s))) ++s; 是必需的

【讨论】:

  • 没有。整个缓冲区初始化为 0,因此在复制的文本之后有一个 0。无论如何,问题不在于函数 filter 中的代码,它有效,而是关于函数 filter2(您需要取消注释并注释出filter)。
猜你喜欢
  • 1970-01-01
  • 2011-05-05
  • 1970-01-01
  • 2016-02-06
  • 1970-01-01
  • 2012-03-04
  • 2013-02-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多