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



作为Unix编程的一个练习,我编写了一个程序,创建两个管道,分叉一个子管道,然后通过管道向子管道发送和接收一些文本。如果在子进程中,我使用函数filter中的代码读取和写入数据,那么它就会工作。但是,如果子级试图将管道重定向到其stdin和stdout(使用dup2)并执行(使用execlp)tr实用程序,则它不起作用,它会被卡住。此代码位于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;
} 

您的父进程主要需要一个小的更改:

/* 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的netstring。链接包含实际的代码,这非常简单,但基本思想是:字符串的长度以十进制数字的形式发送,后跟冒号(:),后跟长度中承诺的字节数。编写器在发送字节之前需要知道它将发送多少字节;读者需要阅读":"(djb使用scanf来做到这一点,这证明了scanf的一个经常被低估的特性);一旦它知道请求中有多少字节,它就可以完全阻止读取这个字节数。这是一个双方都要实现的琐碎协议,因此它是一个简单的实践练习。

HTTP使用了一个类似但复杂得多的协议(与所有不必要的复杂协议一样,由于误解,互操作性错误很常见),但本质上是一样的:发送方需要指示消息(在HTTP的情况下是消息的正文)的长度,这是通过Content-Length:标头来实现的。然而,由于在发送所有字节之前知道要发送多少字节并不总是很方便,HTTP允许"分块"编码(用不同的标头表示);在这种情况下,每个块都由一个长度(十六进制)组成,后面是rn,后面是正文,后面是rn,后面是…好吧,你可以阅读RFC来了解混乱的细节。这里的问题包括一些客户端只发送n而不是rn,以及如何处理尾随的rn有点模糊。正如djb所指出的,网络字符串会简单得多。

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

这里最可能的问题是,默认情况下,stdinstdout流都是行缓冲,因此tr进程正在工作,只是没有将其输入/未将流冲洗到管道中。尝试向子进程发送更多输入,您会看到它有响应,但是。。。

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

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

如果你不想写更多的东西,只要在写完后(阅读前)关上管道就可以了。

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

最新更新