将千兆字节的数据写入管道时会发生什么?

Nea*_*Nit 18 linux pipe yes

假设我有一个名为 的makefile,hour_long_recipe顾名思义,它需要一个小时才能运行。在整个食谱的随机点上,它会问是/否问题。假设它总共问了 10 个问题。

一种可能(并且经常被推荐)的运行方式是:

yes | make hour_long_recipe
Run Code Online (Sandbox Code Playgroud)

用 回答所有问题y。但是,根据我的理解,无论是否实际使用来自其标准输入的数据,都会yes高达每秒 10.2 GiB 的速度输出到标准输出make

即使它只有 10 MiB/s(比任何yes可以相信的 reddit 线程的实现都慢得多),在一小时内它会加起来超过 35 GiB,其中只有 20 个字节将被读取。数据去哪儿了?可以将其保存到磁盘,但这很浪费,如果磁盘填满的速度足够快,它甚至可能导致make失败。

据推测,操作系统会阻止它到达那个状态,但是如何呢?什么是限制,达到该限制时会发生什么?

Chr*_*own 34

tl; dr:在某些时候,yes如果数据没有在另一侧被读取,将被阻止写入。在读取数据或接收到信号之前,它将无法继续执行,因此您通常无需担心yes写入千兆字节和千兆字节的数据。


要记住的重要一点是,管道是一种 FIFO 数据结构,而不仅仅是一个纯粹的流,如果没有立即在接收器上读取数据,它就会丢弃数据。也就是说,虽然在大多数情况下它可能表现为从写入应用程序到读取应用程序的无缝数据流,但它确实需要中间存储来执行该操作,并且该中间存储的大小是有限的。*

如果我们查看pipe(7) 手册页,我们可以阅读以下有关该内部缓冲区大小的信息(已添加重点):

在 2.6.11 之前的 Linux 版本中,管道的容量与系统页面大小相同(例如,在 i386 上为 4096 字节)。从 Linux 2.6.11 开始,管道容量为 16 页(即,在页面大小为 4096 字节的系统中为 65,536 字节)。从 Linux 2.6.35 开始,默认管道容量为 16 页,但可以使用 fcntl(2) F_GETPIPE_SZ 和 F_SETPIPE_SZ 操作来查询和设置容量。

假设您使用的是标准 x86_64 系统,很可能您使用的是 4KiB 页面,因此管道容量的 2^16 上限可能是正确的,除非管道的任一侧在某个时候使用fcntl(F_SETPIPE_SZ)。无论哪种方式,原理都是:管道两侧之间的中间存储是有限的,并且存储在内存中。

在抽象管道中a | b,此存储用于a写入一些数据和b实际读取数据之间的时间段。假设您的make调用(以及任何通过继承连接到该管道的子进程)实际上并没有尝试读取标准输入,或者只是谨慎地这样做,当缓冲区空间耗尽时,write系统调用 fromyes最终将不会yes从睡眠中唤醒. yes然后将等待被唤醒,无论是在缓冲区空间再次可用时,还是接收到信号时。** 所有这些都由内核的进程调度程序处理。您可以在 中看到这一点pipe_write(),它是write()管道的处理程序:

static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
    /* ... */

    if (pipe_full(pipe->head, pipe->tail, pipe->max_usage))
        wake_next_writer = false;

    if (wake_next_writer)
        wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);

    /* ... */
}
Run Code Online (Sandbox Code Playgroud)

make一方最终终止时,yesSIGPIPE作为写入管道的结果发送,另一端没有任何剩余。然后,根据yes实现,这将调用它自己的信号处理程序或默认的内核信号处理程序,并且它将终止。***


* 在简单的情况下,接收方处理数据的速率与写入的速率大致相同,这种传输也可以是零拷贝,没有中间缓冲区,通过使用虚拟内存来映射和使写入过程中的物理页面可用接收者可用。但是,您所描述的情况最终肯定需要使用管道缓冲区来存储未读数据。

** 也可以使用O_NONBLOCK文件描述符上设置的标志来完成写入,从而启用非阻塞模式。在这种情况下,您可能会得到一个不完整的写入,然后写入将返回EAGAIN并且应用程序需要自己处理它。它可能会通过挂起或运行它选择的其他一些代码来处理管道已满的情况。但是,在yes我能找到的每个现代版本和大多数其他应用程序的情况下,上面的描述是发生的情况,因为它们不使用O_NONBLOCK.

*** 应用程序在收到后可以为所欲为SIGPIPE——它甚至可以在理论上决定不终止。然而,所有常见的都yes使用默认SIGPIPE处理程序,它只是终止而不执行任何更多的用户空间指令。

  • 对此的技术术语是“阻塞”。`write` 在管道的缓冲区已满时阻塞,并且直到读取器减少缓冲区内容足以让 `write` 存储它在缓冲区中提供的所有数据时才会返回。 (8认同)
  • 这在本质上与现实生活中的管道相同。 (2认同)