الجزء الاول
الجزء الثاني
موضوع محادثة اليوم هو العمل مع الذاكرة. سأتحدث عن تهيئة دليل الصفحة ، ورسم الخرائط للذاكرة الفعلية ، وإدارة كومة الذاكرة المؤقتة الافتراضية والمؤسسية للمخصص.
كما قلت في المقالة الأولى ، قررت استخدام 4 ميغابايت من الصفحات لتبسيط حياتي وليس من الضروري التعامل مع الجداول الهرمية. في المستقبل ، آمل أن أذهب إلى صفحات بحجم 4 كيلوبايت ، مثل معظم الأنظمة الحديثة. يمكنني أن أستخدم واحدة جاهزة (على سبيل المثال ، أداة تخصيص البلوك هذه ) ، لكن كتابة كتابي كان أكثر إثارة للاهتمام قليلاً وأردت أن أفهم قليلاً كيف تعيش الذاكرة ، لذلك لدي شيء أخبرك به.
في المرة الأخيرة التي استقرت فيها على أسلوب setup_pd الذي يعتمد على الهندسة وأردت المتابعة معه ، ومع ذلك ، كان هناك مزيد من التفاصيل التي لم أغطيها في المقالة السابقة - إخراج VGA باستخدام Rust و الماكرو println القياسي. منذ تنفيذه تافهة ، سأزيله تحت المفسد. الرمز موجود في حزمة التصحيح.
ماكرو 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;
الآن ، بضمير مرتاح ، أعود إلى الذاكرة.
تهيئة دليل الصفحة
أخذت طريقة kmain الخاصة بنا ثلاث وسيطات كمدخلات ، إحداها هي العنوان الظاهري لجدول الصفحة. لاستخدامها لاحقًا للتخصيص وإدارة الذاكرة ، تحتاج إلى تعيين بنية السجلات والدلائل. بالنسبة إلى x86 ، يتم وصف دليل الصفحة وجدول الصفحة جيدًا ، لذلك سأقصر نفسي على دليل تمهيدي صغير. إدخال دليل الصفحة عبارة عن بنية حجم مؤشر ، بالنسبة لنا هو 4 بايت. تحتوي القيمة على عنوان فعلي قدره 4 كيلوبايت للصفحة. البايت الأقل أهمية من السجل محجوز للأعلام. تبدو آلية تحويل العنوان الافتراضي إلى عنوان فعلي (في حالة التفاصيل الدقيقة 4 ميجابايت ، يحدث التغيير بمقدار 22 بت. بالنسبة إلى التفاصيل الدقيقة الأخرى ، سيكون التغيير مختلفًا وسيتم استخدام الجداول الهرمية!):
العنوان الافتراضي 0xC010A110 -> احصل على الفهرس في الدليل بنقل العنوان 22 بت إلى اليمين -> الفهرس 0x300 -> الحصول على العنوان الفعلي للصفحة بواسطة فهرس 0x300 ، والتحقق من الأعلام والحالة -> 0x1000000 -> خذ الجزء السفلي 22 بت من العنوان الظاهري كإزاحة ، أضف إلى العنوان الفعلي للصفحة -> 0x1000000 + 0x10A110 = العنوان الفعلي في الذاكرة 0x110A110
لتسريع الوصول ، يستخدم المعالج TLB - الترجمة lookaside المخزن المؤقت ، والذي يخزن عناوين الصفحات.
لذلك ، إليك كيفية وصف دليلي وإدخالاته ، ويتم تنفيذ طريقة setup_pd ذاتها. لكتابة صفحة ، يتم تطبيق طريقة "المنشئ" ، والتي تضمن المحاذاة بمقدار 4 كيلوبايت وإعداد الإشارات ، وطريقة للحصول على العنوان الفعلي للصفحة. دليل هو مجرد مجموعة من 1024 إدخالات أربعة بايت. يمكن للدليل إقران عنوان افتراضي بصفحة باستخدام طريقة 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); }
لقد جعلت من التهيئة الأولية الثابتة عنوانًا غير موجود حرجًا للغاية ، لذا سأكون ممتنًا إذا كتبت لي كيف يكون من المعتاد في مجتمع Rust القيام بهذه التهيئة مع إعادة تعيين الارتباط.
والآن بعد أن أصبح بمقدورنا إدارة الصفحات من التعليمات البرمجية عالية المستوى ، يمكننا الانتقال إلى تجميع تهيئة الذاكرة. سيحدث ذلك على مرحلتين: من خلال معالجة بطاقة الذاكرة الفعلية وتهيئة المدير الظاهري
match mb_magic { 0x2BADB002 => { println!("multibooted v1, yeah, reading mb info"); boot::init_with_mb1(mb_pointer); }, . . . . . . } memory::init();
بطاقة الذاكرة GRUB وبطاقة الذاكرة الفعلية OS1
من أجل الحصول على بطاقة ذاكرة من GRUB ، في مرحلة التمهيد ، قمت بتعيين العلامة المقابلة في الرأس ، وأعطاني GRUB العنوان الفعلي للبنية. لقد نقلته من الوثائق الرسمية إلى تدوين الصدأ ، وأضفت أيضًا طرقًا للتكرار بشكل مريح عبر بطاقة الذاكرة. لن يتم ملء معظم هيكل GRUB ، وفي هذه المرحلة ، ليس من المثير للاهتمام بالنسبة لي. الشيء الرئيسي هو أنني لا أريد تحديد مقدار الذاكرة المتوفرة يدويًا.
عند التهيئة من خلال Multiboot ، نقوم أولاً بتحويل العنوان الفعلي إلى افتراضي. نظريًا ، يمكن لـ GRUB وضع الهيكل في أي مكان ، لذلك إذا كان العنوان يمتد إلى ما بعد الصفحة ، فأنت بحاجة إلى تخصيص صفحة افتراضية في دليل الصفحة. في الممارسة العملية ، يقع الهيكل تقريبًا دائمًا بجوار أول ميجابايت ، والذي قمنا بتخصيصه بالفعل في مرحلة التمهيد. فقط في حالة التحقق من وجود بطاقة الذاكرة ومتابعة تحليلها.
pub mod multiboot2; pub mod multiboot; use super::arch; unsafe fn process_pointer(mb_pointer: usize) -> usize {
بطاقة الذاكرة هي قائمة مرتبطة يتم تحديد العنوان الفعلي الأولي لها في البنية الأساسية (لا تنس ترجمة كل شيء إلى عناوين افتراضية) وحجم الصفيف بالبايت. يجب عليك تكرار القائمة بناءً على حجم كل عنصر ، نظرًا لأن أحجامها يمكن أن تختلف من الناحية النظرية . هذا هو ما يشبه التكرار:
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) } }
عند تحليل بطاقة الذاكرة ، نكررها عبر بنية GRUB ونحولها إلى صورة نقطية ، والتي سيعمل معها OS1 لإدارة الذاكرة الفعلية. قررت أن أقصر نفسي على مجموعة صغيرة من القيم المتاحة للإدارة - مجانية ، مشغول ، محفوظة ، غير متوفرة ، على الرغم من أن GRUB و BIOS يوفران المزيد من الخيارات. لذلك ، نقوم بالتكرار على إدخالات الخريطة ونحول حالتها من قيم GRUB / BIOS إلى قيم 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 } }
تتم إدارة الذاكرة الفعلية في وحدة الذاكرة :: الفعلية ، والتي نسميها طريقة الخريطة أعلاه ، لتمريرها عنوان المنطقة وطولها وحالتها. يتم تمثيل جميع الذاكرة التي تبلغ 4 غيغابايت والتي يحتمل أن تكون متاحة للنظام وتنقسم إلى أربع صفحات ميغا بايت ببتين في صورة نقطية ، مما يسمح لك بتخزين 4 حالات لـ 1024 صفحة. في المجموع ، يأخذ هذا البناء 256 بايت. تؤدي الصورة النقطية إلى تجزئة ذاكرة فظيعة ، لكنها مفهومة وسهلة التنفيذ ، وهذا هو الشيء الرئيسي لهدفي.
سأقوم بإزالة تطبيق الصورة النقطية ضمن المفسد حتى لا تكتل المقالة. يمكن للهيكل حساب عدد الفصول والذاكرة الخالية ، ووضع علامة على الصفحات حسب الفهرس والعنوان ، وكذلك البحث عن الصفحات المجانية (ستكون هناك حاجة في المستقبل لتنفيذ الكومة). البطاقة نفسها عبارة عن مجموعة مكونة من 64 عنصرًا من عناصر u32 ، لعزل البتات (القطع) الضرورية ، والتحويل إلى ما يسمى القطعة (فهرس في المجموعة ، وتعبئة 16 قطعة) ، ويتم استخدام الكتلة (موضع البت في القطعة).
صورة نقطية الذاكرة الفعلية 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 {
والآن وصلنا إلى تحليل عنصر واحد من الخريطة. إذا وصف عنصر خريطة منطقة ذاكرة أقل من صفحة واحدة بسعة 4 ميجابايت أو مساوية لها ، فإننا نحتفل بهذه الصفحة ككل. إذا كان أكثر - فاز في قطع من 4 ميغابايت وعلامة كل قطعة على حدة من خلال العودية. في مرحلة تهيئة الصورة النقطية ، نعتبر أن جميع أقسام الذاكرة لا يمكن الوصول إليها ، بحيث عندما تنفد البطاقة ، على سبيل المثال ، 128 ميجابايت ، يتم تمييز الأقسام المتبقية على أنها غير قابلة للوصول.
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) {
كومة وإدارة لها
تقتصر إدارة الذاكرة الظاهرية حاليًا على إدارة الكومة فقط ، نظرًا لأن النواة لا تعرف الكثير. في المستقبل ، بالطبع ، سيكون من الضروري إدارة الذاكرة بالكامل ، وسيتم إعادة كتابة هذا المدير الصغير. ومع ذلك ، في الوقت الحالي ، كل ما أحتاج إليه هو ذاكرة ثابتة ، تحتوي على الكود القابل للتنفيذ والمكدس ، وذاكرة كومة الذاكرة الديناميكية ، حيث سأخصص الهياكل الخاصة بمضاعفة مؤشرات الترابط. نحن نخصص ذاكرة ثابتة في مرحلة التمهيد (وحتى الآن لدينا عدد محدود من 4 ميجابايت ، لأن النواة تناسبها) وبصفة عامة لا توجد مشاكل معها الآن. أيضا ، في هذه المرحلة ، ليس لدي أجهزة DMA ، لذلك كل شيء بسيط للغاية ، لكنه مفهوم.
أعطيت 512 ميغابايت من مساحة الذاكرة kernel الأعلى (0xE0000000) إلى كومة الذاكرة المؤقتة ، أقوم بتخزين خريطة استخدام كومة الذاكرة المؤقتة (0xDFC00000) 4 ميغابايت أقل. أستخدم صورة نقطية لوصف الحالة ، تمامًا كما في الذاكرة الفعلية ، ولكن لا يوجد سوى حالتين - مشغول / مجاني. حجم كتلة الذاكرة هو 64 بايت - وهذا كثير للمتغيرات الصغيرة مثل u32 ، u8 ، ولكن ، ربما ، هو الأمثل لتخزين بنيات البيانات. ومع ذلك ، من غير المحتمل أن نحتاج إلى تخزين متغيرات مفردة على الكومة ، والغرض الرئيسي الآن هو تخزين بنى السياق لتعدد المهام.
يتم تجميع كتل 64 بايت في هياكل تصف حالة صفحة 4 ميغابايت بالكامل ، حتى نتمكن من تخصيص كميات صغيرة وكبيرة من الذاكرة لعدة صفحات. يمكنني استخدام المصطلحات التالية: قطعة - 64 بايت ، حزمة - 2 كيلو بايت (واحد من 32 إلى 64 بايت * 32 بت لكل حزمة) ، صفحة - 4 ميغابايت.
#[repr(packed)] #[derive(Copy, Clone)] struct HeapPageInfo {
عند طلب الذاكرة من أحد المُخصصين ، أفكر في ثلاث حالات ، اعتمادًا على التفاصيل:
- طلب للذاكرة أقل من 2 كيلو بايت جاء من المخصص. ستحتاج إلى العثور على حزمة ستكون مجانية [size / 64 ، ويضيف أي جزء غير صفري واحدًا] قطعًا متتالية ، ويضع علامة على هذه القطع على أنها مشغول ، ويعيد عنوان القطعة الأولى.
- جاء طلب من مخصص الذاكرة أقل من 4 ميغابايت ، ولكن أكثر من 2 كيلو بايت. تحتاج إلى العثور على صفحة تحتوي على [size / 2048] مجانًا ، ويضيف أي ما تبقى غير صفري حزمًا واحدة على التوالي. حدد حزم [size / 2048] على أنها مشغول ؛ إذا كان هناك ما تبقى ، قم بتمييز القطع [المتبقية] في العبوة الأخيرة على أنها مشغول.
- جاء طلب للحصول على ذاكرة أكثر من 4 ميغابايت من المخصص. ابحث عن [size / 4 Mi ، أي رصيد غير صفري يضيف صفحة واحدة] على التوالي ، وقم بتمييز الصفحات [size / 4 Mi] على أنها مشغولة ، إذا كان هناك حزم موازنة [balance] مشغول. في الحزمة الأخيرة ، حدد الجزء المتبقي من القطع على أنه مشغول.
يعتمد البحث عن المناطق الحرة أيضًا على التفاصيل - يتم تحديد مجموعة للتكرار أو أقنعة البت. كلما ذهبت إلى الخارج ، يحدث OOM. عند إلغاء تخصيص ، يتم استخدام خوارزمية مماثلة ، فقط لوضع العلامات صدر. لا يتم إعادة تعيين الذاكرة المحررة. الكود كله كبير ، سأضعه تحت المفسد.
صورة نقطية الذاكرة الظاهرية تخصيص و خطأ الصفحة
من أجل استخدام الكومة ، تحتاج إلى مخصص. إن إضافته سيفتح لنا ناقلًا وأشجارًا وجداول تجزئة وصناديق وغير ذلك ، والتي بدونها سيكون من المستحيل العيش عليها. بمجرد أن نقوم بتوصيل وحدة التخصيص وإعلان تخصيص عالمي ، ستصبح الحياة على الفور أسهل.
تنفيذ المخصص بسيط للغاية - إنه يشير ببساطة إلى الآلية الموضحة أعلاه.
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); } }
يتم تشغيل المخصص في lib.rs كما يلي:
#![feature(alloc, alloc_error_handler)] extern crate alloc; #[global_allocator] static ALLOCATOR: memory::allocate::Os1Allocator = memory::allocate::Os1Allocator;
وعندما نحاول تخصيص أنفسنا فقط بهذه الطريقة ، نحصل على استثناء من خطأ الصفحة ، لأننا لم نحدد بعد تخصيص الذاكرة الافتراضية. حسنا ، كيف ذلك! حسنًا ، يجب عليك الرجوع إلى مادة المقالة السابقة وإضافة استثناءات. قررت تطبيق تخصيص كسول للذاكرة الظاهرية ، أي أنه لم يتم تخصيص الصفحة في وقت طلب الذاكرة ، ولكن في وقت محاولة الوصول إليها. لحسن الحظ ، فإن معالج x86 يسمح بذلك ويشجعه. 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 — , — , !
, .
شكرا لاهتمامكم!