Warum müssen wir uns trennen, um neue Prozesse zu erstellen?


95

Wann immer wir in Unix einen neuen Prozess erstellen möchten, verzweigen wir den aktuellen Prozess und erstellen einen neuen untergeordneten Prozess, der genau dem übergeordneten Prozess entspricht. Dann führen wir einen Systemaufruf exec durch, um alle Daten des übergeordneten Prozesses durch die Daten des neuen Prozesses zu ersetzen.

Warum erstellen wir zuerst eine Kopie des übergeordneten Prozesses und nicht direkt einen neuen Prozess?


Antworten:


61

Die kurze Antwort lautet: Unter forkUnix, weil es zu der Zeit leicht war, sich in das bestehende System einzufügen, und weil ein Vorgängersystem bei Berkeley das Konzept der Gabeln verwendet hatte.

Aus der Evolution des Unix-Timesharing-Systems (relevanter Text wurde hervorgehoben ):

Die Prozesssteuerung in ihrer modernen Form wurde innerhalb weniger Tage entworfen und implementiert. Es ist erstaunlich, wie einfach es sich in das bestehende System einfügt. Gleichzeitig ist es leicht zu erkennen, wie einige der etwas ungewöhnlichen Merkmale des Designs vorhanden sind, gerade weil sie kleine, leicht zu codierende Änderungen an dem darstellten, was existierte . Ein gutes Beispiel ist die Trennung der Funktionen fork und exec. Das gebräuchlichste Modell für die Erstellung neuer Prozesse ist die Angabe eines Programms für die Ausführung des Prozesses. In Unix führt ein gegabelter Prozess weiterhin dasselbe Programm wie sein übergeordnetes Programm aus, bis er eine explizite Ausführung ausführt. Die Aufteilung der Funktionen ist sicherlich nicht nur in Unix zu finden , sondern auch in dem bei Thompson bekannten Berkeley-Time-Sharing-System. Trotzdem scheint es vernünftig anzunehmen, dass es in Unix existiert, hauptsächlich wegen der Leichtigkeit, mit der Fork implementiert werden kann, ohne viel anderes zu ändern . Das System hat bereits mehrere (dh zwei) Prozesse verarbeitet. Es gab eine Prozesstabelle, und die Prozesse wurden zwischen dem Hauptspeicher und der Festplatte ausgetauscht. Die anfängliche Implementierung von Fork ist nur erforderlich

1) Erweiterung der Prozesstabelle

2) Hinzufügen eines Verzweigungsaufrufs, der den aktuellen Prozess unter Verwendung der bereits vorhandenen Swap-E / A-Grundelemente in den Plattenaustauschbereich kopiert und einige Anpassungen an der Prozesstabelle vorgenommen hat.

Tatsächlich erforderte der Gabelaufruf des PDP-7 genau 27 Zeilen Assembler-Code. Natürlich waren andere Änderungen im Betriebssystem und in den Benutzerprogrammen erforderlich, und einige von ihnen waren ziemlich interessant und unerwartet. Aber eine kombinierte gabel exec wäre wesentlich komplizierter gewesen , wenn auch nur , weil exec als solche nicht existiert; Seine Funktion wurde bereits mithilfe expliziter E / A von der Shell ausgeführt.

Seit dieser Veröffentlichung hat sich Unix weiterentwickelt. forkgefolgt von execist nicht mehr die einzige Möglichkeit, ein Programm auszuführen.

  • vfork wurde als effizienterer Fork für den Fall entwickelt, dass der neue Prozess direkt nach dem Fork einen Exec ausführen soll. Nach einer vfork-Operation teilen sich der übergeordnete und der untergeordnete Prozess denselben Datenbereich und der übergeordnete Prozess wird angehalten, bis der untergeordnete Prozess ein Programm ausführt oder beendet.

  • posix_spawn erstellt einen neuen Prozess und führt eine Datei in einem einzigen Systemaufruf aus. Es sind eine Reihe von Parametern erforderlich, mit denen Sie die geöffneten Dateien des Anrufers selektiv freigeben und seine Signalverteilung und andere Attribute in den neuen Prozess kopieren können.


