Teil 6 - Multitasking — Änderungen 7128 → 11392

Zeile 9: Zeile 9:
 
==Unsere Ausgabetasks==
 
==Unsere Ausgabetasks==
 
Unsere Tasks implementieren wir vorerst als C-Funktion im Kernel. Das ist einfach genug, daher direkt zum Code:
 
Unsere Tasks implementieren wir vorerst als C-Funktion im Kernel. Das ist einfach genug, daher direkt zum Code:
−
<c>void task_a(void)
+
<syntaxhighlight lang=c>void task_a(void)
 
{
 
{
 
     while (1) {
 
     while (1) {
Zeile 21: Zeile 21:
 
         kprintf("B");
 
         kprintf("B");
 
     }
 
     }
−
}</c>
+
}</syntaxhighlight>
  
 
==Task starten==
 
==Task starten==
Zeile 33: Zeile 33:
  
 
Legen wir also einmal den Startzustand für die beiden Tasks fest, jeder bekommt seinen Stack und dort drin eine struct cpu_state:
 
Legen wir also einmal den Startzustand für die beiden Tasks fest, jeder bekommt seinen Stack und dort drin eine struct cpu_state:
−
<c>static uint8_t stack_a[4096];
+
<syntaxhighlight lang=c>static uint8_t stack_a[4096];
 
static uint8_t stack_b[4096];
 
static uint8_t stack_b[4096];
  
Zeile 75: Zeile 75:
  
 
     return state;
 
     return state;
−
}</c>
+
}</syntaxhighlight>
  
 
Wie im Code angedeutet, können wir ss und esp im Prozessorzustand erst einmal ignorieren. Wir haben sie schon in unsere struct cpu_state aufgenommen, weil sie bei einem Interrupt automatisch von der CPU gespeichert werden, wenn wir dazu von einem normalen unprivilegierten Programm (also aus Ring 3) in den Kernel (also Ring 0) wechseln. Wir lassen bisher aber alles mit vollen Privilegien laufen und bekommen daher bei Interrupts auch keinen Stackwechsel.
 
Wie im Code angedeutet, können wir ss und esp im Prozessorzustand erst einmal ignorieren. Wir haben sie schon in unsere struct cpu_state aufgenommen, weil sie bei einem Interrupt automatisch von der CPU gespeichert werden, wenn wir dazu von einem normalen unprivilegierten Programm (also aus Ring 3) in den Kernel (also Ring 0) wechseln. Wir lassen bisher aber alles mit vollen Privilegien laufen und bekommen daher bei Interrupts auch keinen Stackwechsel.
Zeile 81: Zeile 81:
 
Als nächstes brauchen wir einen primitiven Scheduler, der uns sagt, welcher Task als nächstes dran ist. Ein Teil ist die Initialisierung, die die beiden Tasks startet und die Datenstrukturen bereitstellt. Dieser Teil muss aufgerufen werden, bevor Hardwareinterrupts aktiviert werden. Der zweite Teil ist der Scheduler selbst, der speichert, wo der alte Task gerade steht und den Zustand des nächsten Tasks zurückgibt.
 
Als nächstes brauchen wir einen primitiven Scheduler, der uns sagt, welcher Task als nächstes dran ist. Ein Teil ist die Initialisierung, die die beiden Tasks startet und die Datenstrukturen bereitstellt. Dieser Teil muss aufgerufen werden, bevor Hardwareinterrupts aktiviert werden. Der zweite Teil ist der Scheduler selbst, der speichert, wo der alte Task gerade steht und den Zustand des nächsten Tasks zurückgibt.
  
−
<c>static int current_task = -1;
+
<syntaxhighlight lang=c>static int current_task = -1;
 
static int num_tasks = 2;
 
static int num_tasks = 2;
 
static struct cpu_state* task_states[2];
 
static struct cpu_state* task_states[2];
Zeile 117: Zeile 117:
  
 
     return cpu;
 
     return cpu;
−
}</c>
+
}</syntaxhighlight>
  
 
Jetzt müssen wir den Scheduler nur noch zum Einsatz bringen. Dieser Teil ist das, was das eigentliche Multitasking macht. Der passende Code dazu ist so kurz wie die Idee einfach ist: Anstatt bei einem Interrupt den gerade eben gesicherten Prozessorzustand wiederherzustellen, stellen wir einfach den Zustand eines anderen Tasks wieder her. Dazu wird handle_interrupt so geändert, dass es <i>cpu</i> wieder zurückgibt. Im Fall des Timerinterrupts geben wir allerdings den Zustand des neuen Tasks zurück:
 
