Dans cet article, nous découvrirons comment implémenter la prise en charge de la mémoire de page dans notre noyau. Tout d'abord, nous étudierons différentes méthodes afin que les cadres de la table de pages physiques deviennent disponibles pour le noyau, et discuterons de leurs avantages et inconvénients. Ensuite, nous implémentons la fonction de traduction d'adresse et la fonction de création d'un nouveau mapping.
Cette série d'articles publiée sur
GitHub . Si vous avez des questions ou des problèmes, ouvrez-y le ticket correspondant. Toutes les sources de l'article sont
dans ce fil .
Un autre article sur la pagination?
Si vous suivez ce cycle, vous avez vu l'article «Mémoire de page: niveau avancé» fin janvier. Mais j'ai été critiqué pour les tableaux de pages récursifs. Par conséquent, j'ai décidé de réécrire l'article, en utilisant une approche différente pour accéder aux cadres.Voici une nouvelle option. L'article explique toujours comment fonctionnent les tableaux de pages récursifs, mais nous utilisons une implémentation plus simple et plus puissante. Nous ne supprimerons pas l'article précédent, mais le marquerons comme obsolète et ne le mettrons pas à jour.
J'espère que vous apprécierez la nouvelle option!Table des matières
Présentation
Dans le
dernier article, nous avons découvert les principes de la mémoire de pagination et le fonctionnement des tables de pages à quatre niveaux sur
x86_64
. Nous avons également constaté que le chargeur avait déjà configuré la hiérarchie des tables de pages pour notre noyau, donc le noyau s'exécute sur des adresses virtuelles. Cela augmente la sécurité car l'accès non autorisé à la mémoire provoque une erreur de page au lieu de modifier de façon aléatoire la mémoire physique.
L'article a fini par ne pas pouvoir accéder aux tables de pages à partir de notre noyau, car elles sont stockées dans la mémoire physique et le noyau s'exécute déjà sur des adresses virtuelles. Ici, nous continuons le sujet et explorons différentes options pour accéder aux cadres du tableau des pages à partir du noyau. Nous discuterons des avantages et des inconvénients de chacun d'eux, puis choisirons l'option appropriée pour notre cœur.
La prise en charge du chargeur de démarrage est requise, nous allons donc la configurer en premier. Ensuite, nous implémentons une fonction qui parcourt toute la hiérarchie des tables de pages afin de traduire les adresses virtuelles en adresses physiques. Enfin, nous apprendrons comment créer de nouveaux mappages dans des tables de pages et comment trouver des cadres de mémoire inutilisés pour créer de nouvelles tables.
Mises à jour des dépendances
Cet article vous oblige Ă enregistrer le
bootloader
version 0.4.0 ou supérieure et
x86_64
version 0.5.2 ou supérieure dans les dépendances. Vous pouvez mettre à jour les dépendances dans
Cargo.toml
:
[dependencies] bootloader = "0.4.0" x86_64 = "0.5.2"
Pour les modifications de ces versions, consultez
le journal du chargeur de démarrage et le
journal x86_64 .
Accès aux tableaux de pages
Accéder aux tables de pages à partir du noyau n'est pas aussi simple qu'il y paraît. Pour comprendre le problème, jetez un autre coup d'œil à la hiérarchie des tables à quatre niveaux de l'article précédent:
L'important est que chaque entrée de page stocke l'adresse
physique du tableau suivant. Cela évite la traduction de ces adresses, ce qui réduit les performances et conduit facilement à des boucles sans fin.
Le problème est que nous ne pouvons pas accéder directement aux adresses physiques à partir du noyau, car cela fonctionne également sur les adresses virtuelles. Par exemple, lorsque nous allons à l'adresse
4 KiB
, nous avons accès à l'adresse
virtuelle 4 KiB
, et non Ă l'adresse
physique où est stocké le tableau des pages du 4e niveau. Si nous voulons accéder à l'adresse physique de
4 KiB
, nous devons utiliser une adresse virtuelle, qui est traduite en elle.
Par conséquent, pour accéder aux cadres des tables de pages, vous devez mapper certaines pages virtuelles à ces cadres. Il existe différentes façons de créer de tels mappages.
Cartographie d'identité
Une solution simple est l'
affichage identique de tous les tableaux de pages .
Dans cet exemple, nous voyons l'affichage identique des images. Les adresses physiques des tables de pages sont en même temps des adresses virtuelles valides, afin que nous puissions facilement accéder aux tables de pages de tous les niveaux, à commencer par le registre CR3.
Cependant, cette approche encombre l'espace d'adressage virtuel et rend difficile de trouver de grandes zones contiguës de mémoire libre. Disons que nous voulons créer une zone de mémoire virtuelle de 1000 Ko dans la figure ci-dessus, par exemple, pour
afficher un fichier en mémoire . Nous ne pouvons pas commencer par la région des
28 KiB
, car elle repose sur une page dĂ©jĂ occupĂ©e Ă
1004 KiB
. Par conséquent, vous devrez regarder plus loin jusqu'à ce que nous trouvions un grand fragment approprié, par exemple, avec
1008 KiB
. Il y a le même problème de fragmentation que dans la mémoire segmentée.
De plus, la création de nouveaux tableaux de pages est beaucoup plus compliquée, car nous devons trouver des cadres physiques dont les pages correspondantes ne sont pas encore utilisées. Par exemple, pour notre fichier, nous avons réservé une zone de 1000 Ko de mémoire
virtuelle , Ă partir de l'adresse
1008 KiB
. Maintenant, nous ne pouvons plus utiliser de trame avec une adresse physique comprise entre
1000 KiB
et
2008 KiB
, car elle ne peut pas être affichée de manière identique.
Carte de décalage fixe
Pour éviter d'encombrer l'espace d'adressage virtuel, vous pouvez afficher les tableaux de pages dans une
zone de mémoire distincte . Par conséquent, au lieu d'identifier le mappage, nous mappons des trames avec un décalage fixe dans l'espace d'adressage virtuel. Par exemple, le décalage peut être de 10 TiB:

