Nachdem Sie die grundlegenden Schritte zum Schreiben von Hello World-Kerneln aus der Artikelserie von Habré gelesen haben, ist es an der Zeit, mit der ernsthaften Entwicklung der grundlegendsten Tools zu beginnen: Heap-Allokator und Scheduler.
Wichtig :
1. Diese Unterrichtsreihe für Anfänger. Ziel ist es, ein ganzheitliches Bild der Welt zu erstellen. Ich hatte sehr lange die Tanenbaum-Theorie im Kopf und konnte sie nicht mit der Praxis verbinden. Für diejenigen, die das gleiche haben - ich widme diesen Artikel.
2. Der Code ist der einfachste und dümmste, aber so klar wie möglich. Mein Ziel ist es, Ihnen ein Verständnis zu vermitteln, damit Sie Ihren eigenen Kernel schreiben können, viel cooler als das.
3. Ich werde das Repo veröffentlichen, sobald ich es für ein breites Spektrum bereit halte. Ich schreibe einen kleinen Teil, debugge und drehe ein Video-Tutorial. Ich habe kein fertiges Produkt.
Ehrlich gesagt habe ich lange darüber nachgedacht, ob ich anfangen soll, Artikel zu schreiben und Video-Tutorials zu einem so verdrängten Thema zu machen. Die Leidenschaft für die Systemprogrammierung und der Mangel an strukturierten Informationen auf Russisch haben mich dennoch zu diesem Experiment getrieben. Wenn meine Arbeit gefragt ist, plane ich daher, Artikel mindestens einmal im Monat und höchstens einmal pro Woche zu veröffentlichen.
Ich träumte davon, mein Betriebssystem in der zehnten Klasse zu schreiben. Das Buch Tanenbaum hat eine erstaunliche Eigenschaft. Jeder, der es früher oder später berührt, beginnt davon zu träumen, sein eigenes Betriebssystem zu schreiben. Aber ich habe damals nichts geschrieben, nachdem mir klar wurde, dass es unmöglich ist, an einem Masttag eine saubere Binärdatei mit einem Code zu kompilieren und zu verknüpfen. Ich musste Linux studieren, zur Universität gehen, zur Arbeit gehen. Alles wäre nichts. Ja, erst zehn Jahre später wurde ein Traum wahr. Jetzt, wo ich mir bei React langweilige Formen ausgedacht habe, schaue ich zurück, erinnere mich an die Leidenschaft, mit der ich Stunden für den Disassembler und Debugger verbracht, die Packer entfernt, die Crackboxen zerbrochen, nachts viele Artikel aus Schulromanen gelesen habe ... und ich verstehe, dass mein Leben anders hätte verlaufen können. Ganz anders. Wenn ich nur eine Person hätte, die mir am Anfang helfen könnte. Aber die Zeit ist vorbei. Programme sind schwer zu brechen. Der Linux-Kernel ist unglaublich groß geworden. Hypervisoren erschienen. Intel Assembler ist so groß geworden, dass bei Betrachtung einer Liste von Befehlen im Handbuch jede Begeisterung für das Erlernen dieser Befehle verschwindet. Wir mussten im Internet Brot verdienen. Ja Die Welt der Systemprogrammierung hat ihre goldenen Jahre überlebt.
Daher widme ich allen Studenten, deren Begeisterung noch nicht erschöpft ist, ein detailliertes Tutorial, wie man den einfachsten Multitasking-Mikrokernel-Kern mit eigener Shell von Grund auf neu erstellt. Und ich möchte alle warnen, die noch nicht erschienen sind, dass dies eine undankbare und nicht hilfreiche Angelegenheit ist, wenn auch schrecklich interessant. Und die Jugend hat, wie Sie wissen, ihre eigenen Sorgen.
Lass uns gehen!
Dieser
Artikel enthält ein Video-Tutorial, das in meinem Heimstudio aufgenommen wurde. Es ist direkt dem Code gewidmet.
Beginnen wir mit der Entwicklung des Heap-Managers.
extern void *kmalloc(size_t size); extern void kfree(void *addr);
Um einen Heap-Allokator zu schreiben, müssen Sie die Regionen mit freien und belegten Adressen speichern. Dies ist am einfachsten als Richtungsliste zu implementieren. Die Implementierung einer gerichteten Liste selbst muss über ein statisches Array erfolgen.
struct slist_head_t { struct slist_head_t *prev; struct slist_head_t *next; bool is_valid; void *data; } attribute(packed); struct slist_definition_t { size_t base; u_int slots; size_t slot_size; struct slist_head_t *head; struct slist_head_t *tail; };
Elemente einer solchen begrenzten Liste sind Aufzeichnungen von Speicherbereichen. Darüber hinaus können zwischen benachbarten Regionen keine Löcher vorhanden sein. Wenn freier Speicher vorhanden ist, wird dies durch ein separates Listenelement beschrieben.
struct kheap_entry_t { struct slist_head_t list_head; size_t addr; size_t size; bool is_buzy; } attribute(packed); struct kheap_entry_t kheap_blocks[KHEAP_MAX_ENTRIES]; struct slist_definition_t kheap_list = { .head = null, .tail = null, .slot_size = sizeof(struct kheap_entry_t), .slots = KHEAP_MAX_ENTRIES, .base = (size_t)kheap_blocks };
Am Anfang der kheap_entry_t-Struktur steht slist_head_t, mit der Sie den Heap-Datensatztyp problemlos in ein Listenelement umwandeln können.
Betrachten Sie nun den einfachsten Heap-Allokator-Algorithmus (kmalloc):
- Wenn keine freien Blöcke vorhanden sind, wählen Sie einen neuen Block mit der erforderlichen Größe aus (jedoch nicht mehr als die maximale Heap-Größe).
- Ansonsten schauen wir uns alle freien Blöcke an. Wenn wir einen freien Block finden, schauen Sie sich seine Größe an. Wenn es reicht, nimm es. Wenn es einen Überschuss gibt, erstellen Sie einen neuen leeren Block mit der Größe des Überschusses. Andernfalls erweitern wir, wenn es nicht ausreicht und letzteres, die Heap-Grenze (aber nicht mehr als das Maximum).
Der speicherfreie Algorithmus ist ähnlich (kfree):
- Suchen Sie einen Block, dessen Adresse mit der freizugebenden Adresse beginnt. Überprüfen Sie, ob er beschäftigt ist. Als frei markieren.
- Wenn es rechts oder links einen freien Nachbarn gibt, der sich in einem Block mit ihm vereinigt.
Der nächste Artikel befasst sich mit der Implementierung des Heap-Algorithmus.
Schreiben wir den einfachsten Scheduler. Die Aufgabe sieht folgendermaßen aus:
struct task_t { struct clist_head_t list_head; u_short tid; struct gp_registers_t gp_registers; struct op_registers_t op_registers; struct flags_t flags; u_int time; bool reschedule; u_short status; int msg_count_in; struct message_t msg_buff[TASK_MSG_BUFF_SIZE]; void *kstack; void *ustack; } attribute(packed); struct clist_definition_t task_list = { .head = null, .slot_size = sizeof(struct task_t) };
Wir wissen bereits, wie man Speicher zuweist und Aufgaben daher als Ringliste organisiert. Daher ist es bequemer, die folgende Aufgabe relativ zur aktuellen Aufgabe mit dem gewünschten Status zu übernehmen (wir verwenden den Status TASK_RUNNING, wenn die Aufgabe ausgeführt wird). Zunächst gehen wir davon aus, dass alle Aufgaben im Kernel-Schutzring ausgeführt werden (es ist einfacher, den Scheduler zu debuggen) und mit einem Stapel auskommen. In Zukunft werden wir TSS befestigen.
Wir haben die Aufgaben erledigt, jetzt die Planung selbst. Fügen Sie dem IDT einen Timer-Handler hinzu und aktivieren Sie die Unterbrechung der gewünschten IRQ-Leitung im PIC. Durch Unterbrechen des Timers (und am Ende des Kernel-Initialisierungscodes) übertragen wir die Steuerung an den Scheduler und übergeben die Rücksprungadresse und die Adresse zuvor gespeicherter Register aus dem Timer-Interrupt:
/* * Handle IRQ0 * void asm_ih_timer(unsigned long *addr) */ asm_ih_timer: cli pushal mov %esp,%ebp mov %ebp,%ebx pushl %ebx # ® add $32,%ebx pushl %ebx # &ret addr call ih_timer mov %ebp,%esp popal sti iretl
Im Scheduler prüfen wir, ob zu diesem Zeitpunkt eine Aufgabe ausgeführt wurde. In diesem Fall erhöhen wir den Zähler für die Ausführungszeit und prüfen, ob das Kontingent überschritten wird. Wenn nicht, kehren Sie ruhig zurück. Wenn dies überschritten wird, müssen Sie die Aufgabe neu planen. Wir speichern seinen Status (die Adresse der gespeicherten Register und die im Stapel gespeicherte Rücksprungadresse durch Unterbrechen des Timers sind praktisch).
current_task->op_registers.eip = *ret_addr; current_task->op_registers.cs = *(u16 *)((size_t)ret_addr + 4); *(u32 *)(¤t_task->flags) = *(u32 *)((size_t)ret_addr + 6) | 0x200; current_task->op_registers.u_esp = (size_t)ret_addr + 12; current_task->gp_registers.esp = current_task->op_registers.u_esp; memcpy(¤t_task->gp_registers, (void *)reg_addr, sizeof(struct gp_registers_t));
Wir nehmen die Stapeladresse der neuen Aufgabe und bilden dort einen Rückgaberahmen für den Befehl iret. Dann rufen wir die Assembler-Funktion auf, um den Kontext zu wechseln.
next_task->op_registers.u_esp -= 4; *(u32 *)(next_task->op_registers.u_esp) = (*(u16 *)(&next_task->flags)) | 0x200; next_task->op_registers.u_esp -= 4; *(u32 *)(next_task->op_registers.u_esp) = next_task->op_registers.cs; next_task->op_registers.u_esp -= 4; *(u32 *)(next_task->op_registers.u_esp) = next_task->op_registers.eip; next_task->gp_registers.esp = next_task->op_registers.u_esp; next_task->op_registers.u_esp -= sizeof(struct gp_registers_t); memcpy((void *)next_task->op_registers.u_esp, (void *)&next_task->gp_registers, sizeof(struct gp_registers_t)); current_task = next_task; asm_switch_context(next_task->op_registers.u_esp);
Der Kontextwechsel selbst sieht folgendermaßen aus:
# # Switch context # void asm_switch_context(u32 esp) # asm_switch_context: mov 4(%esp),%ebp # ebp = esp mov %ebp,%esp popal sti iretl
Der Scheduler ist bereit.
Den vollständigen Code finden Sie
im Video-Tutorial zum Artikel. Es berücksichtigt nicht nur den Scheduler, sondern auch den Prozess des Ladens, Kompilierens und Konfigurierens von IDT für diejenigen, die vergessen haben: