Software Multitasking Tutorial

Aus Lowlevel
Wechseln zu:Navigation, Suche
Software Multitasking
Schwierigkeit:

Stern gold.gifStern gold.gifStern weiß.gifStern weiß.gifStern weiß.gif

Benötigtes Vorwissen: IRQ, Scheduler
Sprache: FreeBASIC

Dieses Tutorial ist für alle gedacht, die in ihrem Betriebssystem Multitasking auf Softwarebasis implementieren wollen. Ich bin am Anfang an diesem Thema fast verzweifelt, daher schreibe ich dieses Tutorial, damit andere einen leichten Einstieg in diese Thema haben. Ich werde hier nicht "alles" erklären, aber danach sollte einfache Tests mit zwei Tasks möglich sein.

Bevor wir anfangen...

Eines vorweg: Du solltest mit deinem Betriebssystem bereits auf einem gewissen Stand sein, d.h. du solltest IRQs verwalten und den PIT umprogrammieren können, eine Speicherverwaltung wird auch benötigt. Ansonsten brauchst du nur noch gesunden Menschenverstand. Wenn du das alles hast, kann es losgehen.

Einige Überlegungen

Einfach drauflos zu coden ist hier der falsche Weg. Überlegen wir uns zuerst einmal, wie Software-Multitasking überhaupt funktioniert.

  • Generelles:
    • Wir benötigen eine Tabelle mit den Tasks, in der Stack, ProzessID und der Status gespeichert werden. Dann noch eine Variable, in der wir die ID des aktuellen Prozesses festhalten.
  • Initialisierung:
    • Wir sagen dem PIT, wie oft er in der Sekunde einen IRQ feuern soll. Danach erzeugen wir in der IDT an der Stelle des IRQs 0 einen Eintrag, der auf unsere Task-Switch Routine verweist.
  • Task-Switch:
    • Wir legen die Register auf den Stack (das ist der Stack des unterbrochenen Tasks). Dann rufen wir unseren Scheduler auf. Er legt die übergebene Stackaddresse in der Task-Tabelle ab, sucht den nächsten Task heraus und gibt dessen Stackaddresse zurück. Die Task-Switch Routine lädt dann die Register von dem neuen Stack und kehrt mit einem IRET zurück zum Task.
  • Task erstellen:
    • Diese Routine erhält als Parameter die ID, an der der Task in der Tabelle eingetragen werden soll, und die Adresse des Codes. Wir blocken dann die Interrupts mit CLI und holen uns erst einmal Speicher für den Stack. Wir sichern dann die Adresse des Kernelstacks und laden den neuen. Dann benutzen wir einen kleinen Trick: Wir lassen den Stack des Tasks so aussehen, als wäre der Task gleich am Anfang unterbrochen worden, also legen wir die vorbereiteten Register auf den Stack. Dann stellen wir einfach den Kernelstack wieder her, tragen den Tasks in die Tabelle ein, erlauben die Interrupts wieder und fertig.

Der Code

Kommen wir nun zum Hauptteil dieses Tutorials: Dem Code. Ich werde hier erst ein Stück FreeBASIC-Code zeigen und dann erklären, was dieser Code macht.

Zuerst kommen ein paar Konstanten: <freebasic>

const TASK_EMPTY as BYTE = 0
const TASK_READY as BYTE = 1

</freebasic>

Die zeigen einfach nur an, ob ein Tabelleneintrag der Task-Tabelle mit einem Task belegt ist. <freebasic>

type TaskType
   PID as USHORT
   stack as UINTEGER
   status as BYTE
end type

dim shared Tasks(100) as TaskType
dim shared AllTasks as USHORT
dim shared currentPID as USHORT

</freebasic>

Nicht wirklich schwer. Wir machen eine Typendefinition für die Task-Tabelle. Enthalten sind die ProzessID (positive 16bit Zahl), die Stack-Adresse (positive 32bit Zahl) und der Status des Tasks (8bit Zahl). Danach legen wir ein Array an, das wir jetzt mit den Daten füttern können.

Dann legen wir noch zwei Variablen an. "AllTasks" gibt an, wieviele Einträge der Tabelle belegt sind. "currentPID" beinhaltet die ProzessID des aktuell laufenden Tasks. <freebasic>

