管道如何限制内存使用?

mas*_*mas 40 pipe history

Brian Kernighan 在此视频中解释了早期贝尔实验室对基于内存限制的小型语言/程序的吸引力

一台大机器将是 64 k 字节——K,而不是 M 或 G——这意味着任何单独的程序都不会很大,所以有一种自然的趋势,即编写小程序,然后是管道机制,基本上是输入输出重定向,可以将一个程序链接到另一个程序。

但考虑到数据必须存储在 RAM 中才能在程序之间传输,我不明白这如何限制内存使用。

来自维基百科

在大多数类 Unix 系统中,一个管道的所有进程同时启动[强调我的],它们的流适当连接,并由调度程序与机器上运行的所有其他进程一起管理。将 Unix 管道与其他管道实现区分开来的一个重要方面是缓冲的概念:例如,发送程序每秒可能产生 5000 个字节,而接收程序每秒只能接受 100 个字节,但不能数据丢失。相反,发送程序的输出保存在缓冲区中。当接收程序准备好读取数据时,管道中的下一个程序从缓冲区读取。在 Linux 中,缓冲区的大小为 65536 字节 (64KB)。如果需要,可以使用名为 bfr 的开源第三方过滤器来提供更大的缓冲区。

这让我更加困惑,因为这完全违背了小程序的目的(尽管它们在一定程度上是模块化的)。

我唯一能想到的解决我的第一个问题(内存限制有问题取决于大小数据)的唯一方法是,当时根本没有计算大型数据集,而真正的问题管道是要解决的程序本身所需的内存量。但是鉴于维基百科引用中的粗体文本,即使这让我感到困惑:因为一次没有实施一个程序。

如果使用临时文件,所有这些都会很有意义,但我的理解是管道不会写入磁盘(除非使用交换)。

例子:

sed 'simplesubstitution' file | sort | uniq > file2
Run Code Online (Sandbox Code Playgroud)

我很清楚这sed是读取文件并逐行吐出。但是sort,正如 BK 在链接的视频中所说的那样,是一个句号,所以所有的数据都必须被读入内存(或者是吗?),然后它被传递给uniq,这(在我看来)将是一个-一行一次的程序。但是在第一个和第二个管道之间,所有数据都必须在内存中,不是吗?

Ste*_*itt 49

数据不需要存储在 RAM 中。如果读者不在或跟不上,管道会阻塞他们的作者;在 Linux(和大多数其他实现,我想)下有一些缓冲,但这不是必需的。正如mtraceurJdeBP所提到的(见后者的答案),早期版本的 Unix 缓冲管道到磁盘,这就是它们如何帮助限制内存使用:处理管道可以拆分为小程序,每个程序都会在磁盘缓冲区的限制内处理一些数据。小程序占用更少的内存,管道的使用意味着处理可以序列化:第一个程序将运行,填充其输出缓冲区,暂停,然后第二个程序将被调度,处理缓冲区等。现代系统是命令比早期的 Unix 系统大得多,并且可以并行运行许多管道;但是对于大量数据,您仍然会看到类似的效果(并且这种技术的变体用于“大数据”处理)。

在你的例子中,

sed 'simplesubstitution' file | sort | uniq > file2
Run Code Online (Sandbox Code Playgroud)

sedfile根据需要从中读取数据,然后在sort准备好读取时写入;如果sort未准备好,则写入块。数据确实最终确实存在于内存中,但这是特定于 的sort,并sort准备处理任何问题(如果要排序的数据量太大,它将使用临时文件)。

您可以通过运行查看阻塞行为

strace seq 1000000 -1 1 | (sleep 120; sort -n)
Run Code Online (Sandbox Code Playgroud)

