Kami menulis sistem operasi di Rust. Menerapkan memori halaman (baru)

Pada artikel ini, kami akan mencari tahu bagaimana menerapkan dukungan memori halaman di inti kami. Pertama, kami akan mempelajari berbagai metode sehingga kerangka tabel halaman fisik menjadi tersedia untuk kernel, dan mendiskusikan kelebihan dan kekurangannya. Kemudian kami mengimplementasikan fungsi terjemahan alamat dan fungsi membuat pemetaan baru.

Seri artikel ini diterbitkan di GitHub . Jika Anda memiliki pertanyaan atau masalah, buka tiket terkait di sana. Semua sumber untuk artikel ada di utas ini .

Artikel lain tentang paging?
Jika Anda mengikuti siklus ini, Anda melihat artikel "Memori Halaman: Tingkat Lanjut" pada akhir Januari. Tapi saya dikritik karena tabel halaman rekursif. Oleh karena itu, saya memutuskan untuk menulis ulang artikel, menggunakan pendekatan berbeda untuk mengakses frame.

Ini opsi baru. Artikel ini masih menjelaskan bagaimana tabel halaman rekursif bekerja, tetapi kami menggunakan implementasi yang lebih sederhana dan lebih kuat. Kami tidak akan menghapus artikel sebelumnya, tetapi menandainya sebagai usang dan tidak akan memperbaruinya.

Saya harap Anda menikmati opsi baru!

Isi



Pendahuluan


Dari artikel terakhir, kami belajar tentang prinsip-prinsip memori paging dan bagaimana tabel halaman empat tingkat pada x86_64 . Kami juga menemukan bahwa loader sudah mengatur hierarki tabel halaman untuk kernel kami, jadi kernel tersebut berjalan pada alamat virtual. Ini meningkatkan keamanan karena akses yang tidak sah ke memori menyebabkan kesalahan halaman alih-alih mengubah memori fisik secara acak.

Artikel tersebut akhirnya tidak dapat mengakses tabel halaman dari kernel kami, karena mereka disimpan dalam memori fisik, dan kernel sudah berjalan di alamat virtual. Di sini kami melanjutkan topik dan mengeksplorasi berbagai opsi untuk mengakses bingkai tabel halaman dari kernel. Kami akan membahas kelebihan dan kekurangan masing-masing, dan kemudian memilih opsi yang sesuai untuk inti kami.

Diperlukan dukungan boot loader, jadi kami akan mengonfigurasinya terlebih dahulu. Kemudian kami menerapkan fungsi yang berjalan melalui seluruh hierarki tabel halaman untuk menerjemahkan alamat virtual ke alamat fisik. Terakhir, kita akan belajar cara membuat pemetaan baru dalam tabel halaman dan bagaimana menemukan bingkai memori yang tidak digunakan untuk membuat tabel baru.

Pembaruan Ketergantungan


Artikel ini mengharuskan Anda untuk mendaftar bootloader versi 0.4.0 atau lebih tinggi dan x86_64 versi 0.5.2 atau lebih tinggi dalam dependensi. Anda dapat memperbarui dependensi di Cargo.toml :

 [dependencies] bootloader = "0.4.0" x86_64 = "0.5.2" 

Untuk perubahan dalam versi ini, lihat log bootloader dan x86_64 log .

Akses ke tabel halaman


Mengakses tabel halaman dari kernel tidak semudah kelihatannya. Untuk memahami masalah ini, lihat hierarki tabel empat tingkat dari artikel sebelumnya:



Yang penting adalah bahwa setiap entri halaman menyimpan alamat fisik dari tabel berikut. Ini menghindari terjemahan dari alamat-alamat ini, yang mengurangi kinerja dan dengan mudah mengarah ke loop tanpa akhir.

Masalahnya adalah kita tidak dapat secara langsung mengakses alamat fisik dari kernel, karena ia juga bekerja pada alamat virtual. Sebagai contoh, ketika kita pergi ke alamat 4 KiB , kita mendapatkan akses ke alamat virtual 4 KiB , dan bukan ke alamat fisik di mana tabel halaman level 4 disimpan. Jika kita ingin mengakses alamat fisik 4 KiB , maka kita perlu menggunakan beberapa alamat virtual, yang diterjemahkan ke dalamnya.

Oleh karena itu, untuk mengakses frame dari tabel halaman, Anda perlu memetakan beberapa halaman virtual ke frame ini. Ada berbagai cara untuk membuat pemetaan seperti itu.

Pemetaan identitas


Solusi sederhana adalah tampilan identik dari semua tabel halaman .



Dalam contoh ini, kita melihat tampilan bingkai yang identik. Alamat fisik tabel halaman pada saat yang sama adalah alamat virtual yang valid, sehingga kita dapat dengan mudah mengakses tabel halaman dari semua level, dimulai dengan register CR3.

Namun, pendekatan ini mengacaukan ruang alamat virtual dan membuatnya sulit untuk menemukan area memori bebas yang bersebelahan. Katakanlah kita ingin membuat area memori virtual 1000 KiB pada gambar di atas, misalnya, untuk menampilkan file dalam memori . Kami tidak dapat memulai dengan wilayah 28 KiB , karena terletak pada halaman yang sudah diduduki pada 1004 KiB . Oleh karena itu, Anda harus melihat lebih jauh hingga kami menemukan fragmen besar yang cocok, misalnya, dengan 1008 KiB . Ada masalah fragmentasi yang sama seperti pada memori tersegmentasi.

Selain itu, pembuatan tabel halaman baru jauh lebih rumit, karena kita perlu menemukan bingkai fisik yang halaman yang sesuai belum digunakan. Misalnya, untuk file kami, kami memesan area seluas 1000 KiB memori virtual , mulai dari alamat 1008 KiB . Sekarang kita tidak dapat lagi menggunakan bingkai apa pun dengan alamat fisik antara 1000 KiB dan 2008 KiB , karena tidak dapat ditampilkan secara identik.

Memperbaiki peta offset


Untuk menghindari kekacauan ruang alamat virtual, Anda dapat menampilkan tabel halaman di area memori yang terpisah . Oleh karena itu, alih-alih mengidentifikasi pemetaan, kami memetakan frame dengan offset tetap di ruang alamat virtual. Misalnya, offset bisa 10 TiB:



Dengan mengalokasikan rentang memori virtual ini murni untuk menampilkan tabel halaman, kami menghindari masalah tampilan yang identik. Pemesanan ruang alamat virtual yang begitu besar hanya dimungkinkan jika ruang alamat virtual jauh lebih besar dari ukuran memori fisik. Pada x86_64 ini bukan masalah karena ruang alamat 48-bit adalah 256 TiB.

Tetapi pendekatan ini memiliki kelemahan bahwa ketika membuat setiap tabel halaman, Anda perlu membuat pemetaan baru. Selain itu, itu tidak memungkinkan akses ke tabel di ruang alamat lain, yang akan berguna saat membuat proses baru.

Pemetaan memori fisik penuh


Kami dapat memecahkan masalah ini dengan menampilkan semua memori fisik , dan bukan hanya bingkai tabel halaman:



Pendekatan ini memungkinkan kernel untuk mengakses memori fisik sewenang-wenang, termasuk bingkai tabel halaman dari ruang alamat lainnya. Berbagai memori virtual disediakan dengan ukuran yang sama seperti sebelumnya, tetapi hanya tidak ada halaman yang tidak cocok yang tersisa di dalamnya.

Kerugian dari pendekatan ini adalah bahwa tabel halaman tambahan diperlukan untuk menampilkan memori fisik. Tabel halaman ini harus disimpan di suatu tempat, sehingga mereka menggunakan sebagian dari memori fisik, yang dapat menjadi masalah pada perangkat dengan sejumlah kecil RAM.

Namun, pada x86_64 kita bisa menggunakan halaman 2 MiB besar untuk menampilkan alih-alih ukuran default 4 KiB. Dengan demikian, untuk menampilkan 32 GiB memori fisik, hanya diperlukan 132 KiB per tabel halaman: hanya satu tabel level ketiga dan 32 tabel level kedua. Halaman besar juga di-cache lebih efisien karena mereka menggunakan lebih sedikit entri dalam buffer terjemahan dinamis (TLB).

Tampilan sementara


Untuk perangkat dengan memori fisik sangat sedikit, Anda hanya dapat menampilkan tabel halaman sementara ketika Anda perlu mengaksesnya. Untuk perbandingan sementara, tampilan identik hanya dari tabel level pertama diperlukan:



Dalam gambar ini, tabel level 1 mengelola 2 MiB pertama dari ruang alamat virtual. Hal ini dimungkinkan karena akses dilakukan dari register CR3 melalui entri nol pada tabel level 4, 3 dan 2. Catatan dengan indeks 8 menerjemahkan halaman virtual pada 32 KiB menjadi kerangka fisik pada 32 KiB , dengan demikian mengidentifikasi tabel level 1 itu sendiri. Pada gambar ini ditunjukkan oleh panah horizontal.

Dengan menulis ke tabel level 1 yang dipetakan secara identik, kernel kami dapat membuat hingga 511 perbandingan waktu (512 dikurangi catatan yang diperlukan untuk pemetaan identitas). Pada contoh di atas, kernel membuat dua perbandingan waktu:

  • Memetakan entri nol di tabel level 1 ke bingkai di 24 KiB . Ini membuat pemetaan sementara halaman virtual pada 0 KiB ke bingkai fisik tabel level 2 halaman yang ditunjukkan oleh panah bertitik.
  • Cocokkan catatan ke-9 dari tabel level 1 dengan bingkai pada 4 KiB . Ini membuat pemetaan sementara halaman virtual pada 36 KiB ke bingkai fisik tabel level 4 halaman yang ditunjukkan oleh panah bertitik.

Sekarang kernel dapat mengakses tabel level 2 dengan menulis ke halaman yang dimulai pada 0 KiB dan tabel level 4 dengan menulis ke halaman yang dimulai pada 33 KiB .

Dengan demikian, akses ke bingkai tabel halaman yang sewenang-wenang dengan pemetaan sementara terdiri dari tindakan berikut:

  • Temukan entri gratis di tabel level 1 yang ditampilkan secara identik.
  • Petakan entri ini ke bingkai fisik tabel halaman yang ingin kita akses.
  • Akses bingkai ini melalui halaman virtual yang terkait dengan entri.
  • Atur rekaman kembali ke yang tidak digunakan, sehingga menghapus pemetaan sementara.

Dengan pendekatan ini, ruang alamat virtual tetap bersih, karena 512 halaman virtual yang sama terus digunakan. Kerugiannya adalah beberapa ketidaknyamanan, terutama karena perbandingan baru mungkin memerlukan perubahan beberapa tingkat tabel, yaitu, kita perlu mengulangi proses yang dijelaskan beberapa kali.

Tabel Halaman Rekursif


Pendekatan lain yang menarik yang tidak memerlukan tabel halaman tambahan sama sekali adalah pencocokan rekursif .

Idenya adalah untuk menerjemahkan beberapa catatan dari tabel tingkat keempat ke dalamnya. Dengan demikian, kami benar-benar memesan bagian dari ruang alamat virtual dan memetakan semua frame tabel saat ini dan masa depan ke ruang ini.

Mari kita lihat contoh untuk memahami bagaimana semua ini bekerja:



Satu-satunya perbedaan dari contoh di awal artikel adalah catatan tambahan dengan indeks 511 di tabel level 4, yang dipetakan ke bingkai fisik 4 KiB , yang terletak di tabel ini sendiri.

Saat CPU mencatat, ini tidak merujuk ke tabel level 3, tetapi lagi-lagi merujuk ke tabel level 4. Ini mirip dengan fungsi rekursif yang memanggil dirinya sendiri. Adalah penting bahwa prosesor mengasumsikan bahwa setiap record di tabel level 4 menunjuk ke tabel level 3, jadi sekarang ia memperlakukan tabel level 4 sebagai tabel level 3. Ini berfungsi karena tabel semua level di x86_64 memiliki struktur yang sama.

Dengan mengikuti rekaman rekursif satu atau lebih kali sebelum memulai konversi yang sebenarnya, kita dapat secara efektif mengurangi jumlah level yang dilalui prosesor. Sebagai contoh, jika kita mengikuti catatan rekursif sekali, dan kemudian pergi ke tabel level 3, prosesor berpikir bahwa tabel level 3 adalah tabel level 2. Selanjutnya, dia menganggap tabel level 2 sebagai tabel level 1, dan tabel level 1 dipetakan bingkai dalam memori fisik. Ini berarti bahwa kita sekarang dapat membaca dan menulis ke tabel level 1 halaman karena prosesor berpikir ini adalah bingkai yang dipetakan. Gambar di bawah ini menunjukkan lima langkah terjemahan seperti itu:



Demikian pula, kita dapat mengikuti entri rekursif dua kali sebelum memulai konversi untuk mengurangi jumlah level yang diteruskan menjadi dua:



