# 管道，第 2 部分：管道编程秘密

> 原文：<https://github.com/angrave/SystemProgramming/wiki/Pipes%2C-Part-2%3A-Pipe-programming-secrets>

## 管道陷阱

这是一个完整的例子，不起作用！孩子一次从管道读取一个字节并将其打印出来 - 但我们从未看到过该消息！你能明白为什么吗？

```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int main() {
    int fd[2];
    pipe(fd);
    //You must read from fd[0] and write from fd[1]
    printf("Reading from %d, writing to %d\n", fd[0], fd[1]);

    pid_t p = fork();
    if (p > 0) {
        /* I have a child therefore I am the parent*/
        write(fd[1],"Hi Child!",9);

        /*don't forget your child*/
        wait(NULL);
    } else {
        char buf;
        int bytesread;
        // read one byte at a time.
        while ((bytesread = read(fd[0], &buf, 1)) > 0) {
            putchar(buf);
        }
    }
    return 0;
}

```

父节点将`H,i,(space),C...!`字节发送到管道中（如果管道已满，则可能会阻塞）。孩子一次开始读取一个字节的管道。在上述情况下，子进程将读取并打印每个字符。但它永远不会离开 while 循环！当没有剩下的字符可供读取时，它只是阻塞并等待更多。

调用`putchar`将字符写出，但我们从不刷新`stdout`缓冲区。即我们已将消息从一个进程转移到另一个进程但尚未打印。要查看消息，我们可以刷新缓冲区，例如`fflush(stdout)`（如果输出到达终端，则为`printf("\n")`）。更好的解决方案还可以通过检查消息结束标记来退出循环，

```c
        while ((bytesread = read(fd[0], &buf, 1)) > 0) {
            putchar(buf);
            if (buf == '!') break; /* End of message */
        }
```

当子进程退出时，消息将被刷新到终端。

## 想使用 printf 和 scanf 的管道？使用 fdopen！

POSIX 文件描述符是简单的整数 0,1,2,3 ...在 C 库级别，C 用缓冲区和 printf 和 scanf 等有用的函数包装它们，因此我们可以轻松地打印或解析整数，字符串等。如果您已经有文件描述符，那么您可以使用`fdopen`将其自己“包装”到 FILE 指针中：

```c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
    char *name="Fred";
    int score = 123;
    int filedes = open("mydata.txt", "w", O_CREAT, S_IWUSR | S_IRUSR);

    FILE *f = fdopen(filedes, "w");
    fprintf(f, "Name:%s Score:%d\n", name, score);
    fclose(f);
```

对于写入文件，这是不必要的 - 只需使用与`open`和`fdopen`相同的`fopen`但是对于管道，我们已经有了文件描述符 - 所以这是使用`fdopen`的好时机！

这是一个使用几乎可以工作的管道的完整示例！你能发现错误吗？提示：父母从不打印任何东西！

```c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main() {
    int fh[2];
    pipe(fh);
    FILE *reader = fdopen(fh[0], "r");
    FILE *writer = fdopen(fh[1], "w");
    pid_t p = fork();
    if (p > 0) {
        int score;
        fscanf(reader, "Score %d", &score);
        printf("The child says the score is %d\n", score);
    } else {
        fprintf(writer, "Score %d", 10 + 10);
        fflush(writer);
    }
    return 0;
}
```

请注意，一旦子项和父项都退出，（未命名的）管道资源将消失。在上面的例子中，子节点将发送字节，父节点将从管道接收字节。但是，没有发送行尾字符，因此`fscanf`将继续询问字节，因为它正在等待行的结束，即它将永远等待！修复是为了确保我们发送换行符，以便`fscanf`返回。

```c
change:   fprintf(writer, "Score %d", 10 + 10);
to:       fprintf(writer, "Score %d\n", 10 + 10);
```

## 那么我们也需要`fflush`吗？

是的，如果您希望立即将您的字节发送到管道！在本课程开始时，我们假设文件流始终是 _ 行缓冲 _，即每次发送换行符时 C 库都会刷新其缓冲区。实际上，这仅适用于终端流 - 对于其他文件流，C 库尝试通过仅在内部缓冲区已满或文件关闭时进行刷新来提高性能。

## 我什么时候需要两个管道？

如果需要异步向子级发送数据和从子级发送数据，则需要两个管道（每个方向一个）。否则孩子会尝试读取自己的父母数据（反之亦然）！

## 关闭管道了

当没有进程正在侦听时，进程会收到信号 SIGPIPE！从管道（2）手册页 -

```
If all file descriptors referring to the read end of a pipe have been closed,
 then a write(2) will cause a SIGPIPE signal to be generated for the calling process. 
```

