Wie begrenzen Pipelines die Speichernutzung?

36

Brian Kernighan erklärt in diesem Video die frühe Anziehungskraft von Bell Labs auf kleine Sprachen / Programme, die auf Speicherbeschränkungen basieren

Eine große Maschine würde 64 kByte groß sein - K, nicht M oder G - und das bedeutete, dass jedes einzelne Programm nicht sehr groß sein konnte, und so gab es eine natürliche Tendenz, kleine Programme zu schreiben, und dann den Pipe-Mechanismus. Grundsätzlich Eingabe Ausgabe Umleitung, ermöglicht es, ein Programm zu einem anderen zu verknüpfen.

Ich verstehe jedoch nicht, wie dies die Speichernutzung einschränken könnte, wenn man bedenkt, dass die Daten im RAM gespeichert werden müssen, um zwischen Programmen übertragen zu können.

Aus Wikipedia :

In den meisten Unix-ähnlichen Systemen werden alle Prozesse einer Pipeline gleichzeitig gestartet.Die Streams sind ordnungsgemäß verbunden und werden vom Scheduler zusammen mit allen anderen auf dem Computer ausgeführten Prozessen verwaltet. Ein wichtiger Aspekt dabei, der Unix-Pipes von anderen Pipe-Implementierungen unterscheidet, ist das Konzept der Pufferung: Beispielsweise kann ein Sendeprogramm 5000 Bytes pro Sekunde erzeugen, und ein Empfangsprogramm kann möglicherweise nur 100 Bytes pro Sekunde akzeptieren, aber keine Daten gehen verloren. Stattdessen wird die Ausgabe des Sendeprogramms im Puffer gehalten. Wenn das empfangende Programm zum Lesen von Daten bereit ist, liest das nächste Programm in der Pipeline aus dem Puffer. Unter Linux beträgt die Größe des Puffers 65536 Byte (64 KB). Ein Open-Source-Filter von Drittanbietern namens bfr ist verfügbar, um bei Bedarf größere Puffer bereitzustellen.

Das verwirrt mich umso mehr, als dies den Zweck kleiner Programme völlig zunichte macht (obwohl sie bis zu einem gewissen Ausmaß modular sein würden).

Das einzige, was ich mir als Lösung für meine erste Frage vorstellen kann (die Speicherbeschränkungen sind problematisch und hängen von den Größenangaben ab), ist, dass große Datenmengen damals einfach nicht berechnet wurden und das eigentliche Problem, das Pipelines lösen sollten, das war Speicherplatz, der von den Programmen selbst benötigt wird. Aber angesichts des fettgedruckten Textes im Wikipedia-Zitat verwirrt mich auch das: Es wird immer nur ein Programm gleichzeitig implementiert.

All dies wäre sehr sinnvoll, wenn temporäre Dateien verwendet würden, aber ich verstehe, dass Pipes nicht auf die Festplatte schreiben (es sei denn, Swap wird verwendet).

Beispiel:

sed 'simplesubstitution' file | sort | uniq > file2

Mir sedist klar, dass ich die Datei lese und zeilenweise ausspucke. Aber sort, wie BK in dem verlinkten Video feststellt, ist es ein Punkt, also müssen alle Daten in den Speicher eingelesen werden (oder?), Dann wird es an weitergeleitet uniq, was (meiner Meinung nach) eine Eins wäre -Zeilen-zu-Zeit-Programm. Aber zwischen der ersten und der zweiten Pipe müssen sich alle Daten im Speicher befinden, oder?

malan
quelle
1
unless swap is usedSwap wird immer dann verwendet , wenn nicht genügend RAM
edc65

Antworten:

44

Die Daten müssen nicht im RAM gespeichert werden. Pipes blockieren ihre Autoren, wenn die Leser nicht da sind oder nicht mithalten können. Unter Linux (und den meisten anderen Implementierungen, stelle ich mir vor) gibt es einige Puffer, aber das ist nicht erforderlich. Wie von mtraceur und JdeBP erwähnt (siehe dessen Antwort)), frühe Versionen von Unix, pufferten Pipes auf die Festplatte, und auf diese Weise konnten sie die Speichernutzung begrenzen: Eine Verarbeitungspipeline könnte in kleine Programme aufgeteilt werden, von denen jedes einige Daten innerhalb der Grenzen der Festplattenpuffer verarbeiten würde. Kleine Programme benötigen weniger Speicher und die Verwendung von Pipes bedeutete, dass die Verarbeitung serialisiert werden konnte: Das erste Programm wurde ausgeführt, der Ausgabepuffer wurde gefüllt, das zweite Programm wurde geplant, der Puffer wurde verarbeitet usw. Moderne Systeme sind Aufträge Größer als die frühen Unix-Systeme und kann viele Pipes parallel betreiben. Bei großen Datenmengen wird jedoch immer noch ein ähnlicher Effekt angezeigt (und Varianten dieser Technik werden für die Verarbeitung von „Big Data“ verwendet).

In Ihrem Beispiel

sed 'simplesubstitution' file | sort | uniq > file2

sedLiest Daten filenach Bedarf aus und schreibt sie dann, solange sie sortzum Lesen bereit sind. Wenn sortes nicht fertig ist, blockiert das Schreiben. Die Daten werden zwar irgendwann im Arbeitsspeicher abgelegt, dies ist jedoch spezifisch sortund sortbereit, Probleme zu lösen (es werden temporäre Dateien verwendet , wenn die zu sortierende Datenmenge zu groß ist).

Sie können das Blockierungsverhalten anzeigen, indem Sie ausführen

strace seq 1000000 -1 1 | (sleep 120; sort -n)

Dies erzeugt eine ganze Menge von Daten und leitet sie an einen Prozess weiter, der in den ersten zwei Minuten nicht bereit ist, etwas zu lesen . Es wird eine Reihe von writeVorgängen angezeigt, die jedoch sehr schnell seqangehalten werden und auf den Ablauf von zwei Minuten warten, die vom Kernel blockiert werden (der writeSystemaufruf wartet).

Stephen Kitt
quelle
13
Diese Antwort könnte von der zusätzlichen Erklärung profitieren, warum das Aufteilen von Programmen in viele kleine Programme den Speicherverbrauch senkt: Ein Programm musste in der Lage sein, in den Arbeitsspeicher zu passen, um ausgeführt zu werden, aber nur das aktuell ausgeführte Programm. Jedes andere Programm wurde in früheren Unix-Versionen auf die Festplatte ausgelagert, wobei jeweils nur ein Programm in den tatsächlichen Arbeitsspeicher ausgelagert wurde. Die CPU würde also ein Programm ausführen, das in eine Pipe schreibt (die sich damals auf der Festplatte befand ), dieses Programm austauschen und das aus der Pipe gelesene Programm eintauschen. Elegante Möglichkeit, eine logisch parallele Fertigungslinie in eine inkrementelle serialisierte Ausführung umzuwandeln.
mtraceur
6
@malan: Es können mehrere Prozesse gestartet werden und gleichzeitig ausgeführt werden. Es kann jedoch höchstens ein Prozess auf jeder physischen CPU zu einem bestimmten Zeitpunkt ausgeführt werden, und es ist die Aufgabe des Prozess-Schedulers des Kernels, jedem ausführbaren Prozess "CPU-Zeitscheiben" zuzuweisen. In modernen Systemen verbleibt ein Prozess, der lauffähig ist, aber derzeit keine CPU-Zeitscheibe geplant hat, normalerweise im Speicher, während er auf seine nächste Scheibe wartet, aber der Kernel kann den Speicher jedes Prozesses auf die Festplatte auslagern und wieder in den Speicher zurücksenden als es findet bequem. (Hier zeigen wir einige Details per Hand.)
Daniel Pryden
5
Prozesse auf beiden Seiten einer Pipe können sich effektiv wie Co-Routinen verhalten: Eine Seite schreibt, bis der Puffer und die Schreibblöcke voll sind. An diesem Punkt kann der Prozess mit dem Rest seiner Zeitscheibe nichts mehr anfangen und geht in eine IO-Wartemodus. Dann gibt das Betriebssystem den Rest der Zeitscheibe (oder eine andere bevorstehende Zeitscheibe) an die Leseseite, die liest, bis im Puffer und in den nächsten Leseblöcken nichts mehr übrig ist. An diesem Punkt kann der Leseprozess mit dem Rest von nichts mehr anfangen seine Zeitscheibe und gibt an das Betriebssystem zurück. Die Daten werden nacheinander gepuffert.
Daniel Pryden
6
@malan Die Programme werden konzeptionell auf allen Unix-Systemen "zur gleichen Zeit" gestartet , nur auf modernen Multiprozessorsystemen mit genügend RAM, um sie zu speichern Halten Sie sie nicht alle gleichzeitig im RAM, einige werden auf die Festplatte ausgelagert. Beachten Sie auch, dass "Speicher" in vielen Kontexten virtuellen Speicher bedeutet, der die Summe von RAM-Speicher und Auslagerungsspeicher auf der Festplatte ist. Wikipedia konzentriert sich eher auf das Konzept als auf die Implementierungsdetails, vor allem weil es jetzt weniger relevant ist, wie alt Unix die Dinge gemacht hat.
mtraceur
2
@malan Auch der Widerspruch, den Sie sehen, kommt von den zwei verschiedenen Bedeutungen von "Speicher" (RAM vs RAM + Swap). Ich habe nur über Hardware-RAM gesprochen, und in diesem Kontext muss nur der Code, der gerade von der CPU ausgeführt wird, in den RAM passen (worüber Kernighan entschieden hat), während im Kontext, dass alle Programme logisch ausgeführt werden Zu einem bestimmten Zeitpunkt muss ein Programm vom Betriebssystem (auf der abstrakten Ebene, die zusätzlich zum Time Slicing bereitgestellt wird) nur in den gesamten virtuellen Speicher des Betriebssystems passen, der den Swap-Speicherplatz auf der Festplatte enthält.
mtraceur
34