5
Gute Antwort, aber ich würde hinzufügen, dass Vfork nicht mehr verwendet werden sollte. Der Leistungsunterschied ist jetzt marginal und seine Verwendung kann gefährlich sein. Siehe diese SO-Frage stackoverflow.com/questions/4856255/…, diese Site ewontfix.com/7 und "Erweiterte Unix-Programmierung" Seite 299 über vfork
Raphael Ahrens

4
Die Vorgänge (Einrichtung der Datenstruktur), die erforderlich sind, um posix_spawn()dieselben Post-Fork-Repumbing-Jobs auszuführen, die auch mit fork()Inline-Code problemlos ausgeführt werden können, sind ein überzeugendes Argument für fork()die einfachere Verwendung.
Jonathan Leffler

34

[Ich werde einen Teil meiner Antwort von hier aus wiederholen .]

Warum nicht einfach einen Befehl haben, der einen neuen Prozess von Grund auf neu erstellt? Ist es nicht absurd und ineffizient, eine zu kopieren, die nur sofort ersetzt wird?

In der Tat wäre das wahrscheinlich aus ein paar Gründen nicht so effizient:

  1. Die von erzeugte "Kopie" fork()ist eine Art Abstraktion, da der Kernel ein Copy-on-Write- System verwendet . alles, was wirklich erstellt werden muss, ist eine virtuelle Speicherkarte. Wenn die Kopie dann sofort aufgerufen wird exec(), müssen die meisten Daten, die kopiert worden wären, wenn sie durch die Aktivität des Prozesses geändert worden wären, niemals tatsächlich kopiert / erstellt werden, da der Prozess nichts tut, was seine Verwendung erfordert.

  2. Verschiedene wichtige Aspekte des untergeordneten Prozesses (z. B. seine Umgebung) müssen nicht einzeln dupliziert oder basierend auf einer komplexen Kontextanalyse usw. festgelegt werden. Es wird lediglich davon ausgegangen, dass sie mit denen des aufrufenden Prozesses identisch sind Dies ist das ziemlich intuitive System, mit dem wir vertraut sind.

Um # 1 ein wenig weiter zu erläutern, wird der Speicher, auf den "kopiert", aber später nie zugegriffen wird, zumindest in den meisten Fällen nie wirklich kopiert. Eine Ausnahme in diesem Kontext kann sein, wenn Sie einen Prozess gegabelt haben und dann den übergeordneten Prozess beendet haben, bevor sich das untergeordnete Element durch ersetzt hat exec(). Ich sage möglicherweise, weil ein Großteil des übergeordneten Elements zwischengespeichert werden könnte, wenn genügend freier Speicher vorhanden ist, und ich bin nicht sicher, in welchem ​​Umfang dies ausgenutzt würde (was von der Betriebssystemimplementierung abhängen würde).

Natürlich tut das nicht auf der Oberfläche um eine Kopie mit mehr effizienter als die Verwendung ein unbeschriebenes Blatt - mit Ausnahme von „der unbeschriebenes Blatt“ nichts nicht wörtlich ist, und die Zuweisung beinhalten muss. Das System könnte eine generische leere / neue Prozessvorlage haben, die auf die gleiche Weise kopiert wird, 1 die dann jedoch nichts im Vergleich zur Kopie beim Schreiben wirklich rettet. Die Nummer 1 zeigt also nur, dass die Verwendung eines "neuen" leeren Prozesses nicht effizienter wäre.

Punkt 2 erklärt, warum die Verwendung der Gabel wahrscheinlich effizienter ist. Die Umgebung eines Kindes wird von seiner Eltern geerbt, auch wenn es sich um eine völlig andere ausführbare Datei handelt. Wenn beispielsweise der übergeordnete Prozess eine Shell und der untergeordnete ein Webbrowser ist, $HOMEist dies für beide immer noch derselbe. Da jedoch beide Prozesse dies später ändern könnten, müssen dies zwei separate Kopien sein. Derjenige im Kind wird vom Original produziert fork().

1. Eine Strategie, die wörtlich nicht viel Sinn macht, aber ich möchte betonen, dass das Erstellen eines Prozesses mehr beinhaltet als das Kopieren des Images vom Datenträger in den Speicher.


3
Obwohl beide Punkte zutreffen, unterstützt keiner, warum die Forking-Methode gewählt wurde, anstatt einen neuen Prozess aus einer gegebenen ausführbaren Datei zu erstellen.
SkyDan

