Im vorherigen Artikel haben wir die Grundlagen der Arbeit im IA-32-geschützten Modus untersucht. Heute ist es Zeit zu lernen, wie man mit virtuellem Adressraum arbeitet.
Inhaltsverzeichnis
Build-System (make, gcc, gas). Erster Start (Multiboot). Starten Sie (qemu). C-Bibliothek (strcpy, memcpy, strext).
C-Bibliothek (sprintf, strcpy, strcmp, strtok, va_list ...). Erstellen der Bibliothek im Kernelmodus und im Benutzeranwendungsmodus.
Das Kernel-Systemprotokoll. Videospeicher Ausgabe an das Terminal (kprintf, kpanic, kassert).
Dynamischer Speicher, Heap (kmalloc, kfree).
Organisation der Speicher- und Interrupt-Behandlung (GDT, IDT, PIC, Syscall). Ausnahmen
Virtueller Speicher (Seitenverzeichnis und Seitentabelle).Prozess. Planer Multitasking. Systemaufrufe (kill, exit, ps).
Das Dateisystem des Kernels (initrd), elf und seiner Interna. Systemaufrufe (exec).
Zeichengerätetreiber. Systemaufrufe (ioctl, fopen, fread, fwrite). C-Bibliothek (fopen, fclose, fprintf, fscanf).
Shell als komplettes Programm für den Kernel.
Benutzerschutzmodus (Ring3). Aufgabenstatussegment (tss).
Virtueller Speicher
Virtueller Speicher wird benötigt, damit jeder Prozess von einem anderen isoliert werden kann, d.h. konnte ihn nicht aufhalten. Wenn es keinen virtuellen Speicher gäbe, müssten wir jedes Mal Elf-Dateien an verschiedenen Adressen im Speicher laden. Wie Sie wissen, können ausführbare Dateien Links zu bestimmten Adressen enthalten (absolut). Daher ist beim Kompilieren von elf bereits bekannt, an welcher Adresse im Speicher es geladen wird (siehe Linker-Skript). Daher können wir keine zwei Elf-Dateien ohne virtuellen Speicher laden. Aber auch wenn der virtuelle Speicher aktiviert ist, gibt es dynamische Bibliotheken (wie z. B. .so), die an jeder Adresse geladen werden können. Sie können an jeder Adresse heruntergeladen werden, da sie über einen Umzugsbereich verfügen. In diesem Abschnitt werden alle Stellen registriert, an denen die absolute Adressierung verwendet wird, und der Kernel muss beim Laden einer solchen Elf-Datei diese Adressen mit Stiften reparieren, d. H. Fügen Sie ihnen den Unterschied zwischen der tatsächlichen und der gewünschten Download-Adresse hinzu.
Wir werden in Betracht ziehen, mit 4-Kilobyte-Seiten zu arbeiten. In dieser Situation können wir bis zu 4 Megabyte RAM adressieren. Das reicht uns. Die Adresskarte sieht folgendermaßen aus:
0-1 mb : nicht berühren.
1-2 mb : Code- und Kerneldaten.
2-3 mb : ein Haufen Kernel.
3-4 mb : Benutzerdefinierte Seiten mit hochgeladenen Elf-Dateien.
Die lineare Adresse (vom flachen Modell erhalten), wenn das Seiten-Paging aktiviert ist, entspricht nicht der physischen Adresse. Stattdessen wird die Adresse durch den Offset (niedrige Bits), den Index der Einträge in der Seitentabelle und den Index des Seitenverzeichnisses (hohe Bits) geteilt. Jeder Prozess verfügt über ein eigenes Seitenverzeichnis und entsprechend über Seitentabellen. Auf diese Weise können Sie einen virtuellen Adressraum organisieren.
Der Seitenverzeichniseintrag sieht folgendermaßen aus:
struct page_directory_entry_t { u8 present : 1; u8 read_write : 1; u8 user_supervisor : 1; u8 write_through : 1; u8 cache_disabled : 1; u8 accessed : 1; u8 zero : 1; u8 page_size : 1; u8 ignored : 1; u8 available : 3; u32 page_table_addr : 20; } attribute(packed);
Das Seitentabellenelement sieht folgendermaßen aus:
struct page_table_entry_t { u8 present : 1; u8 read_write : 1; u8 user_supervisor : 1; u8 write_through : 1; u8 cache_disabled : 1; u8 accessed : 1; u8 dirty : 1; u8 zero : 1; u8 global : 1; u8 available : 3; u32 page_phys_addr : 20; } attribute(packed);
Für den Kernel beschreiben wir das Seitenverzeichnis und die Seitentabelle als statische Variablen. Es ist wahr, dass sie am Seitenrand ausgerichtet sein müssen.
static struct page_directory_entry_t kpage_directory attribute(aligned(4096)); static struct page_table_entry_t kpage_table[MMU_PAGE_TABLE_ENTRIES_COUNT] attribute(aligned(4096));
Wir werden den einfachen Weg gehen und den gesamten physischen Adressraum dem Kernel zugänglich machen. Die Berechtigungsstufe der Kernelseiten sollte ein Supervisor sein, damit niemand darauf zugreifen kann. Leichte Prozesse, die im Kernelmodus ausgeführt werden müssen, teilen sich den gleichen Adressraum mit dem Kernel. Mit diesem Prozess haben wir eine Warteschlange für ausstehende Ausführungen, um ausstehende Interrupts zu verarbeiten. Aber mehr dazu in der Lektion über Charaktergerätetreiber. Wir haben Multitasking noch nicht realisiert, bevor wir uns mit diesem Thema befassen.
Erstellen Sie ein Verzeichnis mit Kernelseiten und eine entsprechende Seitentabelle. Wenn der Kernel initialisiert wird, ist er aktiv.
extern void mmu_init() { memset(&kpage_directory, 0, sizeof(struct page_directory_entry_t)); kpage_directory.zero = 1; kpage_directory.accessed = 0; kpage_directory.available = 0; kpage_directory.cache_disabled = 0; kpage_directory.ignored = 0; kpage_directory.page_size = 0; kpage_directory.present = 1; kpage_directory.read_write = 1; kpage_directory.user_supervisor = 1; kpage_directory.write_through = 1; kpage_directory.page_table_addr = (size_t)kpage_table >> 12; for (int i = 0; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { kpage_table[i].zero = 0; kpage_table[i].accessed = 0; kpage_table[i].available = 0; kpage_table[i].cache_disabled = 0; kpage_table[i].dirty = 0; kpage_table[i].global = 1; kpage_table[i].present = 1; kpage_table[i].read_write = 1; kpage_table[i].user_supervisor = 1; kpage_table[i].write_through = 1; kpage_table[i].page_phys_addr = (i * 4096) >> 12; } }
Wenn wir die Elf-Dateien hochladen, müssen wir ein Seitenverzeichnis für den Benutzerprozess erstellen. Sie können dies mit der folgenden Funktion tun:
extern struct page_directory_entry_t* mmu_create_user_page_directory(struct page_table_entry_t* page_table) { struct page_directory_entry_t* upage_dir; upage_dir = malloc_a(sizeof(struct page_directory_entry_t), 4096); upage_dir->zero = 1; upage_dir->accessed = 0; upage_dir->available = 0; upage_dir->cache_disabled = 0; upage_dir->ignored = 0; upage_dir->page_size = 0; upage_dir->present = 1; upage_dir->read_write = 1; upage_dir->user_supervisor = 0; upage_dir->write_through = 1; upage_dir->page_table_addr = (size_t)page_table >> 12; return upage_dir; }
Standardmäßig enthält die Prozessseitentabelle Kernelseiten und leere Einträge für zukünftige Prozessseiten, d. H. Datensätze mit gelöschtem aktuellen Flag und der Adresse der physischen Seite bei 0.
extern struct page_table_entry_t* mmu_create_user_page_table() { struct page_table_entry_t* upage_table; upage_table = malloc_a(sizeof(struct page_table_entry_t) * MMU_PAGE_TABLE_ENTRIES_COUNT, 4096); memcpy(upage_table, kpage_table, sizeof(struct page_table_entry_t) * MMU_KERNEL_PAGES_COUNT); for (int i = MMU_KERNEL_PAGES_COUNT; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { struct page_table_entry_t* current; current = upage_table + i; current->zero = 0; current->accessed = 0; current->available = 0; current->cache_disabled = 0; current->dirty = 0; current->global = 1; current->present = 0; current->read_write = 1; current->user_supervisor = 0; current->write_through = 1; current->page_phys_addr = 0; } return upage_table; }
Wir müssen lernen, wie neue physische Seiten zur Prozessseitentabelle hinzugefügt werden, da standardmäßig keine vorhanden sind. Wir werden dies benötigen, wenn wir Elf-Dateien in den Speicher laden, wenn wir Segmente laden, die in Programm-Headern beschrieben sind. Die Funktion hilft uns dabei:
extern bool mmu_occupy_user_page(struct page_table_entry_t* upage_table, void* phys_addr) { for (int i = MMU_KERNEL_PAGES_COUNT; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { struct page_table_entry_t* current; current = upage_table + i; if (current->present) { continue; } current->zero = 0; current->accessed = 0; current->available = 0; current->cache_disabled = 0; current->dirty = 0; current->global = 1; current->present = 1; current->read_write = 1; current->user_supervisor = 0; current->write_through = 1; current->page_phys_addr = (size_t)phys_addr >> 12; return true; } return false; }
Der Paging-Modus wird im Prozessor-Flag-Register um ein Bit ein- und ausgeschaltet.
/* * Enable paging * void asm_enable_paging(void *page_directory) */ asm_enable_paging: mov 4(%esp),%eax # page_directory mov %eax,%cr3 mov %cr0,%eax or $0x80000001,%eax # set PE & PG bits mov %eax,%cr0 ret /* * Disable paging * void asm_disable_paging() */ asm_disable_paging: mov %eax,%cr3 mov %cr0,%eax xor $0x80000000,%eax # unset PG bit mov %eax,%cr0 ret
Nachdem wir gelernt haben, wie der Adressraum von Prozessen erstellt wird, müssen wir die physischen Seiten irgendwie verwalten, welche beschäftigt und welche frei sind. Hierfür gibt es einen Bitmap-Mechanismus, ein Bit pro Seite. Wir werden keine Seiten bis zum 3. Megabyte beschreiben, da sie zum Kernel gehören und immer beschäftigt sind. Wir beginnen mit der Auswahl von Benutzerseiten von 3 bis 4 Megabyte.
static u32 bitmap[MM_BITMAP_SIZE];
Physische Seiten werden gemäß den folgenden Funktionen zugewiesen und freigegeben. Tatsächlich finden wir einfach das gewünschte Bit in der Karte an der physischen Adresse der Seite und umgekehrt. Der Nachteil ist, dass wir eine begrenzte Speicherzellengröße haben, sodass Sie zwei Koordinaten verwenden müssen: Bytenummer und Bitnummer.
extern void* mm_phys_alloc_pages(u_int count) { for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { bool is_found = true; for (int j = 0; j < count; ++j) { is_found = is_found && !mm_get_bit(i + j); } if (is_found) { for (int j = 0; j < count; ++j) { assert(!mm_get_bit(i + j)); mm_set_bit(i + j); } return (void *)mm_get_addr(i); } } return null; } extern bool mm_phys_free_pages(void* ptr, u_int count) { size_t address = (size_t)ptr; assert(address >= MM_AREA_START); assert(address % MM_PAGE_SIZE == 0); for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { size_t addr = mm_get_addr(i); if (addr == address) { for (int j = 0; j < count; ++j) { assert(mm_get_bit(i + j)); mm_clear_bit(i + j); } return true; } } return false; }
Dies reicht völlig aus, um die vollständige Unterstützung des virtuellen Speichers in Ihrem Kernel einzuführen.
Referenzen
Details und Erklärungen im
Video-Tutorial .
Der Quellcode
im Git-Repository (Sie benötigen den Lektion6-Zweig).
Referenzliste
1. James Molloy. Rollen Sie Ihr eigenes UNIX-Klon-Betriebssystem.
2. Zähne. Assembler für DOS, Windows, Unix
3. Kalaschnikow. Assembler ist einfach!
4. Tanenbaum. Betriebssysteme. Implementierung und Entwicklung.
5. Robert Love. Linux-Kernel Beschreibung des Entwicklungsprozesses.