Ich verstehe jedoch nicht, wie dies die Speichernutzung einschränken könnte, wenn man bedenkt, dass die Daten im RAM gespeichert werden müssen, um zwischen Programmen übertragen zu können.

Dies ist Ihr grundlegender Fehler. Frühe Versionen von Unix enthielten keine Pipe-Daten im RAM. Sie haben sie auf CD gespeichert. Rohre hatten i-Knoten; auf einem Plattengerät, das als Pipe-Gerät bezeichnet wurde . Der Systemadministrator führte ein Programm mit dem Namen aus /etc/config, um (unter anderem) anzugeben, auf welchem ​​Volume sich das Pipe-Gerät befand, auf welchem ​​Volume sich das Root-Gerät befand und auf welchem ​​das Dump-Gerät .

Die Menge an anstehenden Daten wurde durch die Tatsache eingeschränkt, dass nur die direkten Blöcke des i-Knotens auf der Platte zum Speichern verwendet wurden. Dieser Mechanismus machte den Code einfacher, da für das Lesen aus einer Pipe fast derselbe Algorithmus verwendet wurde wie für das Lesen für eine reguläre Datei, mit einigen Verbesserungen, die durch die Tatsache verursacht wurden, dass Pipes nicht suchbar sind und der Puffer kreisförmig ist.

Dieser Mechanismus wurde Mitte bis Ende der 1980er Jahre durch andere ersetzt. SCO XENIX erhielt das "High Performance Pipe System", das i-Nodes durch In-Core-Puffer ersetzte. 4BSD hat unbenannte Pfeifen zu Muffenpaaren verarbeitet. AT & T hat Pipes mithilfe des STREAMS-Mechanismus neu implementiert.

Und natürlich führte das sortProgramm eine begrenzte interne Sortierung von 32 KB großen Eingabestücken durch (oder eine beliebige kleinere Menge an Speicher, die zugewiesen werden könnte, wenn 32 KB nicht verfügbar wären) und schrieb die sortierten Ergebnisse in Zwischendateien stmX??, in /usr/tmp/denen sie dann extern sortiert zusammengeführt wurden, um das endgültige Ergebnis zu erhalten Ausgabe.

Weitere Lektüre

  • Steve D. Pate (1996). "Interprozesskommunikation". UNIX-Interna: Ein praktischer Ansatz . Addison-Wesley. ISBN 9780201877212.
  • Maurice J. Bach (1987). Msgstr "System fordert das Dateisystem an". Das Design des Unix-Betriebssystems . Prentice-Hall. ISBN 0132017571.
  • Steven V. Earhart (1986). " config(1M)". Unix-Programmierhandbuch: 3. Systemverwaltungsfunktionen . Holt, Rinehart und Winston. ISBN 0030093139. S. 23–28.
JdeBP
quelle
1

Sie haben teilweise recht, aber nur aus Versehen .

