Unix-ähnliches Betriebssystemdesign - Virtueller Adressraum (6)

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.

 /* * Api - init kernel page directory * Here assumed each entry addresses 4Kb */ extern void mmu_init() { memset(&kpage_directory, 0, sizeof(struct page_directory_entry_t)); /* set kernel page directory */ 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; /* 4KB */ kpage_directory.present = 1; /* kernel pages always in memory */ kpage_directory.read_write = 1; /* read & write */ kpage_directory.user_supervisor = 1; /* kernel mode pages */ kpage_directory.write_through = 1; kpage_directory.page_table_addr = (size_t)kpage_table >> 12; /* set kernel table */ 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; /* kernel pages always in memory */ kpage_table[i].read_write = 1; /* read & write */ kpage_table[i].user_supervisor = 1; /* kernel mode pages */ kpage_table[i].write_through = 1; kpage_table[i].page_phys_addr = (i * 4096) >> 12; /* assume 4Kb pages */ } } 

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:

 /* * Api - Create user page directory */ 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; /* 4KB */ upage_dir->present = 1; upage_dir->read_write = 1; /* read & write */ upage_dir->user_supervisor = 0; /* user mode pages */ upage_dir->write_through = 1; upage_dir->page_table_addr = (size_t)page_table >> 12; /* assume 4Kb pages */ 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.

 /* * Api - Create user page table */ 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); /* share kernel pages */ memcpy(upage_table, kpage_table, sizeof(struct page_table_entry_t) * MMU_KERNEL_PAGES_COUNT); /* fill user pages */ 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; /* not present as so as there is no user pages yet */ current->read_write = 1; /* read & write */ current->user_supervisor = 0; /* user mode page */ current->write_through = 1; current->page_phys_addr = 0; /* page is not present */ } 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:

 /* * Api - Occupy user page */ 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) { /* page is buzy */ 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; /* read & write */ current->user_supervisor = 0; /* user mode page */ current->write_through = 1; current->page_phys_addr = (size_t)phys_addr >> 12; /* assume 4Kb pages */ 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.

 /* * Api - allocate pages */ extern void* mm_phys_alloc_pages(u_int count) { /* find free pages */ 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) { /* occupy */ 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; } /* * Api - free page */ 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); /* find page */ for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { size_t addr = mm_get_addr(i); if (addr == address) { /* free pages */ 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.

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


All Articles