Artikel ini menjelaskan bagaimana kernel sistem operasi dapat mengakses frame memori fisik. Kami akan mempelajari fungsi untuk mengubah alamat virtual menjadi alamat fisik. Kami juga akan mencari cara membuat pemetaan baru di tabel halaman.
Blog ini diposting di
GitHub . Jika Anda memiliki pertanyaan atau masalah, buka tiket terkait di sana. Semua sumber untuk artikel ada di
sini .
Pendahuluan
Dari
artikel terakhir, kami belajar tentang prinsip-prinsip memori paging dan bagaimana tabel halaman empat tingkat pada x86_64 bekerja. Kami juga menemukan bahwa loader sudah mengatur hierarki tabel halaman untuk kernel kami, jadi kernel tersebut berjalan pada alamat virtual. Ini meningkatkan keamanan, tetapi muncul masalah: bagaimana cara mengakses alamat fisik nyata yang disimpan dalam entri tabel halaman atau
CR3
?
Di bagian pertama artikel, kita akan membahas masalah dan berbagai pendekatan untuk menyelesaikannya. Kemudian kami menerapkan fungsi yang menyelinap melalui hierarki tabel halaman untuk mengubah alamat virtual menjadi yang fisik. Akhirnya, pelajari cara membuat pemetaan baru dalam tabel halaman dan menemukan bingkai memori yang tidak digunakan untuk membuat tabel baru.
Pembaruan Ketergantungan
Untuk bekerja, Anda perlu
x86_64
versi 0.4.0 atau yang lebih baru. Perbarui ketergantungan dalam
Cargo.toml
kami:
[dependencies] x86_64 = "0.4.0" # or later
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 tingkat 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.
1. 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.
2. Pilihan lain adalah untuk
menyiarkan tabel halaman hanya 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). Dalam contoh di atas, kernel cocok dengan catatan nol dari tabel level 1 dengan frame pada
24 KiB
. Ini menciptakan pemetaan sementara dari halaman virtual pada
0 KiB
ke bingkai fisik tabel level halaman 2 yang ditunjukkan oleh panah bertitik. Sekarang kernel dapat mengakses tabel level 2 dengan menulis ke halaman yang dimulai pada
0 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.
3. Meskipun kedua pendekatan di atas bekerja, ada metode ketiga:
tabel halaman rekursif . Ini menggabungkan keuntungan dari kedua pendekatan: itu terus-menerus membandingkan semua frame dari tabel halaman, tanpa memerlukan perbandingan sementara, dan juga menjaga halaman yang berdekatan berdampingan, menghindari fragmentasi ruang alamat virtual. Ini adalah metode yang akan kita gunakan.
Tabel Halaman 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 ini:
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 bahwa ia telah mencapai tabel level 3. Kemudian ia mengikuti catatan 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 untuk | Struktur alamat ( oktal ) |
---|
Halaman | 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE |
Entri di tabel level 1 | 0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD |
Entri dalam tabel level 2 | 0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC |
Entri dalam tabel level 3 | 0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB |
Entri di tabel level 4 | 0o_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 offset-nya.
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, semuanya adalah salinan dari 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.
Implementasi
Setelah semua teori ini, kita akhirnya bisa melanjutkan implementasi. Dengan mudah, loader tidak hanya menghasilkan tabel halaman, tetapi juga tampilan rekursif pada catatan terakhir tabel level 4. Loader melakukan ini karena jika tidak akan ada masalah ayam atau telur: kita perlu mengakses tabel level 4 untuk membuat peta rekursif tetapi kami tidak dapat mengaksesnya tanpa tampilan apa pun.
Kami sudah menggunakan pemetaan rekursif ini di akhir artikel sebelumnya untuk mengakses tabel level 4 melalui alamat hard-code
0xffff_ffff_ffff_f000
. Jika kita mengonversi alamat ini menjadi oktal dan membandingkannya dengan tabel di atas, kita akan melihat bahwa alamat tersebut persis sesuai dengan struktur catatan di tabel level 4 dengan
RRR
=
0o777
,
AAAA
=
0
dan bit ekstensi tanda
1
:
struktur: 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
alamat: 0o_177777_777_777_777_777_0000
Berkat pengetahuan tabel rekursif, sekarang kita dapat membuat alamat virtual untuk mengakses semua tabel aktif. Dan buatlah fungsi siaran.
Terjemahan Alamat
Sebagai langkah pertama, buat fungsi yang mengubah alamat virtual menjadi alamat fisik, melewati hierarki tabel halaman:
Pertama, kami memperkenalkan variabel untuk indeks rekursif (511 =
0o777
) dan bit ekstensi tanda (masing-masing adalah 1). Kemudian kami menghitung indeks tabel halaman dan offset melalui operasi bitwise, seperti yang ditunjukkan dalam ilustrasi:
Langkah selanjutnya adalah menghitung alamat virtual dari empat tabel halaman, seperti yang dijelaskan pada bagian sebelumnya. Selanjutnya, dalam fungsinya, kami mengonversi masing-masing alamat ini ke tautan
PageTable
. Ini adalah operasi yang tidak aman karena kompiler tidak dapat mengetahui bahwa alamat ini valid.
Setelah menghitung alamat, kami menggunakan operator pengindeksan untuk melihat catatan di tabel level 4. Jika catatan ini nol, maka tidak ada tabel level 3 untuk catatan level 4. Ini berarti bahwa
addr
tidak dipetakan ke memori fisik apa pun. Jadi kami mengembalikan
None
. Kalau tidak, kita tahu bahwa tabel level 3 ada. Kemudian kami ulangi prosedurnya, seperti pada tingkat sebelumnya.
Setelah memeriksa tiga halaman dari level yang lebih tinggi, kita akhirnya bisa membaca catatan dari tabel level 1, yang memberi tahu kita kerangka fisik yang dengannya peta dipetakan. Sebagai langkah terakhir, tambahkan offset halaman ke dalamnya - dan kembalikan alamatnya.
Jika kami tahu pasti bahwa alamat itu dipetakan, kami dapat langsung mengakses tabel level 1 tanpa melihat halaman-halaman dari level yang lebih tinggi. Tetapi karena kita tidak mengetahuinya, pertama-tama kita perlu memeriksa apakah tabel level 1 ada, jika tidak fungsi kita akan mengembalikan kesalahan kegagalan halaman untuk alamat yang tidak cocok.
Coba
Mari kita coba menggunakan fungsi terjemahan untuk alamat virtual di fungsi
_start
kami:
Setelah memulai, kami melihat hasil berikut:
Seperti yang diharapkan, alamat 0xb8000 yang terkait dengan pengenal diterjemahkan ke alamat fisik yang sama. Halaman kode dan halaman stack dikonversi ke beberapa alamat fisik yang berubah-ubah, yang tergantung pada bagaimana loader membuat pemetaan awal untuk kernel kami.
RecursivePageTable
x86_64 menyediakan jenis
RecursivePageTable
yang mengimplementasikan abstraksi aman untuk berbagai operasi tabel halaman. Dengan menggunakan tipe ini, Anda dapat mengimplementasikan fungsi
translate_addr
secara lebih ringkas:
Jenis
RecursivePageTable
sepenuhnya merangkum merangkak tabel halaman tidak aman, sehingga kode
unsafe
dalam fungsi
translate_addr
tidak lagi diperlukan. Fungsi
init
tetap tidak aman karena harus menjamin kebenaran
level_4_table_addr
diteruskan.
Fungsi
_start
kami harus diperbarui untuk masuk kembali fungsi sebagai berikut:
Sekarang, alih-alih meneruskan
LEVEL_4_TABLE_ADDR
untuk
translate_addr
dan mengakses tabel halaman melalui pointer mentah yang tidak aman, kami meneruskan referensi ke tipe
RecursivePageTable
. Dengan demikian, kami sekarang memiliki abstraksi yang aman dan semantik kepemilikan yang jelas. Ini memastikan bahwa kami tidak akan dapat secara tidak sengaja mengubah tabel halaman dalam akses bersama, karena mengubahnya memerlukan kepemilikan eksklusif
RecursivePageTable
.
Fungsi ini memberikan hasil yang sama dengan fungsi terjemahan asli yang ditulis secara manual.
Membuat fitur yang tidak aman lebih aman
memory::init
adalah fungsi yang tidak aman: memerlukan blok untuk memanggilnya unsafe
, karena penelepon harus menjamin bahwa persyaratan tertentu dipenuhi. Dalam kasus kami, persyaratannya adalah bahwa alamat yang ditransmisikan secara tepat dipetakan ke kerangka fisik dari tabel halaman level 4. Seluruh tubuh dari fungsi yang tidak aman ditempatkandi blok unsafe
sehingga semua jenis operasi dilakukan tanpa membuat blok tambahan unsafe
. Karena itu, kita tidak perlu blok yang tidak aman untuk dereferencing level_4_table_ptr
: pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = &mut *level_4_table_ptr;
Masalahnya adalah kita tidak segera melihat bagian mana yang tidak aman. Misalnya, tanpa melihat definisi suatu fungsi, RecursivePageTable::new
kita tidak dapat mengatakan apakah itu aman atau tidak. Jadi sangat mudah untuk secara tidak sengaja melewatkan beberapa kode yang tidak aman.Untuk menghindari masalah ini, Anda dapat menambahkan fungsi bawaan yang aman:
Sekarang blok unsafe
diperlukan lagi untuk dereferencing level_4_table_ptr
, dan kami segera melihat bahwa ini adalah satu-satunya operasi yang tidak aman. Rust saat ini memiliki RFC terbuka untuk mengubah properti yang gagal fungsi tidak aman ini.Buat pemetaan baru
Saat kami membaca tabel halaman dan membuat fungsi konversi, langkah selanjutnya adalah membuat pemetaan baru dalam hierarki tabel halaman.Kompleksitas operasi ini tergantung pada halaman virtual yang ingin kami tampilkan. Dalam kasus paling sederhana, tabel halaman level 1 sudah ada untuk halaman ini, dan kita hanya perlu membuat satu entri. Dalam kasus yang paling sulit, halaman tersebut berada di area memori yang level 3-nya belum ada, jadi pertama-tama Anda perlu membuat tabel baru level 3, level 2 dan level 1.Mari kita mulai dengan case sederhana ketika Anda tidak perlu membuat tabel 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
. Kami menggunakan 0xb8000
bingkai buffer teks VGA sebagai bingkai yang diinginkan . Sangat mudah untuk memeriksa cara kerja terjemahan alamat kami.Kami mengimplementasikannya dalam fungsi baru create_maping
dalam modul memory
:
Fungsi menerima referensi yang bisa berubah ke RecursivePageTable
(itu akan mengubahnya) dan FrameAllocator
, yang dijelaskan di bawah ini. Kemudian menerapkan fungsi map_to
dalam baki Mapper
untuk memetakan halaman di alamat 0x1000
dengan bingkai fisik di alamat 0xb8000
. Fungsi ini tidak aman, karena dimungkinkan untuk melanggar keamanan memori dengan argumen yang tidak valid.Selain argumen page
dan frame
, fungsi map_to
membutuhkan dua argumen lagi. Argumen ketiga adalah set bendera untuk tabel halaman. Kami menetapkan bendera yang PRESENT
diperlukan untuk semua entri yang valid dan bendera WRITABLE
untuk kemampuan menulis.Argumen keempat haruslah beberapa struktur yang mengimplementasikan sifat tersebut FrameAllocator
. Argumen ini diperlukan oleh metode.map_to
karena membuat tabel halaman baru mungkin memerlukan bingkai yang tidak digunakan. Implementasi membutuhkan sifat argumen Size4KiB
, seperti jenis Page
dan PhysFrame
yang universal yang untuk sifat tersebut PageSize
, bekerja dengan standar 4 halaman KIB dan dengan halaman yang sangat besar 2 MiB / 1 GiB.Fungsi ini map_to
mungkin gagal, jadi itu kembali Result
. Karena ini hanyalah contoh kode yang seharusnya tidak dapat diandalkan, kami cukup menggunakannya expect
dengan panik ketika terjadi kesalahan. Jika berhasil, fungsi mengembalikan jenis MapperFlush
yang menyediakan cara mudah untuk menghapus halaman yang baru-baru ini cocok dari metode buffer terjemahan terjemahan (TLB) flush
. SepertiResult
, jenisnya menggunakan atribut #[must_use]
dan mengeluarkan peringatan jika kita secara tidak sengaja lupa untuk menerapkannya.Karena kita tahu bahwa alamat 0x1000
itu tidak memerlukan tabel halaman baru, itu FrameAllocator
selalu dapat kembali None
. Untuk menguji fungsi, buat ini EmptyFrameAllocator
:
(Jika metode 'kesalahan allocate_frame
bukan anggota sifat FrameAllocator
' muncul , Anda perlu meningkatkan x86_64
ke versi 0.4.0.)Sekarang kita dapat menguji fungsi terjemahan baru:
Pertama, kami membuat pemetaan untuk halaman di alamat 0x1000
, memanggil fungsi create_example_mapping
dengan tautan yang dapat diubah ke instance RecursivePageTable
. Ini menerjemahkan halaman 0x1000
menjadi buffer teks VGA, jadi kita akan melihat beberapa hasil di layar.Lalu kami menulis nilai di halaman ini 0xf021f077f065f04e
, yang sesuai dengan baris "Baru!" pada latar belakang putih. Hanya saja, tidak perlu menuliskan nilai ini segera ke atas halaman 0x1000
, karena baris teratas akan bergerak berikutnya dari layar println
, dan menuliskannya di offset 0x900
yang terletak kira-kira di tengah layar. Seperti yang kita ketahui dari artikel "Mode Teks VGA" , menulis ke buffer VGA harus volatile, jadi kami menggunakan metode ini write_volatile
.Ketika kami menjalankannya di QEMU, kami melihat ini:Tulisan di layar.Kode berfungsi karena sudah ada tabel level 1 untuk menampilkan halaman 0x1000
. Jika kami mencoba menerjemahkan halaman yang tabelnya belum ada, fungsinya map_to
akan menghasilkan kesalahan, karena ia akan mencoba memilih bingkai dari untuk membuat tabel halaman baru EmptyFrameAllocator
. Kami akan melihat ini jika kami mencoba menerjemahkan halaman 0xdeadbeaf000
alih-alih 0x1000
:
Saat memulai, panik dimulai dengan pesan kesalahan berikut: panik di 'map_to gagal: FrameAllocationFailed', /.../result.rs:999haps
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?Informasi boot
Komputer yang berbeda memiliki jumlah memori fisik yang berbeda dan area yang berbeda disediakan oleh perangkat seperti VGA berbeda. Hanya firmware BIOS atau UEFI yang tahu persis area memori mana yang dapat digunakan dan mana yang dicadangkan. Kedua standar firmware menyediakan fungsi untuk mendapatkan kartu alokasi memori, tetapi hanya dapat dipanggil pada awal pengunduhan. Karena itu, bootloader kami telah meminta informasi ini (dan lainnya) dari BIOS.Untuk meneruskan informasi ke kernel OS, loader sebagai argumen saat memanggil fungsi _start
memberikan tautan ke struktur informasi boot. Tambahkan argumen ini ke fungsi kami:
Struktur ini BootInfo
masih difinalisasi, jadi jangan kaget ketika crash ketika memutakhirkan ke versi bootloader masa depan yang tidak kompatibel dengan semver . Dia saat ini memiliki tiga bidang p4_table_addr
, memory_map
dan package
:- Bidang ini
p4_table_addr
berisi alamat virtual rekursif dari tabel halaman level 4. Berkat ini, Anda tidak perlu mendaftarkan alamat tersebut dengan keras 0o_177777_777_777_777_777_0000
.
- Bidang
memory_map
ini paling menarik, karena berisi daftar semua area memori dan jenisnya (tidak digunakan, dicadangkan, atau yang lain).
- Field
package
adalah fungsi saat ini untuk mengaitkan data tambahan dengan loader. Implementasinya belum selesai, jadi kita bisa mengabaikannya untuk saat ini.
Sebelum menggunakan bidang memory_map
untuk membuat yang benar FrameAllocator
, kami ingin menjamin jenis argumen yang benar boot_info
.Makro entry_point
Karena _start
ini disebut secara eksternal, tanda tangan dari fungsi tidak dicentang. Ini berarti bahwa argumen arbitrer tidak akan menyebabkan kesalahan kompilasi, tetapi dapat menyebabkan crash atau perilaku runtime yang tidak ditentukan.Untuk memverifikasi tanda tangan, kotak bootloader
untuk mendefinisikan fungsi Karat sebagai titik masuk menggunakan makro entry_point
dengan tipe yang divalidasi. Kami menulis ulang fungsi kami untuk makro ini:
Untuk titik masuk, Anda tidak perlu lagi menggunakan extern "C"
atau no_mangle
, karena makro menetapkan titik masuk nyata tingkat rendah _start
. Fungsi kernel_main
sekarang telah menjadi fungsi Rust yang benar-benar normal, sehingga kita dapat memilih nama arbitrer untuk itu. Penting bahwa itu sudah diketik, sehingga kesalahan kompilasi terjadi jika Anda mengubah tanda tangan fungsi, misalnya, dengan menambahkan argumen atau mengubah tipenya.Perhatikan bahwa sekarang kami mengirim ke memory::init
alamat hard-coded, tetapi boot_info.p4_table_addr
. Dengan demikian, kode ini akan berfungsi bahkan jika versi bootloader mendatang memilih entri lain di tabel tabel level 4 halaman untuk tampilan rekursif.Pemilihan Bingkai
Sekarang, berkat informasi dari BIOS, kami memiliki akses ke kartu alokasi memori, sehingga Anda dapat membuat distributor bingkai normal. Mari kita mulai dengan kerangka umum:
Bidang ini frames
diinisialisasi oleh iterator bingkai sewenang-wenang . Ini memungkinkan Anda untuk mendelegasikan panggilan alloc
ke metode Iterator :: next .Inisialisasi BootInfoFrameAllocator
terjadi pada fungsi baru init_frame_allocator
:
Fungsi ini, menggunakan kombinator, mengubah peta alokasi memori asli menjadi iterator dari frame fisik yang digunakan:iter
MemoryRegion
. filter
, . , , (, ) , InUse
. , , - .
map
range Rust .
- Langkah ketiga adalah yang paling sulit: kami mengubah setiap rentang menjadi iterator menggunakan metode
into_iter
, dan kemudian memilih setiap alamat ke-4096 step_by
. Karena ukuran halaman adalah 4096 byte (4 KiB), kami mendapatkan alamat awal setiap frame. Laman pemuat meluruskan semua area memori yang digunakan, jadi kami tidak memerlukan kode pelurusan atau pembulatan. Mengganti map
dengan flat_map
, kita Iterator<Item = u64>
malah mendapatkan Iterator<Item = Iterator<Item = u64>>
.
- Pada tahap akhir, kami akan mengonversi alamat mulai menjadi tipe
PhysFrame
untuk membangun yang diperlukan Iterator<Item = PhysFrame>
. Kemudian gunakan iterator ini untuk membuat dan mengembalikan yang baru BootInfoFrameAllocator
.
Sekarang kita dapat mengubah fungsi kita kernel_main
sehingga melewati instance BootInfoFrameAllocator
sebagai gantinya EmptyFrameAllocator
:
Sekarang terjemahan alamat berhasil - dan kita kembali melihat pesan hitam putih βBaru!β Di layar .
Di belakang layar, metode ini map_to
membuat tabel halaman yang hilang sebagai berikut:- Mengekstrak bingkai yang tidak digunakan dari
frame_allocator
.
- Cocok dengan entri tabel tingkat atas dengan bingkai ini. Bingkai sekarang dapat diakses melalui tabel halaman rekursif.
- Nol bingkai untuk membuat tabel halaman baru yang kosong.
- Pergi ke tabel level berikutnya.
Meskipun fungsi kami create_maping
hanyalah sebuah contoh, kami sekarang dapat membuat pemetaan baru untuk halaman sewenang-wenang. Ini sangat berguna ketika mengalokasikan memori dan mengimplementasikan multithreading di artikel mendatang.Ringkasan
Dalam artikel ini, Anda belajar cara menggunakan tabel rekursif level 4 untuk menerjemahkan semua frame ke alamat virtual yang dapat dihitung. Kami menggunakan metode ini untuk mengimplementasikan fungsi terjemahan alamat dan membuat pemetaan baru dalam tabel halaman.Kami melihat bahwa membuat pemetaan baru membutuhkan bingkai yang tidak digunakan untuk tabel baru. Frame distributor seperti itu dapat diimplementasikan berdasarkan informasi dari BIOS yang diberikan oleh bootloader ke kernel kami.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 .