假设我有一个 makefile hour_long_recipe
,其中有一个名为 的配方,顾名思义,它需要一个小时才能运行。在整个食谱中,它会随机询问是/否问题。假设它总共询问 10 个问题。
一种可能的(也是经常推荐的)运行方法是:
yes | make hour_long_recipe
它回答了所有问题y
。但是,根据我的理解,yes
不断输出到其标准输出高达每秒 10.2 GiB无论是否make
实际使用其标准输入中的数据。
即使只有 10 MiB/s(比yes
Reddit 线程的任何实现都要慢得多),在一小时内,它会总计超过 35 GiB,其中仅读取 20 个字节。数据去哪里?可以将其保存到磁盘,但这很浪费,如果磁盘填满的速度足够快,甚至可能导致make
失败。
据推测,操作系统会阻止它这样做,但是如何呢?限制是什么?达到该限制后会发生什么?
答案1
tl;dr:在某些时候,yes
如果另一端没有读取数据,则写入将被阻止。在读取数据或接收到信号之前,它将无法继续执行,因此您通常不需要担心yes
写入千兆字节的数据。
要记住的重要一点是管道是一个 FIFO 数据结构,而不仅仅是一个纯粹的流,如果没有立即在接收器上读取,就会丢弃数据。也就是说,虽然在大多数情况下它可能看起来是从写入应用程序到读取应用程序的无缝数据流,但它确实需要中间存储来执行此操作,并且中间存储的大小是有限的。*
如果我们看一下管道(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
yes
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);
/* ... */
}
当该make
端最终终止时,将作为写入管道的结果yes
发送,而另一端没有任何剩余。SIGPIPE
然后,这将根据yes
实现调用其自己的信号处理程序或默认的内核信号处理程序,然后它将终止。***
* 在简单的情况下,接收器处理数据的速率与写入数据的速率大致相同,这种传输也可以是零复制,无需中间缓冲区,方法是使用虚拟内存映射并提供写入过程中的物理页可供接收者使用。但是,您所描述的情况最终肯定需要使用管道缓冲区来存储未读数据。
** 写入也可能是通过O_NONBLOCK
文件描述符上设置的标志完成的,这启用了非阻塞模式。在这种情况下,您可能会得到一个不完整的写入,然后写入将返回EAGAIN
,并且应用程序需要自行处理该问题。它可能会通过暂停或运行它选择的一些其他代码来处理管道已满来实现这一点。不过,就我能找到的每个现代版本和大多数其他应用程序而言yes
,上面的描述就是发生的情况,因为它们不使用O_NONBLOCK
.
*** 应用程序可以在接收后执行任何操作SIGPIPE
——理论上它甚至可以决定不终止。然而,所有常见的yes
都使用默认SIGPIPE
处理程序,它只是终止而不执行任何更多的用户空间指令。