提示：请注意，只有编写者（不是读者）才能使用此信号。要通知读者写入器正在关闭管道的末尾，您可以编写自己的特殊字节（例如 0xff）或消息（`"Bye!"`）

这是一个捕捉这个不起作用的信号的例子！你能明白为什么吗？

```c
#include <stdio.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void no_one_listening(int signal) {
    write(1, "No one is listening!\n", 21);
}

int main() {
    signal(SIGPIPE, no_one_listening);
    int filedes[2];

    pipe(filedes);
    pid_t child = fork();
    if (child > 0) { 
        /* I must be the parent. Close the listening end of the pipe */
        /* I'm not listening anymore!*/
        close(filedes[0]);
    } else {
        /* Child writes messages to the pipe */
        write(filedes[1], "One", 3);
        sleep(2);
        // Will this write generate SIGPIPE ?
        write(filedes[1], "Two", 3);
        write(1, "Done\n", 5);
    }
    return 0;
}
```

上面代码中的错误是管道仍有读卡器！孩子仍然打开管道的第一个文件描述符并记住规范？所有读者都必须关闭。

在分叉时，_ 通常的做法是 _ 关闭子进程和父进程中每个管道的不必要（未使用）端。例如，父级可能会关闭读取端，而子级可能会关闭写入端（反之亦然，如果您有两个管道）

## 什么填充管道？管道变满后会发生什么？

当编写器向管道写入太多而没有读取器读取任何管道时，管道会被填满。当管道变满时，所有写入都会失败，直到读取发生。即使这样，如果管道剩余一点空间但对整个消息还不够，写入可能会部分失败。

为避免这种情况，通常会做两件事。要么增加管道的尺寸。或者更常见的是，修复程序设计，以便不断读取管道。

## 管道过程安全吗？

是!管道写入是原子的，直到管道的大小。这意味着如果两个进程尝试写入同一个管道，则内核具有内部互斥锁，该管道将锁定，执行写入和返回。唯一的问题是管子即将变满。如果两个进程正在尝试写入并且管道只能满足部分写入，那么管道写入不是原子的 - 请小心！

## 管道的使用寿命

未命名的管道（我们到目前为止看到的那种）存在于内存中（不占用任何磁盘空间），是一种简单有效的进程间通信（IPC）形式，对流数据和简单消息很有用。关闭所有进程后，将释放管道资源。

_unamed_ 管道的替代品是使用`mkfifo`创建的名为管道的 _。_

## 命名管道

## 如何创建命名管道？

从命令行：`mkfifo`从 C：`int mkfifo(const char *pathname, mode_t mode);`

你给它路径名和操作模式，它就准备好了！命名管道在磁盘上不占用空间。当你有一个命名管道时，操作系统基本上告诉你的是它将创建一个引用命名管道的未命名管道，就是这样！没有额外的魔力。这只是为了方便编程，如果进程是在没有分叉的情况下启动的（这意味着没有办法将文件描述符提供给未命名管道的子进程）

## 为什么我的烟斗挂了？

读取和写入挂在命名管道上，直到至少有一个读取器和一个写入器为止

```source-shell
1$ mkfifo fifo
1$ echo Hello > fifo
# This will hang until I do this on another terminal or another process
2$ cat fifo
Hello
```

在命名管道上调用任何`open`，内核将阻塞，直到另一个进程调用相反的 open。这意味着，回调调用`open(.., O_RDONLY)`但是阻塞直到 cat 调用`open(.., O_WRONLY)`，然后允许程序继续。

## 具有命名管道的竞争条件。

以下程序有什么问题？

```c
//Program 1

int main(){
    int fd = open("fifo", O_RDWR | O_TRUNC);
    write(fd, "Hello!", 6);
    close(fd);
    return 0;
}

//Program 2
int main() {
    char buffer[7];
    int fd = open("fifo", O_RDONLY);
    read(fd, buffer, 6);
    buffer[6] = '\0';
    printf("%s\n", buffer);
    return 0;
}
```

由于竞争条件，这可能永远不会打印你好。由于您在第一个进程中在两个权限下打开了管道，因此您不会等待读者，因为您告诉操作系统您是读者！有时它看起来像是有效的，因为代码的执行看起来像这样。

| 过程 1 | 过程 2 |
| --- | --- |
| 开放（O_RDWR）＆amp;写（） |  |
|  | 打开（O_RDONLY）＆amp;读（） |
| close（）＆amp;出口（） |  |
|  | print（）＆amp;出口（） |

有时它不会

| Process 1 | Process 2 |
| --- | --- |
| open(O_RDWR) & write() |  |
| close() & exit() | （命名管道被销毁） |
| （无限期阻止） | 开（O_RDONLY） |