In Ihrem Beispiel müssen alle Daten tatsächlich "zwischen" den Pipes gelesen worden sein, sie müssen jedoch nicht im Arbeitsspeicher (einschließlich des virtuellen Arbeitsspeichers) gespeichert sein. Übliche Implementierungen von sortkönnen Datensätze sortieren, die nicht in den Arbeitsspeicher passen, indem Teil-Sortierungen in Tempfiles durchgeführt und zusammengeführt werden. Es ist jedoch eine Tatsache, dass Sie unmöglich eine sortierte Sequenz ausgeben können, bevor Sie jedes einzelne Element gelesen haben. Das ist ziemlich offensichtlich. Also ja, sortich kann erst mit der Ausgabe an die zweite Pipe beginnen, nachdem ich alles von der ersten Pipe gelesen habe (und was auch immer getan habe, möglicherweise tempfiles teilweise sortiert habe). Es muss aber nicht unbedingt alles im RAM bleiben.

Dies hat jedoch nichts mit der Funktionsweise von Rohren zu tun. Pipes können benannt werden (traditionell wurden sie alle benannt), was nicht mehr und nicht weniger bedeutet, als dass sie wie Dateien einen Speicherort im Dateisystem haben. Und genau das waren früher Pipes, Dateien (wobei Schreibvorgänge so weit zusammengeführt wurden, wie es die Verfügbarkeit des physischen Speichers zur Optimierung zulässt).

Heutzutage sind Pipes ein kleiner, endlicher Kernel-Puffer, in den Daten kopiert werden, zumindest geschieht dies konzeptionell . Wenn der Kernel dies unterstützt, werden Kopien durch das Abspielen von VM-Tricks entfernt (z. B. stellt das Weiterleiten aus einer Datei in der Regel nur die gleiche Seite zum Lesen für den anderen Prozess zur Verfügung, sodass es sich letztendlich nur um einen Lesevorgang handelt, nicht um zwei Kopien und nicht um einen Es wird ohnehin mehr Speicher benötigt, als bereits vom Puffercache verwendet wird. In einigen Situationen erhalten Sie möglicherweise auch eine 100% -Null-Kopie. Oder es ist etwas sehr Nahes.

Wenn Pipes klein und endlich groß sind, wie kann dies dann für eine unbekannte (möglicherweise große) Datenmenge funktionieren? Das ist ganz einfach: Wenn nichts mehr passt, blockiert das Schreiben, bis wieder Platz ist.

Die Philosophie vieler einfacher Programme war einmal nützlich, als der Speicher sehr knapp war. Sie können nämlich in kleinen Schritten nacheinander arbeiten. Heutzutage sind die Vorteile, abgesehen von einer gewissen zusätzlichen Flexibilität, meiner Meinung nach nicht mehr so ​​großartig.
Pipes sind jedoch sehr effizient implementiert (sie mussten es sein!), Daher gibt es auch keinen Nachteil, und es ist eine etablierte Sache, die gut funktioniert und an die sich die Leute gewöhnt haben. Es besteht also keine Notwendigkeit, das Paradigma zu ändern.

Damon
quelle
Wenn Sie sagen, dass "Pipes wurden benannt" (JdeBP scheint zu sagen, dass es ein "Pipe-Gerät" gab), bedeutet dies, dass die Anzahl der Pipes, die zu einem bestimmten Zeitpunkt verwendet werden konnten, begrenzt war (dh, es gab eine Beschränkung für "Pipe-Gerät") wie oft könnten Sie |in einem Befehl verwenden)?
Malan
2
Ich habe noch nie eine solche Grenze gesehen, und ich glaube nicht, dass es theoretisch jemals eine gegeben hat. In der Praxis benötigt alles, was einen Dateinamen hat, einen Inode, und die Anzahl der Inodes ist natürlich endlich. Wie die Anzahl der physischen Seiten auf einem System, wenn nichts anderes. Moderne Systeme garantieren 4k atomare Schreibvorgänge, daher muss jede Pipe mindestens eine vollständige 4k-Seite besitzen, was die Anzahl der verfügbaren Pipe stark einschränkt. Aber denken Sie daran, ein paar Gigabyte RAM zu haben ... praktisch ist das eine Grenze, auf die Sie niemals stoßen werden. Versuchen Sie, ein paar Millionen Pipes an einem Terminal
Damon