2012-11-30 37 views
1

作为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; 
} 
+1

请定义“不起作用”。 –

+0

尝试'char argv [] = {“tr”,“[a-z]”,“[A-Z]”,NULL}; execvp(* argv,argv);' –

+0

@NikolaiNFetissov'tr'进程在读取时被阻塞(0,...,正如我在strace – piokuc

回答

1

你在主父进程需要一个小的变化:

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

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

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

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

 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指出的那样,网络行为将会简单得多。

除非要使用完整的HTTP客户端/服务器库,否则实现进程间通信的更实用的替代方法是Google的开放源代码protobuf包。对于我的观点,在我看来,技术上优越的解决方案(不幸的是没有一套方便的开源工具)是ASN.1(但不要立即跳入该网站;这很大)。

+0

嘿,谢谢你。但是......你认为你所描述的任何东西是否都被预期计划所使用?我还没有时间正确地分析期望的源代码,但我认为它使用伪teletypes(pty)。 – piokuc

+0

@piokuc,不,期望一次只读一个字符,并依赖ptys同时读取一个字符的事实。我想这是绝对最简单的通信协议,但它远没有最高效。一个类似的协议就是所谓的“一次一行”,但是随着时间的推移逐渐减少到一个字符,因为你必须检查每一个字符,看看它是否是行尾。 – rici

+0

@piokuc,顺便说一句,我很确定'tr'只是使用标准的C库进行I/O,每次从标准输入读取一个字符,并一次写入一个字符到标准输出。如果stdout是tty(其中包括ptys,幸运的是,因为我们中很少人仍然有真正的串行控制台,但不包括管道或套接字),标准库行缓冲区输出。 – rici

0

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

+0

没有。整个缓冲区初始化为0,所以在复制的文本之后有一个0.无论如何,问题不是关于功能'filter'中的代码,它可以工作,而是关于函数'filter2'(你需要取消它的注释并注释掉'filter') – piokuc

1

最有可能的问题,这里要说的是两stdinstdout流是行缓冲是必要的默认情况下,所以tr进程正在工作,只是没有得到其输入/不冲洗流到管道。尝试发送更多的投入到孩子过程中,你会看到它响应,但...

  • 小心字符串零终止 - 现在你要打印从管道中读取的字节,可能不是一个恰当的C -syle 0结尾的字符串,
  • 所有系统的
  • 检查返回值调用像write(2)
  • 避免竞争状态 - 现在你有两个父母和孩子阻塞等待输入,你可能要切换到无阻塞模式并使用select(2)进行IO多路复用。
+0

谢谢。关于行缓冲,注意我没有使用任何高级C函数来读/写,但是低级函数(读写),我只是改变了程序在结尾处添加了几个'\ n',但它没有帮助,我会看看发送更多的文本是否会使'tr'成为'read'去完成。 – piokuc

1

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

如果您不想写更多,请在完成书写时(和阅读前)关闭管道。

+0

我更愿意在完成写入之前从子进程读取数据。假设这是一个交互式程序,就像脚本语言解释器一样。可能吗? – piokuc

+0

当然,您需要某种方式来禁用交互式使用缓冲。例如,参见[这个问题](http://unix.stackexchange.com/q/25372/14898)(推荐expect的unbuffer命令,或者GNU stdbuf ...你会用它们来包装'tr'子过程) – Useless

+0

谢谢,我会研究缓冲的东西。 – piokuc