1

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

4 回答 4

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

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

     read(stdin, buffer, BUFSIZ);

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

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

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

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

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

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

于 2012-12-03T15:15:13.093 回答
1

tr因为它使用缓冲输入而阻塞读取。

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

于 2012-11-30T17:55:19.117 回答
1

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

  • 小心字符串零终止符 - 现在您正在打印从管道读取的字节,该管道可能不是正确的 C-syle 零终止字符串,
  • 检查所有系统调用的返回值,例如write(2)
  • 避免竞争条件- 当前您的父子和子都阻塞等待输入,您可能希望切换到非阻塞模式并select(2)用于 IO 多路复用。
于 2012-11-30T17:43:06.633 回答
0

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

于 2012-11-30T17:26:31.410 回答