Primera parte
Segunda parte
El tema de la conversación de hoy es trabajar con la memoria. Hablaré sobre la inicialización del directorio de la página, el mapeo de la memoria física, la administración virtual y el montón de mi organización para el asignador.
Como dije en el primer artículo, decidí usar páginas de 4 MB para simplificar mi vida y no tener que lidiar con tablas jerárquicas. En el futuro, espero ir a páginas de 4 KB, como la mayoría de los sistemas modernos. Podría usar uno listo (por ejemplo, un asignador de bloques ), pero escribir el mío fue un poco más interesante y quería entender un poco más cómo vive la memoria, así que tengo algo que decirte.
La última vez que me decidí por el método setup_pd dependiente de la arquitectura y quería continuar con él, sin embargo, había un detalle más que no cubrí en el artículo anterior: salida VGA usando Rust y la macro println estándar. Como su implementación es trivial, la eliminaré debajo del spoiler. El código está en el paquete de depuración.
Macro println#[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;
Ahora, con la conciencia tranquila, vuelvo a la memoria.
Inicialización del directorio de páginas
Nuestro método kmain tomó tres argumentos como entrada, uno de los cuales es la dirección virtual de la tabla de páginas. Para usarlo más tarde para la asignación y la administración de la memoria, debe designar la estructura de registros y directorios. Para x86, el directorio de la página y la tabla de la página se describen bastante bien, por lo que me limitaré a una pequeña introducción. La entrada del directorio de la página es una estructura de tamaño de puntero, para nosotros es de 4 bytes. El valor contiene una dirección física de 4KB de la página. El byte menos significativo del registro está reservado para banderas. El mecanismo para convertir una dirección virtual en una física se ve así (en el caso de mi granularidad de 4 MB, el cambio se produce en 22 bits. Para otras granularidades, el cambio será diferente y se usarán tablas jerárquicas):
Dirección virtual 0xC010A110 -> Obtenga el índice en el directorio moviendo la dirección 22 bits a la derecha -> índice 0x300 -> Obtenga la dirección física de la página por índice 0x300, verifique los indicadores y el estado -> 0x1000000 -> Tome los 22 bits inferiores de la dirección virtual como un desplazamiento, agregue a la dirección física de la página -> 0x1000000 + 0x10A110 = dirección física en memoria 0x110A110
Para acelerar el acceso, el procesador utiliza TLB, el búfer de traducción, que almacena en caché las direcciones de las páginas.
Entonces, así es como se describe mi directorio y sus entradas, y se implementa el método setup_pd. Para escribir una página, se implementa el método "constructor", que garantiza la alineación en 4 KB y la configuración de banderas, y un método para obtener la dirección física de la página. Un directorio es solo una matriz de 1024 entradas de cuatro bytes. El directorio puede asociar una dirección virtual con una página utilizando el método 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); }
Muy torpemente hice que la inicialización estática inicial fuera una dirección inexistente, por lo que le agradecería que me escribiera cómo es habitual en la comunidad de Rust hacer tales inicializaciones con la reasignación de enlaces.
Ahora que podemos administrar páginas desde código de alto nivel, podemos pasar a compilar la inicialización de memoria. Esto sucederá en dos etapas: procesando la tarjeta de memoria física e inicializando el administrador virtual
match mb_magic { 0x2BADB002 => { println!("multibooted v1, yeah, reading mb info"); boot::init_with_mb1(mb_pointer); }, . . . . . . } memory::init();
Tarjeta de memoria GRUB y tarjeta de memoria física OS1
Para obtener una tarjeta de memoria de GRUB, en la etapa de arranque configuré la bandera correspondiente en el encabezado, y GRUB me dio la dirección física de la estructura. Lo porté de la documentación oficial a la notación Rust, y también agregué métodos para iterar cómodamente sobre la tarjeta de memoria. La mayor parte de la estructura de GRUB no se completará, y en esta etapa no es muy interesante para mí. Lo principal es que no quiero determinar la cantidad de memoria disponible manualmente.
Al inicializar a través de Multiboot, primero convertimos la dirección física a virtual. Teóricamente, GRUB puede colocar la estructura en cualquier lugar, por lo que si la dirección se extiende más allá de la página, debe asignar una página virtual en el directorio de la página. En la práctica, la estructura casi siempre se encuentra al lado del primer megabyte, que ya hemos asignado en la etapa de arranque. Por si acaso, verificamos la bandera de que la tarjeta de memoria está presente y procedemos a su análisis.
pub mod multiboot2; pub mod multiboot; use super::arch; unsafe fn process_pointer(mb_pointer: usize) -> usize {
Una tarjeta de memoria es una lista vinculada para la cual la dirección física inicial se especifica en la estructura básica (no olvide traducir todo en virtuales) y el tamaño de la matriz en bytes. Debe recorrer la lista en función del tamaño de cada elemento, ya que, en teoría, sus tamaños pueden diferir. Así es como se ve la iteración:
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) } }
Al analizar una tarjeta de memoria, iteramos a través de la estructura GRUB y la convertimos en un mapa de bits, con el cual OS1 trabajará para administrar la memoria física. Decidí limitarme a un pequeño conjunto de valores disponibles para el control: libre, ocupado, reservado, no disponible, aunque GRUB y BIOS ofrecen más opciones. Entonces, iteramos sobre las entradas del mapa y convertimos su estado de valores GRUB / BIOS a valores para 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 memoria física se administra en el módulo memory :: physical, para el cual llamamos al método de mapa anterior, pasándole la dirección de la región, su longitud y estado. Los 4 GB de memoria potencialmente disponibles para el sistema y divididos en páginas de cuatro megabytes están representados por dos bits en un mapa de bits, lo que le permite almacenar 4 estados para 1024 páginas. En total, esta construcción toma 256 bytes. Un mapa de bits conduce a una terrible fragmentación de la memoria, pero es comprensible y fácil de implementar, lo cual es lo más importante para mi propósito.
Eliminaré la implementación de mapa de bits debajo del spoiler para no saturar el artículo. La estructura puede contar el número de clases y memoria libre, marcar páginas por índice y dirección, y también buscar páginas libres (esto será necesario en el futuro para implementar el montón). La tarjeta en sí es una matriz de 64 elementos u32, para aislar los dos bits (bloques) necesarios, se utiliza la conversión al llamado fragmento (índice en la matriz, empaque de 16 bloques) y el bloque (posición de bit en el fragmento).
Mapa de bits de memoria física 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 {
Y ahora llegamos al análisis de un elemento del mapa. Si un elemento del mapa describe un área de memoria de menos de una página de 4 MB o igual, marcamos esta página como un todo. Si es más, batir en pedazos de 4 MB y marcar cada pieza por separado a través de la recursión. En la etapa de inicialización del mapa de bits, consideramos que todas las secciones de la memoria son inaccesibles, de modo que cuando la tarjeta se agota, por ejemplo, a 128 MB, las secciones restantes se marcan como inaccesibles.
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) {
Heap y manejándola
Actualmente, la administración de memoria virtual se limita solo a la administración de almacenamiento dinámico, ya que el núcleo no sabe mucho más. En el futuro, por supuesto, será necesario administrar toda la memoria, y este pequeño administrador se reescribirá. Sin embargo, por el momento, todo lo que necesito es memoria estática, que contiene el código ejecutable y la pila, y memoria dinámica de montón, donde asignaré las estructuras para subprocesamiento múltiple. Asignamos memoria estática en la etapa de arranque (y hasta ahora tenemos 4 MB limitados, porque el núcleo encaja en ellos) y, en general, no hay problemas con eso ahora. Además, en esta etapa, no tengo dispositivos DMA, por lo que todo es extremadamente simple, pero comprensible.
Le di 512 MB del espacio de memoria superior del núcleo (0xE0000000) al montón, almaceno el mapa de uso del montón (0xDFC00000) 4 MB más bajo. Utilizo un mapa de bits para describir el estado, al igual que para la memoria física, pero solo contiene 2 estados: ocupado / libre. El tamaño del bloque de memoria es de 64 bytes; esto es mucho para pequeñas variables como u32, u8, pero, tal vez, es óptimo para almacenar estructuras de datos. Aún así, es poco probable que necesitemos almacenar variables individuales en el montón, ahora su propósito principal es almacenar estructuras de contexto para la multitarea.
Los bloques de 64 bytes se agrupan en estructuras que describen el estado de una página completa de 4 MB, por lo que podemos asignar pequeñas y grandes cantidades de memoria a varias páginas. Utilizo los siguientes términos: fragmento - 64 bytes, paquete - 2 KB (uno u32 - 64 bytes * 32 bits por paquete), página - 4 MB.
#[repr(packed)] #[derive(Copy, Clone)] struct HeapPageInfo {
Cuando solicito memoria de un asignador, considero tres casos, dependiendo de la granularidad:
- Una solicitud de memoria de menos de 2 KB vino del asignador. Debe encontrar un paquete en el que estará libre [tamaño / 64, cualquier resto distinto de cero agrega uno] fragmentos seguidos, marque estos fragmentos como ocupados, devuelva la dirección del primer fragmento.
- El asignador solicitó una memoria de menos de 4 MB pero más de 2 KB. Debe encontrar una página que tenga paquetes gratuitos [tamaño / 2048, cualquier resto distinto de cero agrega uno] seguidos. Marque los paquetes [tamaño / 2048] como ocupados; si hay un resto, marque los fragmentos [restantes] en el último paquete como ocupados.
- Una solicitud de memoria de más de 4 MB vino del asignador. Encuentre [tamaño / 4 Mi, cualquier saldo distinto de cero agrega una] páginas seguidas, marque las páginas [tamaño / 4 Mi] como ocupadas, si hay un saldo - marque los paquetes [saldo] como ocupados. En el último paquete, marque el resto de los fragmentos como ocupados.
La búsqueda de áreas libres también depende de la granularidad: se selecciona una matriz para iteración o máscaras de bits. Cada vez que vas al extranjero, OOM sucede. Cuando se desasigna, se usa un algoritmo similar, solo para el marcado lanzado. La memoria liberada no se restablece. Todo el código es grande, lo pondré debajo del spoiler.
Mapa de bits de memoria virtual Asignación y fallo de página
Para usar el montón, necesita un asignador. Agregarlo nos abrirá un vector, árboles, tablas hash, cajas y más, sin los cuales es casi imposible vivir. Tan pronto como conectemos el módulo de asignación y declaremos un asignador global, la vida se volverá más fácil de inmediato.
La implementación del asignador es muy simple: simplemente se refiere al mecanismo descrito anteriormente.
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); } }
El asignador en lib.rs se activa de la siguiente manera:
#![feature(alloc, alloc_error_handler)] extern crate alloc; #[global_allocator] static ALLOCATOR: memory::allocate::Os1Allocator = memory::allocate::Os1Allocator;
Y cuando tratamos de asignarnos de tal manera, obtenemos una excepción de falla de página, porque todavía no hemos resuelto la asignación de memoria virtual. Bueno, como es eso! Bueno, debe volver al material del artículo anterior y agregar excepciones. Decidí implementar una asignación diferida de memoria virtual, es decir, que la página se asignó no en el momento de la solicitud de memoria, sino en el momento de intentar acceder a ella. Afortunadamente, el procesador x86 permite e incluso fomenta esto. 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 — , — , !
, .
Gracias por su atencion!