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 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 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:
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:
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 bootloader
mendefinisikan 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_map
dan physical_memory_offset
:- Bidang ini
memory_map
menyediakan 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_offset
melaporkan 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 BootInfo
ke kernel sebagai argumen &'static BootInfo
ke fungsi _start
. Tambahkan:
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 _start
dipanggil 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 bootloader
menyediakan makro entry_point
. Kami menulis ulang fungsi kami menggunakan makro ini:
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_main
sekarang 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 terjadiImplementasi
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
:
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_table
akan mengembalikan tautan ke tabel halaman aktif dari tingkat keempat:
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 PageTable
dengan metode ini as_mut_ptr
, dan kemudian buat tautan dengan tidak aman &mut PageTable
. Kami membuat tautan &mut
sebagai 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 fn
sebagai 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:
Kami physical_memory_offset
melewati bidang struktur yang sesuai BootInfo
. Kemudian kami menggunakan fungsi iter
untuk beralih melalui entri tabel halaman dan kombinator enumerate
untuk menambahkan indeks i
ke 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:
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:
Kami merujuk ke fungsi aman translate_addr_inner
untuk membatasi jumlah kode tidak aman. Seperti disebutkan di atas, Rust menganggap seluruh tubuh unsafe fn
sebagai blok besar yang tidak aman. Dengan menjalankan satu fungsi aman, kami kembali membuat setiap operasi eksplisit unsafe
.Fungsi internal khusus memiliki fungsi nyata:
Alih-alih menggunakan kembali fungsi, active_level_4_table
kami membaca kembali frame tingkat keempat dari register CR3
, karena ini menyederhanakan implementasi prototipe. Jangan khawatir, kami akan segera memperbaiki solusinya.Struktur VirtAddr
sudah 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. frame
menunjuk 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_offset
untuk mengubah bingkai ke tautan tabel halaman. Kemudian kita membaca catatan dari tabel halaman saat ini dan menggunakan fungsi PageTableEntry::frame
untuk 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:
Ketika kami menjalankan kode, kami mendapatkan hasil berikut:
Seperti yang diharapkan, dengan pemetaan yang identik, alamat tersebut 0xb8000
dikonversi 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_offset
harus 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_64
menyediakan 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
Mapper
menyediakan fungsi yang berfungsi pada halaman. Misalnya, translate_page
menerjemahkan halaman ini ke dalam bingkai dengan ukuran yang sama, serta map_to
membuat pemetaan baru di tabel.
- Sifat tersebut
MapperAllSizes
menyiratkan aplikasi Mapper
untuk semua ukuran halaman. Selain itu, ia menyediakan fungsi yang berfungsi dengan halaman dengan ukuran yang berbeda, termasuk translate_addr
atau umum translate
.
Ciri-ciri hanya mendefinisikan antarmuka, tetapi tidak menyediakan implementasi apa pun. Sekarang peti x86_64
menyediakan dua jenis yang menerapkan sifat: MappedPageTable
dan 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 init
dalam modul memory
: use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr;
Kami tidak dapat langsung kembali MappedPageTable
dari 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 RecursivePageTable
tanpa mengubah tanda tangan fungsi.Fungsi ini MappedPageTable::new
mengharapkan dua parameter: tautan yang dapat diubah ke tabel halaman level 4 dan penutupan phys_to_virt
yang 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_offset
untuk melakukan konversi.Kami juga menjadikannya sebagai active_level_4_table
fungsi pribadi, karena mulai sekarang hanya akan dipanggil dari init
.Untuk menggunakan metode iniMapperAllSizes::translate_addr
alih-alih fungsi kita sendiri memory::translate_addr
, kita perlu mengubah hanya beberapa baris di kernel_main
:
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_offset
dikonversi 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_to
yang 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_to
dari 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_mapping
yang memetakan halaman ini ke 0xb8000
bingkai 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_mapping
terlihat seperti ini:
Selain halaman page
yang ingin Anda petakan, fungsi tersebut mengharapkan instance dari mapper
dan frame_allocator
. Tipe mapper
mengimplementasikan sifat Mapper<Size4KiB>
yang disediakan metode map_to
. Parameter umum Size4KiB
diperlukan, karena sifat Mapper
tersebut 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 WRITABLE
untuk membuat halaman yang ditampilkan dapat ditulis. Tantanganmap_to
tidak 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_to
mungkin gagal, jadi itu kembali Result
. Karena ini hanyalah contoh kode yang seharusnya tidak dapat diandalkan, kami cukup menggunakannya expect
untuk panik jika terjadi kesalahan. Jika berhasil, fungsi mengembalikan jenis MapperFlush
yang 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 EmptyFrameAllocator
fungsi tampilan untuk pengujian:
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:
Pertama, kami membuat pemetaan untuk halaman 0x1000
, memanggil fungsi create_example_mapping
dengan tautan yang dapat berubah ke instance mapper
dan frame_allocator
. Ini memetakan halaman 0x1000
ke 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_f04e
yang 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_to
gagal karena mencoba mengalokasikan bingkai dari EmptyFrameAllocator
untuk membuat tabel baru. Kami melihat bahwa ini terjadi ketika kami mencoba menampilkan halaman 0xdeadbeaf000
alih-alih 0x1000
:
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:
Bidang frames
dapat diinisialisasi dengan iterator bingkai sewenang-wenang. Ini memungkinkan Anda untuk hanya mendelegasikan panggilan ke alloc
metode ini Iterator::next
.Untuk inisialisasi, kami BootInfoFrameAllocator
menggunakan kartu memori memory_map
yang 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 MemoryRegion
yang 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 BootInfoFrameAllocator
terjadi pada fungsi baru init_frame_allocator
:
Fungsi ini menggunakan kombinator untuk mengubah peta awal MemoryMap
menjadi iterator dari frame fisik yang digunakan:- Pertama, kami memanggil metode
iter
untuk mengkonversi kartu memori ke iterator MemoryRegion
. Kemudian kami menggunakan metode ini filter
untuk 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 InUse
atau serupa. Dengan demikian, kita dapat yakin bahwa frame Usable
tidak 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_main
untuk mengirimkan salinan BootInfoFrameAllocator
gantinya EmptyFrameAllocator
:
Kali ini pemetaan alamat berhasil dan kami kembali melihat hitam dan putih "Baru!" .
Di belakang layar, metode ini map_to
membuat 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_mapping
hanya 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 bootloader
menciptakan pemetaan yang diperlukan melalui fungsi kargo tambahan. Ini menyampaikan informasi yang diperlukan ke kernel sebagai argumen &BootInfo
ke fungsi titik masuk.Untuk implementasi kami, pertama-tama kami secara manual menelusuri tabel halaman, membuat fungsi terjemahan, dan kemudian menggunakan jenis MappedPageTable
petix86_64
. Kami juga belajar cara membuat pemetaan baru di tabel halaman dan cara membuatnya FrameAllocator
di 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 .