通过管道写入和读取子进程不起作用

时间:2012-11-30 17:04:50

标签: c unix exec posix pipe

作为Unix编程的练习,我编写了一个程序,它创建了两个管道,分叉一个孩子,然后通过管道发送和接收一些文本。如果在子进程中我使用函数filter中的代码读取和写入数据,它就有效。但是,如果子项尝试将管道重定向到其stdin和stdout(使用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;
} 

4 个答案:

答案 0 :(得分:2)

您在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));

write s的任何其他组合。接收端只读取一个方便的字节数(方便它),然后处理它们。它可能会做这样的事情:

     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(但不要马上进入该网站;它很大)。

答案 1 :(得分:1)

这里最可能出现的问题是默认情况下stdinstdout流都是行缓冲,因此tr进程正在运行,而不是将其输入/不冲洗流输入管道。尝试向子进程发送更多输入,你会看到它响应,但是......

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

答案 2 :(得分:1)

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

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

答案 3 :(得分:0)

write(pipe1[1], s, strlen(s));不会写出NUL字符,但这对于while((*s = toupper(*s))) ++s;

是必要的