Mari kita menjalani prosedur ini langkah demi langkah. Pertama, CPU mengikuti entri rekursif di tabel level 4 dan berpikir telah mencapai tabel level 3. Kemudian ia mengikuti entri rekursif lagi dan berpikir bahwa ia telah mencapai level 2. Tetapi pada kenyataannya itu masih di level 4. Kemudian CPU pergi ke alamat baru. dan masuk ke tabel level 3, tetapi berpikir itu sudah ada di tabel level 1. Akhirnya, pada titik entri berikutnya di tabel level 2, prosesor berpikir telah mengakses frame memori fisik. Ini memungkinkan kita untuk membaca dan menulis ke tabel level 2.

Tabel level 3 dan 4 juga dapat diakses. Untuk mengakses tabel level 3 kita mengikuti catatan rekursif tiga kali: prosesor berpikir bahwa itu sudah ada di tabel level 1, dan pada langkah berikutnya kita mencapai level 3, yang CPU anggap sebagai bingkai yang dipetakan. Untuk mengakses tabel level 4 itu sendiri, kita cukup mengikuti catatan rekursif empat kali sampai prosesor memproses tabel level 4 itu sendiri sebagai bingkai yang dipetakan (berwarna biru pada gambar di bawah).



Konsep ini sulit dipahami pada awalnya, tetapi dalam praktiknya ia bekerja dengan cukup baik.

Perhitungan Alamat


Jadi, kita dapat mengakses tabel dari semua level dengan mengikuti rekaman rekursif satu atau lebih kali. Karena indeks dalam tabel empat level diturunkan langsung dari alamat virtual, alamat virtual khusus harus dibuat untuk metode ini. Seperti yang kita ingat, indeks tabel halaman diekstraksi dari alamat sebagai berikut:



Misalkan kita ingin mengakses tabel level 1 yang menampilkan halaman tertentu. Seperti yang kita pelajari di atas, Anda harus melalui catatan rekursif sekali, dan kemudian melalui indeks level 4, 3 dan 2. Untuk melakukan ini, kami memindahkan semua blok alamat satu blok ke kanan dan mengatur indeks catatan rekursif ke tempat indeks awal level 4:



Untuk mengakses tabel level 2 halaman ini, kami memindahkan semua blok indeks dua blok ke kanan dan mengatur indeks rekursif ke tempat kedua blok sumber: level 4 dan level 3:



Untuk mengakses tabel level 3, kami melakukan hal yang sama, kami hanya menggeser ke kanan sudah tiga blok alamat.



Akhirnya, untuk mengakses tabel level 4, pindahkan semuanya empat blok ke kanan.



Sekarang Anda dapat menghitung alamat virtual untuk tabel halaman dari keempat level. Kita bahkan dapat menghitung alamat yang secara tepat menunjuk ke entri tabel halaman tertentu dengan mengalikan indeksnya dengan 8, ukuran entri tabel halaman.

Tabel di bawah ini menunjukkan struktur alamat untuk mengakses berbagai jenis bingkai:

Alamat virtual untukStruktur alamat ( oktal )
Halaman0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE
Entri di tabel level 10o_SSSSSS_RRR_AAA_BBB_CCC_DDDD
Entri dalam tabel level 20o_SSSSSS_RRR_RRR_AAA_BBB_CCCC
Entri dalam tabel level 30o_SSSSSS_RRR_RRR_RRR_AAA_BBBB
Entri di tabel level 40o_SSSSSS_RRR_RRR_RRR_RRR_AAAA

Di sini adalah indeks level 4, adalah level 3, adalah level 2, dan DDD adalah indeks level 1 untuk frame yang ditampilkan, EEEE adalah penyeimbangnya. RRR adalah indeks dari catatan rekursif. Indeks (tiga digit) dikonversi menjadi offset (empat digit) dengan mengalikannya dengan 8 (ukuran entri tabel halaman). Dengan offset ini, alamat yang dihasilkan langsung menunjuk ke entri tabel halaman yang sesuai.

SSSS adalah bit ekspansi dari digit yang ditandatangani, yaitu, mereka semua adalah salinan bit 47. Ini adalah persyaratan khusus untuk alamat yang valid dalam arsitektur x86_64, yang kami bahas dalam artikel sebelumnya .

Alamatnya oktal , karena setiap karakter oktal mewakili tiga bit, yang memungkinkan Anda untuk memisahkan indeks tabel 9-bit pada level yang berbeda. Ini tidak mungkin dalam sistem heksadesimal, di mana setiap karakter mewakili empat bit.

Kode karat


Anda dapat membuat alamat tersebut dalam kode Rust menggunakan operasi bitwise:

 // the virtual address whose corresponding page tables you want to access let addr: usize = […]; let r = 0o777; // recursive index let sign = 0o177777 << 48; // sign extension // retrieve the page table indices of the address that we want to translate let l4_idx = (addr >> 39) & 0o777; // level 4 index let l3_idx = (addr >> 30) & 0o777; // level 3 index let l2_idx = (addr >> 21) & 0o777; // level 2 index let l1_idx = (addr >> 12) & 0o777; // level 1 index let page_offset = addr & 0o7777; // calculate the table addresses let level_4_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (r << 12); let level_3_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (l4_idx << 12); let level_2_table_addr = sign | (r << 39) | (r << 30) | (l4_idx << 21) | (l3_idx << 12); let level_1_table_addr = sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12); 

Kode ini mengasumsikan pemetaan rekursif dari catatan level 4 terakhir dengan indeks 0o777 (511) dicocokkan secara rekursif. Ini bukan masalahnya, jadi kodenya belum berfungsi. Lihat di bawah tentang cara memberi tahu loader untuk mengatur pemetaan rekursif.

Sebagai alternatif untuk melakukan operasi bitwise secara manual, Anda dapat menggunakan jenis RecursivePageTable dari x86_64 crate, yang menyediakan abstraksi yang aman untuk berbagai operasi tabel. Misalnya, kode di bawah ini menunjukkan cara mengubah alamat virtual ke alamat fisiknya yang sesuai:

 // in src/memory.rs use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable}; use x86_64::{VirtAddr, PhysAddr}; /// Creates a RecursivePageTable instance from the level 4 address. let level_4_table_addr = […]; let level_4_table_ptr = level_4_table_addr as *mut PageTable; let recursive_page_table = unsafe { let level_4_table = &mut *level_4_table_ptr; RecursivePageTable::new(level_4_table).unwrap(); } /// Retrieve the physical address for the given virtual address let addr: u64 = […] let addr = VirtAddr::new(addr); let page: Page = Page::containing_address(addr); // perform the translation let frame = recursive_page_table.translate_page(page); frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) 