这会产生相当数量的数据并将其传送到一个在前两分钟内还没有准备好读取任何内容的进程。您将看到许多write操作通过,但很快seq就会停止并等待两分钟过去,被内核阻止(write系统调用等待)。

  • 这个答案可以从额外解释为什么将程序分成许多小程序可以节省内存使用中受益:程序必须能够在内存中运行才能运行,但只能是*当前正在运行的*程序。在早期的 Unix 中,所有其他程序都被交换到磁盘上,一次只有一个程序被交换到实际的 RAM 中。因此,CPU 将运行一个程序,该程序将写入管道(当时该管道位于 *磁盘 * 上),然后换出该程序并换入从管道读取的程序。将逻辑上并行的装配线转变为增量串行化执行的优雅方式。 (14认同)
  • @malan:可以启动多个进程,并且可以同时处于可运行状态。但是在任何给定时间,每个物理 CPU 上最多可以执行一个进程,内核进程调度程序的工作是将 CPU 时间的“切片”分配给每个可运行的进程。在现代系统中,可运行但当前未调度 CPU 时间片的进程在等待下一个片时通常仍驻留在内存中,但允许内核将任何进程的内存分页到磁盘并再次返回到内存中,如它觉得方便。(在这里挥手一些细节。) (6认同)
  • @malan 程序在所有 Unix 系统上*概念上*在“同时”启动,只是在现代多处理器系统上有足够的 RAM 来容纳它们,这意味着它们实际上都同时保存在 RAM 中,而在系统上不能同时将它们全部保存在 RAM 中,有些会被换出到磁盘。还要注意,在很多情况下,“内存”意味着*虚拟内存*,它是磁盘上的 RAM 空间和交换空间的总和。维基百科关注的是概念而不是实现细节,特别是因为现在 Unix 真正做了什么事情已经不那么重要了。 (6认同)
  • 管道任一侧的进程都可以像协同例程一样有效地运行:一侧写入直到填满缓冲区和写入块,此时进程无法对其其余时间片做任何事情,并进入IO 等待模式。然后操作系统将剩余的时间片(或另一个即将到来的时间片)提供给读取端,读取端读取直到缓冲区和下一个读取块中没有任何剩余,此时读取器进程无法对其余时间片做任何事情它的时间片并返回给操作系统。数据一次通过一个缓冲区的管道。 (5认同)
  • @malan 另外,您看到的矛盾来自“内存”的两种不同含义(RAM 与 RAM + 交换)。我只是在谈论硬件 RAM,在这种情况下,只有 CPU 当前正在执行的代码需要适合 RAM(这是影响 Kernighan 所说的决定的原因),而在所有程序的上下文中都被逻辑执行由操作系统在给定时间(在时间切片之上提供的抽象级别),程序只需要适合操作系统可用的整个虚拟内存,其中包括磁盘上的交换空间。 (2认同)
  • @mtraceur,您甚至不需要交换管道和公司。帮助减少内存使用,因为可执行文件本身是它们自己的后备存储(虽然可能不是在早期的 Unix 上)。 (2认同)

Jde*_*eBP 35

但考虑到数据必须存储在 RAM 中才能在程序之间传输,我不明白这如何限制内存使用。

这是你的根本错误。早期版本的 Unix 没有在 RAM 中保存管道数据。他们将它们存储在光盘上。管道有 i 节点;在被表示为管道设备的圆盘设备上。系统管理员运行了一个程序/etc/config来指定(除其他外)哪个磁盘上的哪个卷是管道设备,哪个卷是根设备,哪个是转储设备

待处理数据的数量受到以下事实的限制:只有磁盘上 i-node的直接块用于存储。这种机制使代码更简单,因为从管道读取的算法与读取常规文件所采用的算法大致相同,但由于管道不可查找且缓冲区是循环的,因此进行了一些调整。

这种机制在 1980 年代中后期被其他机制所取代。SCO XENIX 获得了“高性能管道系统”,它用内核缓冲区取代了 i 节点。4BSD 将未命名的管道制作成 socketpairs。AT&T 使用 STREAMS 机制重新实现了管道。

当然,sort程序执行了有限的 32KiB 输入块的内部排序(或者如果 32KiB 不可用,它可以分配任何更小的内存量),将排序结果写入中间stmX??文件,/usr/tmp/然后在其中外部合并排序以提供最终结果输出。

进一步阅读

  • 史蒂夫 D. 佩特 (1996)。“进程间通信”。UNIX 内部:实用方法。艾迪生-卫斯理。ISBN 9780201877212。
  • 莫里斯 J. 巴赫 (1987)。“文件系统的系统调用”。 Unix操作系统的设计。普伦蒂斯霍尔。ISBN 0132017571。
  • 史蒂文诉埃尔哈特 (1986)。“ config(1M)”。 Unix 程序员手册: 3. 系统管理工具。霍尔特、莱因哈特和温斯顿。ISBN 0030093139。第 23-28 页。
  • 阿比吉特·梅农-森 (2020-03-23)。Unix 管道是如何实现的?. toroid.org。