function dec (byref tPTR as UINTEGER PTR) as UINTEGER PTR
    tPTR -= 1
    return tPTR
end function

</freebasic>

Das hat jetzt nichts direkt mit Multitasking zu tun, ist aber nützlich. Damit können wir so etwas wie "*--stack = 0" in FreeBASIC folgendermaßen schreiben: "*(dec(stack)) = 0". Kurz gesagt, nimmt sie einen UINTEGER-pointer und setzt ihn um 4bytes (also ein UINTEGER) runter. Das macht später einiges einfacher. <freebasic>

sub Multitasking_Init
   PIT_Timer_init (100)
   IDT_CreateEntry (Interrupt_IRQ0, @Multitasking_Switch, &h08, &h8E)
end sub

</freebasic>

Auch einfach. Wir programmieren den PIT so, dass er 100 mal in der Sekunde (vermutlich kein guter Wert für später) den IRQ 0 auslöst. Dann legen wir in der IDT einen Eintrag an, an der Stelle für IRQ 0. Da tragen wir die Adresse der Task-Switch Routine ein, und geben als Selector den Hex-Wert 0x08 an. Die Flags setzen wir auf 0x8E. <freebasic>

sub Multitasking_Switch naked
   asm
      pusha
      push ds
      push es
      push fs
      push gs
      
      mov eax, &h10
      mov ds, eax
      mov es, eax
      mov fs, eax
      mov gs, eax
      
      push esp
      call MULTITASKING_SCHEDULER
      mov esp, eax
      
      mov al, &h20
      out &h20, al
      
      pop gs
      pop fs
      pop es
      pop ds
      popa
      
      iret
   end asm
end sub

</freebasic>

Kurz, oder? Als erstes fällt wohl das "naked" auf. Damit sagen wir FreeBASIC (Vorsicht, funktioniert erst aber Version 0.21!), dass er am Anfang und Ende nichts am Stack macht und kein "ret" anfügt. Sonst könnten wir die Routine nicht direkt in die IDT eintragen. Zuerst legen wir die Register auf den Stack. Dann laden wir die Segmentregister wieder mit einem gültigen Wert, damit es keine Probleme gibt (wenn der Task die Werte zerstört hat, und wir das nicht machen würden, würde unser Kernel nicht mehr lange mitspielen). Dann folgt ein kleiner Trick, wie rufen den Scheduler direkt aus dem Assembler-Code auf. Dazu legen wir das Stack-Register (das ist der Stack des unterbrochenen Tasks) auf den Stack, da der Scheduler die Stack-Adresse als Parameter erwartet. Dann rufen wir den Scheduler auf, und er gitb uns eine neue Stackadresse in eax zurück. Diesen Wert laden wir in esp, und schon haben wir den Stack des neuen Tasks.

Wir müssen natürlich auch noch dem PIC sagen, dass wir mit unserem Interrupt fertig sind.

Dann laden wir nur noch die Register wieder vom Stack und gehen mit "iret" zum neuen Task.

<freebasic>

function Multitasking_scheduler (stack as UINTEGER) as UINTEGER
   Tasks(currentPID).stack = stack
   
   do
      currentPID += 1
      if currentPID > AllTasks then currentPID = 1
   loop until Tasks(currentPID).status = TASK_READY
   
   return Tasks(currentPID).stack
end function

</freebasic>

Auch recht einfach. Wir sichern die Stackadresse im Tabelleneintrag des aktuellen Tasks. Dann erhöhen wir currentPID bis wir einen Task finden, der auf seine Aktivierung wartet. Sollte der Wert in currentPID zu hoch werden, setzen wir ihn wieder auf den Tabellenanfang. Haben wir als einen Task gefunden, geben wir dessen Stackadresse zurück.

<freebasic>