Sekali lagi, kode ini membutuhkan pemetaan rekursif yang benar. Dengan pemetaan ini, level_4_table_addr hilang dihitung seperti pada contoh kode pertama.



Pemetaan rekursif adalah metode yang menarik yang menunjukkan seberapa kuat pencocokan dapat melalui satu tabel. Ini relatif mudah diimplementasikan dan hanya membutuhkan pengaturan minimal (hanya satu entri rekursif), jadi ini adalah pilihan yang baik untuk percobaan pertama.

Tetapi memiliki beberapa kelemahan:

  • Memori virtual dalam jumlah besar (512 GiB). Ini bukan masalah dalam ruang alamat 48-bit yang besar, tetapi dapat menyebabkan perilaku cache yang kurang optimal.
  • Ini dengan mudah memberikan akses hanya ke ruang alamat yang sedang aktif. Akses ke ruang alamat lain masih dimungkinkan dengan mengubah entri rekursif, tetapi pencocokan sementara diperlukan untuk beralih. Kami menjelaskan cara melakukan ini di artikel sebelumnya (usang).
  • Ini sangat tergantung pada format tabel halaman x86 dan mungkin tidak berfungsi pada arsitektur lain.

Dukungan bootloader


Semua pendekatan yang dijelaskan di atas memerlukan perubahan pada tabel halaman dan pengaturan yang sesuai. Sebagai contoh, untuk memetakan memori fisik secara identik atau rekursif memetakan catatan dari tabel tingkat keempat. Masalahnya adalah kita tidak dapat melakukan pengaturan ini tanpa akses ke tabel halaman.

Jadi, saya butuh bantuan dari bootloader. Dia memiliki akses ke tabel halaman, sehingga dia dapat membuat tampilan yang kita butuhkan. Dalam implementasinya saat ini, kotak bootloader mendukung dua pendekatan di atas menggunakan fungsi kargo :

  • Fungsi map_physical_memory memetakan memori fisik penuh di suatu tempat di ruang alamat virtual. Dengan demikian, kernel mendapatkan akses ke semua memori fisik dan dapat menerapkan pendekatan dengan tampilan memori fisik penuh .
  • Menggunakan fungsi recursive_page_table , loader secara rekursif menampilkan entri tabel halaman tingkat keempat. Ini memungkinkan kernel untuk bekerja sesuai dengan metode yang dijelaskan dalam bagian "Tabel Halaman Rekursif" .

Untuk kernel kami, kami memilih opsi pertama, karena ini adalah pendekatan yang sederhana, platform-independen dan lebih kuat (juga memberikan akses ke frame lain, bukan hanya tabel halaman). Untuk dukungan dari bootloader, tambahkan fungsi ke dependensinya map_physical_memory:

 [dependencies] bootloader = { version = "0.4.0", features = ["map_physical_memory"]} 

Jika fitur ini diaktifkan, bootloader memetakan memori fisik lengkap ke beberapa alamat virtual yang tidak digunakan. Untuk meneruskan serangkaian alamat virtual ke kernel, bootloader meneruskan struktur informasi boot .

Informasi boot


Peti bootloadermendefinisikan struktur bootinfo dengan semua informasi yang dikirimkan oleh inti. Struktur masih diselesaikan, sehingga mungkin ada beberapa kegagalan saat memutakhirkan ke versi masa depan yang tidak kompatibel dengan semver . Saat ini, struktur memiliki dua bidang: memory_mapdan physical_memory_offset:

  • Bidang ini memory_mapmenyediakan ikhtisar memori fisik yang tersedia. Ini memberitahu kernel berapa banyak memori fisik yang tersedia pada sistem dan area memori mana yang dicadangkan untuk perangkat seperti VGA. Kartu memori dapat diminta dari firmware BIOS atau UEFI, tetapi hanya pada awal proses booting. Karena alasan ini, loader harus menyediakannya, karena kernel tidak akan lagi dapat menerima informasi ini. Kartu memori akan berguna nanti di artikel ini.
  • physical_memory_offsetmelaporkan alamat awal virtual pemetaan memori fisik. Menambahkan offset ini ke alamat fisik, kami mendapatkan alamat virtual yang sesuai. Ini memberikan akses dari kernel ke memori fisik sewenang-wenang.

Loader meneruskan struktur BootInfoke kernel sebagai argumen &'static BootInfoke fungsi _start. Tambahkan:

 // in src/main.rs use bootloader::BootInfo; #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // new argument […] } 

Penting untuk menentukan jenis argumen yang benar, karena kompiler tidak mengetahui jenis tanda tangan yang benar dari fungsi titik masuk kami.

Titik masuk makro


Karena fungsi _startdipanggil secara eksternal dari loader, tanda tangan fungsi tidak dicentang. Ini berarti bahwa kita dapat membiarkannya menerima argumen arbitrer tanpa kesalahan kompilasi, tetapi ini akan crash atau menyebabkan perilaku runtime yang tidak ditentukan.

Untuk memastikan bahwa fungsi titik masuk selalu memiliki tanda tangan yang benar, peti bootloadermenyediakan makro entry_point. Kami menulis ulang fungsi kami menggunakan makro ini:

 // in src/main.rs use bootloader::{BootInfo, entry_point}; entry_point!(kernel_main); #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] } 

Anda tidak perlu lagi menggunakan untuk titik masuk extern "C"atau no_mangle, karena makro menentukan bagi kami titik masuk nyata dari tingkat yang lebih rendah _start. Fungsi kernel_mainsekarang telah menjadi fungsi Rust yang benar-benar normal, sehingga kita dapat memilih nama arbitrer untuk itu. Yang penting dicentang berdasarkan jenisnya, jadi jika Anda menggunakan tanda tangan yang salah, misalnya dengan menambahkan argumen atau mengubah jenisnya, kesalahan kompilasi akan terjadi

Implementasi


Sekarang kita memiliki akses ke memori fisik, dan akhirnya kita dapat memulai implementasi sistem. Pertama, pertimbangkan tabel halaman aktif saat ini di mana kernel berjalan. Pada langkah kedua, buat fungsi terjemahan yang mengembalikan alamat fisik yang dipetakan alamat virtual ini. Pada langkah terakhir, kami akan mencoba mengubah tabel halaman untuk membuat pemetaan baru.

Pertama, buat modul baru dalam kode memory:

 // in src/lib.rs pub mod memory; 

Untuk modul, buat file kosong src/memory.rs.

Akses ke tabel halaman


