Nachdem in Junior Computer ][ angefangen wurde über Mulitasking-OS und GeckOS zu reden, mache ich das mal hier auf.
Passt.
Wahrscheinlich macht GeckOS aus euer beider Sicht Mist...
Ned wirklich, weil es greift Regel#1: Was läuft hat recht.
Ansonsten muss ich eh vorausschicken, dass meine Anmerkungen auf einem halb fertigen Kernel, bzw. dessen Planung, für den Apfel II+ (mit 65C02) beruhen. Die Struktur beruht dabei in weiten Teilen auf meinen /370 Erfahrungen (was mangels Stack eh das beste Beispiel ist) angepasst an die Probleme der 6502 mit komplexen dynamischen Strukturen. Entsprechend sind vielleicht einige Namen ungewöhnlich. Das ganze ist ein On/Off-Projekt seit 20+ Jahren :))
In GeckOS wird der "Scheduler" hauptsächlich im Timer-Interrupt ausgeführt, kann aber auch durch freiwillige Aufgabe eines Tasks mit YIELD aufgerufen werden. Beim Aufruf wird einfach der Kontext (also ob Aufruf z.B. durch YIELD, Interrupt oder system call wie blocking RECEIVE) gespeichert und in der (statischen) Liste nach dem nächsten lauffähigen Task gesucht. Prio heisst einfach, ein Task bekommt mehrere Timeslices. Insofern ist die "Scheduler" "Logik" minimal bis nicht vorhanden. Wenn man um Tasks zu schedulen in einen separaten Task springen müsste, wäre es ein starker extra Aufwand.
Einen Idle Task gibt es nicht, wieso auch? Solange ein lauffähiger Task da ist kann er voll durchrennen, nur unterbrochen davon im Timer-Interrupt zu schauen, ob andere Tasks jetzt lauffähig sind.
Ist bei mir recht verwandt, außer dass der Timer den Scheduler nur vorbereitet. Der Scheduler arbeitet nur wenn entweder der Task Zeit abgibt (yield) er eine synchrone Aktion veranlasst (was intern ähnlich yield ist), eine Nachricht absetzt oder ein vorhergehender Interrupt meint dass der Scheduler auch mal wieder was tun sollte.
Yield ist dabei ein VPASS 0, ein Warten mit Zeit Null - weil jedes Warten eine implizite Abgabe von Rechenzeit ist, auch bei 0.
(Ach ja, OS aufrufe sind als SVC gemacht - unter Verwendung von BRK :))
(Zeitscheibe/Timer) Das mit den Zeitscheiben hat bei mir Ähnlichkeit, nur wird die im Timer in einer verkürzten Weise behandelt. Jeder Task hat einen Zeitscheibenbeiwert der normalerweise (also derzeit immer) einer Standardscheibe entspricht, gemessen in Ticks (*1). Wenn der Task aktiviert wird, dann wird die Zeitscheibenlänge-1 in ein ZP Byte des Timers gespeichert. Jeder Tick verringert das um 1. Ist der Wert >= 0 kehrt der Timer einfach zurück. Ist der neue Wert kleiner 0 (*2), dann wird der Scheduler aufgerufen.
*1 - Ein Timer Interrupt kann die Uhr um mehrere Ticks weiterstellen kann, aber das ist ein anderes Thema
*2 - Und ja, das bedeutet dass die maximale Anzahl Ticks einer Zeitscheibe 128 ist, nicht 255. Grund ist dass so ein einzelner (warum auch immer) verhunzter Test auf 0 nicht zu einer 254 Tick langen Zeitscheibe führt. Ist ein dicker, fetter Wachposten :))
(Übergang zum Scheduler) An der Stelle kann man jetzt drüber philosophieren ob mein Scheduler jetzt im Interrupt läuft oder im normalen Kernel-Zustand. Ich seh es logisch als Letzteres, wie bei (einigen) Mainframe-OS wo der Interrupt (P3) nicht in den unterbrochenen Task (P1) sondern im OS-Zustand (P2) weitermacht.
Der Trick ist dass der Interrupt zu einem OS-Funktionsaufruf wird. Hat der Timer erkannt, dass der Scheduler was tun sollte, macht er keinen RTI, sondern springt in einen privaten Eingang im OS-Funktions-Handler. Ab da ist es kein Timer-Interrupt mehr sondern ein "eingefügter" OS-Aufruf durch den Task. Ein Aufruf wie ein Yield, oder jeder andere OS-Aufruf der zur Deaktivierung führen kann.
Der extra Eingang im Funktionshandler ist halt weil dieser Aufruf keine normalen Parameter hat und auch die Rückkehradresse daher nicht anpassen muss. Technisch ist der private Eingang ein kleines Stück Code das eine Funktionsnummer und Parameter hinterlegt und dann hinter der Parameteranalyse weitermacht, wenig mehr als LDA/STA/STZ/BNE. Was folgt ist der Funktionsverteiler.
(Funktion) Die Funktion die aufgerufen wird kann man eigentlich als NOP sehen. Am ihrem Ende verzweigt sie in den Scheduler, genauso wie verschiedene andere Funktionen (z.B. alles blockierende), die nicht (oder nicht immer) direkt zurückkehren.
(Scheduler Schritt A) Wenn der Scheduler startet, egal was vorher war, dann schaut er zuerst in die Aktivierungsliste. Das ist eine Liste mit Ereignissen (Nachrichten an Tasks) diese werden zuerst zugestellt - sprich der Task (*3) aktiviert (siehe unten). Man kann es sich auch als 'synchrone Interrupts' oder 'Superpriorität' vorstellen. Diese Aktivierungen erfolgen mit einer minimalen Zeitscheibe, abgeleitet aus Priorität und Nachricht (derzeit einfach die Standardscheibe). Weiter bei (Aktivierung)
*3 - Das kann durchaus erstmal ein Task eines Systemdiensts sein, z.B. das File-System, welcher noch irgendwas macht und erst das Ergebnis dann an den eigentlichen Empfänger schickt (Derzeit nicht, FS code läuft noch im Usertask).
(Scheduler Schritt B) Wenn kein Eintrag in der Aktivierungsliste ist, dann wird in die Liste der ablaufbereiten Tasks geschaut (*4). Existieren dort Tasks, so wird der mit der höchsten Priorität ausgewählt. War der Aufruf ein Yield und der Top-Eintrag ist der des immer noch aktuellen Tasks, so wird der nächste genommen.
*4 - Die Liste ist eigentlich einfach, komplizierter ist wie ein Task da ein oder ausgetragen wird, bzw, von dort zu anderen wechselt oder zurück. Also eigentlich nicht kompliziert, nur was mit vielen verschiedenen Bedingungen - welche aber nicht der Scheduler machen mus, die laufen Andernorts ab, der Scheduler muss wirklich nur da durchrutschen. Durchlaufen deswegen weil ich mir das Umsortieren sparen will. Hier stecken dann auch die ganzen Schrauben an denen man zum Thema performance drehen kann. Welche Tasktypen will man bevorzugen, Batch, Dialog oder Services? Etc. PP. Aber wie gesagt, anderes Thema. Ach ja, es ist keine linked list, sondern ein einfacher Puffer mit 2 Werten je Task: TSN und Prio.
(Scheduler Schritt C)
Wurde ein ablaufbereiter Task gefunden?
Ja -> Der Neue wird aktiviert. Siehe (Aktivierung)
Nein ->Die CPU läuft auf einen WAI. Fällt es vom WAI runter, dann geht es weiter bei (Scheduler Schritt A)
(Aktivierung) Ist der Neue NICHT der Gleiche wie der (noch) Aktive, so werden
- temporär gespeicherte Werte (Register etc.) in den aktiven TCB übertragen,
- Accounting gemacht
- eine Prüfsumme (EOR) gebildet und gespeichert
- die Prüfsumme des neuen TCB überprüft (Wenn Falsch -> System-Halt wegen Wildläufer)
- Alle nötigen Werte aus dem TCB kopiert
Und zum Schluss der Task entsprechend der Art (*5) gestartet.
*5 - Hier wird unterschieden ob ein Task nach einer Unterbrechung/Deaktivierung wieder angestartet wird, oder ob der Task selber in einer Unterbrechungsebene läuft. Der Einfachheit halber kann man sich so eine Unterbrechungsebene wie einen temporären Thread vorstellen der innerhalb des Tasks eingerichtet und gestartet wird anstelle der Hauptebene. Unterbrechung deswegen weil die Hauptebene wie bei einem Interrupt unterbrochen wird. Die Nützlichkeit sollte auf der Hand liegen - z.B. bei asynchroner Kommunikation oder auch der Fehlerbehandlung. Das OS kann z.B. Fehlermeldungen zu Funktionen als Unterbrechung schicken, und so weiter und so fort. Zugegeben, nix was man für Hello-World braucht :)))
(I/O) Was zum Gesamtbild jetzt noch fehlt ist die I/O (inkl. Timer). Alle I/O (*6), läuft asynchron. Entweder vom Userprogramm aus, oder weil es das OS unter der Haube so gestaltet. Z.B. beim Schreiben auf eine Datei bedeutet dass, dass der Schreibaufruf die Daten an einen ACB kettet, den in die Aktivierungsliste hängt, und dann bei (Scheduler Schritt A) weiter macht. Egal ob das vom Userprogram her ein synchrones oder asynchrones Schreiben war. Der Unterschied ist dann nur ob der Usertask selbst anschließend Ablaufbereit ist oder nicht (blockiert).
*6 - Ausgaben auf die Systemkonsole (Debug, Logging) ist Ausnahme. Die geht zwar eigentlich durch die gleichen Wege, erfolgt aber auf dem kürzest möglichen Weg und (soweit geht) ohne Unterbrechung. Das mag das System langsam machen, aber Debugging sollte schon sein.
Die Struktur erlaubt dann auch Treiber zu schreiben die ähnlich wie in manchen Systemen mit Strategie und Ausführung arbeiten und dabei voll dem Tasking unterliegen.
Ob die I/O selbst dann in Interrupts läuft, oder nicht ist ein anderes Thema, ich geh aber davon aus dass in den meisten Fällen ja. Am Ende wenn die Daten geschrieben wurden hängt der Treiber halt wieder einen ACB ein der dann den Usertask weiterlaufen lässt. Da liegt wiederum ein Stückchen Schwupzidität: Jedwede I/O kommt so schnell wie möglich nach ihrem Ende beim passenden Usertask an, der dann auch reagieren kann. Egal ob auf Mausschupsen oder ein Bild von Platte lesen.
... zu viele Zeichen ... Teil 2 folgt ...