Bagian pertama
Bagian kedua
Topik pembicaraan hari ini adalah bekerja dengan memori. Saya akan berbicara tentang menginisialisasi direktori halaman, memetakan memori fisik, mengelola virtual dan tumpukan organisasi saya untuk pengalokasi.
Seperti yang saya katakan di artikel pertama, saya memutuskan untuk menggunakan halaman 4 MB untuk menyederhanakan hidup saya dan tidak harus berurusan dengan tabel hirarkis. Di masa depan, saya berharap untuk membuka halaman 4 KB, seperti kebanyakan sistem modern. Saya bisa menggunakan yang sudah jadi (misalnya, pengalokasi blok seperti itu ), tetapi menulis sendiri sedikit lebih menarik dan saya ingin memahami sedikit lebih banyak bagaimana memori hidup, jadi saya punya sesuatu untuk memberitahu Anda.
Terakhir kali saya memilih metode setup_pd yang bergantung pada arsitektur dan ingin melanjutkannya, ada satu detail lagi yang tidak saya bahas dalam artikel sebelumnya - output VGA menggunakan Rust dan println macro standar. Karena implementasinya sepele, saya akan menghapusnya di bawah spoiler. Kode tersebut ada dalam paket debug.
Cetak makro#[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;
Sekarang, dengan hati nurani yang bersih, saya kembali ke memori.
Inisialisasi Direktori Halaman
Metode kmain kami mengambil tiga argumen sebagai input, salah satunya adalah alamat virtual dari tabel halaman. Untuk menggunakannya nanti untuk alokasi dan manajemen memori, Anda perlu menentukan struktur catatan dan direktori. Untuk x86, direktori Halaman dan tabel Halaman dijelaskan dengan cukup baik, jadi saya akan membatasi diri hanya untuk pengantar kecil. Entri direktori Halaman adalah struktur ukuran penunjuk, bagi kami ini adalah 4 byte. Nilai tersebut berisi alamat fisik halaman 4KB. Byte terkecil dari catatan dicadangkan untuk bendera. Mekanisme untuk mengubah alamat virtual menjadi alamat fisik terlihat seperti ini (dalam kasus granularitas 4 MB saya, perubahan terjadi oleh 22 bit. Untuk rincian lainnya, perubahan akan berbeda dan tabel hirarkis akan digunakan!):
Alamat virtual 0xC010A110 -> Dapatkan indeks dalam direktori dengan memindahkan alamat 22 bit ke kanan -> indeks 0x300 -> Dapatkan alamat fisik halaman dengan indeks 0x300, centang bendera dan status -> 0x1000000 -> Ambil 22 bit bawah alamat virtual sebagai offset, tambahkan ke alamat fisik halaman -> 0x1000000 + 0x10A110 = alamat fisik dalam memori 0x110A110
Untuk mempercepat akses, prosesor menggunakan TLB - terjemahan lookaside buffer, yang menyimpan alamat halaman.
Jadi, inilah cara direktori saya dan entri-entrinya dijelaskan, dan metode setup_pd diimplementasikan. Untuk menulis halaman, metode "konstruktor" diimplementasikan, yang menjamin penyelarasan sebesar 4 KB dan mengatur flag, dan metode untuk mendapatkan alamat fisik halaman. Direktori hanyalah sebuah array dari 1024 entri empat byte. Direktori dapat mengaitkan alamat virtual dengan halaman menggunakan metode 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); }
Saya sangat canggung membuat inisialisasi statis awal alamat tidak ada, jadi saya akan berterima kasih jika Anda akan menulis kepada saya bagaimana itu kebiasaan di komunitas Rust untuk melakukan inisialisasi tersebut dengan penugasan kembali tautan.
Sekarang kita dapat mengelola halaman dari kode tingkat tinggi, kita dapat beralih ke kompilasi inisialisasi memori. Ini akan terjadi dalam dua tahap: melalui pemrosesan kartu memori fisik dan menginisialisasi manajer virtual
match mb_magic { 0x2BADB002 => { println!("multibooted v1, yeah, reading mb info"); boot::init_with_mb1(mb_pointer); }, . . . . . . } memory::init();
Kartu memori GRUB dan kartu memori fisik OS1
Untuk mendapatkan kartu memori dari GRUB, pada tahap boot saya mengatur flag yang sesuai di header, dan GRUB memberi saya alamat fisik struktur. Saya memindahkannya dari dokumentasi resmi ke notasi Rust, dan juga menambahkan metode untuk beralih dengan nyaman di kartu memori. Sebagian besar struktur GRUB tidak akan diisi, dan pada tahap ini tidak terlalu menarik bagi saya. Yang utama adalah saya tidak ingin menentukan jumlah memori yang tersedia secara manual.
Ketika menginisialisasi melalui Multiboot, pertama-tama kita mengubah alamat fisik menjadi virtual. Secara teoritis, GRUB dapat memposisikan struktur di mana saja, jadi jika alamat melampaui halaman, Anda perlu mengalokasikan halaman virtual di direktori Halaman. Dalam praktiknya, struktur hampir selalu terletak di sebelah megabyte pertama, yang telah kami alokasikan pada tahap boot. Untuk berjaga-jaga, kami memeriksa tanda bahwa kartu memori ada dan melanjutkan analisisnya.
pub mod multiboot2; pub mod multiboot; use super::arch; unsafe fn process_pointer(mb_pointer: usize) -> usize {
Kartu memori adalah daftar tertaut yang alamat fisik awalnya ditentukan dalam struktur dasar (jangan lupa untuk menerjemahkan semuanya menjadi yang virtual) dan ukuran array dalam byte. Anda harus mengulang daftar berdasarkan ukuran masing-masing elemen, karena secara teoritis ukurannya dapat berbeda. Seperti inilah bentuk iterasi:
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) } }
Ketika mem-parsing kartu memori, kita beralih melalui struktur GRUB dan mengubahnya menjadi bitmap, yang OS1 akan bekerja dengan untuk mengelola memori fisik. Saya memutuskan untuk membatasi diri pada serangkaian kecil nilai yang tersedia untuk kontrol - bebas, sibuk, dipesan, tidak tersedia, meskipun GRUB dan BIOS menyediakan lebih banyak opsi. Jadi, kami mengulang entri peta dan mengonversi statusnya dari nilai GRUB / BIOS ke nilai untuk 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 } }
Memori fisik dikelola dalam modul memori :: fisik, yang kami namakan metode peta di atas, meneruskannya alamat wilayah, panjang dan statusnya. Semua 4 GB memori berpotensi tersedia untuk sistem dan dibagi menjadi empat megabyte halaman diwakili oleh dua bit dalam bitmap, yang memungkinkan Anda untuk menyimpan 4 status untuk 1024 halaman. Secara total, konstruksi ini membutuhkan 256 byte. Bitmap mengarah ke fragmentasi memori yang mengerikan, tetapi itu dapat dimengerti dan mudah diimplementasikan, yang merupakan hal utama untuk tujuan saya.
Saya akan menghapus implementasi bitmap di bawah spoiler agar tidak mengacaukan artikel. Struktur ini dapat menghitung jumlah kelas dan memori bebas, menandai halaman dengan indeks dan alamat, dan juga mencari halaman gratis (ini akan diperlukan di masa depan untuk mengimplementasikan heap). Kartu itu sendiri adalah array dari elemen 64 u32, untuk mengisolasi dua bit yang diperlukan (blok), konversi ke chunk yang disebut (indeks dalam array, pengemasan 16 blok) dan blok (posisi bit di chunk) digunakan.
Bitmap memori fisik 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 {
Dan sekarang kita sampai pada analisis satu elemen peta. Jika elemen peta menggambarkan area memori kurang dari satu halaman 4 MB atau sama dengan itu, kami menandai halaman ini secara keseluruhan. Jika lebih - kocok menjadi 4 MB dan tandai setiap bagian secara terpisah melalui rekursi. Pada tahap inisialisasi bitmap, kami menganggap semua bagian dari memori tidak dapat diakses, sehingga ketika kartu habis, misalnya, pada 128 MB, bagian yang tersisa ditandai sebagai tidak dapat diakses.
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) {
Tumpukan dan kelola dia
Manajemen memori virtual saat ini terbatas hanya untuk manajemen tumpukan, karena kernel tidak tahu banyak. Di masa depan, tentu saja, perlu untuk mengatur seluruh memori, dan manajer kecil ini akan ditulis ulang. Namun, saat ini, yang saya butuhkan adalah memori statis, yang berisi kode yang dapat dieksekusi dan tumpukan, dan memori tumpukan dinamis, di mana saya akan mengalokasikan struktur untuk multithreading. Kami mengalokasikan memori statis pada tahap boot (dan sejauh ini kami telah membatasi 4 MB, karena kernel cocok untuk mereka) dan secara umum tidak ada masalah dengan itu sekarang. Juga, pada tahap ini, saya tidak memiliki perangkat DMA, jadi semuanya sangat sederhana, tetapi dapat dimengerti.
Saya memberi 512 MB ruang memori kernel paling atas (0xE0000000) ke heap, saya menyimpan peta penggunaan heap (0xDFC00000) 4 MB lebih rendah. Saya menggunakan bitmap untuk menggambarkan keadaan, seperti halnya untuk memori fisik, tetapi hanya ada 2 status di dalamnya - sibuk / gratis. Ukuran blok memori adalah 64 byte - ini banyak untuk variabel kecil seperti u32, u8, tetapi, mungkin, ini optimal untuk menyimpan struktur data. Namun, kecil kemungkinan bahwa kita perlu menyimpan variabel tunggal di heap, sekarang tujuan utamanya adalah menyimpan struktur konteks untuk multitasking.
Blok 64 byte dikelompokkan ke dalam struktur yang menggambarkan keadaan seluruh halaman 4 MB, sehingga kami dapat mengalokasikan memori dalam jumlah kecil dan besar ke beberapa halaman. Saya menggunakan istilah berikut: chunk - 64 bytes, paket - 2 KB (satu u32 - 64 bytes * 32 bit per paket), halaman - 4 MB.
#[repr(packed)] #[derive(Copy, Clone)] struct HeapPageInfo {
Saat meminta memori dari pengalokasi, saya mempertimbangkan tiga kasus, tergantung pada rinciannya:
- Permintaan untuk memori kurang dari 2 KB datang dari pengalokasi. Anda perlu menemukan paket di mana ia akan [ukuran / 64 gratis, sisa non-nol menambahkan satu] potongan berturut-turut, tandai potongan ini sebagai sibuk, kembalikan alamat potongan pertama.
- Permintaan datang dari pengalokasi untuk memori kurang dari 4 MB, tetapi lebih dari 2 KB. Anda perlu menemukan halaman dengan [ukuran / 2048 gratis, sisa bukan nol menambahkan satu] paket berturut-turut. Tandai [size / 2048] paket sebagai sibuk, jika ada sisa, tandai [sisa] potongan dalam paket terakhir sebagai sibuk.
- Permintaan untuk memori lebih dari 4 MB datang dari pengalokasi. Cari [size / 4 Mi, sembarang saldo nol menambahkan satu] halaman berturut-turut, tandai halaman [size / 4 Mi] sebagai sibuk, jika ada paket [balance] tandai balance sebagai sibuk. Di paket terakhir, tandai sisa bidak sibuk.
Pencarian untuk area gratis juga tergantung pada granularity - array dipilih untuk iterasi atau bit mask. Setiap kali Anda pergi ke luar negeri, OOM terjadi. Saat deallokasi, algoritma yang sama digunakan, hanya untuk menandai dirilis. Memori yang dibebaskan tidak diatur ulang. Seluruh kode besar, saya akan meletakkannya di bawah spoiler.
Alokasi dan kesalahan Halaman
Untuk menggunakan heap, Anda memerlukan pengalokasi. Menambahkannya akan membuka bagi kita vektor, pohon, tabel hash, kotak, dan lainnya, yang tanpanya mustahil untuk hidup. Segera setelah kita pasang modul alokasi dan mendeklarasikan pengalokasi global, hidup akan segera menjadi lebih mudah.
Implementasi pengalokasi sangat sederhana - hanya mengacu pada mekanisme yang dijelaskan di atas.
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); } }
Alokasi di lib.rs dinyalakan sebagai berikut:
#![feature(alloc, alloc_error_handler)] extern crate alloc; #[global_allocator] static ALLOCATOR: memory::allocate::Os1Allocator = memory::allocate::Os1Allocator;
Dan ketika kami mencoba untuk mengalokasikan diri kami sedemikian rupa, kami mendapatkan pengecualian kesalahan Halaman, karena kami belum mengerjakan alokasi memori virtual. Nah, bagaimana bisa begitu! Nah, Anda harus kembali ke materi artikel sebelumnya dan menambahkan pengecualian. Saya memutuskan untuk menerapkan alokasi memori virtual yang malas, yaitu, bahwa halaman tersebut dialokasikan bukan pada saat permintaan memori, tetapi pada saat upaya untuk mengaksesnya. Untungnya, prosesor x86 memungkinkan dan bahkan mendorong ini. 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 — , — , !
, .
Terima kasih atas perhatian anda!