Erster Teil
Zweiter Teil
Das Thema des heutigen Gesprächs ist die Arbeit mit dem Gedächtnis. Ich werde über das Initialisieren des Seitenverzeichnisses, das Zuordnen des physischen Speichers, das Verwalten des virtuellen und meines Organisationsheaps für den Allokator sprechen.
Wie ich im ersten Artikel sagte, habe ich beschlossen, 4 MB Seiten zu verwenden, um mein Leben zu vereinfachen und mich nicht mit hierarchischen Tabellen befassen zu müssen. Ich hoffe, dass ich in Zukunft wie bei den meisten modernen Systemen auf 4-KB-Seiten gehen kann. Ich könnte ein fertiges verwenden (zum Beispiel einen solchen Blockzuweiser ), aber mein eigenes zu schreiben war etwas interessanter und ich wollte ein bisschen mehr verstehen, wie das Gedächtnis lebt, also habe ich Ihnen etwas zu sagen.
Als ich mich das letzte Mal für die architekturabhängige Methode setup_pd entschieden habe und damit fortfahren wollte, gab es jedoch ein weiteres Detail, das ich im vorherigen Artikel nicht behandelt habe - die VGA-Ausgabe mit Rust und dem Standard-Println-Makro. Da seine Implementierung trivial ist, werde ich es unter dem Spoiler entfernen. Der Code befindet sich im Debug-Paket.
Makrodruck#[macro_export] macro_rules! print { ($($arg:tt)*) => ($crate::debug::_print(format_args!($($arg)*))); } #[macro_export] macro_rules! println { () => ($crate::print!("\n")); ($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*))); } #[cfg(target_arch = "x86")] pub fn _print(args: core::fmt::Arguments) { use core::fmt::Write; use super::arch::vga; vga::VGA_WRITER.lock().write_fmt(args).unwrap(); } #[cfg(target_arch = "x86_64")] pub fn _print(args: core::fmt::Arguments) { use core::fmt::Write; use super::arch::vga;
Jetzt kehre ich mit gutem Gewissen in die Erinnerung zurück.
Seitenverzeichnis-Initialisierung
Unsere kmain-Methode hat drei Argumente als Eingabe verwendet, von denen eines die virtuelle Adresse der Seitentabelle ist. Um es später für die Zuordnung und Speicherverwaltung zu verwenden, müssen Sie die Struktur von Datensätzen und Verzeichnissen festlegen. Für x86 sind das Seitenverzeichnis und die Seitentabelle recht gut beschrieben, daher beschränke ich mich auf eine kleine Einführung. Der Seitenverzeichniseintrag ist eine Zeigergrößenstruktur, für uns sind es 4 Bytes. Der Wert enthält eine physikalische Adresse von 4 KB der Seite. Das niedrigstwertige Byte des Datensatzes ist für Flags reserviert. Der Mechanismus zum Konvertieren einer virtuellen Adresse in eine physische Adresse sieht folgendermaßen aus (bei meiner 4-MB-Granularität erfolgt die Verschiebung um 22 Bit. Bei anderen Granularitäten ist die Verschiebung unterschiedlich und es werden hierarchische Tabellen verwendet!):
Virtuelle Adresse 0xC010A110 -> Holen Sie sich den Index in das Verzeichnis, indem Sie die Adresse 22 Bit nach rechts verschieben -> Index 0x300 -> Holen Sie sich die physische Adresse der Seite durch Index 0x300, überprüfen Sie Flags und Status -> 0x1000000 -> Nehmen Sie die unteren 22 Bits der virtuellen Adresse als Offset, fügen Sie hinzu an die physikalische Adresse der Seite -> 0x1000000 + 0x10A110 = physikalische Adresse im Speicher 0x110A110
Um den Zugriff zu beschleunigen, verwendet der Prozessor TLB - Translation Lookaside Buffer, der Seitenadressen zwischenspeichert.
Hier ist also, wie mein Verzeichnis und seine Einträge beschrieben werden und die Methode setup_pd implementiert ist. Zum Schreiben einer Seite wird die Konstruktormethode implementiert, die die Ausrichtung um 4 KB und das Setzen von Flags garantiert, sowie eine Methode zum Abrufen der physischen Adresse der Seite. Ein Verzeichnis ist nur ein Array von 1024 Vier-Byte-Einträgen. Das Verzeichnis kann einer Seite mithilfe der Methode set_by_addr eine virtuelle Adresse zuordnen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PDirectoryEntry(u32); impl PDirectoryEntry { pub fn by_phys_address(address: usize, flags: PDEntryFlags) -> Self { PDirectoryEntry((address as u32) & ADDRESS_MASK | flags.bits()) } pub fn flags(&self) -> PDEntryFlags { PDEntryFlags::from_bits_truncate(self.0) } pub fn phys_address(&self) -> u32 { self.0 & ADDRESS_MASK } pub fn dbg(&self) -> u32 { self.0 } } pub struct PDirectory { entries: [PDirectoryEntry; 1024] } impl PDirectory { pub fn at(&self, idx: usize) -> PDirectoryEntry { self.entries[idx] } pub fn set_by_addr(&mut self, logical_addr: usize, entry: PDirectoryEntry) { self.set(PDirectory::to_idx(logical_addr), entry); } pub fn set(&mut self, idx: usize, entry: PDirectoryEntry) { self.entries[idx] = entry; unsafe { invalidate_page(idx); } } pub fn to_logical_addr(idx: usize) -> usize { (idx << 22) } pub fn to_idx(logical_addr: usize) -> usize { (logical_addr >> 22) } } use lazy_static::lazy_static; use spin::Mutex; lazy_static! { static ref PAGE_DIRECTORY: Mutex<&'static mut PDirectory> = Mutex::new( unsafe { &mut *(0xC0000000 as *mut PDirectory) } ); } pub unsafe fn setup_pd(pd: usize) { let mut data = PAGE_DIRECTORY.lock(); *data = &mut *(pd as *mut PDirectory); }
Ich habe die anfängliche statische Initialisierung sehr umständlich zu einer nicht vorhandenen Adresse gemacht. Ich wäre Ihnen dankbar, wenn Sie mir schreiben würden, wie es in der Rust-Community üblich ist, solche Initialisierungen mit Neuzuweisung von Links durchzuführen.
Jetzt, da wir Seiten aus Code auf hoher Ebene verwalten können, können wir mit der Kompilierung der Speicherinitialisierung fortfahren. Dies geschieht in zwei Schritten: durch Verarbeiten der physischen Speicherkarte und Initialisieren des virtuellen Managers
match mb_magic { 0x2BADB002 => { println!("multibooted v1, yeah, reading mb info"); boot::init_with_mb1(mb_pointer); }, . . . . . . } memory::init();
GRUB-Speicherkarte und OS1-Speicherkarte
Um eine Speicherkarte von GRUB zu erhalten, habe ich beim Booten das entsprechende Flag im Header gesetzt und GRUB hat mir die physikalische Adresse der Struktur gegeben. Ich habe es aus der offiziellen Dokumentation in die Rust-Notation portiert und Methoden hinzugefügt, um bequem über die Speicherkarte zu iterieren. Der größte Teil der GRUB-Struktur wird nicht gefüllt, und zu diesem Zeitpunkt ist es für mich nicht sehr interessant. Die Hauptsache ist, dass ich die Menge des verfügbaren Speichers nicht manuell bestimmen möchte.
Bei der Initialisierung über Multiboot konvertieren wir zuerst die physische Adresse in eine virtuelle. Theoretisch kann GRUB die Struktur an einer beliebigen Stelle positionieren. Wenn die Adresse über die Seite hinausgeht, müssen Sie eine virtuelle Seite im Seitenverzeichnis zuweisen. In der Praxis liegt die Struktur fast immer neben dem ersten Megabyte, das wir bereits beim Booten zugewiesen haben. Für alle Fälle überprüfen wir das Flag, dass die Speicherkarte vorhanden ist, und fahren mit ihrer Analyse fort.
pub mod multiboot2; pub mod multiboot; use super::arch; unsafe fn process_pointer(mb_pointer: usize) -> usize {
Eine Speicherkarte ist eine verknüpfte Liste, für die die anfängliche physikalische Adresse in der Grundstruktur angegeben ist (vergessen Sie nicht, alles in virtuelle zu übersetzen) und die Größe des Arrays in Bytes. Sie müssen die Liste basierend auf der Größe jedes Elements durchlaufen, da sich ihre Größen theoretisch unterscheiden können. So sieht die Iteration aus:
impl MultibootInfo { . . . . . . pub unsafe fn get_mmap(&self, index: usize) -> Option<*const MemMapEntry> { use crate::arch::get_mb_pointer_base; let base: usize = get_mb_pointer_base(self.mmap_addr as usize); let mut iter: *const MemMapEntry = (base as u32 + self.mmap_addr) as *const MemMapEntry; for _i in 0..index { iter = ((iter as usize) + ((*iter).size as usize) + 4) as *const MemMapEntry; if ((iter as usize) - base) >= (self.mmap_addr + self.mmap_lenght) as usize { return None } else {} } Some(iter) } }
Beim Parsen einer Speicherkarte durchlaufen wir die GRUB-Struktur und konvertieren sie in eine Bitmap, mit der OS1 den physischen Speicher verwaltet. Ich habe mich entschlossen, mich auf einen kleinen Satz verfügbarer Werte für die Steuerung zu beschränken - frei, beschäftigt, reserviert, nicht verfügbar, obwohl GRUB und BIOS mehr Optionen bieten. Also durchlaufen wir die Karteneinträge und konvertieren ihren Status von GRUB / BIOS-Werten in Werte für OS1:
pub fn parse_mmap(mbi: &MultibootInfo) { unsafe { let mut mmap_opt = mbi.get_mmap(0); let mut i: usize = 1; loop { let mmap = mmap_opt.unwrap(); crate::memory::physical::map((*mmap).addr as usize, (*mmap).len as usize, translate_multiboot_mem_to_os1(&(*mmap).mtype)); mmap_opt = mbi.get_mmap(i); match mmap_opt { None => break, _ => i += 1, } } } } pub fn translate_multiboot_mem_to_os1(mtype: &u32) -> usize { use crate::memory::physical::{RESERVED, UNUSABLE, USABLE}; match mtype { &MULTIBOOT_MEMORY_AVAILABLE => USABLE, &MULTIBOOT_MEMORY_RESERVED => UNUSABLE, &MULTIBOOT_MEMORY_ACPI_RECLAIMABLE => RESERVED, &MULTIBOOT_MEMORY_NVS => UNUSABLE, &MULTIBOOT_MEMORY_BADRAM => UNUSABLE, _ => UNUSABLE } }
Der physische Speicher wird im memory :: Physical-Modul verwaltet, für das wir die obige Map-Methode aufrufen und ihm die Adresse, die Länge und den Status der Region übergeben. Alle 4 GB Speicher, die dem System möglicherweise zur Verfügung stehen und in vier Megabyte-Seiten unterteilt sind, werden in einer Bitmap durch zwei Bits dargestellt, sodass Sie 4 Status für 1024 Seiten speichern können. Insgesamt benötigt diese Konstruktion 256 Bytes. Eine Bitmap führt zu einer schrecklichen Speicherfragmentierung, ist aber verständlich und einfach zu implementieren, was für meinen Zweck die Hauptsache ist.
Ich werde die Bitmap-Implementierung unter dem Spoiler entfernen, um den Artikel nicht zu überladen. Die Struktur kann die Anzahl der Klassen und den freien Speicher zählen, Seiten nach Index und Adresse markieren und auch nach freien Seiten suchen (dies wird in Zukunft benötigt, um den Heap zu implementieren). Die Karte selbst ist ein Array von 64 u32-Elementen. Um die erforderlichen zwei Bits (Blöcke) zu isolieren, wird die Umwandlung in den sogenannten Chunk (Index im Array, Packen von 16 Blöcken) und Block (Bitposition im Chunk) verwendet.
Bitmap für den physischen Speicher pub const USABLE: usize = 0; pub const USED: usize = 1; pub const RESERVED: usize = 2; pub const UNUSABLE: usize = 3; pub const DEAD: usize = 0xDEAD; struct PhysMemoryInfo { pub total: usize, used: usize, reserved: usize, chunks: [u32; 64], } impl PhysMemoryInfo {
Und jetzt kommen wir zur Analyse eines Elements der Karte. Wenn ein Kartenelement einen Speicherbereich mit weniger als einer Seite von 4 MB oder mehr beschreibt, markieren wir diese Seite als Ganzes. Wenn mehr - in Stücke von 4 MB schlagen und jedes Stück durch Rekursion separat markieren. In der Phase der Initialisierung der Bitmap betrachten wir alle Speicherabschnitte als unzugänglich, sodass die verbleibenden Abschnitte als unzugänglich markiert werden, wenn die Karte beispielsweise 128 MB leer ist.
use lazy_static::lazy_static; use spin::Mutex; lazy_static! { static ref RAM_INFO: Mutex<PhysMemoryInfo> = Mutex::new(PhysMemoryInfo { total: 0, used: 0, reserved: 0, chunks: [0xFFFFFFFF; 64] }); } pub fn map(addr: usize, len: usize, flag: usize) {
Haufen und sie verwalten
Die Verwaltung des virtuellen Speichers beschränkt sich derzeit nur auf die Heap-Verwaltung, da der Kernel nicht viel mehr weiß. In Zukunft wird es natürlich notwendig sein, den gesamten Speicher zu verwalten, und dieser kleine Manager wird neu geschrieben. Im Moment brauche ich jedoch nur statischen Speicher, der den ausführbaren Code und den Stapel enthält, sowie dynamischen Heap-Speicher, in dem ich die Strukturen für Multithreading zuweisen werde. Wir weisen statischen Speicher beim Booten zu (und bisher haben wir 4 MB begrenzt, weil der Kernel in sie passt) und im Allgemeinen gibt es jetzt keine Probleme damit. Außerdem habe ich zu diesem Zeitpunkt keine DMA-Geräte, daher ist alles sehr einfach, aber verständlich.
Ich habe dem Heap 512 MB des obersten Kernel-Speicherplatzes (0xE0000000) gegeben, ich speichere die Heap-Nutzungszuordnung (0xDFC00000) 4 MB niedriger. Ich benutze eine Bitmap, um den Zustand zu beschreiben, genau wie für den physischen Speicher, aber es gibt nur 2 Zustände darin - beschäftigt / frei. Die Größe des Speicherblocks beträgt 64 Byte - dies ist viel für kleine Variablen wie u32, u8, aber möglicherweise optimal zum Speichern von Datenstrukturen. Es ist jedoch unwahrscheinlich, dass wir einzelne Variablen auf dem Heap speichern müssen. Jetzt besteht der Hauptzweck darin, Kontextstrukturen für Multitasking zu speichern.
Blöcke mit 64 Bytes sind in Strukturen gruppiert, die den Status einer gesamten 4-MB-Seite beschreiben, sodass wir mehreren Seiten sowohl kleine als auch große Speichermengen zuweisen können. Ich verwende die folgenden Begriffe: Chunk - 64 Bytes, Pack - 2 KB (ein U32 - 64 Bytes * 32 Bit pro Paket), Seite - 4 MB.
#[repr(packed)] #[derive(Copy, Clone)] struct HeapPageInfo {
Wenn ich Speicher von einem Allokator anfordere, betrachte ich drei Fälle, abhängig von der Granularität:
- Eine Anforderung für einen Speicher von weniger als 2 KB kam vom Allokator. Sie müssen ein Paket finden, in dem es frei ist [Größe / 64, jeder Rest ungleich Null fügt eins hinzu], Chunks hintereinander markieren, diese Chunks als beschäftigt markieren und die Adresse des ersten Chunks zurückgeben.
- Vom Allokator wurde eine Anforderung für Speicher mit weniger als 4 MB, aber mehr als 2 KB gestellt. Sie müssen eine Seite mit kostenlosen [Größe / 2048, jeder Rest ungleich Null fügt eine hinzu] Packs in einer Reihe finden. Markieren Sie [size / 2048] -Packs als beschäftigt. Wenn ein Rest vorhanden ist, markieren Sie [rest] Chunks im letzten Pack als beschäftigt.
- Eine Anforderung für einen Speicher von mehr als 4 MB kam vom Allokator. Suchen Sie [Größe / 4 Mi, jeder Kontostand ungleich Null fügt eine] Seite in einer Reihe hinzu, markieren Sie [Größe / 4 Mi] Seiten als belegt, wenn ein Kontostand vorhanden ist - markieren Sie [Kontostand] -Pakete als belegt. Markieren Sie in der letzten Packung den Rest der Blöcke als beschäftigt.
Die Suche nach freien Bereichen hängt auch von der Granularität ab - ein Array wird für Iterations- oder Bitmasken ausgewählt. Wann immer Sie ins Ausland gehen, passiert OOM. Bei der Freigabe wird ein ähnlicher Algorithmus verwendet, nur zum Markieren freigegeben. Der freigegebene Speicher wird nicht zurückgesetzt. Der ganze Code ist groß, ich werde ihn unter den Spoiler stellen.
Bitmap für virtuellen Speicher Zuordnung und Seitenfehler
Um den Heap verwenden zu können, benötigen Sie einen Allokator. Wenn wir es hinzufügen, öffnen sich für uns ein Vektor, Bäume, Hash-Tabellen, Kisten und mehr, ohne die es fast unmöglich ist, weiterzuleben. Sobald wir das Allokationsmodul anschließen und einen globalen Allokator deklarieren, wird das Leben sofort einfacher.
Die Implementierung des Allokators ist sehr einfach - sie bezieht sich einfach auf den oben beschriebenen Mechanismus.
use alloc::alloc::{GlobalAlloc, Layout}; pub struct Os1Allocator; unsafe impl Sync for Os1Allocator {} unsafe impl GlobalAlloc for Os1Allocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { use super::logical::{KHEAP_CHUNK_SIZE, allocate_n_chunks}; let size = layout.size(); let mut chunk_count: usize = 1; if size > KHEAP_CHUNK_SIZE { chunk_count = size / KHEAP_CHUNK_SIZE; if KHEAP_CHUNK_SIZE * chunk_count != size { chunk_count += 1; } } allocate_n_chunks(chunk_count, layout.align()) } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { use super::logical::{KHEAP_CHUNK_SIZE, free_chunks}; let size = layout.size(); let mut chunk_count: usize = 1; if size > KHEAP_CHUNK_SIZE { chunk_count = size / KHEAP_CHUNK_SIZE; if KHEAP_CHUNK_SIZE * chunk_count != size { chunk_count += 1; } } free_chunks(ptr as usize, chunk_count); } }
Der Allokator in lib.rs wird wie folgt aktiviert:
#![feature(alloc, alloc_error_handler)] extern crate alloc; #[global_allocator] static ALLOCATOR: memory::allocate::Os1Allocator = memory::allocate::Os1Allocator;
Und wenn wir versuchen, uns nur so zuzuweisen, erhalten wir eine Seitenfehlerausnahme, da wir die Zuweisung des virtuellen Speichers noch nicht ausgearbeitet haben. Nun, wie so! Nun, Sie müssen zum Material des vorherigen Artikels zurückkehren und Ausnahmen hinzufügen. Ich habe mich für die verzögerte Zuweisung des virtuellen Speichers entschieden, dh, die Seite wurde nicht zum Zeitpunkt der Speicheranforderung, sondern zum Zeitpunkt des Zugriffsversuchs zugewiesen. Glücklicherweise erlaubt und fördert der x86-Prozessor dies. Page fault , , , — , , CR2 — , .
, . 32 ( , , 32 ), . Rust. , . , , iret , , Page fault Protection fault. Protection fault — , .
eE_page_fault: pushad mov eax, [esp + 32] push eax mov eax, cr2 push eax call kE_page_fault pop eax pop eax popad add esp, 4 iret
Rust , . , . . .
bitflags! { struct PFErrorCode: usize { const PROTECTION = 1;
, . , . . , . , , :
println!("memory: total {} used {} reserved {} free {}", memory::physical::total(), memory::physical::used(), memory::physical::reserved(), memory::physical::free()); use alloc::vec::Vec; let mut vec: Vec<usize> = Vec::new(); for i in 0..1000000 { vec.push(i); } println!("vec len {}, ptr is {:?}", vec.len(), vec.as_ptr()); println!("Still works, check reusage!"); let mut vec2: Vec<usize> = Vec::new(); for i in 0..10 { vec2.push(i); } println!("vec2 len {}, ptr is {:?}, vec is still here? {}", vec2.len(), vec2.as_ptr(), vec.get(1000).unwrap()); println!("Still works!"); println!("memory: total {} used {} reserved {} free {}", memory::physical::total(), memory::physical::used(), memory::physical::reserved(), memory::physical::free());
:

, , . 3,5 + 3 , . 3,5 .
IRQ 1 — Alt + PrntScrn :)
, , Rust — , — , !
, .
Vielen Dank für Ihre Aufmerksamkeit!