
Dieser Beitrag ist im Wesentlichen eine Fortsetzung einesfrühere Fragevon mir.
fork
Aus der Antwort auf diese Frage wurde mir klar, dass ich nicht nur das gesamte Konzept einer „Unterschale“ nicht ganz verstehe, sondern, ganz allgemein, die Beziehung zwischen -ing und untergeordneten Prozessen nicht verstehe .
Ich dachte immer, wenn ein Prozess X
ein fork
, einneuProzess Y
wird erstellt, dessen übergeordnetes Element ist X
, aber gemäß der Antwort auf diese Frage,
[a] Subshell ist kein völlig neuer Prozess, sondern ein Fork des bestehenden Prozesses.
Dies impliziert, dass eine „Fork“ kein „völlig neuer Prozess“ ist (oder nicht zu einem solchen führt).
Ich bin jetzt sehr verwirrt, eigentlich zu verwirrt, um eine zusammenhängende Frage zu formulieren, die meine Verwirrung direkt zerstreuen würde.
Ich kann jedoch eine Frage formulieren, die indirekt zur Aufklärung führen kann.
Da laut zshall(1)
immer $ZDOTDIR/.zshenv
dann eine neue Instanz von zsh
gestartet wird, wird jeder Befehl in , $ZDOTDIR/.zshenv
der zur Erstellung eines „völlig neuen [zsh]-Prozesses“ führt, zu einem unendlichen Regress führen. Andererseits führt das Einfügen einer der folgenden Zeilen in eine $ZDOTDIR/.zshenv
Datei nicht zunichtführen zu einem infiniten Regress:
echo $(date; printenv; echo $$) > /dev/null #1
(date; printenv; echo $$) #2
Die einzige Möglichkeit, die ich gefunden habe, um mit dem oben beschriebenen Mechanismus einen infiniten Regress herbeizuführen, bestand darin, eine Zeile wie die folgende 1 in die $ZDOTDIR/.zshenv
Datei einzufügen:
$SHELL -c 'date; printenv; echo $$' #3
Meine Fragen sind:
welcher Unterschied zwischen den oben mit gekennzeichneten Befehlen
#1
und#2
dem mit gekennzeichneten#3
Befehl ist auf diesen Verhaltensunterschied zurückzuführen?Wenn die Shells, die in erstellt werden
#1
und#2
„Sub-Shells“ genannt werden, wie heißen dann die, die wie die von generierte#3
aussehen?ist es möglich, die oben beschriebenen empirischen/anekdotischen Ergebnisse im Hinblick auf die „Theorie“ (in Ermangelung eines besseren Wortes) der Unix-Prozesse zu rationalisieren (und möglicherweise zu verallgemeinern)?
Die Motivation für die letzte Frage besteht darin, feststellen zu können,im Voraus(d. h. ohne auf Experimente zurückzugreifen) welche Befehle würden zu einem infiniten Regress führen, wenn sie in enthalten wären $ZDOTDIR/.zshenv
?
1 Die spezielle Befehlsfolge date; printenv; echo $$
, die ich in den verschiedenen Beispielen oben verwendet habe, ist nicht so wichtig. Es sind zufällig Befehle, deren Ausgabe möglicherweise hilfreich bei der Interpretation der Ergebnisse meiner „Experimente“ war. (Ich wollte jedoch, dass diese Sequenzen aus mehr als einem Befehl bestehen, aus dem erklärten GrundHier.)
Antwort1
Da laut zshall(1) $ZDOTDIR/.zshenv immer dann als Quelle verwendet wird, wenn eine neue Instanz von zsh gestartet wird
Wenn Sie sich hier auf das Wort „beginnt“ konzentrieren, werden Sie die Dinge besser verstehen. Der Effekt fork()
ist, einen weiteren Prozess zu schaffendas genau dort ansetzt, wo der aktuelle Prozess bereits beginnt. Es klont einen bestehenden Prozess, wobei der einzige Unterschied der Rückgabewert von ist fork
. Die Dokumentation verwendet „startet“, um zu bedeuten, dass das Programm von Anfang an aufgerufen wird.
Ihr Beispiel Nr. 3 führt aus $SHELL -c 'date; printenv; echo $$'
und startet einen völlig neuen Prozess von Anfang an. Er durchläuft das normale Startverhalten. Sie können dies beispielsweise veranschaulichen, indem Sie in einer anderen Shell Folgendes austauschen: run bash -c ' ... '
statt zsh -c ' ... '
. Die Verwendung von ist $SHELL
hier nichts Besonderes.
Beispiele Nr. 1 und Nr. 2 führen Subshells aus. Die Shell fork
führt Ihre Befehle in diesem untergeordneten Prozess aus und fährt dann mit ihrer eigenen Ausführung fort, wenn der untergeordnete Prozess fertig ist.
Die Antwort auf Ihre Frage Nr. 1 lautet wie folgt: Beispiel 3 führt von Anfang an eine völlig neue Shell aus, während die anderen beiden Subshells ausführen. Das Startverhalten umfasst das Laden .zshenv
.
Der Grund, warum dieses Verhalten ausdrücklich erwähnt wird und der wahrscheinlich auch zu Ihrer Verwirrung führt, besteht darin, dass diese Datei (im Gegensatz zu einigen anderen) sowohl in interaktiven als auch in nicht-interaktiven Shells geladen wird.
Zu Ihrer Frage Nr. 2:
Wenn die in Nr. 1 und Nr. 2 erstellten Shells „Subshells“ genannt werden, wie heißen dann die, die beispielsweise in Nr. 3 generiert werden?
Wenn Sie einen Namen möchten, können Sie es „untergeordnete Shell“ nennen, aber eigentlich ist es nichts. Es ist nicht anders als jeder andere Prozess, den Sie von der Shell aus starten, sei es dieselbe Shell, eine andere Shell oder cat
…
Zu Ihrer Frage Nr. 3:
ist es möglich, die oben beschriebenen empirischen/anekdotischen Ergebnisse im Hinblick auf die „Theorie“ (in Ermangelung eines besseren Wortes) der Unix-Prozesse zu rationalisieren (und möglicherweise zu verallgemeinern)?
fork
erstellt einen neuen Prozess mit einer neuen PID, der parallel genau dort ausgeführt wird, wo der vorherige aufgehört hat.exec
ersetzt den aktuell ausgeführten Code durch ein neues Programm, das von irgendwoher geladen wurde und von Anfang an ausgeführt wird. Wenn Sie ein neues Programm erzeugen, sind Sie zuerst fork
Sie selbst und dann exec
dieses Programm im Kind. Das ist die grundlegende Theorie der Prozesse, die überall gilt, innerhalb und außerhalb von Shells.
Subshells sind fork
s, und jeder nicht integrierte Befehl, den Sie ausführen, führt sowohl zu einem fork
als auch zu einem exec
.
Beachten Sie, dass sich dies $$
auf die PID der übergeordneten Shell ausdehntin jeder POSIX-kompatiblen Shell, daher erhalten Sie möglicherweise trotzdem nicht die erwartete Ausgabe. Beachten Sie auch, dass zsh die Ausführung der Subshell ohnehin aggressiv optimiert und normalerweise exec
den letzten Befehl ausführt oder die Subshell überhaupt nicht startet, wenn alle Befehle ohne sie sicher sind.
Ein nützlicher Befehl zum Testen Ihrer Intuitionen ist:
strace -e trace=process -f $SHELL -c ' ... '
Dadurch werden alle prozessbezogenen Ereignisse (und keine anderen) für den Befehl, den ...
Sie in einer neuen Shell ausführen, in die Standardfehlerausgabe gedruckt. Sie können sehen, was in einem neuen Prozess ausgeführt wird und was nicht, und wo exec
s auftreten.
Ein weiterer möglicherweise nützlicher Befehl ist pstree -h
, der den Baum der übergeordneten Prozesse des aktuellen Prozesses ausgibt und hervorhebt. Sie können sehen, wie viele Ebenen tief Sie sich in der Ausgabe befinden.
Antwort2
Wenn im Handbuch steht, dass die Befehle in .zshenv
"sourced" sind, bedeutet das, dass sie innerhalb der Shell ausgeführt werden, die sie ausführt. Sie verursachen keinen Aufruf von und fork()
erzeugen daher keine Subshell. Ihr drittes Beispiel führt explizit eine Subshell aus, die einen Aufruf von auslöst fork()
und somit unendlich rekursiv ist. Das sollte, glaube ich, Ihre erste Frage (zumindest teilweise) beantworten.
In den Befehlen 1 und 2 wird nichts „erstellt“, sodass auch nichts aufgerufen werden kann – diese Befehle werden im Kontext der Sourcing-Shell ausgeführt.
Die Verallgemeinerung ist der Unterschied zwischen dem „Aufrufen“ einer Shell-Routine oder eines Shell-Programms und dem „Sourcing“ einer Shell-Routine oder eines Shell-Programms – wobei letzteres normalerweise nur auf Shell-Befehle/-Skripte anwendbar ist, nicht auf externe Programme. Das „Sourcing“ eines Shell-Skripts erfolgt normalerweise über
. <scriptname>
im Gegensatz zu./<scriptname>
oder/full/path/to/script
– beachten Sie die „Punkt-Leerzeichen“-Sequenz am Anfang der Sourcing-Direktive. Sourcing kann auch mit aufgerufen werdensource <scriptname>
, wobei dersource
Befehl ein interner Shell-Befehl ist.
Antwort3
fork
, vorausgesetzt, alles läuft gut, kehrt zweimal zurück. Eine Rückgabe erfolgt im übergeordneten Prozess (der die ursprüngliche Prozess-ID hat) und die andere im neuen untergeordneten Prozess (eine andere Prozess-ID, die aber ansonsten viel mit dem übergeordneten Prozess gemeinsam hat). An diesem Punkt könnte der untergeordnete Prozess exec(3)
etwas ausführen, was dazu führen würde, dass eine „neue“ Binärdatei in diesen Prozess geladen wird, obwohl der untergeordnete Prozess dies nicht tun muss und anderen Code ausführen könnte, der bereits über den übergeordneten Prozess geladen wurde (z. B. zsh-Funktionen). Daher fork
kann ein Prozess zu einem „völlig neuen“ Prozess führen oder auch nicht, wenn „völlig neu“ so verstanden wird, dass etwas über einen exec(3)
Systemaufruf geladen wurde.
Es ist schwierig, im Voraus zu erraten, welche Befehle einen unendlichen Regress verursachen. Neben dem Fall, bei dem ein Fork einen Fork aufruft (auch als „Forkbombe“ bekannt), gibt es auch einen einfachen Fall, bei dem ein naiver Funktionswrapper um einen Befehl herum verwendet wird.
function ssh() {
ssh -o UseRoaming=no "$@"
}
was stattdessen wahrscheinlich geschrieben werden sollte als
function ssh() {
=ssh -o UseRoaming=no "$@"
}
oder command ssh ...
um unendliche Funktionsaufrufe der ssh
Funktion zu vermeiden, die die ssh
Funktion aufruft, die ... aufruft. Dies hat in keiner Weise zur Folge fork
, da die Funktionsaufrufe intern für den ZSH-Prozess sind, aber munter bis ins Unendliche weiterlaufen, bis dieser einzelne ZSH-Prozess an eine Grenze stößt.
strace
ist wie immer praktisch, um genau zu zeigen, welche Systemaufrufe für einen Befehl erforderlich sind (insbesondere hier fork
und vielleicht für einige exec
Aufrufe); Shells können mit -x
oder ähnlichem debuggt werden, das zeigt, was die Shell intern tut (z. B. Funktionsaufrufe). Weitere Informationen finden Sie in Stevens' "Advanced Programming in the Unix Environment", das einige Kapitel zur Erstellung und Handhabung neuer Prozesse enthält.