3
Ich denke, das beantwortet die Frage. Gabel wird verwendet, da in Fällen, in denen das Erstellen eines neuen Prozesses der effizienteste Weg ist, die Kosten für die Verwendung der Gabel gering sind (wahrscheinlich weniger als 1% der Prozesserstellungskosten). Auf der anderen Seite gibt es viele Stellen, an denen Fork wesentlich effizienter oder weitaus einfacher als eine API ist (z. B. die Handhabung von Dateihandles). Die Entscheidung von Unix war, nur eine API zu unterstützen, was die Spezifikation vereinfacht.
Cort Ammon

1
@SkyDan Du hast Recht, es ist eher eine Antwort auf das Warum als auf das Warum , die Mark Plotnick direkter beantwortet - was ich so interpretieren würde, dass dies nicht nur die einfachste Wahl, sondern wahrscheinlich auch die effizienteste war Auswahl (laut Dennis Richie-Zitat: "Der Gabelaufruf des PDP-7 erforderte genau 27 Montagelinien ... exec als solches existierte nicht; seine Funktion wurde bereits ausgeführt"). Also das „warum nicht“ ist wirklich ein sinniert über zwei Strategien , wobei eine oberflächlich erscheint einfacher und effizienter, wenn vielleicht ist es nicht (Zeuge die zweifelhafte Schicksal ...
goldilocks

1
Goldlöckchen ist richtig. Es gibt Situationen, in denen das Forken und Ändern billiger ist als das Erstellen eines neuen von Grund auf. Das extremste Beispiel ist natürlich, wann immer Sie ein Gabelverhalten selbst wollen. fork()kann es sehr schnell (wie GL erwähnt, in der Größenordnung von 27 Montagelinien). Wenn Sie in die andere Richtung blicken und einen "Prozess von Grund auf neu erstellen" möchten, fork()kostet dies nur ein kleines bisschen mehr, als wenn Sie von einem leeren erstellten Prozess ausgehen (27 Assemblierungszeilen + Kosten für das Schließen von Dateihandles). So lassen sich forksowohl Gabel als auch Schaffen gut createhandhaben , während sich Schaffen nur gut handhaben lässt.
Cort Ammon

2
Ihre Antwort bezog sich auf die Hardware-Verbesserungen: virtueller Speicher, Copy-on-Write. Davor wurde forktatsächlich der gesamte Prozessspeicher kopiert, und es war sehr teuer.
Barmar

6

Ich denke, der Grund, warum Unix nur die forkFunktion hatte, neue Prozesse zu erstellen, ist ein Ergebnis der Unix-Philosophie

Sie bauen eine Funktion auf, die eine Sache gut macht. Es wird ein untergeordneter Prozess erstellt.

Was man mit dem neuen Prozess macht, liegt dann beim Programmierer. Er kann eine der exec*Funktionen verwenden und ein anderes Programm starten, oder er kann nicht exec verwenden und die beiden Instanzen desselben Programms verwenden, was nützlich sein kann.

So erhalten Sie einen größeren Freiheitsgrad, da Sie verwenden können

  1. Gabel ohne Exec *
  2. Gabel mit exec * oder
  3. nur exec * ohne gabel

und außerdem müssen Sie sich nur die forkund die exec*Funktionsaufrufe merken , die Sie in den 1970er Jahren machen mussten.


3
Ich verstehe, wie Gabeln funktionieren und wie man sie benutzt. Aber warum sollte ich einen neuen Prozess erstellen wollen, wenn ich dasselbe tun kann, aber mit weniger Aufwand? Beispielsweise gab mir mein Lehrer einen Auftrag, bei dem ich für jede an argv übergebene Zahl einen Prozess erstellen musste, um zu überprüfen, ob die Zahl eine Primzahl ist. Aber ist das nicht nur ein Umweg, um letztendlich das Gleiche zu tun? Ich hätte einfach ein Array und eine Funktion für jede Zahl verwenden können ... Warum erstellen wir dann untergeordnete Prozesse, anstatt die gesamte Verarbeitung im Hauptprozess durchzuführen?
user1534664

2
Ich möchte sagen, dass Sie verstehen, wie Gabeln funktionieren und wie sie verwendet werden, weil Sie einmal einen Lehrer hatten, der Ihnen einen Auftrag erteilte, bei dem Sie eine Reihe von Prozessen erstellen mussten (wobei die Nummer zur Laufzeit angegeben wurde). Kontrollieren Sie sie, koordinieren Sie sie und kommunizieren Sie untereinander. Natürlich würde niemand im wirklichen Leben so etwas Triviales tun. Wenn Sie jedoch ein großes Problem haben, das leicht in Teile zerlegt werden kann, die parallel behandelt werden können (z. B. Kantenerkennung in einem Bild), können Sie beim Forken mehrere CPU-Kerne gleichzeitig verwenden.
Scott

5

Es gibt zwei Philosophien für die Erstellung von Prozessen: Gabelung mit Vererbung und Erstellung mit Argumenten. Unix benutzt natürlich fork. (OSE zum Beispiel und VMS verwenden die create-Methode.) Unix verfügt über VIELE vererbbare Eigenschaften und weitere werden regelmäßig hinzugefügt. Über die Vererbung können diese neuen Merkmale hinzugefügt werden, ohne dass bestehende Programme geändert werden müssen! Bei Verwendung eines Modells zum Erstellen mit Argumenten würde das Hinzufügen neuer Merkmale das Hinzufügen neuer Argumente zum Aufruf "create" bedeuten. Das Unix-Modell ist einfacher.

Es bietet auch das äußerst nützliche Modell „Fork-without-Exec“, bei dem sich ein Prozess in mehrere Teile aufteilen kann. Dies war von entscheidender Bedeutung, als es keine Form von asynchroner E / A gab, und ist nützlich, wenn mehrere CPUs in einem System ausgenutzt werden. (Pre-Threads.) Ich habe dies im Laufe der Jahre viel getan, auch in letzter Zeit. Im Wesentlichen können mehrere 'Programme' in einem einzigen Programm zusammengefasst werden, so dass absolut kein Platz für Korruption, Versionsinkongruenzen usw. vorhanden ist.

Das Fork / Exec-Modell bietet einem bestimmten Kind auch die Möglichkeit, eine radikal verrückte Umgebung zu erben, die zwischen Fork und Exec eingerichtet wurde. Vor allem Dinge wie geerbte Dateideskriptoren. (Eine Erweiterung von stdio fd's.) Das Erstellungsmodell bietet nicht die Möglichkeit, etwas zu erben, das von den Erstellern des Erstellungsaufrufs nicht vorgesehen war.

Einige Systeme unterstützen auch die dynamische Kompilierung von nativem Code, wobei der Prozess tatsächlich ein eigenes Programm für nativen Code schreibt. Mit anderen Worten, es möchte ein neues Programm, das sich selbst im laufenden Betrieb schreibt, ohne den Quellcode- / Compiler- / Linker-Zyklus durchlaufen zu müssen und Speicherplatz zu belegen. (Ich glaube, es gibt ein Verilog-Sprachsystem, das dies tut.) Das Fork-Modell unterstützt dies, das Create-Modell normalerweise nicht.


Dateideskriptoren sind keine „Erweiterung von stdio“. stdio-Dateizeiger sind ein Wrapper um Dateideskriptoren. Dateideskriptoren standen an erster Stelle und sind die grundlegenden E / A-Handles von Unix. Ansonsten ist das aber ein guter Punkt.
Scott

2

Die Funktion fork () kopiert nicht nur den Vaterprozess, sondern gibt einen Wert zurück, der darauf hinweist, dass der Prozess der Vater- oder der Sohnprozess ist. In der folgenden Abbildung wird erläutert, wie Sie fork () als Vater und a verwenden können Sohn:

Bildbeschreibung hier eingeben

Wie gezeigt, wenn der Prozess der Vater ist, gibt fork () die Sohn-Prozess-ID zurück, PID ansonsten wird sie zurückgegeben0

Sie können es beispielsweise verwenden, wenn Sie einen Prozess (Webserver) haben, der die Anforderungen empfängt und bei jeder Anforderung einen erstellt son process, um diese Anforderung zu verarbeiten. Hier haben der Vater und seine Söhne unterschiedliche Aufgaben.

Also, keine Kopie eines Prozesses ausführen ist nicht die exakte Sache wie fork ().


5
Dies beantwortet zwar die Frage nicht. Warum ist das Forken für die Prozesserstellung erforderlich, wenn ich eine andere ausführbare Datei ausführen möchte?
SkyDan

1
Ich stimme SkyDan zu - dies beantwortet die Frage nicht. posix_spawn ist eine etwas ausgefallenere Version dessen, was man sich vor 30 Jahren (bevor Posix existierte) als fork_execve- Funktion vorgestellt hat. eines, das einen neuen Prozess erstellt, dessen Image aus einer ausführbaren Datei initialisiert, ohne dass das Image des übergeordneten Prozesses (mit Ausnahme der Argumentliste, der Umgebung und der Prozessattribute (z. B. Arbeitsverzeichnis)) kopiert wird, und das zurückgibt PID des neuen Prozesses an den Aufrufer (der übergeordnete Prozess) .
Scott

1
Es gibt andere Möglichkeiten, "Eltern" -Informationen an ein Kind zu übergeben. Der Rückgabewert - Technik geschieht nur die effizienteste Weg , um es von einem zu tun , fork wenn Sie annehmen , dass Sie wollen forkin erster Linie
Cort Ammon

0

Die E / A-Umleitung lässt sich am einfachsten nach dem Verzweigen und vor dem Ausführen implementieren. Das Kind ist sich dessen bewusst, dass es das Kind ist. Es kann Dateideskriptoren schließen, neue öffnen, dup () oder dup2 (), um sie auf die richtige fd-Nummer usw. zu bringen, ohne dass dies Auswirkungen auf das Elternteil hat. Danach kann das neue Programm in der angepassten Umgebung ausgeführt werden. Eventuell werden Änderungen an der Umgebungsvariablen vorgenommen (dies wirkt sich auch nicht auf die übergeordnete Komponente aus).


Alles, was Sie hier tun, ist, den dritten Absatz von Jim Catheys Antwort mit ein wenig mehr Details zu wiederholen .
Scott

-2

Ich denke, jeder hier weiß, wie Gabel funktioniert, aber die Frage ist, warum wir mit Gabel ein genaues Duplikat des übergeordneten Elements erstellen müssen. Antwort ==> Nehmen Sie ein Beispiel für einen Server (ohne Verzweigung), während Client-1 auf den Server zugreift, wenn zur gleichen Zeit der zweite Client-2 angekommen ist und auf den Server zugreifen möchte, der Server jedoch dem neu eingetroffenen nicht die Berechtigung erteilt client-2, da der Server beschäftigt ist, um client-1 zu bedienen, sodass client-2 warten muss. Nachdem alle Dienste für client-1 abgeschlossen sind, kann client-2 jetzt auf den Server zugreifen client-3 muss warten, bis alle Dienste für client-2 abgeschlossen sind. Nehmen Sie das Szenario an, in dem Tausende von Clients gleichzeitig auf den Server zugreifen müssen. Dann müssen alle Clients warten Warten Sie (Server ist ausgelastet !!).

Dies wird vermieden, indem (mithilfe von fork) eine exakte doppelte Kopie (dh ein untergeordnetes Objekt) des Servers erstellt wird, wobei jedes untergeordnete Objekt (dh die exakte doppelte Kopie des übergeordneten Objekts, dh des Servers) dem neu angekommenen Client zugeordnet ist, sodass alle Clients gleichzeitig auf denselben zugreifen Server.


Aus diesem Grunde Server - Prozesse sollen nicht single-threaded sein, Client - Anfragen Umgang nacheinander , wenn sie gehandhabt werden können , gleichzeitig - beispielsweise in separaten Prozessen. Das Multithread-Servermodell kann jedoch problemlos mit einem Listener-Prozess implementiert werden, der Anforderungen von Clients akzeptiert und einen brandneuen Prozess zum Ausführen des Client-Service-Programms erstellt. Der einzige Vorteil, den der forkAufruf bietet, der den übergeordneten Prozess kopiert, besteht darin, dass Sie nicht zwei separate Programme haben müssen, sondern separate Programme (z. B. inetd), die das System modularer machen können.
Scott
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.