Pada akhir artikel sebelumnya, kami mencoba melihat tabel halaman tempat kernel bekerja, tetapi tidak dapat mengakses kerangka fisik yang ditunjukkan oleh register CR3. Sekarang kita dapat terus bekerja dari tempat ini: fungsi active_level_4_tableakan mengembalikan tautan ke tabel halaman aktif dari tingkat keempat:

 // in src/memory.rs use x86_64::structures::paging::PageTable; /// Returns a mutable reference to the active level 4 table. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable { use x86_64::{registers::control::Cr3, VirtAddr}; let (level_4_table_frame, _) = Cr3::read(); let phys = level_4_table_frame.start_address(); let virt = VirtAddr::new(phys.as_u64() + physical_memory_offset); let page_table_ptr: *mut PageTable = virt.as_mut_ptr(); &mut *page_table_ptr // unsafe } 

Pertama, kita membaca kerangka fisik tabel aktif tingkat 4 dari register CR3. Kemudian kami mengambil alamat awal fisiknya dan mengubahnya menjadi alamat virtual dengan menambahkan physical_memory_offset. Akhirnya, ubah alamat menjadi pointer mentah *mut PageTabledengan metode ini as_mut_ptr, dan kemudian buat tautan dengan tidak aman &mut PageTable. Kami membuat tautan &mutsebagai gantinya &, karena nanti dalam artikel kami akan memodifikasi tabel halaman ini.

Tidak perlu memasukkan blok yang tidak aman di sini, karena Rust menganggap seluruh tubuh unsafe fnsebagai satu blok besar yang tidak aman. Ini meningkatkan risiko, karena dimungkinkan untuk secara tidak sengaja memperkenalkan operasi yang tidak aman di baris sebelumnya. Ini juga membuat sulit untuk mendeteksi operasi yang tidak aman. RFC telah dibuat untuk memodifikasi perilaku Rust ini.

Sekarang kita bisa menggunakan fungsi ini untuk menampilkan catatan dari tabel tingkat keempat:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::active_level_4_table; let l4_table = unsafe { active_level_4_table(boot_info.physical_memory_offset) }; for (i, entry) in l4_table.iter().enumerate() { if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); } } println!("It did not crash!"); blog_os::hlt_loop(); } 

Kami physical_memory_offsetmelewati bidang struktur yang sesuai BootInfo. Kemudian kami menggunakan fungsi iteruntuk beralih melalui entri tabel halaman dan kombinator enumerateuntuk menambahkan indeks ike setiap elemen. Hanya entri yang tidak kosong yang ditampilkan, karena semua 512 entri tidak akan muat di layar.

Ketika kami menjalankan kode, kami melihat hasil ini:



Kami melihat beberapa catatan tidak kosong yang dipetakan ke berbagai tabel tingkat ketiga. Begitu banyak area memori yang digunakan karena area terpisah diperlukan untuk kode kernel, tumpukan kernel, terjemahan memori fisik, dan informasi booting.

Untuk melihat tabel halaman dan melihat tabel level ketiga, kita dapat kembali mengkonversi frame yang ditampilkan ke alamat virtual:

 // in the for loop in src/main.rs use x86_64::{structures::paging::PageTable, VirtAddr}; if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); // get the physical address from the entry and convert it let phys = entry.frame().unwrap().start_address(); let virt = phys.as_u64() + boot_info.physical_memory_offset; let ptr = VirtAddr::new(virt).as_mut_ptr(); let l3_table: &PageTable = unsafe { &*ptr }; // print non-empty entries of the level 3 table for (i, entry) in l3_table.iter().enumerate() { if !entry.is_unused() { println!(" L3 Entry {}: {:?}", i, entry); } } } 

Untuk melihat tabel level kedua dan pertama, ulangi proses ini, masing-masing, untuk catatan level ketiga dan kedua. Seperti yang dapat Anda bayangkan, jumlah kode berkembang sangat cepat, jadi kami tidak akan mempublikasikan listing lengkap.

Tabel traverse manual menarik karena membantu memahami bagaimana prosesor menerjemahkan alamat. Tetapi biasanya kami hanya tertarik untuk menampilkan satu alamat fisik untuk alamat virtual tertentu, jadi mari kita buat fungsi untuk ini.

Terjemahan Alamat


Untuk menerjemahkan alamat virtual ke alamat fisik, kita harus melalui tabel halaman empat tingkat sampai kita mencapai bingkai yang dipetakan. Mari kita buat fungsi yang melakukan terjemahan alamat ini:

 // in src/memory.rs use x86_64::{PhysAddr, VirtAddr}; /// Translates the given virtual address to the mapped physical address, or /// `None` if the address is not mapped. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { translate_addr_inner(addr, physical_memory_offset) } 

Kami merujuk ke fungsi aman translate_addr_inneruntuk membatasi jumlah kode tidak aman. Seperti disebutkan di atas, Rust menganggap seluruh tubuh unsafe fnsebagai blok besar yang tidak aman. Dengan menjalankan satu fungsi aman, kami kembali membuat setiap operasi eksplisit unsafe.

Fungsi internal khusus memiliki fungsi nyata:

 // in src/memory.rs /// Private function that is called by `translate_addr`. /// /// This function is safe to limit the scope of `unsafe` because Rust treats /// the whole body of unsafe functions as an unsafe block. This function must /// only be reachable through `unsafe fn` from outside of this module. fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { use x86_64::structures::paging::page_table::FrameError; use x86_64::registers::control::Cr3; // read the active level 4 frame from the CR3 register let (level_4_table_frame, _) = Cr3::read(); let table_indexes = [ addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index() ]; let mut frame = level_4_table_frame; // traverse the multi-level page table for &index in &table_indexes { // convert the frame into a page table reference let virt = frame.start_address().as_u64() + physical_memory_offset; let table_ptr: *const PageTable = VirtAddr::new(virt).as_ptr(); let table = unsafe {&*table_ptr}; // read the page table entry and update `frame` let entry = &table[index]; frame = match entry.frame() { Ok(frame) => frame, Err(FrameError::FrameNotPresent) => return None, Err(FrameError::HugeFrame) => panic!("huge pages not supported"), }; } // calculate the physical address by adding the page offset Some(frame.start_address() + u64::from(addr.page_offset())) } 

Alih-alih menggunakan kembali fungsi, active_level_4_tablekami membaca kembali frame tingkat keempat dari register CR3, karena ini menyederhanakan implementasi prototipe. Jangan khawatir, kami akan segera memperbaiki solusinya.

