作为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;
}
答案 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)
答案 2 :(得分:1)
tr
在读取时阻塞,因为它使用缓冲输入。
如果您不想写更多内容,只需在写完后(以及阅读前)关闭管道。
答案 3 :(得分:0)
write(pipe1[1], s, strlen(s));
不会写出NUL字符,但这对于while((*s = toupper(*s))) ++s;