Betriebssystem schreiben: Multitasking

    Bild
    Guten Tag, sehr geehrter Leser, höchstwahrscheinlich haben Sie in meinem letzten Artikel gesehen, dass Sie in relativ kurzer Zeit selbst ein funktionierendes Betriebssystem schreiben können. Heute werden wir über die Implementierung von Multitasking in meinem Betriebssystem sprechen.

    Nun, Sie können sich wahrscheinlich kein einziges Betriebssystem für 2018 vorstellen, deshalb habe ich beschlossen, über die Implementierung von Multitasking in meinem Betriebssystem zu sprechen. Zuerst müssen Sie sich für die Art des Multitaskings entscheiden. Ich habe mich für Präventivmaßnahmen entschieden.
    Wie ist sie? Preemptive Multitasking ist ein System zum Verteilen der Verarbeitungsleistung des Prozessors zwischen Prozessen: Jeder hat seine eigene Zeitscheibe, jeder hat seine eigene Priorität. Und das erste Problem ist, welches Quantum in der Länge zu wählen ist, wie man den Prozess nach Ablauf des Quantums anhält. In der Tat ist alles einfacher als je zuvor! Wir verwenden PIT mit der anfänglich eingestellten Frequenz von 10026 mit Interrupt-Kopeken pro Sekunde. Gleich dort lösen wir ein anderes Problem: Wir stoppen bereits den vorherigen Prozess. Beginnen wir also mit PIT.

    PIT


    PIT - Programmable Interval Timer - Ein programmierbarer Intervall-Timer - ein Zähler, der bei Erreichen einer programmierten Anzahl von Inkrementen ein Signal erzeugt. Mit Hilfe dieses Timers können Sie auch ein Quietschen in einem Computer (das, was nach dem Passieren der Testgeräte quietscht) quietschen. Er glaubt also mit einer Frequenz von 1193182 Hertz, das bedeutet, dass wir ihn auf 119 programmieren müssen (1193182/119 entspricht ungefähr 10026). Dazu müssen Sie 2 Bytes an den Port des ersten Generators senden, zuerst das Low-Byte und dann das High-Byte:

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


    Jetzt ist es an der Zeit, den PIT-Interrupt zu programmieren. Er hat IRQ 0, und nach der Neuzuordnung von PIC wird dies 0x20m sein. Für das IRQ des ersten Bildes 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


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


    Zuerst müssen wir Folgendes verstehen: Wir können irgendwo an der globalen Adresse, beispielsweise 0xDEAD, die Nummer des aktuell ausgeführten Prozesses eingeben. Wenn Sie Code ausführen, können wir sicher sein, dass wir die Nummer des derzeit ausgeführten Prozesses haben Wenn wir malloc aufrufen, wissen wir, für wen wir Speicher zuweisen, und wir können die Adresse des zugewiesenen Speichers sofort der Zuweisungsliste hinzufügen.
    voidaddProcessAlloc(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, was als nächstes, wie die Aufgaben zu wechseln sind.
    Zunächst möchte ich anmerken, dass beispielsweise im Handler lokale Variablen auf dem Stack gespeichert werden, das heißt, der Compiler verwöhnt uns nach Eingabe des Handlers mit esp. Um dies zu verhindern, erstellen Sie eine Variable mit einer absoluten Adresse, und bevor Sie den Handler aufrufen, werden wir ESP hineinschieben. 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: er ist so einfach wie ein Stau). Als Nächstes müssen wir alle Register und Flags des aktuellen Prozesses speichern. Unmittelbar vor dem Einfügen von ESP in eine Variable speichern wir alle Register (einschließlich Segmentregister) auf dem Stack. Im Handler selbst müssen wir sie sehr vorsichtig aus dem Stack entfernen, indem Sie lediglich die Flags und die Absenderadresse speichern. Ich möchte anmerken, dass der Stack größer wird (dh ESP nimmt ab), dh das letzte Register
    Bild
    Nun müssen wir die Werte der Register des Prozesses, zu dem wir gewechselt und ausgeführt haben, in die Register schieben. Gewinn!

    Laufende Prozesse


    Beim Starten des Prozesses müssen wir nur einen Stack für den Prozess zuweisen. Danach fügen wir argc und argv hinzu, die Adresse der Funktion, die nach Abschluss des Prozesses die Kontrolle erhält. Sie müssen auch die Prozessor-Flags auf den gewünschten Wert setzen. Für mein Betriebssystem ist es beispielsweise 0x216. Sie können das Flag-Register in Wikipedia lesen.

    Zum Schluss wünsche ich Ihnen viel Erfolg und schreibe in kurzer Zeit über die Arbeit mit dem Gedächtnis und anderen für Sie interessanten Artikeln.
    Viel Glück und ethisches Hacken!

    Jetzt auch beliebt: