Prozesse

Es gibt viele Definitionen für den Begriff Prozess. Grundlegend kann ein Prozess als Code in Ausführung beschrieben werden, bzw. als Ausführungsstrang. Ein Prozess wird im Betriebssystem durch den so genannten Process-Control-Block (Prozesssteuerblock) identifiziert. Dieser enthält einige Informationen über den Prozess, wie bspw. Identifikationsnummer, Program-Counter, Stackpointer oder Prozesszustand.

Mögliche Prozesszustände könnten sein:

  • befindet sich aktuell in Ausführung
  • bereit
  • wartend / blockiert

Der Speicherbereich eines Prozesses besteht aus Code-Bereich, Daten-Bereich, Heap und Stack. In den Heap werden dynamisch alloziierte Datenelemente gelegt, auf den Stack werden Variablen gespeichert. Außerdem wird dort i.d.R. die Rücksprungadresse abgelegt, wenn in eine Funktion gesprungen wird.

Prozesse erzeugen

Einen Prozess zu erzeugen ist nicht trivial. Wir können nicht einfach den Code laden und an die erste Stelle springen, das wäre schlechter Stil, weil jeder Eintritt in einen Prozess nur aus dem Umschalten heraus erfolgen soll. Außerdem würde so der Kontext des Prozesses verloren gehen, aus dem wir herausspringen. Dh. wir laden den Code, legen Heap und Stack so an, als würde sich der Prozess gerade in einem Zustand befinden, in dem er umgeschalten wurde (dh. der Stack muss entsprechend mit Rücksprungadressen und Registerwerten belegt werden). Beim Rücksprung landen wir dann an der ersten Stelle im Code.

Es ist sinnvoll beim Betriebssystementwurf Code nur einmal zu schreiben, und damit einen Single Point of Truth zu haben. Dh. wenn im Code ein Bug ist, ist er genau einmal vorhanden. Ist aber bspw. Adressraumwechsel mehrfach implementiert, könnten sich die verschiedenen Implementationen unterschiedlich verhalten, was zu chaotischem Verhalten des Betriebssystems führen würde.

CPU-Zeit

Wir gehen zunächst von einem Einkern-Prozessor aus. Ein Prozessorkern kann nur einen Befehl gleichzeitig ausführen (ungeachtet Pipelining) und arbeitet ein Programm von oben nach unten ab (ungeachtet Sprungbefehlen). Wie also können wir mehrere Prozesse auf einem Computer laufen lassen?

Der Trick besteht darin, in (kurzen) Abständen die Prozesse zu wechseln, die aktuell von der CPU bearbeitet werden dürfen, sodass für den Nutzer der Eindruck entsteht, die CPU würde nebenläufig Prozesse verarbeiten.

Nebenläufigkeit bezeichnet, dass zwei Befehle unabhängig voneinander gleichzeitig ausgeführt werden können. Im Gegensatz dazu steht Parallelität, was darstellt, dass die gleiche Aktion auf beiden Recheneinheiten ausgeführt wird (siehe SIMD).

Aber wie können die Prozesse umgeschalten werden, wenn zu einem Zeitpunkt nur ein Prozess ausgeführt werden kann (und damit auch nur der Prozess, und nicht gleichzeitig das Betriebssystem!)? Dafür gibt es zwei Möglichkeiten:

  • kooperatives Umschalten
  • konkurrierendes Umschalten (?)

Kooperatives Umschalten

Beim Kooperativen Umschalten geben die Prozesse freiwillig den Prozessor ab, sei es bewusst oder unterbewusst. Dh. im Betriebssystem ist ein Systemruf yield vorgegeben, der die Kontrolle über den Prozessor ans Betriebssystem zurück gibt. Auch möglich ist, in jedem Systemruf ein solches Abgeben zu integrieren.

In älteren Windows-Versionen konnte ein Prozess, der lange Zeit nicht in die Windows-Funktion eingetreten ist, bzw. lange Zeit für eine Berechnung gebraucht hatte, das System lahmlegen, weil er den Prozessor nicht freigegeben hat. Weil diese BS-Version auf kooperativen Umschalten beruht hatte, gab es keine Möglichkeit mehr die Kontrolle über den Prozessor zurück zu erlangen. Diese Methode basiert sozusagen auf dem Willen zur Zusammenarbeit der einzelnen Prozesse.

Konkurrierendes Umschalten

Beim konkurrierenden Umschalten ist es möglich einen Prozess zu verdrängen, und an irgendeiner Stelle in der Prozessausführung zu unterbrechen, ohne dass dieser Prozess das explizit mitteilt. Der einzige Weg einen Prozess zu unterbrechen ist allerdings ein Interrupt, der die Befehlsabarbeitung der CPU unterbricht und in die Interrupt Service Routine springt.

Dazu existiert eine Art Uhr, die in einem bestimmten Abstand einen Interrupt am Prozessor auslöst und den Prozessor damit zwingt in die ISR zu springen. Dort kann dann in den nächsten Prozess gesprungen werden.

Das Umschalten

In unserer bisherigen Betrachtung sind wir einfach in den Code des anderen Prozesses gesprungen. Aber das ist eigentlich nicht ohne weiteres möglich. Zunächst müssen wir den Zustand des aktuellen Prozesses speichern, dh. alle Registerwerte, weil diese für weitere Berechnungen im Prozess benötigt werden könnten, sowie der aktuelle Program-Counter. Diese Werte werden auf den Stack des Prozesses gelegt. Zum Schluss werden Programm-Counter und Stackpointer in der PCB gespeichert, damit die gesicherten Daten vom Stack wieder geholt werden können und die Programmabarbeitung bei der Fortsetzung des Prozesses wieder weitergehen kann.

Wollen wir in einen Prozess springen, müssen wir dessen Werte von seinem Stack holen und in die Register legen. Dann springen wir an die Stelle im Code, an der wir ihn unterbrochen haben.

In späteren Kapiteln wird der virtuelle Speicher diskutiert, hier sei nur angemerkt, dass auch der virtuelle Speicherbereich entsprechend umgeschalten werden muss.

Threads

Threads werden auch als sog. Leichtgewichtsprozesse bezeichnet. Ein Thread hat immer einen Eltern-Prozess, dem er zugeordnet ist, und in dessen virtuellem Speicher er läuft. Dh. beim Umschalten vom Prozess in einen seiner Threads, müssen lediglich die Register gesichert werden, der virtuelle Adressraum bleibt allerdings der gleiche.

Zu Beachten ist, dass Threads zwar einen eigenen Stack haben, nicht aber einen eigenen Heap, sondern der Heap wird sich mit dem Eltern-Prozess gespeichert. Dh. der Thread kann auf Variablen seines Elternprozesses zugreifen und sogar Werte verändern. Im Gegensatz dazu steht ein anderer Prozess, der genau das nicht kann.