Struktur VirtAddrsudah menyediakan metode untuk menghitung indeks dalam tabel halaman dari empat level. Kami menyimpan indeks ini dalam array kecil, karena memungkinkan Anda untuk mengulang semua tabel for. Di luar loop, kita ingat frame terakhir yang dikunjungi untuk menghitung alamat fisik nanti. framemenunjuk ke bingkai tabel halaman selama iterasi dan ke frame terkait setelah iterasi terakhir, yaitu, setelah melewati catatan level 1.

Di dalam loop, kami sekali lagi menerapkanphysical_memory_offsetuntuk mengubah bingkai ke tautan tabel halaman. Kemudian kita membaca catatan dari tabel halaman saat ini dan menggunakan fungsi PageTableEntry::frameuntuk mengambil frame yang cocok. Jika catatan tidak dipetakan ke bingkai, kembali None. Jika catatan menampilkan halaman besar 2 MiB atau 1 GiB, sejauh ini kita akan panik.

Jadi, mari kita periksa fungsi terjemahan di beberapa alamat:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::translate_addr; use x86_64::VirtAddr; let addresses = [ // the identity-mapped vga buffer page 0xb8000, // some code page 0x20010a, // some stack page 0x57ac_001f_fe48, // virtual address mapped to physical address 0 boot_info.physical_memory_offset, ]; for &address in &addresses { let virt = VirtAddr::new(address); let phys = unsafe { translate_addr(virt, boot_info.physical_memory_offset) }; println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); } 

Ketika kami menjalankan kode, kami mendapatkan hasil berikut:



Seperti yang diharapkan, dengan pemetaan yang identik, alamat tersebut 0xb8000dikonversi ke alamat fisik yang sama. Halaman kode dan halaman stack dikonversi ke alamat fisik yang berubah-ubah, yang tergantung pada bagaimana loader membuat pemetaan awal untuk kernel kami. Pemetaan physical_memory_offsetharus menunjuk ke alamat fisik 0, tetapi gagal, karena terjemahan menggunakan halaman besar untuk efisiensi. Versi bootloader yang akan datang mungkin menerapkan optimasi yang sama untuk halaman kernel dan stack.

Menggunakan MappedPageTable


Penerjemahan alamat virtual ke alamat fisik adalah tugas khas dari kernel OS, oleh karena itu peti x86_64menyediakan abstraksi untuk itu. Itu sudah mendukung halaman besar dan beberapa fungsi lainnya, kecuali translate_addr, oleh karena itu, kami menggunakannya alih-alih menambahkan dukungan untuk halaman besar ke implementasi kami sendiri.

Dasar abstraksi adalah dua sifat yang mendefinisikan berbagai fungsi terjemahan dari tabel halaman:

  • Ciri tersebut Mappermenyediakan fungsi yang berfungsi pada halaman. Misalnya, translate_pagemenerjemahkan halaman ini ke dalam bingkai dengan ukuran yang sama, serta map_tomembuat pemetaan baru di tabel.
  • Sifat tersebut MapperAllSizesmenyiratkan aplikasi Mapperuntuk semua ukuran halaman. Selain itu, ia menyediakan fungsi yang berfungsi dengan halaman dengan ukuran yang berbeda, termasuk translate_addratau umum translate.

Ciri-ciri hanya mendefinisikan antarmuka, tetapi tidak menyediakan implementasi apa pun. Sekarang peti x86_64menyediakan dua jenis yang menerapkan sifat: MappedPageTabledan RecursivePageTable. Yang pertama mengharuskan setiap frame dari tabel halaman ditampilkan di suatu tempat (misalnya, dengan offset). Tipe kedua dapat digunakan jika tabel tingkat keempat ditampilkan secara rekursif.

Kami memiliki semua memori fisik yang dipetakan physical_memory_offset, sehingga Anda dapat menggunakan tipe MappedPageTable. Untuk menginisialisasi, buat fungsi baru initdalam modul memory:

 use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr; /// Initialize a new MappedPageTable. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn init(physical_memory_offset: u64) -> impl MapperAllSizes { let level_4_table = active_level_4_table(physical_memory_offset); let phys_to_virt = move |frame: PhysFrame| -> *mut PageTable { let phys = frame.start_address().as_u64(); let virt = VirtAddr::new(phys + physical_memory_offset); virt.as_mut_ptr() }; MappedPageTable::new(level_4_table, phys_to_virt) } // make private unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable {…} 

Kami tidak dapat langsung kembali MappedPageTabledari suatu fungsi karena itu umum untuk tipe penutupan. Kami akan menyelesaikan masalah ini dengan konstruk sintaksis impl Trait. Keuntungan tambahan adalah Anda kemudian dapat beralih ke kernel RecursivePageTabletanpa mengubah tanda tangan fungsi.

Fungsi ini MappedPageTable::newmengharapkan dua parameter: tautan yang dapat diubah ke tabel halaman level 4 dan penutupan phys_to_virtyang mengubah bingkai fisik menjadi penunjuk tabel halaman *mut PageTable. Untuk parameter pertama, kita dapat menggunakan kembali fungsinya active_level_4_table. Untuk yang kedua, kami membuat penutupan yang digunakan physical_memory_offsetuntuk melakukan konversi.

Kami juga menjadikannya sebagai active_level_4_tablefungsi pribadi, karena mulai sekarang hanya akan dipanggil dari init.

Untuk menggunakan metode iniMapperAllSizes::translate_addralih-alih fungsi kita sendiri memory::translate_addr, kita perlu mengubah hanya beberapa baris di kernel_main:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS // new: different imports use blog_os::memory; use x86_64::{structures::paging::MapperAllSizes, VirtAddr}; // new: initialize a mapper let mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let addresses = […]; // same as before for &address in &addresses { let virt = VirtAddr::new(address); // new: use the `mapper.translate_addr` method let phys = mapper.translate_addr(virt); println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); } 

Setelah memulai, kami melihat hasil terjemahan yang sama seperti sebelumnya, tetapi hanya halaman besar sekarang juga yang berfungsi:



Seperti yang diharapkan, alamat virtual physical_memory_offsetdikonversi ke alamat fisik 0x0. Menggunakan fungsi terjemahan untuk jenis ini MappedPageTable, kami menghilangkan kebutuhan untuk mengimplementasikan dukungan untuk halaman besar. Kami juga memiliki akses ke fungsi halaman lain, seperti map_toyang akan kami gunakan di bagian selanjutnya. Pada tahap ini, kita tidak lagi membutuhkan fungsinya memory::translate_addr, Anda dapat menghapusnya jika mau.

Buat pemetaan baru


Sejauh ini, kami hanya melihat tabel halaman, tetapi belum mengubah apa pun. Mari kita buat pemetaan baru untuk halaman yang sebelumnya tidak ditampilkan.

Kami akan menggunakan fungsi map_todari sifat Mapper, jadi pertama-tama kami akan mempertimbangkan fungsi ini. Dokumentasi mengatakan bahwa itu memerlukan empat argumen: halaman yang ingin kami tampilkan; Bingkai di mana halaman harus dipetakan. set bendera untuk menulis tabel halaman dan distributor bingkai frame_allocator. Pengalokasi bingkai diperlukan karena memetakan halaman ini mungkin memerlukan pembuatan tabel tambahan yang membutuhkan bingkai yang tidak digunakan sebagai penyimpanan cadangan.

Fungsi create_example_mapping


Langkah pertama dalam implementasi kami adalah membuat fungsi baru create_example_mappingyang memetakan halaman ini ke 0xb8000bingkai fisik buffer teks VGA. Kami memilih bingkai ini karena memudahkan untuk memeriksa apakah tampilan dibuat dengan benar: kita hanya perlu menulis ke halaman yang baru saja ditampilkan dan melihat apakah itu muncul di layar.

Fungsi create_example_mappingterlihat seperti ini:

 // in src/memory.rs use x86_64::structures::paging::{Page, Size4KiB, Mapper, FrameAllocator}; /// Creates an example mapping for the given page to frame `0xb8000`. pub fn create_example_mapping( page: Page, mapper: &mut impl Mapper<Size4KiB>, frame_allocator: &mut impl FrameAllocator<Size4KiB>, ) { use x86_64::structures::paging::PageTableFlags as Flags; let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); let flags = Flags::PRESENT | Flags::WRITABLE; let map_to_result = unsafe { mapper.map_to(page, frame, flags, frame_allocator) }; map_to_result.expect("map_to failed").flush(); } 

Selain halaman pageyang ingin Anda petakan, fungsi tersebut mengharapkan instance dari mapperdan frame_allocator. Tipe mappermengimplementasikan sifat Mapper<Size4KiB>yang disediakan metode map_to. Parameter umum Size4KiBdiperlukan, karena sifat Mappertersebut umum untuk sifat tersebut PageSize, bekerja dengan halaman standar 4 KiB dan halaman besar 2 MiB dan 1 GiB. Kami hanya ingin membuat 4 halaman KiB, jadi kami dapat menggunakannya Mapper<Size4KiB>alih-alih persyaratannya MapperAllSizes.

Sebagai perbandingan, atur flag PRESENT, karena itu diperlukan untuk semua entri yang valid, dan flag WRITABLEuntuk membuat halaman yang ditampilkan dapat ditulis. Tantanganmap_totidak aman: Anda dapat melanggar keamanan memori dengan argumen yang tidak valid, jadi Anda harus menggunakan blok unsafe. Untuk daftar semua kemungkinan bendera, lihat bagian "Format Tabel Halaman" dari artikel sebelumnya .

Fungsi ini map_tomungkin gagal, jadi itu kembali Result. Karena ini hanyalah contoh kode yang seharusnya tidak dapat diandalkan, kami cukup menggunakannya expectuntuk panik jika terjadi kesalahan. Jika berhasil, fungsi mengembalikan jenis MapperFlushyang menyediakan cara mudah untuk menghapus halaman yang baru-baru ini ditampilkan dari buffer terjemahan dinamis (TLB) menggunakan metode ini flush. Seperti Result, tipe ini menerapkan atribut [ #[must_use]] kemengeluarkan peringatan jika kita tidak sengaja lupa menggunakannya .

Fiktif FrameAllocator


Untuk menelepon create_example_mapping, Anda harus membuatnya terlebih dahulu FrameAllocator. Seperti disebutkan di atas, kerumitan membuat tampilan baru tergantung pada halaman virtual yang ingin kita tampilkan. Dalam kasus paling sederhana, tabel level 1 untuk halaman sudah ada, dan kita hanya perlu membuat satu catatan. Dalam kasus yang paling sulit, halaman berada di area memori yang level 3 belum dibuat, jadi pertama-tama Anda harus membuat tabel halaman level 3, 2 dan 1.

Mari kita mulai dengan kasing sederhana dan berasumsi bahwa Anda tidak perlu membuat tabel halaman baru. Distributor kerangka yang selalu kembali sudah cukup untuk ini None. Kami membuat EmptyFrameAllocatorfungsi tampilan untuk pengujian:

 // in src/memory.rs /// A FrameAllocator that always returns `None`. pub struct EmptyFrameAllocator; impl FrameAllocator<Size4KiB> for EmptyFrameAllocator { fn allocate_frame(&mut self) -> Option<PhysFrame> { None } } 

Sekarang Anda perlu menemukan halaman yang dapat ditampilkan tanpa membuat tabel halaman baru. Pemuat dimuat ke megabyte pertama ruang alamat virtual, jadi kami tahu bahwa untuk wilayah ini ada tabel level 1. yang valid. Sebagai contoh kami, kami dapat memilih halaman yang tidak digunakan di area memori ini, misalnya, halaman di alamat 0x1000.

Untuk menguji fungsinya, pertama-tama kita menampilkan halaman 0x1000, dan kemudian menampilkan isi dari memori:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory; use x86_64::{structures::paging::Page, VirtAddr}; let mut mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let mut frame_allocator = memory::EmptyFrameAllocator; // map a previously unmapped page let page = Page::containing_address(VirtAddr::new(0x1000)); memory::create_example_mapping(page, &mut mapper, &mut frame_allocator); // write the string `New!` to the screen through the new mapping let page_ptr: *mut u64 = page.start_address().as_mut_ptr(); unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)}; println!("It did not crash!"); blog_os::hlt_loop(); } 

Pertama, kami membuat pemetaan untuk halaman 0x1000, memanggil fungsi create_example_mappingdengan tautan yang dapat berubah ke instance mapperdan frame_allocator. Ini memetakan halaman 0x1000ke frame buffer teks VGA, jadi kita harus melihat apa yang tertulis di layar.

Kemudian konversikan halaman menjadi pointer mentah dan tulis nilainya ke offset 400. Kami tidak menulis ke bagian atas halaman karena garis atas buffer VGA secara langsung digeser dari layar sebagai berikut println. Tulis nilai 0x_f021_f077_f065_f04eyang sesuai dengan string "Baru!" pada latar belakang putih. Seperti yang kita pelajari di artikel "Mode Teks VGA" , menulis ke buffer VGA harus volatile, jadi kami menggunakan metode ini write_volatile.

Ketika kami menjalankan kode di QEMU, kami melihat hasil berikut:



Setelah menulis ke halaman 0x1000, tulisan "Baru!" .Jadi, kami telah berhasil membuat pemetaan baru di tabel halaman.

Susunan ini berfungsi karena sudah ada tabel level 1 untuk susunan 0x1000. Ketika kami mencoba memetakan halaman di mana tabel level 1 belum ada, fungsinya map_togagal karena mencoba mengalokasikan bingkai dari EmptyFrameAllocatoruntuk membuat tabel baru. Kami melihat bahwa ini terjadi ketika kami mencoba menampilkan halaman 0xdeadbeaf000alih-alih 0x1000:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); […] } 

