TL; DR : Weil dies die optimale Methode ist, um neue Prozesse zu erstellen und die Kontrolle über die interaktive Shell zu behalten
fork () wird für Prozesse und Pipes benötigt
Um den spezifischen Teil dieser Frage zu beantworten , würde ein Elternteil das Vorhandensein seiner PID mit allen Ressourcen übernehmen , wenn grep blabla foo
er über exec()
direkt im Elternteil aufgerufen würde grep blabla foo
.
Lassen Sie uns jedoch allgemein über exec()
und sprechen fork()
. Der Hauptgrund für ein solches Verhalten ist, dass dies fork()/exec()
die Standardmethode zum Erstellen eines neuen Prozesses unter Unix / Linux ist, und dies ist keine bashspezifische Sache. Diese Methode war von Anfang an in Kraft und wurde von bereits existierenden Betriebssystemen dieser Zeit beeinflusst. Die Antwort von goldilocks auf eine verwandte Frage etwas zu paraphrasieren , fork()
um einen neuen Prozess zu erstellen, ist einfacher, da der Kernel beim Zuweisen von Ressourcen weniger Arbeit hat und viele Eigenschaften (wie Dateideskriptoren, Umgebung usw.) alle können vom übergeordneten Prozess (in diesem Fall von bash
) geerbt werden .
Zweitens können Sie in Bezug auf interaktive Shells keine externen Befehle ausführen, ohne zu forken. Um eine ausführbare Datei zu starten, die sich auf der Festplatte befindet (z. B. /bin/df -h
), müssen Sie eine der exec()
Familienfunktionen aufrufen , z. B. execve()
die das übergeordnete Element durch den neuen Prozess ersetzen, die PID und die vorhandenen Dateideskriptoren übernehmen usw. Für die interaktive Shell möchten Sie, dass das Steuerelement an den Benutzer zurückgegeben wird und die übergeordnete interaktive Shell fortgesetzt wird. Daher ist es am besten, einen Unterprozess über zu erstellen fork()
und diesen Prozess über zu übernehmen execve()
. Die interaktive Shell PID 1156 würde ein fork()
untergeordnetes Element über PID 1157 erzeugen und dann aufrufen execve("/bin/df",["df","-h"],&environment)
, wodurch /bin/df -h
sie mit PID 1157 ausgeführt wird. Jetzt muss die Shell nur noch warten, bis der Prozess beendet ist und die Steuerung an sie zurückgibt.
Wenn Sie beispielsweise eine Pipe zwischen zwei oder mehr Befehlen df | grep
erstellen müssen, müssen Sie zwei Dateideskriptoren erstellen (das ist das Lese- und Schreibende der Pipe, das von pipe()
syscall stammt), und dann lassen Sie zwei neue Prozesse diese erben. Das ist erledigt, indem Sie einen neuen Prozess forken und dann das Write-Ende der Pipe per dup2()
Call auf das stdout
aka fd 1 kopieren (wenn Write-Ende also fd 4 ist, tun wir das dup2(4,1)
). Wenn es exec()
zu einem Spawn df
kommt, denkt der Child-Prozess an nichts stdout
und schreibt an ihn, ohne zu wissen (es sei denn, er prüft aktiv), dass seine Ausgabe tatsächlich eine Pipe durchläuft. Gleicher Prozess geschieht grep
, außer uns fork()
, mit fd 3 Lese Ende des Rohres nehmen und dup(3,0)
vor dem Laichen grep
mitexec()
. Der gesamte übergeordnete Prozess ist noch vorhanden und wartet darauf, die Steuerung wiederherzustellen, sobald die Pipeline abgeschlossen ist.
Bei eingebauten Befehlen funktioniert die Shell im Allgemeinen nicht fork()
, mit Ausnahme von Befehlen source
. Unterschalen erfordern fork()
.
Kurz gesagt, dies ist ein notwendiger und nützlicher Mechanismus.
Nachteile von Gabeln und Optimierungen
Dies ist jetzt anders bei nicht interaktiven Shells , wie z bash -c '<simple command>'
. Obwohl fork()/exec()
es sich um eine optimale Methode handelt, bei der Sie viele Befehle verarbeiten müssen, ist es eine Verschwendung von Ressourcen, wenn Sie nur einen einzigen Befehl haben. Um Stéphane Chazelas aus diesem Beitrag zu zitieren :
Das Verzweigen ist teuer, in Bezug auf CPU-Zeit, Speicher, zugewiesene Dateideskriptoren ... Es ist nur eine Verschwendung von Ressourcen, wenn ein Shell-Prozess nur darauf wartet, dass ein anderer Prozess beendet wird. Außerdem ist es schwierig, den Beendigungsstatus des separaten Prozesses, der den Befehl ausführen würde (z. B. wenn der Prozess beendet wird), korrekt zu melden.
Daher verwenden viele Shells (nicht nur bash
), um exec()
zu ermöglichen, dass bash -c ''
dies von diesem einzelnen einfachen Befehl übernommen wird. Und genau aus den oben genannten Gründen ist es besser, Pipelines in Shell-Skripten zu minimieren. Oft sieht man Anfänger, die so etwas machen:
cat /etc/passwd | cut -d ':' -f 6 | grep '/home'
Natürlich wird dies fork()
3 Prozesse. Dies ist ein einfaches Beispiel. Betrachten Sie jedoch eine große Datei im Gigabyte-Bereich. Mit einem Prozess wäre es weitaus effizienter:
awk -F':' '$6~"/home"{print $6}' /etc/passwd
Verschwendung von Ressourcen kann tatsächlich eine Form von Denial - of - Service - Angriff sein, insbesondere Gabel Bomben über Shell - Funktionen erstellt , die sich in der Pipeline aufrufen, die mehrere Kopien von sich selbst Gabeln. Heutzutage wird dies durch die Begrenzung der maximalen Anzahl von Prozessen in cgroups auf systemd gemildert , die Ubuntu seit Version 15.04 ebenfalls verwendet.
Das heißt natürlich nicht, dass das Gabeln nur schlecht ist. Es ist nach wie vor ein nützlicher Mechanismus, den Sie nach Möglichkeit vermeiden sollten, fork()
wenn Sie mit weniger Prozessen und folglich weniger Ressourcen und damit einer besseren Leistung davonkommen können.
Siehe auch