sub Task_Create (ID as USHORT, priority as UBYTE, address as any ptr)
    if Tasks(ID).status <> TASK_EMPTY then return
    
    dim stack as UINTEGER PTR
    
    asm cli
    
    stack = cast(UINTEGER PTR, malloc(4096) + 1024)
    
    *(dec(stack)) = &h202                    '// EFLAGS
    *(dec(stack)) = &h08                     '// CS
    *(dec(stack)) = cast(UINTEGER, address)  '// EIP
    
    *(dec(stack)) = &h0                      '// EDI
    *(dec(stack)) = &h0                      '// ESI
    *(dec(stack)) = &h0                      '// EBP
    *(dec(stack)) = &h0                      '// NULL
    *(dec(stack)) = &h0                      '// EBX
    *(dec(stack)) = &h0                      '// EDX
    *(dec(stack)) = &h0                      '// ECX
    *(dec(stack)) = &h0                      '// EAX
    
    *(dec(stack)) = &h10                     '// DS
    *(dec(stack)) = &h10                     '// ES
    *(dec(stack)) = &h10                     '// FS
    *(dec(stack)) = &h10                     '// GS
    
    Tasks(ID).status = TASK_READY
    Tasks(ID).stack = cast(UINTEGER, stack)
    Tasks(ID).PID = ID
    
    AllTasks += 1                                                 '// We now have more tasks than before (what a surprise!)
    
    asm sti                                                       '// allow Interrupts again
end sub

</freebasic>

Puh. Auf zur Erklärung: Wir prüfen zuerst, ob die gewünschte ProzessID schon belegt ist. Wenn ja, kerhen wir sofort zurück (besser wäre es, wenn wir einen Fehler-Wert zurückgeben würden). Dann erstellen wir eine Variable, in der wir die Stack-Adresse des Tasks zwischenspeichern.

Wichtig ist, das wir hier mit "cli" die Interrupts ausschalten.

Dann holen wir uns 4kb Speicher und legen die Adresse dazu in "stack" ab (das "+ 1024" ist notwendig, weil der Stack von oben nach unten wächst. Also ist der Stackanfang am oberen Ende des reservierten Speicherbereichs! Zu beachten ist hier, das wir zwar 1024 addieren, aber die Adresse in Wirklichkeit um 4096 Bytes ansteigt. Das liegt daran, das wir das zu einem UINTEGER-Pointer addieren. Das ist also alles korrekt so.)

Jetzt basteln wir den Task-Stack zusammen, und zwar ohne den Stack zu wechseln! Hier wird auch klar, für was unsere Hilfsfunktion genutzt wird. Wir simulieren damit praktisch einen "push"-Befehl. Natürlich legen wir hier keine richtigen Register auf den Stack, sondern nur Anfangs-Werte.Dann sieht es so aus, als ob der Task sofot am Anfang unterbrochen worden wäre, so gibt es keine Probleme beim ersten aktivieren des Tasks.

Würden wir noch ein Register cr3 (für Paging) dazu legen, müssten wir natürlich auch unsere Switch-Routine anpassen, sonst schrotten wir unseren Stack.

Nun legen wir den Tabelleneintrag für unseren Task an. Wir setzen ihn auf ready und legen die Stackadresse rein. Die Stack-Adresse ist tatsächlich korrekt, dafür hat unsere Hilfsfunktion schon gesorgt. Naja, dann schreiben wir noch die ProzessID rein und erhöhen den Wert von "AllTasks" um eins, da wir ja jetzt einen Task mehr haben.

Abschließend erlauben wir mit "sti" die Interrupts wieder und fertig sind wir.

Abschließende Worte

Puh. Fertig. Es hat mir Spaß gemacht, dieses Tutorial zu schreiben, und ich hoffe, es hat euch auch Spaß gemacht, es zu lesen. Solltet ihr etwas nicht verstanden haben, könnt ihr eure Fragen im Forum posten. Wahrscheinlich wäre den meisten C-Code hier lieber gewesen, aber ich kann nicht gut C, außerdem ist so das Risiko geringer, dass jemand per copy&paste den Code übernimmt ohne ihn zu verstehen ;). Ich hoffe aber trotzdem, das alles verständlich war.

Natürlich sind wir hier noch nicht beim perfekten Multitasking angelangt. Wie gesagt müsste cr3 noch gesichert werden, auch Prioritäten wären wünschenswert. Also hört hier nicht einfach auf, sondern bastelt noch ein wenig, und verbessert euren Code. Dieses Tutorial zeigt euch nur wie es geht, was ihr letztendlich damit anstellt, ist eure Sache ;)

Noch ein Hinweis: Der Code stammt aus meinem Kernel, den ihr hier finden könnt: http://sourceforge.net/projects/frostkernel

Happy Coding, TheThing


Links

Protected-Mode Tutorial der FH-Zwickau Software-Multitasking Tutorial auf google code