使用C中的两个管道或一个管道进行2次以上的读/写?如何?

时间:2018-10-28 14:16:11

标签: c linux pipe fork

我有以下简化的代码模板:

pid_t pid;
int pipe1[2], pipe2[2];
pid = fork();
pipe(pipe1); pipe(pipe2)
if(pid == 0)  //child
{
    read(pipe1[0],...);
    write(pipe1[1],...);
    close(pipe1[0]);
    close(pipe1[1]);

    close(pipe2[1]);
    read(pipe2[0]...);
}
else //parent
{
    write(pipe1[1],...);
    wait(NULL);
    read(pipe1[0]...);
    close(pipe1[0]);
    close(pipe1[1]);

    close(pipe2[0]);
    write(pipe2[1]...);
}

如果我没有在父级和子级中使用pipe2,则代码可以正常工作,但是如果我这样做了,则似乎子级没有任何读取(程序不会执行任何操作,直到我将其中断为止)。另外,有没有办法只使用一个管道进行两次以上的读/写操作?我试过多次使用wait(NULL),但这没用。

2 个答案:

答案 0 :(得分:2)

简单地说,您的代码模板是垃圾。让我解释一下原因。

  1. 每个管道都是单向的。

    如果使用管道将数据从子级发送到父级,请在子级中关闭读取端,并在父级中关闭写入端。这样,父级就可以查看子级(写端)何时关闭管道或退出,因为read()随后将返回-1errno == EPIPE

    如果使用管道将数据从父级发送到子级,请在父级中关闭读取端,并在子级中关闭写入端。这样可以让父母检测孩子是否过早退出,因为write()随后将与-1一起返回errno == EPIPE,并在父母中引发SIGPIPE信号。

  2. 如果需要父级和子级之间的双向“管道”,请通过socketpair(AF_UNIX, SOCK_STREAM, 0, fdpair)使用Unix域流套接字对。

    这种套接字对的工作方式非常类似于管道,只是套接字对是双向的。您也可以使用send(descriptor, buffer, length, MSG_NOSIGNAL)代替write(descriptor, buffer, length);在前一种情况下,如果套接字的另一端已经关闭,则不会发出SIGPIPE信号。

    在父级中使用一个描述符,在子级中使用另一个描述符。父母和孩子都应关闭另一个描述符。否则,一端无法检测到另一端何时关闭其描述符。

    在某些情况下,Unix域数据报套接字对可能更可取。每个send()生成一个单独的数据报,并使用单个recv()接收。 (即,保留了消息边界。)如果接收端知道发送方可能发送的数据报的最大大小,则这是在父进程和子进程之间实现双向通信的一种极其健壮和简单的方法。我个人经常使用它。

  3. 管道和插座的
  4. read()write()可能是 short

    (尽管POSIX指出您应该始终能够在管道中填充至少512个字节;如果我没记错的话,Linux至少支持整个页面。)

    这意味着您需要进行循环操作,直到您拥有所需的数据为止。

    在使用套接字的情况下,send()发送所有数据,或者以-1(通过errno == EMSGSIZE或其他错误代码)失败。

    对于数据报套接字(Unix域数据报套接字,UDP套接字),如果缓冲区足够大,则recv()要么接收整个数据报,要么以-1失败(设置errno) )。接收零长度数据报很困难,所以不要尝试这样做。

    对于流套接字,recv()可能仅返回某些数据(即短接收或部分接收)。

  5. 当两个进程相互发送/接收或读取/写入数据时,deadlock是一个严重的常见问题。

    简单地说,两端可能最终都在等待另一端同时进行读/写,而什么也没有发生。

    在这种情况下,有三种典型的解决方案可以避免死锁:

    1. 使用查询-响应协议,以便一个端点始终启动通信,然后等待另一个端点响应。在任何给定时间,最多有一个端点正在传输数据。

    2. 使用非阻塞/异步I / O。也就是说,在尝试进行write() / send()之前,每个端点都会进行read() / recv()来查看另一端是否已经发送了某些内容。这支持全双工通信(信息可以同时双向流动)。

    3. 使用单独的线程连续进行read() / recv(),而另一个执行write() / send()。这实际上将每个套接字分成两个单向“通道”,一个线程仅处理它们的方向。这对于一端产生大量数据而另一端偶尔发送命令的协议很有用。

结合以上所有内容,我们将发现没有一个模板可以使用。有些变体之间存在显着差异,在某些用例中使它们变得更好,而在另一些用例中却变得更难/更糟。一个人应该根据当前情况选择一个。如果OP希望看到一个更好的示例(“模板”),则应描述一个实际的问题案例,包括所需的行为。

答案 1 :(得分:2)

您的代码中有2个错误。

  1. 您使用wait语句创建了一个死锁。您的父母在等孩子,而孩子在等父母写东西。
  2. 您在进行管道操作之前已完成fork。因此,您的孩子和您的父母会看到不同版本的未连接管道。

这是程序的固定版本:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>

int main() {
  pid_t pid;
  int pipe1[2], pipe2[2];
  char *m1 = "hello";
  char *m2 = "world";
  char *m3 = "bye";


  pipe(pipe1);
  pipe(pipe2);

  pid = fork();

  if(pid == 0) {  //chil
    char buf1[256], buf2[256];
    int len;
    len = read(pipe1[0], buf1, 255);
    buf1[len] = 0;
    write(pipe1[1], m2, strlen(m2));

    close(pipe1[0]);
    close(pipe1[1]);

    close(pipe2[1]);
    len = read(pipe2[0], buf2, 255);
    buf2[len] = 0;

    printf("child read %s %s\n", buf1, buf2);

  }
  else {
    char buf[256];
    int len;

    write(pipe1[1], m1, strlen(m1));
    //wait(NULL);
    len = read(pipe1[0], buf, 255);
    buf[len] = 0;

    close(pipe1[0]);
    close(pipe1[1]);

    close(pipe2[0]);
    write(pipe2[1], m3, strlen(m3));

    wait(NULL);
    printf("Parent read %s\n", buf);

  }
  return 0;
}