Primeira parte
Segunda parte
O tópico da conversa de hoje está trabalhando com memória. Vou falar sobre como inicializar o diretório da página, mapear a memória física, gerenciar o virtual e o heap da minha organização para o alocador.
Como disse no primeiro artigo, decidi usar páginas de 4 MB para simplificar minha vida e não ter que lidar com tabelas hierárquicas. No futuro, espero ir para páginas de 4 KB, como a maioria dos sistemas modernos. Eu poderia usar um já pronto (por exemplo, um alocador de blocos ), mas escrever o meu próprio era um pouco mais interessante e eu queria entender um pouco mais como a memória vive, então tenho algo a lhe dizer.
A última vez que me deparei com o método setup_pd, dependente da arquitetura, e queria continuar com ele, no entanto, havia mais um detalhe que não cobri no artigo anterior - saída VGA usando Rust e a macro println padrão. Como sua implementação é trivial, vou removê-lo sob o spoiler. O código está no pacote de depuração.
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;
Agora, com a consciência limpa, volto à memória.
Inicialização do diretório de páginas
Nosso método kmain recebeu três argumentos como entrada, um dos quais é o endereço virtual da tabela de páginas. Para usá-lo posteriormente para alocação e gerenciamento de memória, você precisa designar a estrutura de registros e diretórios. Para x86, o diretório Page e a tabela Page são descritas muito bem, então vou me limitar a um pequeno introdutório. A entrada do diretório Page é uma estrutura de tamanho de ponteiro, para nós é de 4 bytes. O valor contém um endereço físico de 4KB da página. O byte menos significativo do registro é reservado para sinalizadores. O mecanismo para converter um endereço virtual em um físico se parece com o seguinte (no caso da minha granularidade de 4 MB, o deslocamento ocorre em 22 bits. Para outras granularidades, o deslocamento será diferente e tabelas hierárquicas serão usadas!):
Endereço virtual 0xC010A110 -> Obtenha o índice no diretório movendo o endereço 22 bits para a direita -> índice 0x300 -> Obtenha o endereço físico da página pelo índice 0x300, verifique sinalizadores e status -> 0x1000000 -> Leve os 22 bits inferiores do endereço virtual como um deslocamento, adicione para o endereço físico da página -> 0x1000000 + 0x10A110 = endereço físico na memória 0x110A110
Para acelerar o acesso, o processador usa o TLB - buffer lookaside de tradução, que armazena em cache os endereços das páginas.
Então, aqui está como meu diretório e suas entradas são descritos, e o próprio método setup_pd é implementado. Para escrever uma página, o método "construtor" é implementado, o que garante o alinhamento em 4 KB e a configuração de sinalizadores, além de um método para obter o endereço físico da página. Um diretório é apenas uma matriz de 1024 entradas de quatro bytes. O diretório pode associar um endereço virtual a uma página usando o 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); }
Desajeitadamente, tornei a inicialização estática inicial um endereço inexistente; portanto, ficaria grato se você me escrevesse como é habitual na comunidade Rust fazer essas inicializações com a reatribuição de links.
Agora que podemos gerenciar páginas a partir de código de alto nível, podemos prosseguir para a compilação da inicialização da memória. Isso acontecerá em duas etapas: processando o cartão de memória físico e inicializando o gerenciador virtual
match mb_magic { 0x2BADB002 => { println!("multibooted v1, yeah, reading mb info"); boot::init_with_mb1(mb_pointer); }, . . . . . . } memory::init();
Cartão de memória GRUB e cartão de memória física OS1
Para obter um cartão de memória do GRUB, no estágio de inicialização, defino o sinalizador correspondente no cabeçalho, e o GRUB me deu o endereço físico da estrutura. Eu o transportei da documentação oficial para a notação Rust e também adicionei métodos para iterar confortavelmente no cartão de memória. A maior parte da estrutura do GRUB não será preenchida e, neste estágio, não é muito interessante para mim. O principal é que eu não quero determinar a quantidade de memória disponível manualmente.
Ao inicializar através da inicialização múltipla, primeiro convertemos o endereço físico em virtual. Teoricamente, o GRUB pode posicionar a estrutura em qualquer lugar; portanto, se o endereço se estender além da página, você precisará alocar uma página virtual no diretório Page. Na prática, a estrutura quase sempre fica próxima ao primeiro megabyte, que já alocamos no estágio de inicialização. Por precaução, verificamos a bandeira de que o cartão de memória está presente e procedemos à sua análise.
pub mod multiboot2; pub mod multiboot; use super::arch; unsafe fn process_pointer(mb_pointer: usize) -> usize {
Um cartão de memória é uma lista vinculada para a qual o endereço físico inicial é especificado na estrutura básica (não se esqueça de converter tudo em virtual) e o tamanho da matriz em bytes. Você precisa percorrer a lista com base no tamanho de cada elemento, pois teoricamente seus tamanhos podem ser diferentes. É assim que a iteração se parece:
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) } }
Ao analisar um cartão de memória, iteramos na estrutura do GRUB e o convertemos em um bitmap, com o qual o OS1 trabalhará para gerenciar a memória física. Decidi me limitar a um pequeno conjunto de valores disponíveis para controle - livre, ocupado, reservado, indisponível, embora o GRUB e o BIOS forneçam mais opções. Portanto, iteramos sobre as entradas do mapa e convertemos seu estado dos valores do GRUB / BIOS em 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 } }
A memória física é gerenciada no módulo memory :: physical, para o qual chamamos o método map acima, passando o endereço da região, seu comprimento e estado. Todos os 4 GB de memória potencialmente disponíveis para o sistema e divididos em quatro páginas de megabytes são representados por dois bits em um bitmap, o que permite armazenar 4 estados para 1024 páginas. No total, essa construção leva 256 bytes. Um bitmap leva a uma terrível fragmentação da memória, mas é compreensível e fácil de implementar, o que é a principal coisa para o meu propósito.
Vou remover a implementação de bitmap sob o spoiler para não bagunçar o artigo. A estrutura é capaz de contar o número de classes e liberar memória, marcar páginas por índice e endereço e também procurar por páginas livres (isso será necessário no futuro para implementar o heap). O cartão em si é uma matriz de 64 elementos u32, para isolar os dois bits necessários (blocos), é usada a conversão para o chamado pedaço (índice na matriz, empacotamento de 16 blocos) e bloco (posição do bit no pedaço).
Bitmap de memória 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 {
E agora chegamos à análise de um elemento do mapa. Se um elemento do mapa descrever uma área de memória menor que uma página de 4 MB ou igual a ela, marcaremos essa página como um todo. Se houver mais - bata em pedaços de 4 MB e marque cada pedaço separadamente por recursão. No estágio de inicialização do bitmap, consideramos inacessíveis todas as seções da memória, para que, quando o cartão acabar, por exemplo, com 128 MB, as demais seções sejam marcadas como inacessíveis.
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) {
Empilhe e gerencie-a
Atualmente, o gerenciamento de memória virtual está limitado apenas ao gerenciamento de heap, pois o kernel não sabe muito mais. No futuro, é claro, será necessário gerenciar toda a memória, e esse pequeno gerente será reescrito. No entanto, no momento, tudo o que preciso é de memória estática, que contém o código executável e a pilha, e memória dinâmica de heap, onde alocarei as estruturas para multithreading. Alocamos memória estática no estágio de inicialização (e até agora limitamos 4 MB, porque o kernel se encaixa nelas) e, em geral, não há problemas com isso agora. Além disso, nesta fase, eu não tenho dispositivos DMA, então tudo é extremamente simples, mas compreensível.
Dei 512 MB do espaço de memória do kernel mais alto (0xE0000000) ao heap, armazenei o mapa de uso do heap (0xDFC00000) 4 MB mais baixo. Eu uso um bitmap para descrever o estado, assim como para a memória física, mas existem apenas 2 estados nele - ocupado / livre. O tamanho do bloco de memória é de 64 bytes - isso é muito para pequenas variáveis como u32, u8, mas, talvez, seja ideal para armazenar estruturas de dados. Ainda assim, é improvável que precisamos armazenar variáveis únicas no heap, agora seu principal objetivo é armazenar estruturas de contexto para multitarefa.
Blocos de 64 bytes são agrupados em estruturas que descrevem o estado de uma página inteira de 4 MB, para que possamos alocar pequenas e grandes quantidades de memória para várias páginas. Uso os seguintes termos: bloco - 64 bytes, pacote - 2 KB (um u32 - 64 bytes * 32 bits por pacote), página - 4 MB.
#[repr(packed)] #[derive(Copy, Clone)] struct HeapPageInfo {
Ao solicitar memória de um alocador, considero três casos, dependendo da granularidade:
- Uma solicitação de memória com menos de 2 KB veio do alocador. Você precisa encontrar um pacote no qual ele estará livre [tamanho / 64, qualquer resto diferente de zero adiciona um] pedaços seguidos, marque esses pedaços como ocupados, retorne o endereço do primeiro pedaço.
- Uma solicitação veio do alocador para memória com menos de 4 MB, mas com mais de 2 KB. Você precisa encontrar uma página que tenha [tamanho / 2048, qualquer restante diferente de zero adicione um] pacotes seguidos. Marque os pacotes [tamanho / 2048] como ocupados; se houver um restante, marque os pedaços [restantes] no último como ocupados.
- Uma solicitação de memória com mais de 4 MB veio do alocador. Localize [tamanho / 4 Mi, qualquer saldo diferente de zero adiciona uma] página em uma linha, marque as páginas [tamanho / 4 Mi] como ocupadas, se houver uma marca de equilíbrio [marca] como ocupada. No último pacote, marque o restante dos pedaços como ocupado.
A pesquisa de áreas livres também depende da granularidade - uma matriz é selecionada para iteração ou máscaras de bits. Sempre que você vai para o exterior, o OOM acontece. Quando a desalocação, um algoritmo semelhante é usado, apenas para a marcação liberada. A memória liberada não é redefinida. Todo o código é grande, vou colocá-lo sob o spoiler.
Bitmap de memória virtual Alocação e falha de página
Para usar o heap, você precisa de um alocador. Adicioná-lo abrirá para nós um vetor, árvores, tabelas de hash, caixas e muito mais, sem o qual é quase impossível viver. Assim que conectarmos o módulo de alocação e declararmos um alocador global, a vida se tornará imediatamente mais fácil.
A implementação do alocador é muito simples - refere-se simplesmente ao mecanismo descrito acima.
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); } }
O alocador em lib.rs está ativado da seguinte maneira:
#![feature(alloc, alloc_error_handler)] extern crate alloc; #[global_allocator] static ALLOCATOR: memory::allocate::Os1Allocator = memory::allocate::Os1Allocator;
E quando tentamos nos alocar dessa maneira, obtemos uma exceção de falha de página, porque ainda não calculamos a alocação de memória virtual. Bem, como sim! Bem, você precisa voltar ao material do artigo anterior e adicionar exceções. Decidi implementar uma alocação lenta da memória virtual, ou seja, que a página fosse alocada não no momento da solicitação de memória, mas no momento de uma tentativa de acessá-la. Felizmente, o processador x86 permite e até incentiva isso. 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 — , — , !
, .
Obrigado pela atenção!