Jetzt müssen wir den Scheduler nur noch zum Einsatz bringen. Dieser Teil ist das, was das eigentliche Multitasking macht. Der passende Code dazu ist so kurz wie die Idee einfach ist: Anstatt bei einem Interrupt den gerade eben gesicherten Prozessorzustand wiederherzustellen, stellen wir einfach den Zustand eines anderen Tasks wieder her. Dazu wird handle_interrupt so geändert, dass es <i>cpu</i> wieder zurückgibt. Im Fall des Timerinterrupts geben wir allerdings den Zustand des neuen Tasks zurück:
−
<c>struct cpu_state* handle_interrupt(struct cpu_state* cpu)
+
<syntaxhighlight lang=c>struct cpu_state* handle_interrupt(struct cpu_state* cpu)
 
{
 
{
 
     struct cpu_state* new_cpu = cpu;
 
     struct cpu_state* new_cpu = cpu;
Zeile 133: Zeile 133:
  
 
     return new_cpu;
 
     return new_cpu;
−
}</c>
+
}</syntaxhighlight>
  
 
Der Interrupt-Handler muss jetzt nur noch diesen Rückgabewert verarbeiten, d.h. den Stack entsprechend wechseln:
 
Der Interrupt-Handler muss jetzt nur noch diesen Rückgabewert verarbeiten, d.h. den Stack entsprechend wechseln:
−
<asm>    ...
+
<syntaxhighlight lang=asm>    ...
 
     // Handler aufrufen
 
     // Handler aufrufen
 
     // Der Rueckgabewert ist der Prozessorzustand des moeglicherweise
 
     // Der Rueckgabewert ist der Prozessorzustand des moeglicherweise
Zeile 146: Zeile 146:
  
 
     // CPU-Zustand wiederherstellen
 
     // CPU-Zustand wiederherstellen
−
     ...</asm>
+
     ...</syntaxhighlight>
  
 
Das war's. Beim nächsten Timerinterrupt fängt der Kernel fröhlich an, Multitasking zu machen, und abwechselnd As und Bs auf den Bildschirm zu schreiben. Wer nicht auf den Timerinterrupt warten möchte kann auch den Interrupt 0x20 als Software-Interrupt auslösen.
 
Das war's. Beim nächsten Timerinterrupt fängt der Kernel fröhlich an, Multitasking zu machen, und abwechselnd As und Bs auf den Bildschirm zu schreiben. Wer nicht auf den Timerinterrupt warten möchte kann auch den Interrupt 0x20 als Software-Interrupt auslösen.
Zeile 157: Zeile 157:
 
Aber fangen wir beim leichten Teil an. Stellen wir den Taskzustand auf Ring 3 um. Dazu brauchen wir jetzt zusätzlich zum Kernelstack jedes Tasks, der für den Prozessorzustand und die Ausführung der Interrupt-Handler benutzt wird, einen zweiten getrennten Stack für jeden Task, auf dem dieser beliebig arbeiten kann. Außerdem benutzen wir jetzt die Ring-3-Segmente aus der GDT:
 
Aber fangen wir beim leichten Teil an. Stellen wir den Taskzustand auf Ring 3 um. Dazu brauchen wir jetzt zusätzlich zum Kernelstack jedes Tasks, der für den Prozessorzustand und die Ausführung der Interrupt-Handler benutzt wird, einen zweiten getrennten Stack für jeden Task, auf dem dieser beliebig arbeiten kann. Außerdem benutzen wir jetzt die Ring-3-Segmente aus der GDT:
  
−
<c>static uint8_t user_stack_a[4096];
+
<syntaxhighlight lang=c>static uint8_t user_stack_a[4096];
 
static uint8_t user_stack_b[4096];
 
static uint8_t user_stack_b[4096];
  
Zeile 170: Zeile 170:
 
         /* Ring-3-Segmentregister */
 
         /* Ring-3-Segmentregister */
 
         .cs  = 0x18 | 0x03,
 
         .cs  = 0x18 | 0x03,
−
         .ss  = 0x20 | 0x03,</c>
+
         .ss  = 0x20 | 0x03,</syntaxhighlight>
  
 
