> For the complete documentation index, see [llms.txt](https://tianyuch.gitbook.io/dive-into-linux/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tianyuch.gitbook.io/dive-into-linux/ipc/pipe.md).

# 管道

## 一. 前言

  上文中我们介绍了进程间通信的方法之一：信号，本文将继续介绍另一种进程间通信的方法，即管道。管道是Linux中使用`shell`经常用到的一个技术，本文将深入剖析管道的实现和运行逻辑。

## 二. 管道简介

  在Linux的日常使用中，我们常常会用到管道，如下所示

```bash
ps -ef | grep 关键字 | awk '{print $2}' | xargs kill -9
```

  这里面的竖线`|`就是一个管道。它会将前一个命令的输出，作为后一个命令的输入。从管道的这个名称可以看出来，管道是一种单向传输数据的机制，它其实是一段缓存，里面的数据只能从一端写入，从另一端读出。如果想互相通信，我们需要创建两个管道才行。

  管道分为两种类型，`|` 表示的管道称为匿名管道，意思就是这个类型的管道没有名字，用完了就销毁了。就像上面那个命令里面的一样，竖线代表的管道随着命令的执行自动创建、自动销毁。用户甚至都不知道自己在用管道这种技术，就已经解决了问题。另外一种类型是命名管道。这个类型的管道需要通过 `mkfifo` 命令显式地创建。

```bash
mkfifo hello
```

  我们可以往管道里面写入东西。例如，写入一个字符串。

```bash
echo "hello world" > hello
```

  这个时候管道里面的内容没有被读出，这个命令就会停在这里。这个时候，我们就需要重新连接一个终端。在终端中用下面的命令读取管道里面的内容：

```bash
cat < hello hello world
```

  一方面，我们能够看到，管道里面的内容被读取出来，打印到了终端上；另一方面，echo 那个命令正常退出了。这就是有名管道的执行流程。

## 三. 匿名管道创建

  实际管道的创建调用的是系统调用`pipe()`，该函数建了一个管道 `pipe`，返回了两个文件描述符，这表示管道的两端，一个是管道的读取端描述符 `fd[0]`，另一个是管道的写入端描述符 `fd[1]`。

```c
int pipe(int fd[2])
```

  其内核实现如下所示，`pipe2 ()`调用 `__do_pipe_flags()` 创建一个数组 files来存放管道的两端的打开文件，另一个数组 `fd` 存放管道的两端的文件描述符。如果 `__do_pipe_flags()` 没有错误，那就调用 `fd_install()`将两个 `fd` 和两个 `struct file` 关联起来，这一点和打开一个文件的过程类似。

```c
SYSCALL_DEFINE1(pipe, int __user *, fildes)
{
    return sys_pipe2(fildes, 0);
}
​
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
{
    struct file *files[2];
    int fd[2];
    int error;
​
    error = __do_pipe_flags(fd, files, flags);
    if (!error) {
        if (unlikely(copy_to_user(fildes, fd, sizeof(fd)))) {
......
            error = -EFAULT;
        } else {
            fd_install(fd[0], files[0]);
            fd_install(fd[1], files[1]);
        }
    }
    return error;
}
```

  `__do_pipe_flags()`调用了`create_pipe_files()`生成`fd`，然后调用`get_unused_fd_flags()`赋值`fdr`和`fdw`，即读文件描述符和写文件描述符。由此也可以看出管道的特性：由一端写入，由另一端读出。

```c
static int __do_pipe_flags(int *fd, struct file **files, int flags)
{
    int error;
    int fdw, fdr;
......
    error = create_pipe_files(files, flags);
......
    error = get_unused_fd_flags(flags);
......
    fdr = error;
    error = get_unused_fd_flags(flags);
......
    fdw = error;
    audit_fd_pair(fdr, fdw);
    fd[0] = fdr;
    fd[1] = fdw;
    return 0;
......
}
```

  `create_pipe_files()`是管道创建的关键逻辑，从这里可以看出来管道实际上也是一种抽象的文件系统`pipefs`，有着对应的特殊文件以及`inode`。这里首先通过`get_pipe_inode()`获取特殊`inode`，然后调用`alloc_file_pseudo()`通过`inode`以及对应的挂载结构体`pipe_mnt`，文件操作结构体`pipefifo_fops`创建关联的`dentry`并以此创建文件结构体并分配内存，通过`alloc_file_clone()`创建一份新的`file`后将两个文件分别保存在`res[0]`和`res[1]`中。

```c
int create_pipe_files(struct file **res, int flags)
{
    struct inode *inode = get_pipe_inode();
    struct file *f;
    if (!inode)
        return -ENFILE;
    f = alloc_file_pseudo(inode, pipe_mnt, "",
                O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT)),
                &pipefifo_fops);
    if (IS_ERR(f)) {
        free_pipe_info(inode->i_pipe);
        iput(inode);
        return PTR_ERR(f);
    }
    f->private_data = inode->i_pipe;
    res[0] = alloc_file_clone(f, O_RDONLY | (flags & O_NONBLOCK),
                  &pipefifo_fops);
    if (IS_ERR(res[0])) {
        put_pipe_info(inode, inode->i_pipe);
        fput(f);
        return PTR_ERR(res[0]);
    }
    res[0]->private_data = inode->i_pipe;
    res[1] = f;
    return 0;
}
```

  其虚拟文件系统`pipefs`对应的结构体和操作如下：

```c
static struct file_system_type pipe_fs_type = {
  .name    = "pipefs",
  .mount    = pipefs_mount,
  .kill_sb  = kill_anon_super,
};

static int __init init_pipe_fs(void)
{
    int err = register_filesystem(&pipe_fs_type);

    if (!err) {
        pipe_mnt = kern_mount(&pipe_fs_type);
    }
......
}

const struct file_operations pipefifo_fops = {
    .open    = fifo_open,
    .llseek    = no_llseek,
    .read_iter  = pipe_read,
    .write_iter  = pipe_write,
    .poll    = pipe_poll,
    .unlocked_ioctl  = pipe_ioctl,
    .release  = pipe_release,
    .fasync    = pipe_fasync,
};

static struct inode * get_pipe_inode(void)
{
    struct inode *inode = new_inode_pseudo(pipe_mnt->mnt_sb);
    struct pipe_inode_info *pipe;
......
    inode->i_ino = get_next_ino();

    pipe = alloc_pipe_info();
......
    inode->i_pipe = pipe;
    pipe->files = 2;
    pipe->readers = pipe->writers = 1;
    inode->i_fop = &pipefifo_fops;
    inode->i_state = I_DIRTY;
    inode->i_mode = S_IFIFO | S_IRUSR | S_IWUSR;
    inode->i_uid = current_fsuid();
    inode->i_gid = current_fsgid();
    inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);

  return inode;
......
}
```

  至此，一个匿名管道就创建成功了。如果对于 `fd[1]`写入，调用的是 `pipe_write()`，向 `pipe_buffer` 里面写入数据；如果对于 `fd[0]`的读入，调用的是 `pipe_read()`，也就是从 `pipe_buffer` 里面读取数据。至此，我们在一个进程内创建了管道，但是尚未实现进程间通信。

## 四. 匿名管道通信

  在上文中我们提到了匿名管道通过`|`符号实现进程间的通信，传递输入给下一个进程作为输出，其实现原理如下：

* 利用`fork`创建子进程，复制`file_struct`会同样复制`fd`输入输出数组，但是`fd`指向的文件仅有一份，即两个进程间可以通过`fd`数组实现对同一个管道文件的跨进程读写操作
* 禁用父进程的读，禁用子进程的写，即从父进程写入从子进程读出，从而实现了单向管道，避免了混乱
* 对于`A|B`来说，`shell`首先创建子进程`A`，接着创建子进程`B`，由于二者均从`shell`创建，因此共用`fd`数组。`shell`关闭读写，A开写B开读，从而实现了`A` 和`B`之间的通信。

![img](https://static001.geekbang.org/resource/image/81/fa/81be4d460aaa804e9176ec70d59fdefa.png)

  接着我们需要调用`dup2()`实现输入输出和管道两端的关联，该函数会将`fd`赋值给`fd2`

```c
/* Duplicate FD to FD2, closing the old FD2 and making FD2 be
   open the same file as FD is.  Return FD2 or -1.  */
int
__dup2 (int fd, int fd2)
{
  if (fd < 0 || fd2 < 0)
    {
      __set_errno (EBADF);
      return -1;
    }
  if (fd == fd2)
    /* No way to check that they are valid.  */
    return fd2;
  __set_errno (ENOSYS);
  return -1;
}
```

  在 `files_struct` 里面，有这样一个表，下标是 `fd`，内容指向一个打开的文件 `struct file`。在这个表里面，前三项是定下来的，其中第零项 `STDIN_FILENO` 表示标准输入，第一项 `STDOUT_FILENO` 表示标准输出，第三项 `STDERR_FILENO` 表示错误输出。

```c
struct files_struct {
    struct file __rcu * fd_array[NR_OPEN_DEFAULT];
}
```

* 在 A 进程写入端通过`dup2(fd[1],STDOUT_FILENO)`将 `STDOUT_FILENO`（也即第一项）不再指向标准输出，而是指向创建的管道文件，那么以后往标准输出写入的任何东西，都会写入管道文件。
* 在 B 进程中读取端通过`dup2(fd[0],STDIN_FILENO)`将 `STDIN_FILENO` 也即第零项不再指向标准输入，而是指向创建的管道文件，那么以后从标准输入读取的任何东西，都来自于管道文件。

  至此，我们将 `A|B` 的功能完成。

![img](https://static001.geekbang.org/resource/image/c0/e2/c042b12de704995e4ba04173e0a304e2.png)

## 五. 有名管道

对于有名管道，我们需要通过`mkfifo`创建，实际调用`__xmknod()`函数，最终调用`mknod()`，和字符设备创建一样。

```c
/* Create a named pipe (FIFO) named PATH with protections MODE.  */
int
mkfifo (const char *path, mode_t mode)
{
    dev_t dev = 0;
    return __xmknod (_MKNOD_VER, path, mode | S_IFIFO, &dev);
}

/* Create a device file named PATH, with permission and special bits MODE
   and device number DEV (which can be constructed from major and minor
   device numbers with the `makedev' macro above).  */
int
__xmknod (int vers, const char *path, mode_t mode, dev_t *dev)
{
    unsigned long long int k_dev;
    if (vers != _MKNOD_VER)
        return INLINE_SYSCALL_ERROR_RETURN_VALUE (EINVAL);
    /* We must convert the value to dev_t type used by the kernel.  */
    k_dev =  (*dev) & ((1ULL << 32) - 1);
    if (k_dev != *dev)
        return INLINE_SYSCALL_ERROR_RETURN_VALUE (EINVAL);
    return INLINE_SYSCALL (mknod, 3, path, mode, (unsigned int) k_dev);
}
```

   `mknod` 在[字符设备](https://ty-chen.github.io/linux-kernel-char-device/)那一节已经解析过了，先是通过 `user_path_create()` 对于这个管道文件创建一个 `dentry`，然后因为是 `S_IFIFO`，所以调用 `vfs_mknod()`。由于这个管道文件是创建在一个普通文件系统上的，假设是在 `ext4` 文件上，于是 `vfs_mknod` 会调用 `ext4_dir_inode_operations` 的 `mknod`，也即会调用 `ext4_mknod()`。

  在 `ext4_mknod()` 中，`ext4_new_inode_start_handle()` 会调用 `__ext4_new_inode()`，在 ext4 文件系统上真的创建一个文件，但是会调用 `init_special_inode()`，创建一个内存中特殊的 `inode`，这个函数我们在字符设备文件中也遇到过，只不过当时 `inode` 的 `i_fop` 指向的是 `def_chr_fops`，这次换成管道文件了，`inode` 的 `i_fop` 变成指向 `pipefifo_fops`，这一点和匿名管道是一样的。这样，管道文件就创建完毕了。

  接下来，要打开这个管道文件，我们还是会调用文件系统的 `open()` 函数。还是沿着文件系统的调用方式，一路调用到 `pipefifo_fops` 的 `open()` 函数，也就是 `fifo_open()`。在 `fifo_open()` 里面会创建 `pipe_inode_info`，这一点和匿名管道也是一样的。这个结构里面有个成员是 `struct pipe_buffer *bufs`。我们可以知道，所谓的命名管道，其实是也是内核里面的一串缓存。接下来，对于命名管道的写入，我们还是会调用 `pipefifo_fops` 的 `pipe_write()` 函数，向 `pipe_buffer` 里面写入数据。对于命名管道的读入，我们还是会调用 `pipefifo_fops` 的 `pipe_read()`，也就是从 `pipe_buffer` 里面读取数据。

```c
static int fifo_open(struct inode *inode, struct file *filp)
{
    struct pipe_inode_info *pipe;
    bool is_pipe = inode->i_sb->s_magic == PIPEFS_MAGIC;
    int ret;
    filp->f_version = 0;
    spin_lock(&inode->i_lock);
    if (inode->i_pipe) {
        pipe = inode->i_pipe;
        pipe->files++;
        spin_unlock(&inode->i_lock);
    } else {
        spin_unlock(&inode->i_lock);
        pipe = alloc_pipe_info();
        if (!pipe)
            return -ENOMEM;
        pipe->files = 1;
        spin_lock(&inode->i_lock);
        if (unlikely(inode->i_pipe)) {
            inode->i_pipe->files++;
            spin_unlock(&inode->i_lock);
            free_pipe_info(pipe);
            pipe = inode->i_pipe;
        } else {
            inode->i_pipe = pipe;
            spin_unlock(&inode->i_lock);
        }
    }
    filp->private_data = pipe;
    /* OK, we have a pipe and it's pinned down */
    __pipe_lock(pipe);
    /* We can only do regular read/write on fifos */
    filp->f_mode &= (FMODE_READ | FMODE_WRITE);
    switch (filp->f_mode) {
    case FMODE_READ:
    /*
     *  O_RDONLY
     *  POSIX.1 says that O_NONBLOCK means return with the FIFO
     *  opened, even when there is no process writing the FIFO.
     */
        pipe->r_counter++;
        if (pipe->readers++ == 0)
            wake_up_partner(pipe);
        if (!is_pipe && !pipe->writers) {
            if ((filp->f_flags & O_NONBLOCK)) {
                /* suppress EPOLLHUP until we have
                 * seen a writer */
                filp->f_version = pipe->w_counter;
            } else {
                if (wait_for_partner(pipe, &pipe->w_counter))
                    goto err_rd;
            }
        }
        break;
    
    case FMODE_WRITE:
    /*
     *  O_WRONLY
     *  POSIX.1 says that O_NONBLOCK means return -1 with
     *  errno=ENXIO when there is no process reading the FIFO.
     */
        ret = -ENXIO;
        if (!is_pipe && (filp->f_flags & O_NONBLOCK) && !pipe->readers)
            goto err;
        pipe->w_counter++;
        if (!pipe->writers++)
            wake_up_partner(pipe);
        if (!is_pipe && !pipe->readers) {
            if (wait_for_partner(pipe, &pipe->r_counter))
                goto err_wr;
        }
        break;
    
    case FMODE_READ | FMODE_WRITE:
    /*
     *  O_RDWR
     *  POSIX.1 leaves this case "undefined" when O_NONBLOCK is set.
     *  This implementation will NEVER block on a O_RDWR open, since
     *  the process can at least talk to itself.
     */
        pipe->readers++;
        pipe->writers++;
        pipe->r_counter++;
        pipe->w_counter++;
        if (pipe->readers == 1 || pipe->writers == 1)
            wake_up_partner(pipe);
        break;
    default:
        ret = -EINVAL;
        goto err;
    }
    /* Ok! */
    __pipe_unlock(pipe);
    return 0;
......
}
```

## 总结

  无论是匿名管道还是命名管道，在内核都是一个文件。只要是文件就要有一个 `inode`。在这种特殊的 `inode` 里面，`file_operations` 指向管道特殊的 `pipefifo_fops`，这个 `inode` 对应内存里面的缓存。当我们用文件的 `open` 函数打开这个管道设备文件的时候，会调用 `pipefifo_fops` 里面的方法创建 `struct file` 结构，他的 `inode` 指向特殊的 `inode`，也对应内存里面的缓存，`file_operations` 也指向管道特殊的 `pipefifo_fops`。写入一个 `pipe` 就是从 `struct file` 结构找到缓存写入，读取一个 `pipe` 就是从 `struct file` 结构找到缓存读出。匿名管道和命名管道区别就在于匿名管道会通过`dup2()`指定输入输出源，完成之后立即释放，而命名管道通过`mkfifo`创建挂载后，需要手动调用`pipe_read()`和`pipe_write()`来完成其功能，表现到用户端即为前面提到的例子。

## 源码资料

\[1] [\_\_do\_pipe\_flag()](https://code.woboq.org/linux/linux/fs/pipe.c.html#798)

\[2] [dup2()](https://code.woboq.org/userspace/glibc/io/dup2.c.html)

\[3] [mkfifo()](https://code.woboq.org/userspace/glibc/sysdeps/posix/mkfifo.c.html)

\[4] [fifo\_open()](https://code.woboq.org/linux/linux/fs/pipe.c.html#897)

## 参考资料

\[1] wiki

\[2] [elixir.bootlin.com/linux](https://elixir.bootlin.com/linux/v5.7-rc1/source)

\[3] [woboq](https://code.woboq.org/)

\[4] Linux-insides

\[5] 深入理解Linux内核

\[6] Linux内核设计的艺术

\[7] 极客时间 趣谈Linux操作系统
