Première partie
Deuxième partie
Le sujet de la conversation d'aujourd'hui est de travailler avec la mémoire. Je vais parler de l'initialisation du répertoire des pages, du mappage de la mémoire physique, de la gestion du virtuel et de mon tas d'organisation pour l'allocateur.
Comme je l'ai dit dans le premier article, j'ai décidé d'utiliser des pages de 4 Mo pour me simplifier la vie et ne pas avoir à gérer de tableaux hiérarchiques. À l'avenir, j'espère aller sur des pages de 4 Ko, comme la plupart des systèmes modernes. Je pourrais en utiliser un prêt à l'emploi (par exemple, un tel allocateur de blocs ), mais écrire le mien était un peu plus intéressant et je voulais comprendre un peu plus comment vit la mémoire, alors j'ai quelque chose à vous dire.
La dernière fois que j'ai opté pour la méthode setup_pd dépendante de l'architecture et que je voulais continuer, il y avait un autre détail que je n'avais pas couvert dans l'article précédent - la sortie VGA utilisant Rust et la macro println standard. Étant donné que son implémentation est triviale, je vais le supprimer sous le spoiler. Le code est dans le package de débogage.
Impression macro#[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;
Maintenant, avec une conscience claire, je reviens à la mémoire.
Initialisation du répertoire de pages
Notre méthode kmain a pris trois arguments en entrée, dont l'un est l'adresse virtuelle de la table des pages. Pour l'utiliser ultérieurement pour l'allocation et la gestion de la mémoire, vous devez désigner la structure des enregistrements et des répertoires. Pour x86, le répertoire Page et la table Page sont assez bien décrits, je vais donc me limiter à une petite introduction. L'entrée du répertoire Page est une structure de taille de pointeur, pour nous, elle est de 4 octets. La valeur contient une adresse physique de 4 Ko de la page. L'octet le moins significatif de l'enregistrement est réservé aux drapeaux. Le mécanisme de conversion d'une adresse virtuelle en adresse physique ressemble à ceci (dans le cas de ma granularité de 4 Mo, le décalage se produit sur 22 bits. Pour les autres granularités, le décalage sera différent et des tableaux hiérarchiques seront utilisés!):
Adresse virtuelle 0xC010A110 -> Obtenez l'index dans le répertoire en déplaçant l'adresse 22 bits vers la droite -> index 0x300 -> Obtenez l'adresse physique de la page par l'index 0x300, vérifiez les drapeaux et l'état -> 0x1000000 -> Prenez les 22 bits inférieurs de l'adresse virtuelle comme décalage, ajoutez à l'adresse physique de la page -> 0x1000000 + 0x10A110 = adresse physique en mémoire 0x110A110
Pour accélérer l'accès, le processeur utilise TLB - tampon de traduction de cache, qui met en cache les adresses de page.
Alors, voici comment mon répertoire et ses entrées sont décrits, et la méthode très setup_pd est implémentée. Pour écrire une page, la méthode «constructeur» est implémentée, qui garantit l'alignement de 4 Ko et la définition des drapeaux, ainsi qu'une méthode pour obtenir l'adresse physique de la page. Un répertoire n'est qu'un tableau de 1024 entrées de quatre octets. Le répertoire peut associer une adresse virtuelle à une page à l'aide de la méthode set_by_addr.
#[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); }
J'ai très maladroitement fait de l'initialisation statique initiale une adresse inexistante, donc je vous serais reconnaissant de m'écrire comment il est habituel dans la communauté Rust de faire de telles initialisations avec une réaffectation de liens.
Maintenant que nous pouvons gérer les pages à partir de code de haut niveau, nous pouvons passer à la compilation de l'initialisation de la mémoire. Cela se fera en deux étapes: par le traitement de la carte mémoire physique et l'initialisation du gestionnaire virtuel
match mb_magic { 0x2BADB002 => { println!("multibooted v1, yeah, reading mb info"); boot::init_with_mb1(mb_pointer); }, . . . . . . } memory::init();
Carte mémoire GRUB et carte mémoire physique OS1
Afin d'obtenir une carte mémoire de GRUB, au démarrage, j'ai mis le drapeau correspondant dans l'en-tête, et GRUB m'a donné l'adresse physique de la structure. Je l'ai porté de la documentation officielle à la notation Rust, et j'ai également ajouté des méthodes pour itérer confortablement sur la carte mémoire. La majeure partie de la structure GRUB ne sera pas remplie, et à ce stade, elle n'est pas très intéressante pour moi. L'essentiel est que je ne souhaite pas déterminer manuellement la quantité de mémoire disponible.
Lors de l'initialisation via Multiboot, nous convertissons d'abord l'adresse physique en virtuelle. En théorie, GRUB peut positionner la structure n'importe où, donc si l'adresse s'étend au-delà de la page, vous devez allouer une page virtuelle dans le répertoire Page. En pratique, la structure se situe presque toujours à côté du premier mégaoctet, que nous avons déjà alloué au démarrage. Au cas où, nous vérifions l'indicateur de présence de la carte mémoire et procédons à son analyse.
pub mod multiboot2; pub mod multiboot; use super::arch; unsafe fn process_pointer(mb_pointer: usize) -> usize {
Une carte mémoire est une liste liée pour laquelle l'adresse physique initiale est spécifiée dans la structure de base (n'oubliez pas de tout traduire en virtuelle) et la taille du tableau en octets. Vous devez parcourir la liste en fonction de la taille de chaque élément, car en théorie, leurs tailles peuvent différer. Voici à quoi ressemble l'itération:
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) } }
Lors de l'analyse d'une carte mémoire, nous parcourons la structure GRUB et la convertissons en bitmap, avec lequel OS1 travaillera pour gérer la mémoire physique. J'ai décidé de me limiter à un petit ensemble de valeurs disponibles pour le contrôle - libre, occupé, réservé, indisponible, bien que GRUB et BIOS fournissent plus d'options. Ainsi, nous parcourons les entrées de la carte et convertissons leur état des valeurs GRUB / BIOS en valeurs pour 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 } }
La mémoire physique est gérée dans le module memory :: physical, pour lequel nous appelons la méthode map ci-dessus, en lui passant l'adresse de la région, sa longueur et son état. Les 4 Go de mémoire potentiellement disponibles pour le système et divisés en quatre pages mégaoctets sont représentés par deux bits dans une image bitmap, ce qui vous permet de stocker 4 états pour 1024 pages. Au total, cette construction prend 256 octets. Un bitmap entraîne une terrible fragmentation de la mémoire, mais il est compréhensible et facile à implémenter, ce qui est la principale chose à faire.
Je vais supprimer l'implémentation bitmap sous le spoiler afin de ne pas encombrer l'article. La structure est capable de compter le nombre de classes et de mémoire libre, de marquer les pages par index et adresse, et également de rechercher des pages libres (cela sera nécessaire à l'avenir pour implémenter le tas). La carte elle-même est un tableau de 64 éléments u32, pour isoler les deux bits (blocs) nécessaires, la conversion en soi-disant bloc (index dans le tableau, emballage de 16 blocs) et bloc (position du bit dans le bloc) est utilisée.
Bitmap de mémoire physique 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 {
Et maintenant, nous sommes arrivés à l'analyse d'un élément de la carte. Si un élément de carte décrit une zone mémoire inférieure à une page de 4 Mo ou égale à celle-ci, nous marquons cette page dans son ensemble. Si plus - battez en morceaux de 4 Mo et marquez chaque morceau séparément par récursivité. Au stade de l'initialisation du bitmap, nous considérons toutes les sections de mémoire inaccessibles, de sorte que lorsque la carte s'épuise, par exemple à 128 Mo, les sections restantes sont marquées comme inaccessibles.
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) {
Tas et gestion d'elle
La gestion de la mémoire virtuelle est actuellement limitée à la gestion de tas, car le noyau n'en sait pas beaucoup plus. À l'avenir, bien sûr, il sera nécessaire de gérer toute la mémoire, et ce petit gestionnaire sera réécrit. Cependant, pour le moment, tout ce dont j'ai besoin est de la mémoire statique, qui contient le code exécutable et la pile, et de la mémoire de tas dynamique, où j'allouerai les structures pour le multithreading. Nous allouons de la mémoire statique au démarrage (et jusqu'à présent, nous avons limité 4 Mo, car le noyau y tient) et en général, il n'y a plus de problème maintenant. De plus, à ce stade, je n'ai pas d'appareils DMA, donc tout est extrêmement simple, mais compréhensible.
J'ai donné 512 Mo de l'espace mémoire du noyau le plus élevé (0xE0000000) au tas, je stocke la carte d'utilisation du tas (0xDFC00000) 4 Mo plus bas. J'utilise un bitmap pour décrire l'état, tout comme pour la mémoire physique, mais il n'y a que 2 états - occupé / libre. La taille du bloc de mémoire est de 64 octets - c'est beaucoup pour de petites variables comme u32, u8, mais, peut-être, c'est optimal pour stocker des structures de données. Pourtant, il est peu probable que nous ayons besoin de stocker des variables uniques sur le tas, maintenant son objectif principal est de stocker des structures de contexte pour le multitâche.
Les blocs de 64 octets sont regroupés en structures qui décrivent l'état d'une page entière de 4 Mo, afin que nous puissions allouer des quantités de mémoire petites et grandes à plusieurs pages. J'utilise les termes suivants: bloc - 64 octets, pack - 2 Ko (un u32 - 64 octets * 32 bits par package), page - 4 Mo.
#[repr(packed)] #[derive(Copy, Clone)] struct HeapPageInfo {
Lorsque je demande de la mémoire à un allocateur, je considère trois cas, selon la granularité:
- Une demande de mémoire inférieure à 2 Ko est venue de l'allocateur. Vous devez trouver un pack dans lequel il sera libre [taille / 64, tout reste différent de zéro ajoute un] morceaux consécutifs, marquez ces morceaux comme occupés, renvoyez l'adresse du premier morceau.
- Une demande est venue de l'allocateur pour une mémoire inférieure à 4 Mo, mais supérieure à 2 Ko. Vous devez trouver une page qui a gratuitement [taille / 2048, tout reste différent de zéro ajoute un] packs d'affilée. Marquez les packs [taille / 2048] comme occupés; s'il y a un reste, marquez les morceaux [restants] dans le dernier pack comme occupés.
- Une demande de mémoire de plus de 4 Mo est venue de l'allocateur. Recherchez [taille / 4 Mi, tout solde différent de zéro ajoute une] pages de suite, marquez [taille / 4 Mi] pages comme occupées, s'il y a un solde - marquez [packs] comme étant occupé. Dans le dernier pack, marquez le reste des morceaux comme occupé.
La recherche de zones libres dépend également de la granularité - un tableau est sélectionné pour l'itération ou les masques de bits. Chaque fois que vous partez à l'étranger, OOM arrive. Lors de la désallocation, un algorithme similaire est utilisé, uniquement pour le marquage libéré. La mémoire libérée n'est pas réinitialisée. Tout le code est gros, je vais le mettre sous le spoiler.
Bitmap de mémoire virtuelle Allocation et erreur de page
Pour utiliser le tas, vous avez besoin d'un allocateur. L'ajout nous ouvrira un vecteur, des arbres, des tables de hachage, des boîtes et plus, sans lesquels il est presque impossible de vivre. Dès que nous brancherons le module alloc et déclarerons un allocateur global, la vie deviendra immédiatement plus facile.
La mise en œuvre de l'allocateur est très simple - elle se réfère simplement au mécanisme décrit ci-dessus.
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); } }
L'allocateur dans lib.rs est activé comme suit:
#![feature(alloc, alloc_error_handler)] extern crate alloc; #[global_allocator] static ALLOCATOR: memory::allocate::Os1Allocator = memory::allocate::Os1Allocator;
Et lorsque nous essayons de nous allouer simplement de cette manière, nous obtenons une exception de défaut de page, car nous n'avons pas encore défini l'allocation de mémoire virtuelle. Eh bien, comment ça! Eh bien, vous devez revenir à la matière de l'article précédent et ajouter des exceptions. J'ai décidé d'implémenter l'allocation différée de mémoire virtuelle, c'est-à -dire que la page a été allouée non pas au moment de la demande de mémoire, mais au moment d'une tentative d'accès. Heureusement, le processeur x86 le permet et l'encourage même. 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 — , — , !
, .
Merci de votre attention!