Este artículo explica cómo el núcleo del sistema operativo puede acceder a los marcos de memoria física. Estudiaremos la función para convertir direcciones virtuales en físicas. También descubriremos cómo crear nuevas asignaciones en las tablas de páginas.
Este blog está publicado en
GitHub . Si tiene alguna pregunta o problema, abra el ticket correspondiente allí. Todas las fuentes para el artículo están
aquí .
Introduccion
Desde el
último artículo, aprendimos sobre los principios de la memoria de paginación y cómo funcionan las tablas de páginas de cuatro niveles en x86_64. También encontramos que el cargador ya configuró la jerarquía de la tabla de páginas para nuestro núcleo, por lo que el núcleo se ejecuta en direcciones virtuales. Esto mejora la seguridad, pero surge el problema: ¿cómo acceder a las direcciones físicas reales que se almacenan en las entradas de la tabla de páginas o en el
CR3
?
En la primera sección del artículo, discutiremos el problema y los diferentes enfoques para resolverlo. Luego implementamos una función que se cuela a través de la jerarquía de las tablas de páginas para convertir las direcciones virtuales en físicas. Finalmente, aprenda a crear nuevas asignaciones en tablas de páginas y encuentre marcos de memoria no utilizados para crear nuevas tablas.
Actualizaciones de dependencia
Para trabajar, necesita
x86_64
versión 0.4.0 o posterior. Actualice la dependencia en nuestro
Cargo.toml
:
[dependencies] x86_64 = "0.4.0" # or later
Acceso a tablas de páginas.
Acceder a las tablas de páginas desde el núcleo no es tan fácil como parece. Para comprender el problema, eche otro vistazo a la jerarquía de tablas de cuatro niveles del artículo anterior:
Lo importante es que cada entrada de página almacena la dirección
física de la siguiente tabla. Esto evita la traducción de estas direcciones, lo que reduce el rendimiento y conduce fácilmente a bucles sin fin.
El problema es que no podemos acceder directamente a las direcciones físicas desde el núcleo, ya que también funciona en direcciones virtuales. Por ejemplo, cuando vamos a la dirección
4 KiB
, tenemos acceso a la dirección
virtual 4 KiB
, y no a la dirección
física donde se almacena la tabla de páginas del 4to nivel. Si queremos acceder a la dirección física de
4 KiB
, entonces necesitamos usar alguna dirección virtual, que se traduce en ella.
Por lo tanto, para acceder a los marcos de las tablas de páginas, debe asignar algunas páginas virtuales a estos marcos. Hay diferentes formas de crear tales asignaciones.
1. Una solución simple es la
visualización idéntica de todas las tablas de páginas .
En este ejemplo, vemos la visualización idéntica de cuadros. Las direcciones físicas de las tablas de páginas son al mismo tiempo direcciones virtuales válidas, por lo que podemos acceder fácilmente a las tablas de páginas de todos los niveles, comenzando con el registro CR3.
Sin embargo, este enfoque satura el espacio de direcciones virtuales y hace que sea difícil encontrar grandes áreas contiguas de memoria libre. Digamos que queremos crear un área de memoria virtual de 1000 KiB en la figura anterior, por ejemplo, para
mostrar un archivo en la memoria . No podemos comenzar con la región
28 KiB
, porque descansa en una página ya ocupada en
1004 KiB
. Por lo tanto, tendrá que buscar más hasta encontrar un fragmento grande adecuado, por ejemplo, con
1008 KiB
. Existe el mismo problema de fragmentación que en la memoria segmentada.
Además, la creación de nuevas tablas de páginas es mucho más complicada, ya que necesitamos encontrar marcos físicos cuyas páginas correspondientes aún no se utilizan. Por ejemplo, para nuestro archivo, reservamos un área de 1000 KiB de memoria
virtual , comenzando en la dirección
1008 KiB
. Ahora ya no podemos usar ningún marco con una dirección física entre
1000 KiB
y
2008 KiB
, porque no se puede mostrar de forma idéntica.
2. Otra opción es
transmitir tablas de páginas solo temporalmente cuando necesite acceder a ellas. Para comparaciones temporales, se requiere una visualización idéntica de solo la tabla de primer nivel:
En esta figura, una tabla de nivel 1 gestiona los primeros 2 MiB de espacio de direcciones virtuales. Esto es posible porque el acceso se realiza desde el registro CR3 a través de entradas nulas en las tablas de los niveles 4, 3 y 2. El registro con el índice
8 traduce la página virtual a
32 KiB
en un marco físico a
32 KiB
, identificando así la tabla de nivel 1. En la figura, esto se muestra con una flecha horizontal.
Al escribir en la tabla de nivel 1 asignada de forma idéntica, nuestro núcleo puede crear hasta 511 comparaciones de tiempo (512 menos el registro necesario para el mapeo de identidad). En el ejemplo anterior, el núcleo coincide con el registro nulo de una tabla de nivel 1 con un marco a
24 KiB
. Esto creó un mapeo temporal de la página virtual en
0 KiB
al marco físico de la tabla de nivel de página 2 indicada por la flecha punteada. Ahora el núcleo puede acceder a la tabla de nivel 2 escribiendo en una página que comienza en
0 KiB
.
Por lo tanto, el acceso a un marco arbitrario de la tabla de páginas con asignaciones temporales consta de las siguientes acciones:
- Encuentre una entrada gratuita en la tabla de nivel 1 que se muestra idénticamente.
- Asigne esta entrada al marco físico de la tabla de páginas a la que queremos acceder.
- Acceda a este marco a través de la página virtual asociada con la entrada.
- Vuelva a establecer el registro como no utilizado, eliminando así la asignación temporal.
Con este enfoque, el espacio de direcciones virtuales permanece limpio, ya que las mismas 512 páginas virtuales se usan constantemente. La desventaja es algo engorroso, especialmente porque una nueva comparación puede requerir cambiar varios niveles de la tabla, es decir, necesitamos repetir el proceso descrito varias veces.
3. Aunque los dos enfoques anteriores funcionan, hay un tercer método:
tablas de páginas recursivas . Combina las ventajas de ambos enfoques: compara constantemente todos los marcos de las tablas de páginas sin requerir comparaciones temporales, y también mantiene las páginas asignadas una al lado de la otra, evitando la fragmentación del espacio de direcciones virtuales. Este es el método que usaremos.
Tablas de página recursivas
La idea es traducir algunos registros de la tabla de cuarto nivel en ella misma. Por lo tanto, en realidad reservamos una parte del espacio de direcciones virtuales y asignamos todos los marcos de tabla actuales y futuros a este espacio.
Veamos un ejemplo para entender cómo funciona todo esto:
La única diferencia con el ejemplo al principio del artículo es un registro adicional con el índice
511
en la tabla de nivel 4, que se asigna al marco físico
4 KiB
, que se encuentra en esta tabla.
Cuando la CPU pasa a este registro, no se refiere a la tabla de nivel 3, sino que nuevamente se refiere a la tabla de nivel 4. Esto es similar a una función recursiva que se llama a sí misma. Es importante que el procesador asuma que cada entrada en la tabla de nivel 4 apunta a una tabla de nivel 3, por lo que ahora trata la tabla de nivel 4 como una tabla de nivel 3. Esto funciona porque las tablas de todos los niveles en x86_64 tienen la misma estructura.
Al seguir un registro recursivo una o más veces antes de comenzar la conversión real, podemos reducir efectivamente el número de niveles por los que pasa el procesador. Por ejemplo, si seguimos el registro recursivo una vez, y luego vamos a la tabla de nivel 3, el procesador piensa que la tabla de nivel 3 es una tabla de nivel 2. Continuando, considera la tabla de nivel 2 como una tabla de nivel 1, y la tabla de nivel 1 como asignada marco en la memoria física. Esto significa que ahora podemos leer y escribir en la tabla de nivel de página 1 porque el procesador piensa que este es un marco mapeado. La siguiente figura muestra los cinco pasos de dicha traducción:
Del mismo modo, podemos seguir una entrada recursiva dos veces antes de comenzar la conversión para reducir el número de niveles pasados a dos:
Veamos este procedimiento paso a paso. Primero, la CPU sigue una entrada recursiva en la tabla de nivel 4 y piensa que ha alcanzado la tabla de nivel 3. Luego sigue el registro recursivo nuevamente y piensa que ha alcanzado el nivel 2. Pero en realidad todavía está en el nivel 4. Luego la CPU va a la nueva dirección y entra en la tabla de nivel 3, pero cree que ya está en la tabla de nivel 1. Finalmente, en el siguiente punto de entrada en la tabla de nivel 2, el procesador cree que ha accedido al marco de memoria física. Esto nos permite leer y escribir en una tabla de nivel 2.
También se accede a las tablas de los niveles 3 y 4. Para acceder a la tabla del nivel 3, seguimos un registro recursivo tres veces: el procesador cree que ya está en la tabla del nivel 1, y en el siguiente paso llegamos al nivel 3, que la CPU considera como un marco mapeado. Para acceder a la tabla de nivel 4, simplemente seguimos el registro recursivo cuatro veces hasta que el procesador procese la tabla de nivel 4 como un marco mapeado (en azul en la figura a continuación).
El concepto es difícil de entender al principio, pero en la práctica funciona bastante bien.
Cálculo de dirección
Por lo tanto, podemos acceder a las tablas de todos los niveles siguiendo un registro recursivo una o más veces. Dado que los índices en tablas de cuatro niveles se derivan directamente de la dirección virtual, se deben crear direcciones virtuales especiales para este método. Como recordamos, los índices de la tabla de páginas se extraen de la dirección de la siguiente manera:
Supongamos que queremos acceder a una tabla de nivel 1 que muestra una página específica. Como aprendimos anteriormente, debe pasar por un registro recursivo una vez, y luego a través de los índices de los niveles 4º, 3º y 2º. Para hacer esto, movemos todos los bloques de direcciones un bloque a la derecha y establecemos el índice del registro recursivo en el lugar del índice inicial del nivel 4:
Para acceder a la tabla de nivel 2 de esta página, movemos todos los bloques de índice dos bloques a la derecha y establecemos el índice recursivo en el lugar de ambos bloques de origen: nivel 4 y nivel 3:
Para acceder a la tabla de nivel 3, hacemos lo mismo, solo nos desplazamos a la derecha con tres bloques de direcciones.
Finalmente, para acceder a la tabla de nivel 4, mueva todo cuatro bloques a la derecha.
Ahora puede calcular direcciones virtuales para tablas de páginas de los cuatro niveles. Incluso podemos calcular una dirección que apunta exactamente a una entrada específica de la tabla de páginas multiplicando su índice por 8, el tamaño de la entrada de la tabla de páginas.
La siguiente tabla muestra la estructura de direcciones para acceder a varios tipos de marcos:
Dirección virtual para | Estructura de direcciones ( octal ) |
---|
Pagina | 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE |
Entrada en la mesa de nivel 1 | 0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD |
Entrada en una mesa de nivel 2 | 0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC |
Entrada en una mesa de nivel 3 | 0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB |
Entrada en la mesa del nivel 4 | 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA |
Aquí
es el índice de nivel 4,
es el nivel 3,
es el nivel 2 y
DDD
es el índice de nivel 1 para el cuadro visualizado,
EEEE
es su desplazamiento.
RRR
es el índice del registro recursivo. Un índice (tres dígitos) se convierte en un desplazamiento (cuatro dígitos) al multiplicar por 8 (el tamaño de la entrada de la tabla de páginas). Con este desplazamiento, la dirección resultante apunta directamente a la entrada de la tabla de páginas correspondiente.
SSSS
son bits de expansión del dígito firmado, es decir, son todas copias del bit 47. Este es un requisito especial para direcciones válidas en la arquitectura x86_64, que discutimos en un
artículo anterior .
Las direcciones son
octales , ya que cada carácter octal representa tres bits, lo que le permite separar claramente los índices de tablas de 9 bits en diferentes niveles. Esto no es posible en el sistema hexadecimal, donde cada carácter representa cuatro bits.
Implementación
Después de toda esta teoría, finalmente podemos proceder con la implementación. Convenientemente, el cargador generó no solo tablas de páginas, sino también una visualización recursiva en el último registro de la tabla de nivel 4. El cargador hizo esto porque de lo contrario habría un problema de huevo o gallina: necesitamos acceder a la tabla de nivel 4 para crear un mapa recursivo pero no podemos acceder a él sin ninguna pantalla.
Ya utilizamos esta asignación recursiva al final del artículo anterior para acceder a la tabla de nivel 4 a través de la dirección codificada
0xffff_ffff_ffff_f000
. Si convertimos esta dirección a octal y la comparamos con la tabla anterior, veremos que corresponde exactamente a la estructura del registro en la tabla de nivel 4 con
RRR
=
0o777
,
AAAA
=
0
y los bits de extensión del signo
1
:
estructura: 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
dirección: 0o_177777_777_777_777_777_0000
Gracias al conocimiento de las tablas recursivas, ahora podemos crear direcciones virtuales para acceder a todas las tablas activas. Y hacer que la transmisión funcione.
Traducción de direcciones
Como primer paso, cree una función que convierta una dirección virtual en una dirección física, pasando por la jerarquía de las tablas de páginas:
Primero, introducimos variables para el índice recursivo (511 =
0o777
) y los bits de extensión de signo (cada uno es 1). Luego calculamos los índices de las tablas de páginas y el desplazamiento mediante operaciones bit a bit, como se indica en la ilustración:
El siguiente paso es calcular las direcciones virtuales de las tablas de cuatro páginas, como se describe en la sección anterior. Luego, en la función, convertimos cada una de estas direcciones a enlaces de
PageTable
. Estas son operaciones inseguras porque el compilador no puede saber que estas direcciones son válidas.
Después de calcular la dirección, utilizamos el operador de indexación para ver el registro en la tabla de nivel 4. Si este registro es cero, entonces no hay una tabla de nivel 3 para este registro de nivel 4. Esto significa que
addr
no
addr
asignado a ninguna memoria física. Entonces devolvemos
None
. De lo contrario, sabemos que existe una tabla de nivel 3. Luego repetimos el procedimiento, como en el nivel anterior.
Después de verificar tres páginas de un nivel superior, finalmente podemos leer el registro de la tabla de nivel 1, que nos dice el marco físico con el que se asigna la dirección. Como último paso, agregue el desplazamiento de página y devuelva la dirección.
Si supiéramos con certeza que la dirección estaba asignada, podríamos acceder directamente a la tabla de nivel 1 sin mirar las páginas de un nivel superior. Pero como no sabemos esto, primero debemos verificar si existe una tabla de nivel 1, de lo contrario, nuestra función devolverá un error de página faltante para direcciones no coincidentes.
Prueba
Intentemos usar la función de traducción para direcciones virtuales en nuestra función
_start
:
Después de comenzar, vemos el siguiente resultado:
Como se esperaba, la dirección 0xb8000 asociada con el identificador se traduce en la misma dirección física. La página de códigos y la página de la pila se convierten en algunas direcciones físicas arbitrarias, que dependen de cómo el cargador creó la asignación inicial para nuestro núcleo.
RecursivePageTable
x86_64 proporciona un tipo
RecursivePageTable
que implementa abstracciones seguras para varias operaciones de tabla de páginas. Con este tipo, puede implementar la función
translate_addr
mucho más sucintamente:
El tipo
RecursivePageTable
encapsula completamente los rastreos de tablas de páginas inseguras, por lo que ya no se necesita el código
unsafe
en la función
translate_addr
. La función
init
permanece insegura debido a la necesidad de garantizar la corrección del nivel
level_4_table_addr
.
Nuestra función
_start
debe actualizarse para volver a firmar la función de la siguiente manera:
Ahora, en lugar de pasar
LEVEL_4_TABLE_ADDR
para
translate_addr
y acceder a las tablas de página a través de punteros sin
LEVEL_4_TABLE_ADDR
seguros, pasamos referencias al tipo
RecursivePageTable
. Por lo tanto, ahora tenemos una abstracción segura y una semántica clara de propiedad. Esto asegura que no podremos cambiar accidentalmente la tabla de páginas en acceso compartido, porque para cambiarla, debemos tomar posesión de
RecursivePageTable
exclusivamente.
Esta función da el mismo resultado que la función de traducción original escrita manualmente.
Hacer que las características inseguras sean más seguras
memory::init
es una función insegura: requiere un bloque para llamarlo unsafe
, porque la persona que llama debe garantizar que se cumplan ciertos requisitos. En nuestro caso, el requisito es que la dirección transmitida se asigne con precisión al marco físico de la tabla de páginas de nivel 4. Todo el cuerpo de la función insegura se colocaen el bloque unsafe
para que todo tipo de operaciones se realicen sin crear bloques adicionales unsafe
. Por lo tanto, no necesitamos un bloque inseguro para desreferenciar 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;
El problema es que no vemos de inmediato qué partes son inseguras. Por ejemplo, sin mirar la definición de una función, RecursivePageTable::new
no podemos decir si es segura o no. Por lo tanto, es muy fácil omitir accidentalmente algún código inseguro.Para evitar este problema, puede agregar una función incorporada segura:
Ahora se unsafe
requiere nuevamente el bloqueo para desreferenciar level_4_table_ptr
, e inmediatamente vemos que estas son las únicas operaciones inseguras. Rust actualmente tiene un RFC abierto para cambiar esta propiedad fallida de funciones inseguras.Crea un nuevo mapeo
Cuando leemos las tablas de páginas y creamos la función de conversión, el siguiente paso es crear una nueva asignación en la jerarquía de tablas de páginas.La complejidad de esta operación depende de la página virtual que queremos mostrar. En el caso más simple, ya existe una tabla de páginas de nivel 1 para esta página, y solo tenemos que hacer una entrada. En el caso más difícil, la página está en el área de memoria para la cual el nivel 3 aún no existe, por lo que primero debe crear nuevas tablas de nivel 3, nivel 2 y nivel 1.Comencemos con un caso simple cuando no necesite crear nuevas tablas. El cargador se carga en el primer megabyte del espacio de direcciones virtuales, por lo que sabemos que para esta región hay una tabla válida de nivel 1. Para nuestro ejemplo, podemos seleccionar cualquier página no utilizada en esta área de memoria, por ejemplo, la página en la dirección 0x1000
. Utilizamos el 0xb8000
marco del búfer de texto VGA como el marco deseado . Es muy fácil comprobar cómo funciona nuestra traducción de direcciones.Lo implementamos en una nueva función create_maping
en el módulo memory
:
La función acepta una referencia mutable a RecursivePageTable
(la cambiará) y FrameAllocator
, que se explica a continuación. Luego aplica la función map_to
en la bandeja Mapper
para mapear la página en la dirección 0x1000
con el marco físico en la dirección 0xb8000
. La función no es segura, porque es posible violar la seguridad de la memoria con argumentos no válidos.Además de los argumentos page
y frame
, la función map_to
toma dos argumentos más. El tercer argumento es el conjunto de banderas para la tabla de páginas. Establecemos la bandera PRESENT
necesaria para todas las entradas válidas y la bandera WRITABLE
para la escritura.El cuarto argumento debería ser alguna estructura que implemente el rasgo FrameAllocator
. Este argumento es necesario por el método.map_to
porque crear nuevas tablas de páginas puede requerir marcos no utilizados. La aplicación requiere el rasgo argumento Size4KiB
, como los tipos Page
y PhysFrame
son universales para el rasgo PageSize
, trabajando con 4 páginas estándar KiB y con enormes páginas 2 MiB / 1 GIB.La función map_to
puede fallar, por lo que vuelve Result
. Dado que este es solo un ejemplo de código que no debería ser confiable, simplemente lo usamos expect
con pánico cuando se produce un error. Si tiene éxito, la función devuelve un tipo MapperFlush
que proporciona una manera fácil de borrar la página recientemente emparejada del método del búfer de traducción asociativa (TLB) flush
. Me gustaResult
, el tipo usa el atributo #[must_use]
y emite una advertencia si accidentalmente olvidamos aplicarlo.Como sabemos que la dirección 0x1000
no requiere nuevas tablas de páginas, FrameAllocator
siempre puede volver None
. Para probar la función, cree esto EmptyFrameAllocator
:
(Si aparece el error 'el método allocate_frame
no es miembro del rasgo FrameAllocator
', debe actualizar x86_64
a la versión 0.4.0.)Ahora podemos probar la nueva función de traducción:
Primero, creamos una asignación para la página en la dirección 0x1000
, llamando a la función create_example_mapping
con un enlace mutable a la instancia RecursivePageTable
. Esto traduce la página 0x1000
a un búfer de texto VGA, por lo que veremos algunos resultados en la pantalla.Luego escribimos un valor en esta página 0xf021f077f065f04e
, que corresponde a la línea "¡Nuevo!" sobre un fondo blanco Simplemente no es necesario que escriba este valor inmediatamente en la parte superior de la página 0x1000
, porque la línea superior se moverá a continuación de la pantalla println
y lo escribirá en un desplazamiento 0x900
que se encuentra aproximadamente en el medio de la pantalla. Como sabemos por el artículo "Modo de texto VGA" , escribir en el búfer VGA debe ser volátil, por lo que utilizamos el método write_volatile
.Cuando lo ejecutamos en QEMU, vemos esto:La inscripción en la pantalla.El código funcionó porque ya había una tabla de nivel 1 para mostrar la página 0x1000
. Si intentamos traducir una página para la que aún no existe dicha tabla, la función map_to
devolverá un error, ya que intentará seleccionar marcos para crear nuevas tablas de páginas EmptyFrameAllocator
. Lo veremos si intentamos traducir la página en 0xdeadbeaf000
lugar de 0x1000
:
Al comenzar, un pánico comienza con el siguiente mensaje de error: entró en pánico en 'map_to falló: FrameAllocationFailed', /.../result.rs:999haps
Para mostrar páginas que aún no tienen una tabla de nivel de página 1, debe crear la correcta FrameAllocator
. Pero, ¿cómo saber qué cuadros son gratuitos y cuánta memoria física está disponible?Información de arranque
Las diferentes computadoras tienen diferentes cantidades de memoria física y las diferentes áreas reservadas por dispositivos como VGA difieren. Solo el BIOS o el firmware UEFI saben exactamente qué áreas de memoria se pueden usar y cuáles están reservadas. Ambos estándares de firmware proporcionan funciones para obtener una tarjeta de asignación de memoria, pero solo se pueden llamar al comienzo de la descarga. Por lo tanto, nuestro gestor de arranque ya ha solicitado esta (y otra) información del BIOS.Para pasar información al núcleo del sistema operativo, el cargador como argumento al llamar a la función _start
proporciona un enlace a la estructura de información del arranque. Agregue este argumento a nuestra función:
La estructura BootInfo
aún se está finalizando, así que no se sorprenda cuando se cuelgue al actualizar a futuras versiones del gestor de arranque que no son compatibles con semver . Por el momento se cuenta con tres campos p4_table_addr
, memory_map
y package
:- El campo
p4_table_addr
contiene una dirección virtual recursiva de la tabla de páginas de nivel 4. Gracias a esto, no es necesario registrar la dirección de forma rígida 0o_177777_777_777_777_777_0000
.
- El campo
memory_map
es de mayor interés, ya que contiene una lista de todas las áreas de memoria y su tipo (no utilizado, reservado u otros).
- El campo
package
es la función actual para asociar datos adicionales con el cargador. La implementación no está completa, por lo que podemos ignorarla por ahora.
Antes de usar el campo memory_map
para crear el correcto FrameAllocator
, queremos garantizar el tipo correcto de argumento boot_info
.Macro entry_point
Como se _start
llama externamente, no se verifica la firma de la función. Esto significa que los argumentos arbitrarios no conducirán a errores de compilación, pero pueden causar un bloqueo o un comportamiento de tiempo de ejecución indefinido.Para verificar la firma, la caja bootloader
para definir la función Rust como punto de entrada utiliza una macro entry_point
con tipos validados. Reescribimos nuestra función para esta macro:
Para el punto de entrada, ya no necesita usar extern "C"
o no_mangle
, ya que la macro establece el punto de entrada real de bajo nivel _start
. La función kernel_main
ahora se ha convertido en una función Rust completamente normal, por lo que podemos elegir un nombre arbitrario para ella. Es importante que ya esté escrito, de modo que se produzca un error de compilación si cambia la firma de la función, por ejemplo, agregando un argumento o cambiando su tipo.Tenga en cuenta que ahora estamos enviando a una memory::init
dirección codificada, pero boot_info.p4_table_addr
. Por lo tanto, el código funcionará incluso si la versión futura del gestor de arranque selecciona otra entrada en la tabla de tabla de nivel de página 4 para la visualización recursiva.Selección de marco
Ahora, gracias a la información del BIOS, tenemos acceso a la tarjeta de asignación de memoria, para que pueda hacer un distribuidor de trama normal. Comencemos con el esqueleto general:
El campo se frames
inicializa mediante un iterador de marco arbitrario . Esto le permite simplemente delegar llamadas alloc
al método Iterator :: next .La inicialización BootInfoFrameAllocator
tiene lugar en una nueva función init_frame_allocator
:
Esta función, utilizando un combinador, convierte el mapa de asignación de memoria original en un iterador de las tramas físicas utilizadas:iter
MemoryRegion
. filter
, . , , (, ) , InUse
. , , - .
map
range Rust .
- El tercer paso es el más difícil: convertimos cada rango en un iterador usando el método
into_iter
, y luego seleccionamos cada 4096a dirección con step_by
. Como el tamaño de la página es de 4096 bytes (4 KiB), obtenemos la dirección del comienzo de cada marco. La página del cargador alinea todas las áreas de memoria utilizadas, por lo que no necesitamos un código de alineación o redondeo. Reemplazando map
con flat_map
, obtenemos en su Iterator<Item = u64>
lugar Iterator<Item = Iterator<Item = u64>>
.
- En la etapa final, convertiremos las direcciones iniciales a tipos
PhysFrame
para construir la requerida Iterator<Item = PhysFrame>
. Luego use este iterador para crear y devolver uno nuevo BootInfoFrameAllocator
.
Ahora podemos cambiar nuestra función kernel_main
para que pase la instancia en su BootInfoFrameAllocator
lugar EmptyFrameAllocator
:
Ahora la traducción de la dirección es exitosa, y nuevamente vemos el mensaje en blanco y negro "¡Nuevo!" En la pantalla .
Detrás de escena, el método map_to
crea tablas de páginas faltantes de la siguiente manera:- Extrae un marco no utilizado de
frame_allocator
.
- Coincide con una entrada de tabla de nivel superior con este marco. Ahora se puede acceder al marco a través de una tabla de páginas recursivas.
- Cero el marco para crear una nueva tabla de páginas vacía.
- Va a la tabla del siguiente nivel.
Aunque nuestra función create_maping
es solo un ejemplo, ahora podemos crear nuevas asignaciones para páginas arbitrarias. Esto es muy útil al asignar memoria e implementar múltiples subprocesos en futuros artículos.Resumen
En este artículo, aprendió a usar una tabla recursiva de nivel 4 para traducir todos los marcos a direcciones virtuales computables. Utilizamos este método para implementar la función de traducción de direcciones y crear una nueva asignación en las tablas de páginas.Vimos que crear nuevas asignaciones requiere marcos no utilizados para nuevas tablas. Tal distribuidor de trama puede implementarse en base a la información del BIOS que el gestor de arranque pasa a nuestro núcleo.Que sigue
En el siguiente artículo, crearemos un área de memoria de almacenamiento dinámico para nuestro kernel, que nos permitirá asignar memoria y usar diferentes tipos de colecciones .