Code und Stack wären damit abgedeckt, aber wir brauchen auch noch die Datensegmente. Unser Aufruf des Interrupthandlers sieht damit folgendermaßen aus:
 
Code und Stack wären damit abgedeckt, aber wir brauchen auch noch die Datensegmente. Unser Aufruf des Interrupthandlers sieht damit folgendermaßen aus:
−
<asm>    // Kernel-Datensegmente laden
+
<syntaxhighlight lang=asm>    // Kernel-Datensegmente laden
 
     mov $0x10, %ax
 
     mov $0x10, %ax
 
     mov %ax, %ds
 
     mov %ax, %ds
Zeile 189: Zeile 189:
 
     mov $0x23, %ax
 
     mov $0x23, %ax
 
     mov %ax, %ds
 
     mov %ax, %ds
−
     mov %ax, %es</asm>
+
     mov %ax, %es</syntaxhighlight>
  
 
Als nächstes legen wir das TSS an. Der Aufbau wird im Artikel [[Task State Segment]] beschrieben, wir brauchen davon nur die Felder esp0 und ss0. ss0 können wir konstant auf unser Ring-0-Datensegment setzen, esp0 muss für jeden Task angepasst werden.
 
Als nächstes legen wir das TSS an. Der Aufbau wird im Artikel [[Task State Segment]] beschrieben, wir brauchen davon nur die Felder esp0 und ss0. ss0 können wir konstant auf unser Ring-0-Datensegment setzen, esp0 muss für jeden Task angepasst werden.
  
−
<c>static uint32_t tss[32] = { 0, 0, 0x10 };
+
<syntaxhighlight lang=c>static uint32_t tss[32] = { 0, 0, 0x10 };
  
 
...
 
...
Zeile 200: Zeile 200:
 
             new_cpu = schedule(cpu);
 
             new_cpu = schedule(cpu);
 
             tss[1] = (uint32_t) (new_cpu + 1);
 
             tss[1] = (uint32_t) (new_cpu + 1);
−
         }</c>
+
         }</syntaxhighlight>
  
 
Außerdem müssen wir das TSS noch aktivieren. Dazu fügen wir der GDT einen zusätzlichen Eintrag hinzu und laden anschließend auch das Taskregister TR neu (nicht vergessen, das Limit der GDT zu erhöhen, wenn nötig):
 
Außerdem müssen wir das TSS noch aktivieren. Dazu fügen wir der GDT einen zusätzlichen Eintrag hinzu und laden anschließend auch das Taskregister TR neu (nicht vergessen, das Limit der GDT zu erhöhen, wenn nötig):
−
<c>    set_entry(5, (uint32_t) tss, sizeof(tss),
+
<syntaxhighlight lang=c>    set_entry(5, (uint32_t) tss, sizeof(tss),
 
         GDT_FLAG_TSS | GDT_FLAG_PRESENT | GDT_FLAG_RING3);
 
         GDT_FLAG_TSS | GDT_FLAG_PRESENT | GDT_FLAG_RING3);
  
 
     // Taskregister neu laden
 
     // Taskregister neu laden
−
     asm volatile("ltr %%ax" : : "a" (5 << 3));</c>
+
     asm volatile("ltr %%ax" : : "a" (5 << 3));</syntaxhighlight>
  
 
Damit wäre auch der Userspace implementiert. Wer möchte, kann das gern durch ein <i>asm("cli; hlt");</i> in einem der Tasks ausprobieren. Bevor die Tasks nach Ring 3 verschoben worden sind, durften sie die Befehle ausführen und der Kernel kam zum Stillstand. Jetzt wird eine Exception ausgelöst (13 = #GP = General Protection Fault) und der Kernel gibt eine Fehlermeldung aus (bevor er sich ebenfalls in eine Endlosschleife begibt – normal würde er hier stattdessen einfach den Task beenden).
 
Damit wäre auch der Userspace implementiert. Wer möchte, kann das gern durch ein <i>asm("cli; hlt");</i> in einem der Tasks ausprobieren. Bevor die Tasks nach Ring 3 verschoben worden sind, durften sie die Befehle ausführen und der Kernel kam zum Stillstand. Jetzt wird eine Exception ausgelöst (13 = #GP = General Protection Fault) und der Kernel gibt eine Fehlermeldung aus (bevor er sich ebenfalls in eine Endlosschleife begibt – normal würde er hier stattdessen einfach den Task beenden).