GeckOS Multitasking Betriebssystem for 6502

  • Nachdem in Junior Computer ][ angefangen wurde über Mulitasking-OS und GeckOS zu reden, mache ich das mal hier auf.


    Zitat von 2ee

    Edit: Nochmal zum WAI.


    Wenn du ein Multitasking OS hast, wird der Scheduler normalerwiese als Interrupt-Routine - angestoßen durch einen Timer IRQ - implementiert. Zum Auswählen des nächsten Tasks wird also der Scheduler geweckt, der nächste Task gewählt der rechenbereit ist und die CPU an diesen Task durch setzen der Rücksprungadresse aus der IRQ Routine durch ein RTI ( ReTurn from Interrupt) übergeben. Falls es aber keinen rechenbereiten Task gibt, wird und muss der Idle-Task ausgewählt werden.

    Zitat von Raffzahn

    Erzähl dass mal besser nicht den Mainframe-Leuten. Scheduler im Interrupt ist eine potentielle potentielle Fehlerquelle und eine mögliche Quelle richtig ekeliger Performanceprobleme. Interrupts sollen so kurz wie möglich sein und nur die Arbeit machen die direkt für die Hardwarebedienung nötig sind. Alles andere, ist außerhalb zu erledigen. Und das ist was der Code nach dem WAI macht. Einen Idle Task basteln ist zwar recht hübsch aus theoretischer Sicht und um gut um Studenten im ersten Semester nicht zu verwirren, in der Praxis isses aber weniger gut.


    Wahrscheinlich macht GeckOS aus euer beider Sicht Mist... ;)


    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.


    Wenn der Scheduler ausgehend vom aktuellen Task keinen lauffähigen Task mehr findet (also auch nicht den originalen) wird ein Dead system erkannt und neu gebootet (könnte man ggf. auch mal einen "bluescreen" einführen ;)


    Ich finde immer noch dass das für einen 6502 eine ziemlich effiziente Lösung ist.


    Als grobe Ideen sind allerdings in der Tat noch Verbesserungen geplant, wie eine Linked List der lauffähigen Tasks. Muss ich aber erst noch durchdenken ob das so viel bringt. Auch eine Monitoring-Lösung die Statistiken macht, wie viele Tasks im Schnitt lauffähig sind etc. gehört dazu.


    Ich freue mich auf die Diskussion :)

  • 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 ...

  • Teil 2 - deppertes system:

    (Interrupts und Zeitscheiben) Damit das dann klappt haben Interrupts auch noch die Möglichkeit den gerade laufenden (User) Task beschleunigt zu beenden. Dazu gibt es eine Funktion die in das Byte der aktuellen Zeitscheibe den Wert 1 schreibt - dadurch wird dieser nach mindestens einem, spätestens zwei Ticks an den Scheduler gegeben (*7). Die Idee dahinter ist, dass die meisten Tasks eh nicht im Dauerbetrieb laufen und damit die Chance bekommen das fertigzustellen was sie gerade machen und damit keine Folgeaktivierungen nötig werden (jemand fertig machen lassen ist immer besser als dauernd stören) während Langläufer ausgebremst werden zwecks besserer Reaktion. Das ist insbesondere Hilfreich wenn das System mit Swapping arbeiten sollte und Deaktivierung eines Task auch Auslagerung bedeuten könnte. 6502 ist halt kein Speicherriese :))


    *7 - Ob 1 oder einer höherer Wert richtig ist kommt auf CPU und Ticklänge an. Eine der vielen Schrauben für Performance.



    Das wär das Grundkonzept. Da drüber sind dann schon die Systemfunktionen die dem Anwender zugängig sind. Das normale Zeug, aber auch die Kommunkationssachen und die Steuerung für die Unterbrechungen (Threads), was eigentlich dass gleiche ist.


    Wenn der Scheduler ausgehend vom aktuellen Task keinen lauffähigen Task mehr findet (also auch nicht den originalen) wird ein Dead system erkannt und neu gebootet.


    Das gibts bei meinem System nicht, das OS läuft immer, auch wenn aktuell kein Task da ist.


    könnte man ggf. auch mal einen "bluescreen" einführen ;)


    LOL. Klar, Bluescreen muss sein :)


    Ich hab an der Stelle den System-Halt vorgesehen. Der springt in eine Art Minimal-Monitor mit dem man im Speicher rumstochern kann.


    Ich finde immer noch dass das für einen 6502 eine ziemlich effiziente Lösung ist.


    Wie gesagt, wenn läuft, und das tuts, dann ist es eine gute Lösung.


    Als grobe Ideen sind allerdings in der Tat noch Verbesserungen geplant, wie eine Linked List der lauffähigen Tasks. Muss ich aber erst noch durchdenken ob das so viel bringt. Auch eine Monitoring-Lösung die Statistiken macht, wie viele Tasks im Schnitt lauffähig sind etc. gehört dazu.


    Da drinnen steckt das ganze Potential zur Optimierung. Ich hab das in meinem Konzept erstmal nach hinten geschoben, bzw. nur minimal angedacht, weiß aber wie wichtig das ist. Letztendlich gehts da um einen Satz and Statemachines die das kodifizieren wie auch Last die weit über das rausgeht an das wir so denken verwaltet werden kann.


    Ich freue mich auf die Diskussion :)


    DHDEDWE :))


    (Ach ja, obiges hat sicher ne halbe million Typos und Dreher, dürfen alle frei verteilt werden :))

  • Wahrscheinlich macht GeckOS aus euer beider Sicht Mist...

    Ne, ne, GeckOS ist super :):thumbup: .Ich finde es wirklich erstaunlich, was du da auf einer so beschränkten CPU (bzgl. vor allem natürlich Stack) fertiggebracht hast. Mein erstes Multitask System lief auf einem 286er und hatte auch beim Speicher natürlich viel mehr Resourcen. Deshalb kann ich da nur den Hut ziehen.

    Ausserdem sind Hobby System nie Mist. Die sollen schließlich Spass machen und den Horizont des Erbauers erweitern. Und ich denke, du bist mit GeckOS da an Machbarkeitsgrenzen gestoßen, die sich Chuck Peddle und Bill Mensch bei der 6502 vermutlich garnicht vorstellen konnten.

  • 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.

    Nachtrag: Das da erinnert mich extrem an das Multitasking-Etwas (nee, OS will ichs nicht nennen) das ich ca 1982 auf dem Apfel gebastelt habe.


    Ich hatte damals frisch eine Karte mit einem 6522 und das musste ausprobiert werden und Taskswitching war die erste Idee.


    • Fest n Tasks
    • 3 Listen mit je einem Wert pro Task in der ZP
      • TCB_ZS mit den Zeitscheiben
      • TCB_AS mit der aktuellen Scheibe
      • TCB_SP mit dem Stackpointer des Tasks
    • 1 Byte ZP Taskpointer
    • Feste Zuordnung von Stackbereichen

    Der Code ging ungefähr so

    Fetisch.


    Um einen Task zu erzeugen musste man

    • ein Stück Stack suchen,
    • dort
      • Flags,
      • Startadresse,
      • X und
      • A ablegen,
    • den Stackpointer in TCB_SP,* ablegen.

    Sowie man jetzt in TCB_ZS,* einen wert Ungleich 0 schrieb wurde der Task ausgeführt.


    :))

  • Ohne Raffzahn jetzt im einzelnen zu zitieren - am Ende sind wir denke ich gar nicht so weit auseinander.


    Auch bei mir ist der IRQ ein "Sprung in den kernel" eben mit einer eigenen Routine. Das wird im Taskstatus vermerkt ob eben aus dem Interrupt oder aus einem Blocking call oder YIELD. Und wenn der Task wieder dran ist wird entsprechend zurückgesprungen.

    Der "Scheduler" ist fast ein eigener Task, nur läuft er eben im kernel space - aus Effizienzgründen. Der Scheduler ist sogar interruptbar. Wenn ein Interrupt passiert, werden zuerst die Interrupt-Routinen aufgerufen und damit das Interrupt flag gelöscht. Damit kann der kernel ein CLI machen, und in den Scheduler springen. (Ich bin mir allerdings nicht mehr ganz sicher ob in jedem Port, weil ich mich erinnere dass ich im Original-Port einen I/O hatte in dem ich die IRQ Leitung separat lesen konnte und ich nicht mehr weiss ob das genutzt wird)


    Bezgl. Lauffähiger Tasks - da Interrupts nur bedingt in den Scheduler eingreifen können (aktuell) können sie dafür Signale absetzen. Da wird dann der Kontext und Stack des wartenden/interrupteten Tasks entsprechend modifiziert, so dass beim Wiederanlauf erst die Signal-Routine aufgerufen wird, und die dann mit IIRC RTI in den ursprünglichen Code zurückspringt. D.h. ein "waiting for signal" task gilt auch als lauffähig. "waiting for receive" (wie z.B. die Dateisystem-Tasks wenn sie nichts zu tun haben) sind nicht lauffähig, denn sie warten auf einen anderen Task.


    Für IO gibt es zwei Möglichkeiten: mit char-devices via Streams zu kommunizieren, wie z.B. Konsole oder Serial. Alles andere ist asynchron. Typischerweise gibt es einen eigenen Task, der mit RECEIVE auf Aufträge wartet etwas zu tun. Wobei die eigentliche Datenkommunikation auch über Streams abläuft. Der "FSDEV" task übernimmt dabei die Aufgabe, die Devices auf ein Filesystem abzubilden, so dass man Devices wie Dateien ansprechen kann und nicht nur über den DEVCMD kernel call.


    Da der SEND/RECEIVE buffer als eines der letzten Überbleibsel noch an einer fixen Adresse steht ($0200) und die character-weise Übertragung von Dateien via Streams laaaanngggsssaaaammm ist, will ich beides über sog. Buffer und Channel neu umsetzen: https://github.com/fachat/Geck…er/GeckOS-NG-Buffers.adoc

  • Und da wir gerade von interruptbaren Schedulern reden....


    System: meine Port für die Original-Maschine CS/A, i.e. PET-Clone mit MMU at $effx, die die 16x4k CPU address pages einzeln aus einem 1M bus address space mappen kann. Also $eff3 mapped dann den Speicher $3xxx der CPU eben in den physikalischen Adressraum.


    Symptom: häufig aber nicht immer hängt sich die Maschine beim Booten auf, und zwar wenn Programme von der Disk geladen werden. In diesem Fall ein Monitor auf der zweiten Konsole und eine Shell auf der ersten Konsole. Heisenbug, ziemlich nervig.


    Den Fehler habe ich genau hier im interrupteten Scheduler gefunden....:



    Hinweis: Jeder Thread hat zwei Bytes in der Zeropage (zth, 6/7), die für ihn reserviert sind, jeder Task ebenso (zta, 4/5), und jedes "environment" (zen, 2/3). Diese müssen natürlich bei jedem Context-Switch umgeschrieben werden. In Systemen wie dem C64 oder PET, die nur ein "Environment" (also im Grunde nur eine zeropage/stack) kann man die dann einfach in die gespeicherten Task-/Thread-Tabellen umkopieren.


    In einem system wie dem CS/A hier mit MMU hat aber jeder Task sein eigenes Environment mit eigenem Stack und zeropage. Da muss das umkopieren darüber erfolgen, dass die Task-spezifische Page woanders eingeblendet wird und die Daten von/nach dort kopiert werden. Hier wird Page 3 ($3xxx, bzw. MMU entry an $eff3) verwendet.


    In obigem Code switcht "settask" auf einen neuen Thread/Task um. Mittendrin kommt ein Interrupt, der mit "memsys" versucht "nochmal" in den kernel zu kommen. Das merkt er auch schnell und macht via devr einen short path.... Allerdings muss er, um das zu merken, den Entry-Counter des Kernel lesen - dazu mappt die Routine den kernel zero-block ($0xxx) auf die CPU page 3 - genau dahin, wo der Scheduler in setthread den zero-block eines Tasks gemappt hat, um dessen zta/zth values umzuschreiben...


    Warum fällt das so spät auf - weil erst die lib6502-basierten Programme diese zta und zth Speicher via die lib6502 implementierung nutzen ....


    Edit: das log-file enthält im Prinzip den kompletten Boot-Prozess, ist ca. 3.6GB groß, und es hat > 2 Tage gebraucht das zu flöhen....!

  • Ohne Raffzahn jetzt im einzelnen zu zitieren - am Ende sind wir denke ich gar nicht so weit auseinander.

    Äh jain. Ich muss an der Stelle vielleicht etwas Historie einfügen. Der Kernel basiert auf einem kooperativen multitasking/threading System das ich 1990 auf 286ern implementiert habe. Dabei gab es (fast) keine normalen user Tasks, sondern alles bestand aus asynchronen Aktivierungen die wiederum über Aktivierungen miteinander redeten. Das ganze war ein Kommunikationssystem das eine oder mehrere Hostverbindungen (zu einer /370) bediente und bis zu 32 Clientverbindungen (Modem bis 14400). Über die Modems waren mobile Systeme angebunden welche über den Knoten mit verschiedenen Services auf dem Host redeten. So klassisch client-server wie es nur geht.


    Da (nahezu) alles in dem system aus einer Vielzahl von asynchronen Aufrufen/Nachrichten zwischen den Komponenten und zum Host bzw. Client bestand gab es keinen Bedarf auf preemptive Verwaltung. Die Hauptaufgabe des Timertasks war die Verwaltung von Timerlisten. Zusätzlich wurden (im Interrupt) die Aufgaben auf Langläufer, also solche die länger brauchten als erwartet, überwacht, was auf der Konsole gemeldet wurde und bei noch mehr Überschreitung zu Systemhalt führte. Der Vergleichswert wurde dabei über eine Statistikfunktion im laufenden Betrieb ermittelt.


    Sie 6502 Version ist nun das Ganze etwas runterskaliert (nur 8 bit Handles für Kontrollblöcke, Speicherbereiche, Tasks, und so weiter) und mit einem Layer versehen der es erlaubt Prozesse zu unterbrechen - das ist der oben geschilderte neue Timer - der im Originalsystem lief nicht in den Scheduler. Der Scheduler kümmerte sich daher nur um die Aktivierungsliste und hatte mit Prioritäten nix am Hut (ok, die Hostschlange wurde immer zuerst behandelt schliesslich ging es darum den Mainframe am arbeiten zu halten, damit die Clients schnell ihre Antworten bekamen :))


    Das es den Layer gibt ist eigentlich nur dafür da dass auch Idiotenprogramme laufen können, da meine eigenen Sachen eh asynchron aufgezogen sind. Ist einfach einfacher.

    Bezgl. Lauffähiger Tasks - da Interrupts nur bedingt in den Scheduler eingreifen können (aktuell) können sie dafür Signale absetzen. Da wird dann der Kontext und Stack des wartenden/interrupteten Tasks entsprechend modifiziert, so dass beim Wiederanlauf erst die Signal-Routine aufgerufen wird, und die dann mit IIRC RTI in den ursprünglichen Code zurückspringt.


    Hihi. Meine ursprüngliche Idee für das neue System sah ganz ähnlich aus. Da waren Aktivierungen nicht einfach eine globale Lise, sondern die Blöcke waren eine verkettete Liste je Task. Im Prinzip hatte jeder Task einen TCB der Seine Daten enthielt und einen Zeiger auf einen Aktivierungsblock der im Prinzip den Programmzustand der letzten Zeitscheibe enthielt. Wenn jetzt ein asynchrones Ereignis kommt dann wird dafür ein neuer ACB angelegt und entsprechend der Unterbrechungspriorität eingehängt (der standard-ACB hat niedrigste mögliche Priorität). In der Praxis bedeutet es er wird am Anfang der Kette zwischengeschoben. Das führt dann dazu, dass bei der nächsten Zeitscheibe diese Ebene zuerst ausgeführt wird.


    Wenn die Arbeit getan ist, dann beendet sich die Routine mit TERM, was dazu führt dass der ACB ausgefügt wird und für den Rest der Zeitscheibe (oder der nächsten) die Ebene drannkommt die 'Unterbrochen' wurde. Wird der letzte ACB ausgefügt dann wird der Task beendet (*1)- das ist der Fall wenn das Hauptprogramm TERM aufruft :))


    Ich hab das ganze nur verworfen weil es einfach zu viel Verwaltungsarbeit für die keine 6502 ist - zumindest für 1 MHz. Wobei die Idee noch nicht tot ist. Dann sähe der Scheduler etwas anders aus.


    *1 - eigentlich doch nicht, weil interaktive Tasks - die die ein Terminal haben, dann in die Kommandozeile zurückfallen - aber das ist ne andere Geschichte.


    D.h. ein "waiting for signal" task gilt auch als lauffähig. "waiting for receive" (wie z.B. die Dateisystem-Tasks wenn sie nichts zu tun haben) sind nicht lauffähig, denn sie warten auf einen anderen Task.


    Jo, wobei bei mir die Tasks da keine verschiedenen Zustände haben. Sie sind einfach nicht Ablauffähig. das macht den Scheduler einfach.


    Am einfachsten ist das wohl erklärt wie ein blockierender (synchroner) Schreib oder Leseaufruf funktioniert:


    • Der Befehl wird in ein Aktivierungspacket für den zuständigen Dienst verpackt. Das enthält z.B. FCB und Pufferadresse des Datenblocks zum lesen
    • Das Paket wird in die Aktivierungsliste übernommen
    • Der Task erklärt sich für nicht ablauffähig (wie ein Yield der aber nicht zurückkommt wenn die nächste Zeitscheibe verfügbar ist)
    • Mit dem Paket wird (hoffentlich bald) das DMS (Datei-I/O) aktiviert
    • Das DMS macht was immer gefordert ist
    • Wenn DMS fertig, wird ein Paket mit der Quittung fürs Schreiben oder Lesen (letztere hat den Puffer schon gefüllt) erzeugt
    • Paket kommt in die Aktivierungsliste
    • DMS kehrt in den Scheduler zurück
    • Der Scheduler sieht das neue Paket in der Liste (hoffentlich oben an)
    • Scheduler aktiviert den Empfänger im Usertask
    • Die Aktivierung landet (bei so einem blockierenden Aufruf) in einem Default-Handler für 'DMS Fertig'
    • (Wars ein asynchroner Aufruf so hat der Task ja einen eigenen Handler installiert)
    • Der Default-Handler macht
      • aus der Meldung einen Returncode wie er im Handbuch steht (eigentlich nur ein kopieren),
      • schreibt den RC in den TCB,
      • gibt dem Task unter Umständen einen Prioritätsboost,
      • erzeugt ggf. eine Aktivierung für einen Loggingeintrag (erleichtert userside Debugging enorm),
      • setzt den Task auf Ablaufbereit (was ggf. die Priorität neu berechnet),
      • Und kehrt in den Scheduler zurück (einfacher Return)
    • Der Scheduler macht in der in der Aktivierungsliste weiter
    • Ist keine Aktivierung mehr da, dann sucht er den höchsten ablaufbereiten Task (hoffentich unserer) und startet ihn

    Klingt jetzt kompliziert, ist es aber nicht. dafür aber extrem flexibel - auch weil man alles umleiten kann. Z.B. um zwecks debugging, weil um jede Funktion dynamisch eine Mantelfunktion machen kann die z.B. Logging macht, zusätzlich parameter überprüft und so weiter. Das dauernde hin und her zwischen Sicherheits- (viel Überprüfung) und Performance-Versionen wird zum Kinderspiel und kann task- und funktionsspezifisch gesteuert werden.


    Dienste haben zwar eine Tasknummer und werden so verwaltet, sind aber eigentlich nicht selbstständig lauffähig sondern eher eine Funktionsbibliothek die indirekt aufgerufen wird. Wobei das jetzt halt wieder so nur halb stimmt, weil natürlich kann eine Aktivierung in einem dienst mehr als eine Folgeaktivierung haben - neben dem zurück an den Usertask könnte ein zweiter ACB (oder noch mehr) erzeugt werden, der z.B. einen FS-Check oder einen Defrag anstößt. So gesehen haben Dienste natürlich ein Eigenleben, aber halt nicht so wie Usertasks.


    (Die Codestückchen die da so aktiviert werden heißen bei mir Contingency Routinen - weil sie auch für jede art von Fehlerausgängen verwendet werden)

    Für IO gibt es zwei Möglichkeiten: mit char-devices via Streams zu kommunizieren, wie z.B. Konsole oder Serial. Alles andere ist asynchron. [...]


    Da der SEND/RECEIVE buffer als eines der letzten Überbleibsel noch an einer fixen Adresse steht ($0200) und die character-weise Übertragung von Dateien via Streams laaaanngggsssaaaammm ist, will ich beides über sog. Buffer und Channel neu umsetzen: https://github.com/fachat/Geck…er/GeckOS-NG-Buffers.adoc


    Wie gesagt, mein Ansatz ist eher Mainframe ohne den Wunsch Unix nachzubauen. Daher mit Records als Basis für den Datenzugriff , damit gibts das Problem nicht. Ich hab aber auch schon drüber nachgedacht - wegen 08/15 Programmen in C :))


    Dir ist sicher schon aufgefallen, dass das in Teilen einem L4 artigen Nanokernel ähnelt (war nicht beabsichtigt, hab ich erst später kennengelernt). Dort ist praktisch alles was das OS macht ein Aufruf an irgendwelche Dienste. Und neben 'normalen' Nachrichten, die meinen Aktivierungen ähneln, hat der L4 auch eine superschnelle Kurznachricht eingeführt. Das ist im Prinzip der Aufruf einer Funktion in einem Dienst mit minimalen Daten (nur 2 Registern) und ohne kompletten Prozesswechsel. Klingt doch wie gemacht für character I/O, oder?


    (Mein eigenes Konzept hatte vorgesehen solche blocking-/deblocking-Funktionen durch Systemcode innerhalb des Usertasks zu erledigen, aber das L4 Prinzip scheint mir besser)


    Was an der stelle richtig nervt ist dass dynamische Speicherverwaltung fr die 6502 echt hart ist.


    Edit: das log-file enthält im Prinzip den kompletten Boot-Prozess, ist ca. 3.6GB groß, und es hat > 2 Tage gebraucht das zu flöhen....!

    Genau deswegen ist Logging die einzige Ecke an der ich eine Ausnahme mach und hart am ansonsten asynchronen System vorbeiprogrammier.

  • BTW logging... ich gebe zu, das Log ist aus einem Emulator den ich für das System geschrieben habe...

    In system logging gibt es im Moment nicht wirklich. Nur stdio, was man auf eine Konsole oder Datei legen kann...