Jika ini dimulai, panik terjadi dengan pesan kesalahan berikut:

 panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5 

Untuk menampilkan halaman yang belum memiliki tabel level 1 halaman, Anda harus membuat yang benar FrameAllocator. Tapi bagaimana Anda tahu frame mana yang gratis dan berapa banyak memori fisik yang tersedia?

Pemilihan Bingkai


Untuk tabel halaman baru, Anda harus membuat distributor bingkai yang benar. Mari kita mulai dengan kerangka umum:

 // in src/memory.rs pub struct BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { frames: I, } impl<I> FrameAllocator<Size4KiB> for BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { fn allocate_frame(&mut self) -> Option<PhysFrame> { self.frames.next() } } 

Bidang framesdapat diinisialisasi dengan iterator bingkai sewenang-wenang. Ini memungkinkan Anda untuk hanya mendelegasikan panggilan ke allocmetode ini Iterator::next.

Untuk inisialisasi, kami BootInfoFrameAllocatormenggunakan kartu memori memory_mapyang ditransfer oleh bootloader sebagai bagian dari struktur BootInfo. Sebagaimana dijelaskan di bagian Informasi Booting , kartu memori disediakan oleh firmware BIOS / UEFI. Ini dapat diminta hanya pada awal proses boot, sehingga bootloader telah memanggil fungsi yang diperlukan.

