Einführung in Betriebssysteme
Hallo, Habr! Ich möchte euch eine Serie von Übersetzungen einer interessanten Literatur vorstellen — OSTEP. In diesem Material wird die Funktionsweise von unixähnlichen Betriebssystemen recht tiefgehend behandelt, insbesondere die Arbeit mit Prozessen, verschiedenen Planern, Speicher und anderen ähnlichen Komponenten, die ein modernes Betriebssystem ausmachen. Das Original aller Materialien könnt ihr hier ansehen . Bitte beachtet, dass die Übersetzung nicht professionell (recht frei) durchgeführt wurde, aber ich hoffe, dass ich die allgemeine Bedeutung erhalten habe.
Die Laborarbeiten zu diesem Thema findet ihr hier:
Weitere Teile:
Ihr könnt auch mal auf meinen Kanal in =)
Alarm! Es gibt ein Labor zu dieser Vorlesung! Siehe
Prozess-API
Lassen Sie uns ein Beispiel für die Erstellung eines Prozesses im UNIX-System betrachten. Dies erfolgt über zwei Systemaufrufe. fork() und exec().
Aufruf von fork()

Betrachten wir ein Programm, das den fork()-Aufruf ausführt. Das Ergebnis seiner Ausführung wird wie folgt aussehen.

Zunächst betreten wir die Funktion main() und geben eine Zeile auf dem Bildschirm aus. Diese Zeile enthält die Prozess-ID, die im Original als PID oder Prozessbezeichner. Dieser Bezeichner wird in UNIX verwendet, um auf einen Prozess zuzugreifen. Der folgende Befehl wird den Aufruf von fork() ausführen. Dabei wird fast eine identische Kopie des Prozesses erstellt. Aus Sicht des Betriebssystems sieht es so aus, als wären in der Systemumgebung zwei Instanzen desselben Programms aktiv, die beide aus der Funktion fork() zurückkehren. Der neu erzeugte Kindprozess (im Verhältnis zu seinem Elternprozess) wird nicht ab der Funktion main() ausgeführt. Es ist wichtig zu beachten, dass der Kindprozess keine exakte Kopie des Elternprozesses darstellt, insbesondere hat er seinen eigenen Adressraum, eigene Register, einen eigenen Zeiger auf die ausführbaren Anweisungen und ähnliches. Somit wird der Rückgabewert der Funktion fork() unterschiedlich sein. Insbesondere erhält der Elternprozess als Rückgabewert die PID des Kindprozesses, während der Kindprozess einen Wert von 0 erhält. Anhand dieser Rückgabewerte kann man die Prozesse weiter unterscheiden und jeden von ihnen seine spezifische Aufgabe erledigen lassen. Dabei ist die Ausführung dieses Programms nicht strikt definiert. Nach der Trennung in zwei Prozesse beginnt das Betriebssystem, sie zu überwachen und ihre Ausführung zu planen. Bei der Ausführung auf einem Einkernprozessor wird einer der Prozesse die Arbeit fortsetzen, in diesem Fall der Elternprozess, und dann erhält der Kindprozess die Kontrolle. Bei einem erneuten Start kann die Situation anders aussehen.
Der Aufruf wait()

Betrachten wir das folgende Programm. In diesem Programm sorgt der Vorhandensein des Aufrufs wait() dafür, dass der Elternprozess immer auf die Beendigung des Kindprozesses wartet. In diesem Fall erhalten wir eine genau definierte Ausgabe auf dem Bildschirm.

Der Aufruf exec()

Lassen Sie uns den Aufruf exec()betrachten. Dieser Systemaufruf ist nützlich, wenn wir ein ganz anderes Programm starten möchten. Hier werden wir execvp() aufrufen. Um das Programm wc zu starten, das ein Programm zur Wortzählung ist. Was passiert also beim Aufruf von exec()? Dieser Aufruf erhält als Argumente den Namen der ausführbaren Datei und einige Parameter. Daraufhin wird der Code und die statischen Daten aus dieser ausführbaren Datei geladen und das eigene Codesegment überschrieben. Andere Speicherbereiche wie der Stack und der Heap werden neu initialisiert. Anschließend führt das Betriebssystem das Programm aus und übergibt ihm eine Reihe von Argumenten. Somit haben wir keinen neuen Prozess erstellt, sondern einfach das aktuelle laufende Programm in ein anderes laufendes Programm umgewandelt. Nach dem Aufruf von exec() hat der Kindprozess den Eindruck, als wäre das ursprüngliche Programm überhaupt nicht gestartet worden.
Diese Komplexität beim Start ist für die Unix-Shell absolut normal und ermöglicht es dieser Shell, Code nach dem Aufruf auszuführen fork(), aber vor dem Aufruf exec(). Ein Beispiel für solchen Code könnte die Anpassung der Umgebung der Shell an die Bedürfnisse des zu startenden Programms sein, bevor es direkt gestartet wird.
Shell — nur ein benutzerdefiniertes Programm. Es zeigt Ihnen die Eingabeaufforderung an und wartet darauf, dass Sie etwas eingeben. In den meisten Fällen, wenn Sie den Namen eines Programms eingeben, findet die Shell dessen Speicherort, ruft die Methode fork() auf und verwendet dann einen der exec()-Typen, um einen neuen Prozess zu erstellen und wartet auf dessen Ausführung mit einem wait()-Aufruf. Wenn der Kindprozess abgeschlossen ist, gibt die Shell den wait()-Aufruf zurück und zeigt erneut die Eingabeaufforderung an, um auf den nächsten Befehl zu warten.
Die Trennung von fork() & exec() ermöglicht es der Shell, folgende Dinge zu tun, zum Beispiel:
wc file > new_file.
In diesem Beispiel wird die Ausgabe des Programms wc in eine Datei umgeleitet. Der Weg, wie die Shell dies erreicht, ist ziemlich einfach – beim Erstellen des Kindprozesses vor dem Aufruf von exec(), schließt die Shell den Standardausgabestrom und öffnet die Datei new_file, sodass die gesamte Ausgabe des danach gestarteten Programms wc in die Datei umgeleitet wird, anstatt auf den Bildschirm zu erscheinen.
Unix-Pipe Sie sind auf ähnliche Weise implementiert, wobei der Unterschied darin besteht, dass sie den Aufruf pipe() verwenden. In diesem Fall wird der Ausgabestrom des Prozesses an eine im Kernel befindliche Pipe-Warteschlange angeschlossen, die dann mit dem Eingabestrom eines anderen Prozesses verbunden wird.
Quelle: habr.com
