PilotOS: Prozesse
PilotOS [1] soll ein einfaches Betriebssystem für den Raspberry Pi werden. Der Sinn hinter dem Betriebssystem ist hauptsächlich der Wissenszuwachs für mich, und die Einblicke in die Entwicklung von Betriebssystem, angefangen von einfachen Funktionalitäten, bis hin zur Architektur von Betriebssystemen. PilotOS zielt dabei in keinster Weise auf Benutzbarkeit, sondern dient einzig und allein den Spielerei-Zwecken.
Gestern habe ich Prozesse hinzugefügt und einen einfachen Scheduler sowie Dispatcher für Prozesse. Ein Dispatcher ist dafür da, Prozesse umzuschalten, d.h. die CPU anzuweisen in einen anderen Prozess zu springen, sowie dessen Kontext wiederherzustellen, nachdem der Kontext des verdrängten Prozess abgespeichert worden ist. Scheduling stellt die Strategie dar, nach welcher der nächste Prozess gewählt wird.
Es gibt sowohl kooperatives als auch preemptives Umschalten. Beim kooperativen Umschalten geben die Prozesse ihre Kontrolle über die CPU freiwillig ab, bspw. durch Rufen einer bestimmten Funktion. Manchmal ist das auch so implementiert, dass jeder Systemruf dazu führt, dass umgeschalten werden kann. Preemptives Umschalten wird durch Timer-Interrupts realisiert. D.h. beim Start des Systems wird der Hardware-Timer auf eine bestimmte Zeit programmiert, nach der er einen Interrupt auslösen soll. Dieser Interrupt wird von der CPU gefangen, und in einer bestimmten Interrupt Service Routine (ISR) verarbeitet. Diese ISR sorgt dann für das Umschalten. Das hat zur Folge, dass jeder Prozess zu jedem Zeitpunkt umgeschalten werden kann (siehe Race Conditions).
In PilotOS ist aktuell ein kooperatives Umschalten implementiert. Genau genommen steht erst einmal nur die Möglichkeit für kooperatives Umschalten. Der Code wird aktuell noch nirgends verwendet, und ist noch ungetestet. Der Scheduler wählt einen Prozess aus, mithilfe einer Vergleichfunktion, die anhand irgendeines beliebigen Merkmals aus zwei Prozessen (bzw. Prozess-Kontroll-Blöcken, PCB) einen zurück liefert. Die eigentliche Scheduling-Routine wendet diese Funktion also auf alle PCBs an. Ein PCB beinhaltet Informationen über die Prozesse, hier sind das Informationen wie ID, Deadline, Execution-Time, Last-Use-Informationen und der StackPointer. Nicht alle Attribute werden aktuell (sinnvoll) eingesetzt.
Zum Abgeben der Kontrolle der CPU gibt es die Funktion yield, die direkt in die Funktion dispatch springt. Die Trennung der beiden Funktionen ist aktuell unnötig, kann aber eventuell später nützlich sein, daher habe ich das erst einmal so implementiert. Der Dispatcher speichert sich das Link-Register (lr) auf dem Stack. Das Link-Register gibt bei ARM die Rücksprungaddresse an, nach einem Link-Branch (Assembler-Befehl bl), der eine Art Funktionsaufruf darstellt. In der aufgerufenen Funktion steht dann das Link-Register auf dem nächsten Befehl, in dem Code vor dem Funktionsruf, d.h. mithilfe von mov pc, lr lässt sich wieder zurückspringen (PC - Program Counter, Instruction Pointer).
Dann werden alle Register (r1,..r12) gespeichert. r13, r14, r15 werden nicht mit abgespeichert, weil diese besondere Funktionen im ARM-Kern haben. r13 ist der Stackpointer - den auf den Stack weg zu speichern wäre relativ sinnlos. r14 ist das Link-Register. Man könnt überlegen, ob es Sinn machen würde, dieses Register zusammen mit den anderen abzuspeichern - dazu müsst ich mir die Semantik von push und pop genauer zu Gemüte führen. Vor allem müsst geklärt werden, inwiefern die Befehle die Register gleichzeitig speichern und laden, oder ob der Befehl vom Assembler in mehrere kleinere Befehle zerteilt wird. Den Programm-Counter (r15) auf dem Stack zu speichern ist komplett sinnlos, weil der aktuell in der dispatch-Funktion steckt. Den abzuspeichern, und wieder zu laden würde verhindern, dass wir diese Funktion jemals wieder verlassen.
Nachdem die Register abgespeichert sind, können sie verwendet werden, ohne riskieren zu müssen, dass Werte des eigentlichen Prozesses verloren gehen. Zunächst müssen wir aber den Stackpointer des verdrängten Prozesses in den PCB schreiben. Nun lade ich die Adressen der Funktionen, die von der schedule-Funktion benutzt werden sollen, um den nächsten Prozess zu finden und rufe die Funktion. Das Ergebnis liegt in r0. Nun muss nur der Stackpointer umgesetzt und die zuvor gespeicherten Register wieder geladen werden. Zum Schluss hole ich dann das Link-Register noch vom Stack, allerdings speichere das Ergebnis im Program-Counter, sodass der nächste ausgeführte Befehl der nächste Befehl des Prozesses ist, der nun ausgeführt werden soll.
[1] https://github.com/naums/pilotOS
Beitragsbild: https://openclipart.org/detail/175363/pilot-penguin
Letzte Bearbeitung: 21.05.2016 12:14