前編
第二部
今日の会話のトピックは、記憶を扱うことです。 ページディレクトリの初期化、物理メモリのマッピング、仮想管理とアロケータの組織ヒープについて説明します。
最初の記事で述べたように、私は4 MBのページを使用して生活を簡素化し、階層テーブルを処理しないことにしました。 将来的には、最新のシステムのように、4 KBページに移動したいと考えています。 既製のもの(たとえば、 このようなブロックアロケーター )を使用することもできますが、自分で書くのはもう少し面白く、メモリがどのように生きているかをもう少し理解したかったので、あなたに伝えることがあります。
前回、アーキテクチャに依存するsetup_pdメソッドに落ち着いて、それを継続したかったのですが、前回の記事ではカバーしていなかった詳細がもう1つありました。Rustと標準のprintlnマクロを使用したVGA出力です。 その実装は簡単なので、ネタバレの下で削除します。 コードはデバッグパッケージに含まれています。
マクロ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メソッドは、入力として3つの引数を取りました。そのうちの1つはページテーブルの仮想アドレスです。 後で割り当てとメモリ管理に使用するには、レコードとディレクトリの構造を指定する必要があります。 x86の場合、PageディレクトリとPageテーブルは非常によく説明されているので、私は小さな入門者に限定します。 ページディレクトリエントリはポインタサイズ構造であり、私たちにとっては4バイトです。 値には、ページの4KBの物理アドレスが含まれます。 レコードの最下位バイトはフラグ用に予約されています。 仮想アドレスを物理アドレスに変換するメカニズムは次のようになります(私の4 MBの粒度の場合、シフトは22ビットで発生します。他の粒度の場合、シフトは異なり、階層テーブルが使用されます!)。
仮想アドレス0xC010A110->アドレスを22ビット右に移動してディレクトリ内のインデックスを取得->インデックス0x300->インデックス0x300でページの物理アドレスを取得、フラグとステータスを確認-> 0x1000000->仮想アドレスの下位22ビットをオフセットとして追加ページの物理アドレス-> 0x1000000 + 0x10A110 =メモリ0x110A110の物理アドレス
アクセスを高速化するために、プロセッサはTLB(ページアドレスをキャッシュする変換ルックアサイドバッファー)を使用します。
それで、ここに私のディレクトリとそのエントリがどのように記述され、まさにsetup_pdメソッドが実装されています。 ページを書き込むために、「コンストラクター」メソッドが実装されます。これは、4 KBによるアライメントとフラグの設定を保証し、ページの物理アドレスを取得するメソッドです。 ディレクトリは、1024個の4バイトエントリの配列です。 ディレクトリは、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コミュニティで慣習的である方法をご連絡いただければ幸いです。
上位レベルのコードからページを管理できるようになったので、メモリの初期化のコンパイルに移ります。 これは、物理メモリカードの処理と仮想マネージャーの初期化の2段階で行われます。
match mb_magic { 0x2BADB002 => { println!("multibooted v1, yeah, reading mb info"); boot::init_with_mb1(mb_pointer); }, . . . . . . } memory::init();
GRUBメモリーカードとOS1物理メモリーカード
GRUBからメモリカードを取得するために、ブート段階でヘッダーに対応するフラグを設定し、GRUBは構造の物理アドレスを提供しました。 公式ドキュメントからRust表記に移植し、メモリカードを快適に反復するメソッドも追加しました。 GRUB構造の大部分は埋められず、この段階ではあまり面白くありません。 主なことは、使用可能なメモリの量を手動で決定したくないということです。
マルチブートを使用して初期化する場合、最初に物理アドレスを仮想に変換します。 理論的には、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 } }
物理メモリは、メモリ::物理モジュールで管理されます。このモジュールに対して、上記のmapメソッドを呼び出し、領域のアドレス、長さ、状態を渡します。 システムで潜在的に使用可能な4 MBのメモリはすべて、4メガバイトページに分割され、ビットマップの2ビットで表されます。これにより、1024ページの4つの状態を保存できます。 合計で、この構築には256バイトが必要です。 ビットマップはひどいメモリの断片化につながりますが、理解しやすく、実装が簡単です。これが私の目的の主な目的です。
記事が乱雑にならないように、スポイラーの下のビットマップの実装を削除します。 この構造は、クラスと空きメモリの数をカウントし、インデックスとアドレスでページをマークし、空きページを検索することもできます(これは将来、ヒープを実装するために必要になります)。 カード自体は64 u32要素の配列であり、必要な2ビット(ブロック)を分離するために、いわゆるチャンク(配列内のインデックス、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 {
そして、マップの1つの要素の分析に取りかかりました。 マップ要素が4 MBで1ページ未満またはそれに等しいメモリの一部を記述する場合、このページ全体をマークします。 複数の場合-4 MBの破片にビートし、各破片は再帰的に別々にマークされます。 ビットマップの初期化の段階で、メモリのすべてのセクションにアクセスできないと見なします。たとえば、128 MBなどのカードがなくなると、残りのセクションはアクセス不可としてマークされます。
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 MBに制限されています)。一般的には、現在は問題ありません。 また、この段階では、DMAデバイスはありません。そのため、すべてが非常にシンプルですが、理解できます。
512 MBのカーネルメモリ領域(0xE0000000)をヒープに割り当て、ヒープ使用マップ(0xDFC00000)を4 MB低く保存しました。 状態を説明するために、物理メモリと同様にビットマップを使用しますが、ビジー/フリーという2つの状態しかありません。 メモリブロックのサイズは64バイトです。これは、u32、u8などの小さな変数には非常に多くなりますが、おそらく、データ構造の格納に最適です。 それでも、ヒープに単一の変数を格納する必要はほとんどありません。現在の主な目的は、マルチタスクのコンテキスト構造を格納することです。
64バイトのブロックは、4 MBページ全体の状態を記述する構造にグループ化されるため、少量のメモリと大量のメモリの両方を複数のページに割り当てることができます。 私は次の用語を使用します:チャンク-64バイト、パック-2 KB(1 u32-64バイト*パッケージあたり32ビット)、ページ-4 MB。
#[repr(packed)] #[derive(Copy, Clone)] struct HeapPageInfo {
アロケーターからメモリーを要求するとき、粒度に応じて3つのケースを検討します。
- 2 KB未満のメモリの要求がアロケーターから送信されました。 無料のパックを見つける必要があります[サイズ/ 64、ゼロ以外の余りは1を追加]チャンクを連続して、これらのチャンクをビジーとしてマークし、最初のチャンクのアドレスを返します。
- 4 MB未満、2 KBを超えるメモリの要求がアロケーターから送信されました。 空き[サイズ/ 2048、ゼロ以外の残りが1を追加]パックを連続して含むページを見つける必要があります。 [サイズ/ 2048]パックをビジーとしてマークし、残りがある場合は、最後のパックの[残り]チャンクをビジーとしてマークします。
- 4 MBを超えるメモリの要求がアロケーターから送信されました。 [サイズ/ 4 Mi、ゼロ以外の余りは1つ追加]ページを連続して検索し、[サイズ/ 4 Mi]ページを使用中としてマークします。 最後のパックで、残りのチャンクをビジーとしてマークします。
空き領域の検索も粒度に依存します。反復またはビットマスク用に配列が選択されます。 海外に行くたびに、OOMが起こります。 割り当てを解除する場合、同様のアルゴリズムが使用されますが、マークが解除されるだけです。 解放されたメモリはリセットされません。 コード全体が大きいので、ネタバレの下に置きます。
割り当てとページ違反
ヒープを使用するには、アロケーターが必要です。 それを追加すると、ベクター、ツリー、ハッシュテーブル、ボックスなどが開かれ、それなしでは生きることはほとんど不可能になります。 allocモジュールを接続してグローバルアロケータを宣言すると、すぐにすぐに使いやすくなります。
アロケーターの実装は非常に単純です-それは単に上記のメカニズムを指します。
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());
:

ご覧のように、メモリの割り当てはうまくいきますが、もちろん、多数の実現を伴う百万回目のサイクルには長い時間がかかります。予想どおり、すべての割り当て後の整数が100万のベクトルは、3.5 GB + 3 MBのアドレスを占有します。ヒープの最初のメガバイトが解放され、2番目の小さなベクターがその3.5 GBアドレスに配置されました。
IRQ 1と不可解な文字-Alt + PrntScrnキーからの割り込みに対するカーネルの反応:)
それで作業束ができたので、Rustデータ構造-好きなだけオブジェクトを作成できるようになりました。つまり、プロセッサが実行するタスクの状態を保存できるということです。
次の記事では、基本的なマルチタスクの実装方法と、同じプロセッサ上の並列スレッドで2つの愚かな関数を実行する方法について説明します。
ご清聴ありがとうございました!