OS-Schreiben: Multitasking

Bild
Guten Tag, lieber Leser, höchstwahrscheinlich haben Sie in meinem vorherigen Artikel gesehen, dass Sie selbst in relativ kurzer Zeit ein funktionsfähiges Betriebssystem schreiben können. Nun, heute werden wir über die Implementierung von Multitasking in meinem Betriebssystem sprechen.

Nun, Sie können sich 2018 wahrscheinlich kein Single-Tasking-Betriebssystem vorstellen, deshalb habe ich beschlossen, über die Implementierung von Multitasking in meinem Betriebssystem zu sprechen. Und als erstes müssen Sie sich für die Art des Multitasking entscheiden. Ich habe mich für präventiv entschieden.
Wie ist sie Preemptive Multitasking ist ein System zur Verteilung der Prozessor-Rechenleistung auf Prozesse: Jeder hat seine eigene Zeitmenge, jeder hat seine eigene Priorität. Und das erste Problem ist, welches Quantum in der Länge gewählt werden soll, wie der Prozess nach Ablauf des Quantums am Laufen gehalten werden kann. In der Tat ist alles einfacher als je zuvor! Wir werden PIT mit der ursprünglich eingestellten Frequenz von 10026 mit einem Cent Unterbrechungen pro Sekunde verwenden. Genau dort lösen wir ein weiteres Problem: Wir stoppen bereits den vorherigen Prozess. Beginnen wir also mit PIT.

Grube


PIT - Programmable Interval Timer - ein Zähler, der bei Erreichen einer programmierten Anzahl von Inkrementen ein Signal gibt. Mit diesem Timer können Sie auch einen Quietscher im Computer quietschen (das, was nach dem Bestehen des Gerätetests quietscht). Und so zählt er mit einer Frequenz von 1193182 Hertz, was bedeutet, dass wir es mit 119 programmieren müssen (1193182/119 ist ungefähr gleich 10026). Senden Sie dazu 2 Bytes an den Port des ersten Generators, zuerst das Low-Byte und dann das High:

unsigned short hz = 119; outportb(0x43, 0x34); outportb(0x40, (unsigned char)hz & 0xFF); //Low outportb(0x40, (unsigned char)(hz >> 8) & 0xFF); //Hight, about 10026 times per second 


Jetzt lohnt es sich, die Programmierung des Interrupts von PIT aus zu starten. Er hat einen IRQ von 0 und nach der Neuzuordnung von PIC beträgt er 0 x 20 m. Für den IRQ des ersten PIC habe ich dieses Makro geschrieben:

 //PIC#0; port 0x20 #define IRQ_HANDLER(func) char func = 0x90;\ __asm__(#func ": \npusha \n call __"#func " \n movb $0x20,\ %al \n outb %al, $0x20 \n popa \n iret \n");\ void _## func() 


Struktur und Prozesse


Wie Sie verstehen, müssen wir für jeden Prozess eine Struktur sowie eine Struktur entwickeln, die es mir ermöglicht, mich an alle meine Speicherzuordnungen zu erinnern.
Folgendes habe ich:
 typedef struct _pralloc { void * addr; struct _pralloc * next; } processAlloc; typedef struct { void * entry; processAlloc *allocs; } ELF_Process; typedef struct __attribute__((packed)) _E { unsigned int eax;//4 unsigned int ebx;//8 unsigned int ecx;//12 unsigned int edx;//16 unsigned int ebp;//20 unsigned int esp;//24 unsigned int esi;//28 unsigned int edi;//32 unsigned int eflags;//36 unsigned int state;//40 void * startAddr;//44 void * currentAddr;//48 void * stack;//52 unsigned int sse[4 * 8];// unsigned int mmx[2 * 8];//244 unsigned int priority;//248 unsigned int priorityL;//252 void * elf_process;//256 char ** argv;//260 unsigned int argc;//264 unsigned int runnedFrom;//268 char * workingDir;//272 unsigned int cs;//276 - pop is 4 byte in IRET unsigned int ds;//280 } Process; 


Zunächst müssen wir Folgendes verstehen: Wir können irgendwo an der globalen Adresse, z. B. bei 0xDEAD, die Nummer des aktuell ausgeführten Prozesses eingeben. Wenn wir dann einen Code ausführen, können wir sicher sein: Wir haben die Nummer des aktuell ausgeführten Prozesses. Dies bedeutet, dass Wenn wir auf malloc zugreifen, wissen wir, wem wir Speicher zuweisen, und wir können sofort die Adresse des zugewiesenen Speichers zur Liste der Zuordnungen hinzufügen.
 void addProcessAlloc(ELF_Process * p, void * addr) { void * z = p->allocs; p->allocs = malloc_wo_adding_to_process(sizeof(processAlloc)); p->allocs->addr = addr; p->allocs->next = z; } 


Nun, wir haben die Struktur der Tabelle mit der Beschreibung der Prozesse geschrieben. Wie geht es weiter, wie werden Aufgaben gewechselt?
Zunächst möchte ich darauf hinweisen, dass beispielsweise im Handler lokale Variablen auf dem Stapel gespeichert sind, was bedeutet, dass der Compiler uns nach dem Aufrufen des Handlers besonders verwöhnt. Um dies zu verhindern, erstellen Sie eine Variable mit einer absoluten Adresse. Bevor Sie den Handler aufrufen, wird das ESP dort abgelegt. Im Handler müssen wir die EOI an den ersten PIC senden und den Prozess finden, zu dem wir wechseln müssen (ich werde den Prioritätsmechanismus nicht beschreiben: Es ist einfach, wie ein Stau). Als nächstes müssen wir alle Register und Flags des aktuellen Prozesses speichern. Kurz bevor wir den ESP in eine Variable einfügen, speichern wir alle Register (einschließlich der Segmentregister) auf dem Stapel. Im Handler selbst müssen wir sie sehr sorgfältig vom Stapel entfernen und gleichzeitig die Flags und die Rücksprungadresse beibehalten. Ich möchte darauf hinweisen, dass der Stapel größer wird (d. H. ESP nimmt ab), was bedeutet, dass das letzte Register, das Sie auf dem Stapel gespeichert haben, bei ESP ist, das vorletzte ist ESP +4 usw.:
Bild
Jetzt bleibt es uns überlassen, die Werte der Prozessregister in die Register zu setzen, auf die wir umgeschaltet haben, und IRET auszuführen. Gewinn!

Prozessstart


Wenn wir den Prozess starten, reicht es aus, den Stapel für den Prozess zuzuweisen und dann argc und argv darin abzulegen, die Adresse der Funktion, die nach Abschluss des Prozesses gesteuert wird. Sie müssen auch die Prozessor-Flags auf den Wert setzen, den Sie benötigen. Für mein Betriebssystem ist es beispielsweise 0x216. Sie können das Flag-Register auf Wikipedia lesen.

Am Ende möchte ich Ihnen viel Erfolg wünschen, bald werde ich über die Arbeit mit dem Gedächtnis und andere Artikel schreiben, die Sie interessieren.
Viel Glück und ethisches Hacken!

Source: https://habr.com/ru/post/de426807/


All Articles