Kartu memori terdiri dari daftar struktur MemoryRegionyang berisi alamat awal, panjang, dan jenis (misalnya, tidak digunakan, dicadangkan, dll.) Dari setiap area memori. Dengan membuat iterator yang menghasilkan bingkai dari area yang tidak digunakan, kita bisa membuat yang valid BootInfoFrameAllocator.

Inisialisasi BootInfoFrameAllocatorterjadi pada fungsi baru init_frame_allocator:

 // in src/memory.rs use bootloader::bootinfo::{MemoryMap, MemoryRegionType}; /// Create a FrameAllocator from the passed memory map pub fn init_frame_allocator( memory_map: &'static MemoryMap, ) -> BootInfoFrameAllocator<impl Iterator<Item = PhysFrame>> { // get usable regions from memory map let regions = memory_map .iter() .filter(|r| r.region_type == MemoryRegionType::Usable); // map each region to its address range let addr_ranges = regions.map(|r| r.range.start_addr()..r.range.end_addr()); // transform to an iterator of frame start addresses let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096)); // create `PhysFrame` types from the start addresses let frames = frame_addresses.map(|addr| { PhysFrame::containing_address(PhysAddr::new(addr)) }); BootInfoFrameAllocator { frames } } 

Fungsi ini menggunakan kombinator untuk mengubah peta awal MemoryMapmenjadi iterator dari frame fisik yang digunakan:

  • Pertama, kami memanggil metode iteruntuk mengkonversi kartu memori ke iterator MemoryRegion. Kemudian kami menggunakan metode ini filteruntuk melewati daerah yang tidak dapat diakses atau tidak dapat diakses. Loader memperbarui kartu memori untuk semua pemetaan yang dibuatnya, sehingga frame yang digunakan oleh kernel (kode, data atau tumpukan) atau untuk menyimpan informasi tentang boot sudah ditandai sebagai InUseatau serupa. Dengan demikian, kita dapat yakin bahwa frame Usabletidak digunakan di tempat lain .
  • map range Rust .
  • : into_iter , 4096- step_by . 4096 (= 4 ) β€” , . , . flat_map map , Iterator<Item = u64> Iterator<Item = Iterator<Item = u64>> .
  • PhysFrame , Iterator<Item = PhysFrame> . BootInfoFrameAllocator .

Sekarang Anda dapat mengubah fungsi kita kernel_mainuntuk mengirimkan salinan BootInfoFrameAllocatorgantinya EmptyFrameAllocator:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let mut frame_allocator = memory::init_frame_allocator(&boot_info.memory_map); […] } 

Kali ini pemetaan alamat berhasil dan kami kembali melihat hitam dan putih "Baru!" .Di belakang layar, metode ini map_tomembuat tabel halaman yang hilang sebagai berikut:

  • Pilih bingkai yang tidak digunakan dari yang dikirimkan frame_allocator.
  • Nol bingkai untuk membuat tabel halaman kosong baru.
  • Petakan entri tabel tingkat lebih tinggi ke bingkai ini.
  • Pergi ke tingkat tabel berikutnya.

Meskipun fungsi kami create_example_mappinghanya kode sampel, kami sekarang dapat membuat pemetaan baru untuk halaman sewenang-wenang. Ini akan diperlukan untuk mengalokasikan memori dan mengimplementasikan multithreading di artikel mendatang.

Ringkasan


Dalam artikel ini, kami belajar tentang berbagai metode mengakses kerangka fisik tabel halaman, termasuk pemetaan identitas, pemetaan memori fisik penuh, pemetaan sementara, dan tabel halaman rekursif. Kami memilih untuk menampilkan memori fisik penuh sebagai metode yang sederhana dan kuat.

Kami tidak dapat memetakan memori fisik dari kernel tanpa akses ke tabel halaman, sehingga diperlukan dukungan bootloader. Rak bootloadermenciptakan pemetaan yang diperlukan melalui fungsi kargo tambahan. Ini menyampaikan informasi yang diperlukan ke kernel sebagai argumen &BootInfoke fungsi titik masuk.

Untuk implementasi kami, pertama-tama kami secara manual menelusuri tabel halaman, membuat fungsi terjemahan, dan kemudian menggunakan jenis MappedPageTablepetix86_64. Kami juga belajar cara membuat pemetaan baru di tabel halaman dan cara membuatnya FrameAllocatordi kartu memori yang dikirimkan oleh bootloader.

Apa selanjutnya


Pada artikel selanjutnya, kita akan membuat area memori heap untuk kernel kita, yang akan memungkinkan kita untuk mengalokasikan memori dan menggunakan berbagai jenis koleksi .

Source: https://habr.com/ru/post/id445618/


All Articles