En allouant cette plage de mémoire virtuelle uniquement pour l'affichage des tables de pages, nous évitons les problèmes d'affichage identiques. La réservation d'une si grande zone d'espace d'adressage virtuel n'est possible que si l'espace d'adressage virtuel est beaucoup plus grand que la taille de la mémoire physique. Sur
x86_64
ce n'est pas un problème car l'espace d'adressage 48 bits est de 256 TiB.
Mais cette approche présente l'inconvénient que lors de la création de chaque table de pages, vous devez créer un nouveau mappage. De plus, il ne permet pas d'accéder aux tables dans d'autres espaces d'adressage, ce qui serait utile lors de la création d'un nouveau processus.
Mappage complet de la mémoire physique
Nous pouvons résoudre ces problèmes en
affichant toute la mémoire physique , et pas seulement les cadres de table de pages:

Cette approche permet au noyau d'accéder à la mémoire physique arbitraire, y compris les cadres de table de pages d'autres espaces d'adressage. Une plage de mémoire virtuelle est réservée de la même taille qu'auparavant, mais seulement il n'y a plus de pages sans correspondance.
L'inconvénient de cette approche est que des tables de pages supplémentaires sont nécessaires pour afficher la mémoire physique. Ces tableaux de pages doivent être stockés quelque part, afin qu'ils utilisent une partie de la mémoire physique, ce qui peut être un problème sur les appareils avec une petite quantité de RAM.
Cependant, sur x86_64, nous pouvons utiliser d'
énormes pages de 2 Mio pour afficher au lieu de la taille par défaut de 4 Ko. Ainsi, pour afficher 32 Go de mémoire physique, seulement 132 Ko par table de pages sont nécessaires: une seule table de troisième niveau et 32 ​​tables de deuxième niveau. Les pages volumineuses sont également mises en cache plus efficacement car elles utilisent moins d'entrées dans le tampon de traduction dynamique (TLB).
Affichage temporaire
Pour les appareils avec très peu de mémoire physique, vous ne pouvez
afficher les tableaux de pages que temporairement lorsque vous devez y accéder. Pour les comparaisons temporaires, un affichage identique de la seule table de premier niveau est requis:
Sur cette figure, une table de niveau 1 gère les 2 premiers Mo d'espace d'adressage virtuel. Cela est possible car l'accès est effectué à partir du registre CR3 via des entrées nulles dans les tableaux des niveaux 4, 3 et 2. L'enregistrement avec index
8
traduit la page virtuelle Ă
32 KiB
en un cadre physique Ă
32 KiB
, identifiant ainsi la table de niveau 1 elle-même. Dans la figure, cela est indiqué par une flèche horizontale.
En écrivant dans la table de niveau 1 à mappage identique, notre noyau peut créer jusqu'à 511 comparaisons temporelles (512 moins l'enregistrement nécessaire pour le mappage d'identité). Dans l'exemple ci-dessus, le noyau crée deux comparaisons temporelles:
- Mappage d'une entrĂ©e nulle dans une table de niveau 1 Ă une trame Ă
24 KiB
. Cela crée un mappage temporaire de la page virtuelle à 0 KiB
au cadre physique du tableau de niveau de page 2 indiquĂ© par la flèche en pointillĂ©s. - Faites correspondre le 9e enregistrement d'une table de niveau 1 avec un cadre Ă
4 KiB
. Cela crée une correspondance temporaire de la page virtuelle à 36 KiB
avec le cadre physique de la table de niveau de page 4 indiquée par la flèche en pointillés.
Maintenant, le noyau peut accĂ©der Ă une table de niveau 2 en Ă©crivant sur une page qui commence Ă
0 KiB
et Ă une table de niveau 4 en Ă©crivant sur une page qui commence Ă
33 KiB
.
Ainsi, l'accès à un cadre arbitraire de la table des pages avec des mappages temporaires comprend les actions suivantes:
- Trouvez une entrée gratuite dans le tableau de niveau 1 affiché de manière identique.
- Mappez cette entrée au cadre physique du tableau de pages auquel nous voulons accéder.
- Accédez à ce cadre via la page virtuelle associée à l'entrée.
- Redéfinissez l'enregistrement sur inutilisé, supprimant ainsi le mappage temporaire.
Avec cette approche, l'espace d'adressage virtuel reste propre, car les mêmes 512 pages virtuelles sont constamment utilisées. L'inconvénient est une certaine lourdeur, d'autant plus qu'une nouvelle comparaison peut nécessiter de modifier plusieurs niveaux de la table, c'est-à -dire que nous devons répéter le processus décrit plusieurs fois.
Tables de pages récursives
Une autre approche intéressante qui ne nécessite pas du tout de tables de pages supplémentaires est la
correspondance récursive .
L'idée est de traduire certains enregistrements de la table de quatrième niveau en elle-même. Ainsi, nous réservons en fait une partie de l'espace d'adressage virtuel et mappons tous les cadres de table actuels et futurs à cet espace.
Regardons un exemple pour comprendre comment tout cela fonctionne:
La seule différence avec l'exemple au début de l'article est un enregistrement supplémentaire avec l'index
511
dans la table de niveau 4, qui est mappé à la trame physique
4 KiB
, qui se trouve dans cette table elle-mĂŞme.
Lorsque le processeur va sur cet enregistrement, il ne fait pas référence à la table de niveau 3, mais se réfère à nouveau à la table de niveau 4. Ceci est similaire à une fonction récursive qui s'appelle elle-même. Il est important que le processeur suppose que chaque entrée de la table de niveau 4 pointe vers une table de niveau 3. Par conséquent, il traite maintenant la table de niveau 4 comme une table de niveau 3. Cela fonctionne car les tables de tous les niveaux dans x86_64 ont la même structure.
En suivant une ou plusieurs fois un enregistrement récursif avant de démarrer la conversion réelle, nous pouvons effectivement réduire le nombre de niveaux que le processeur traverse. Par exemple, si nous suivons une fois l'enregistrement récursif, puis passons à la table de niveau 3, le processeur pense que la table de niveau 3 est une table de niveau 2. En poursuivant, il considère la table de niveau 2 comme une table de niveau 1 et la table de niveau 1 comme mappée trame dans la mémoire physique. Cela signifie que nous pouvons maintenant lire et écrire dans la table de niveau de page 1 car le processeur pense qu'il s'agit d'un cadre mappé. La figure ci-dessous montre les cinq étapes d'une telle traduction:
De même, nous pouvons suivre une entrée récursive deux fois avant de démarrer la conversion pour réduire le nombre de niveaux passés à deux:
Passons en revue cette procédure étape par étape. Tout d'abord, le CPU suit une entrée récursive dans la table de niveau 4 et pense qu'il a atteint la table de niveau 3. Ensuite, il suit à nouveau l'enregistrement récursif et pense qu'il a atteint le niveau 2. Mais en réalité, il est toujours au niveau 4. Ensuite, le CPU va à la nouvelle adresse et entre dans la table de niveau 3, mais pense qu'il est déjà dans la table de niveau 1. Enfin, au point d'entrée suivant dans la table de niveau 2, le processeur pense qu'il a accédé à la trame de mémoire physique. Cela nous permet de lire et d'écrire dans une table de niveau 2.
Les tables des niveaux 3 et 4 sont également accessibles. Pour accéder à la table du niveau 3, nous suivons une entrée récursive trois fois: le processeur pense qu'il est déjà dans la table du niveau 1, et à l'étape suivante, nous atteignons le niveau 3, que le CPU considère comme un cadre mappé. Pour accéder à la table de niveau 4 elle-même, nous suivons simplement l'enregistrement récursif quatre fois jusqu'à ce que le processeur traite la table de niveau 4 elle-même comme un cadre mappé (en bleu dans la figure ci-dessous).
Le concept est difficile à comprendre au début, mais dans la pratique, il fonctionne plutôt bien.
Calcul d'adresse
Ainsi, nous pouvons accéder aux tables de tous les niveaux en suivant une ou plusieurs fois un enregistrement récursif. Étant donné que les index des tables de quatre niveaux sont dérivés directement de l'adresse virtuelle, des adresses virtuelles spéciales doivent être créées pour cette méthode. Comme nous le rappelons, les index des tables de pages sont extraits de l'adresse comme suit:
Supposons que nous voulons accéder à un tableau de niveau 1 qui affiche une page spécifique. Comme nous l'avons appris ci-dessus, vous devez passer par un enregistrement récursif une fois, puis par les indices des 4e, 3e et 2e niveaux. Pour ce faire, nous déplaçons tous les blocs d'adresse d'un bloc vers la droite et mettons l'index de l'enregistrement récursif à la place de l'index initial de niveau 4:
Pour accéder au tableau de niveau 2 de cette page, nous déplaçons tous les blocs d'index deux blocs vers la droite et définissons l'index récursif à la place des deux blocs source: niveau 4 et niveau 3:
Pour accéder au tableau de niveau 3, on fait de même, on décale juste à droite déjà trois blocs d'adresse.
Enfin, pour accéder à la table de niveau 4, déplacez les quatre blocs vers la droite.
Vous pouvez maintenant calculer des adresses virtuelles pour les tables de pages des quatre niveaux. Nous pouvons même calculer une adresse qui pointe exactement vers une entrée de table de pages spécifique en multipliant son index par 8, la taille de l'entrée de table de pages.
Le tableau ci-dessous montre la structure des adresses pour accéder à différents types de trames:
Adresse virtuelle pour | Structure d'adresse ( octale ) |
---|
La page | 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE |
Entrée dans le tableau de niveau 1 | 0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD |
Entrée dans une table de niveau 2 | 0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC |
Entrée dans une table de niveau 3 | 0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB |
Entrée dans le tableau de niveau 4 | 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA |
Ici,
est l'indice de niveau 4,
est de niveau 3,
est de niveau 2 et
DDD
est l'indice de niveau 1 pour la trame affichée,
EEEE
est son décalage.
RRR
est l'indice de l'enregistrement récursif. Un index (trois chiffres) est converti en décalage (quatre chiffres) en multipliant par 8 (la taille de l'entrée du tableau de pages). Avec ce décalage, l'adresse résultante pointe directement vers l'entrée de table de pages correspondante.
SSSS
sont des bits d'extension du chiffre signé, c'est-à -dire qu'ils sont tous des copies du bit 47. Il s'agit d'une exigence spéciale pour les adresses valides dans l'architecture x86_64, dont nous avons discuté dans un
article précédent .
Les adresses sont
octales , car chaque caractère octal représente trois bits, ce qui vous permet de séparer clairement les index de 9 bits des tables à différents niveaux. Ce n'est pas possible dans le système hexadécimal, où chaque caractère représente quatre bits.
Code rouille
Vous pouvez construire de telles adresses dans le code Rust à l'aide d'opérations au niveau du bit:
Ce code suppose qu'un mappage récursif du dernier enregistrement de niveau 4 avec l'index
0o777
(511) est récursivement mis en correspondance. Ce n'est actuellement pas le cas, donc le code ne fonctionnera pas encore. Voir ci-dessous comment dire au chargeur de configurer un mappage récursif.
Au lieu d'effectuer des opérations au niveau du bit manuellement, vous pouvez utiliser le type
RecursivePageTable
de la caisse
x86_64
, qui fournit des abstractions sûres pour diverses opérations de table. Par exemple, le code ci-dessous montre comment convertir une adresse virtuelle en son adresse physique correspondante:
Encore une fois, ce code nécessite un mappage récursif correct. Avec ce mappage, le
level_4_table_addr
manquant
level_4_table_addr
calculé comme dans le premier exemple de code.
Le mappage récursif est une méthode intéressante qui montre à quel point la correspondance peut être puissante à travers une seule table. Il est relativement facile à implémenter et ne nécessite qu'une configuration minimale (une seule entrée récursive), c'est donc un bon choix pour les premières expériences.
Mais cela présente certains inconvénients:
- Une grande quantité de mémoire virtuelle (512 Gio). Ce n'est pas un problème dans un grand espace d'adressage 48 bits, mais peut conduire à un comportement de cache sous-optimal.
- Il donne facilement accès uniquement à l'espace d'adressage actuellement actif. L'accès à d'autres espaces d'adressage est toujours possible en modifiant l'entrée récursive, mais une correspondance temporaire est requise pour la commutation. Nous avons décrit comment procéder dans un article précédent (obsolète).
- Cela dépend fortement du format de table des pages x86 et peut ne pas fonctionner sur d'autres architectures.
Prise en charge du chargeur de démarrage
Toutes les approches décrites ci-dessus nécessitent des modifications des tableaux de pages et des paramètres correspondants. Par exemple, pour mapper de façon identique ou récursive la mémoire physique des enregistrements d'une table de quatrième niveau. Le problème est que nous ne pouvons pas définir ces paramètres sans accéder aux tableaux de pages.
J'ai donc besoin de l'aide du chargeur de démarrage. Il a accès aux tableaux de pages, il peut donc créer tous les affichages dont nous avons besoin. Dans sa mise en œuvre actuelle, la caisse du
bootloader
prend en charge les deux approches ci-dessus en utilisant
les fonctions de chargement :
- La fonction
map_physical_memory
mappe la mémoire physique complète quelque part dans l'espace d'adressage virtuel. Ainsi, le noyau accède à toute la mémoire physique et peut appliquer une approche avec l' affichage de la mémoire physique complète .
- À l'aide de la fonction
recursive_page_table
, le chargeur affiche récursivement une entrée de table de pages de quatrième niveau. Cela permet au noyau de fonctionner selon la méthode décrite dans la section "Tables de pages récursives" .
, , ( , ).
map_physical_memory
:
[dependencies] bootloader = { version = "0.4.0", features = ["map_physical_memory"]}
, . ,
.
bootloader
BootInfo , . , ,
semver . :
memory_map
physical_memory_offset
:
memory_map
. , , VGA. BIOS UEFI, . , . .
physical_memory_offset
. , . .
BootInfo
&'static BootInfo
_start
. :
, .
_start
, . , , .
, ,
bootloader
entry_point
. :
extern "C"
no_mangle
,
_start
.
kernel_main
Rust, . , , , , ,
, , . -, , . , , . , .
memory
:
src/memory.rs
.
, , ,
CR3
. :
active_level_4_table
:
4-
CR3
. ,
physical_memory_offset
. ,
*mut PageTable
as_mut_ptr
,
&mut PageTable
.
&mut
&
, .
unsafe, Rust
unsafe fn
. , . .
RFC Rust.
:
physical_memory_offset
BootInfo
.
iter
enumerate
i
. , 512 .
, :

, . , , , .
, :
, , . , , .
, , . , .
, . , :
translate_addr_inner
, . , Rust
unsafe fn
. ,
unsafe
.
:
active_level_4_table
CR3
, . , .
VirtAddr
. ,
for
. , .
frame
, . . 1.
physical_memory_offset
.
PageTableEntry::frame
. ,
None
. 2 1 , .
, :
, :

,
0xb8000
. , , .
physical_memory_offset
0
, , . .
MappedPageTable
— ,
x86_64
. ,
translate_addr
, .
— , :
, .
x86_64
, :
MappedPageTable
RecursivePageTable
. , - (, ). , .
physical_memory_offset
, MappedPageTable. ,
init
memory
:
use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr;
MappedPageTable
, .
impl Trait
. ,
RecursivePageTable
.
MappedPageTable::new
: 4
phys_to_virt
,
*mut PageTable
.
active_level_4_table
. ,
physical_memory_offset
.
active_level_4_table
,
init
.
MapperAllSizes::translate_addr
memory::translate_addr
,
kernel_main
:
, , :

,
physical_memory_offset
0x0
.
MappedPageTable
, . ,
map_to
, .
memory::translate_addr
, , .
, . .
map_to
Mapper
, . , : , ; , ;
frame_allocator
. , , .
create_example_mapping
—
create_example_mapping
,
0xb8000
, VGA. , , : , .
create_example_mapping
:
page
, ,
mapper
frame_allocator
.
mapper
Mapper<Size4KiB>
,
map_to
.
Size4KiB
,
Mapper
PageSize
, 4 , 2 1 . 4 ,
Mapper<Size4KiB>
MapperAllSizes
.
PRESENT
, ,
WRITABLE
, .
map_to
: ,
unsafe
. . « »
.
map_to
,
Result
. , ,
expect
.
MapperFlush
, (TLB)
flush
.
Result
, [
#[must_use]
]
, .
FrameAllocator
create_example_mapping
,
FrameAllocator
. , , . 1 , . , 3 , 3, 2 1.
, . ,
None
.
EmptyFrameAllocator
:
, . , , 1. , ,
0x1000
.
,
0x1000
, :
0x1000
,
create_example_mapping
mapper
frame_allocator
.
0x1000
VGA, , .
400
. , VGA
println
.
0x_f021_f077_f065_f04e
,
“New!” .
« VGA» , VGA ,
write_volatile
.
QEMU, :

0x1000
“New!” . , .
, 1
0x1000
. , 1,
map_to
,
EmptyFrameAllocator
. , ,
0xdeadbeaf000
0x1000
:
, :
panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5
, 1,
FrameAllocator
. , ?
. :
frames
.
alloc
Iterator::next
.
BootInfoFrameAllocator
memory_map
,
BootInfo
.
« » , BIOS/UEFI. , .
MemoryRegion
, , (, , . .) . , ,
BootInfoFrameAllocator
.
BootInfoFrameAllocator
init_frame_allocator
:
MemoryMap
:
- -,
iter
MemoryRegion
. filter
. , , , (, ) , InUse
. , , Usable
- .
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
.
kernel_main
,
BootInfoFrameAllocator
EmptyFrameAllocator
:
-
“New!” .
map_to
:
create_example_mapping
— , . .
Résumé
, , , . .
, .
bootloader
cargo.
&BootInfo
.
, ,
MappedPageTable
x86_64
. ,
FrameAllocator
, .
?
,
.