¿Cómo sabe el kernel la dirección base de la memoria física?
Estoy tratando de comprender dos problemas estrechamente relacionados.
El código del kernel que se ejecuta después del cargador de arranque y antes de habilitar la MMU opera en la memoria virtual física / de identidad asignada. ¿Cómo se hace portátil este código entre diferentes CPU que pueden tener DRAM en diferentes rangos de direcciones físicas?
Para que el kernel administre la tabla de páginas, necesita conocer qué recursos de memoria física están disponibles, incluida la dirección base de la memoria física y la memoria física disponible, de modo que no asigne direcciones físicas que estén fuera del rango de DRAM.
Me imagino que esto depende un poco de la implementación, pero se agradecerían las referencias a cómo las diferentes arquitecturas manejan este problema. Algunas ideas que tengo hasta ahora:
El rango de la DRAM de la dirección física, o al menos la dirección base, se incluye en el momento de la compilación del kernel. Esto implica que se necesita la recompilación para diferentes CPU incluso con el mismo ISA. Esto está inspirado en esta respuesta aquí , que, si entiendo correctamente, describe la misma solución para la dirección base del kernel. Dado que la dirección base se conoce en el momento de la compilación, el código del kernel hace referencia a direcciones literales en lugar de compensaciones de la dirección base de DRAM / kernel.
La información de DRAM se lee y aprende del árbol de dispositivos con el resto del mapa de memoria física. Esta es mi impresión para al menos Xilinx Zynq SoC, basada en publicaciones en foros como esta . Si bien esta solución ofrece más flexibilidad y nos permite simplemente recompilar el cargador de arranque en lugar de todo el kernel para portar CPU, me deja preguntándome cómo mi máquina personal X86 puede detectar en tiempo de ejecución cuánta DRAM he instalado. El código para administrar la tabla de páginas solo hace referencia a las compensaciones de la dirección base de la DRAM y es portátil sin volver a compilar entre las CPU con diferentes rangos de direcciones físicas de DRAM.
Respuestas
Es posible que todos los DIMM de memoria física que están disponibles en el momento del arranque no estén asignados a un solo rango contiguo del espacio de direcciones de memoria física, por lo que no hay una "dirección base". En un restablecimiento completo, después de que el firmware de la CPU completa la ejecución, se ejecuta el firmware de la plataforma, que generalmente es BIOS heredado o UEFI. Una placa base determinada solo es compatible con un conjunto limitado de colecciones de CPU que normalmente tienen el mismo método para descubrir la memoria física, incluidos los DIMM y el dispositivo de memoria del firmware de la plataforma. Una implementación del firmware de la plataforma utiliza este método para crear una tabla de entradas de descripción de memoria donde cada entrada describe un rango de direcciones de memoria física. Para obtener más información sobre el aspecto de este procesador, consulte: ¿Cómo inicializa el BIOS la DRAM? . Esta tabla se almacena en una dirección en la memoria principal (DIMM) que se sabe que está reservada para este propósito y se supone que está respaldada por la memoria real (un sistema puede iniciarse sin DIMM).
La mayoría de las implementaciones de BIOS de PC x86 desde mediados de los 90 ofrecen la INT 15h E820h
función de modo real (15h es el número de interrupción y E820h es un argumento que se pasa en el AX
registro). Esta es una función de BIOS específica del proveedor que se introdujo por primera vez en PhoenixBIOS v4.0 (1992-1994, no puedo precisar el año exacto) y luego fue adoptada por otros proveedores de BIOS. Esta interfaz fue ampliada por la especificación ACPI 1.0 lanzada en 1996 y revisiones posteriores de ACPI compatible con PhoenixBIOS. La interfaz UEFI correspondiente es GetMemoryMap()
, que es un servicio de tiempo de inicio UEFI (lo que significa que solo se puede llamar en el momento del inicio como se define en la especificación UEFI). El kernel puede usar una de estas interfaces para obtener el mapa de direcciones que describe la memoria en todos los nodos NUMA. Otros métodos (más antiguos) en plataformas x86 se describen en Detección de memoria (x86) . ¿Tanto la especificación ACPI comenzando con la versión? y la especificación UEFI comenzando con la versión? admite los tipos de rango de memoria DRAM DIMM y NVDIMM.
Considere, por ejemplo, cómo el kernel de Linux compatible con ACPI determina qué rangos de direcciones físicas están disponibles (es decir, respaldados por memoria real) y utilizables (es decir, gratis) en una plataforma BIOS compatible con ACPI x86. El firmware del BIOS carga el cargador de arranque desde el dispositivo de almacenamiento de arranque especificado en una ubicación de memoria dedicada para este propósito. Una vez que el firmware completa la ejecución, salta al cargador de arranque que encontrará la imagen del kernel en el medio de almacenamiento, la carga en la memoria y transfiere el control al kernel. El propio cargador de arranque necesita conocer el mapa de memoria actual y asignar algo de memoria para su funcionamiento. Intenta obtener el mapa de memoria llamando a la E820h
función y, si no es compatible, recurrirá a interfaces BIOS de PC más antiguas. El protocolo de arranque del kernel define qué rangos de memoria puede usar el cargador de arranque y qué rangos de memoria deben dejarse disponibles para el kernel.
El cargador de arranque en sí no modifica el mapa de memoria ni proporciona el mapa al kernel. En cambio, cuando el kernel comienza a ejecutarse, llama a la E820h
función y le pasa un puntero de 20 bits (in ES:DI
) a un búfer que el kernel sabe que está libre en plataformas x86 de acuerdo con el protocolo de arranque. Cada llamada devuelve un descriptor de rango de memoria cuyo tamaño es de al menos 20 bytes. Para obtener más información, consulte la última versión de la especificación ACPI. La mayoría de las implementaciones de BIOS son compatibles con ACPI.
Suponiendo un kernel de Linux con parámetros de arranque predeterminados en sentido ascendente, puede usar el comando dmesg | grep 'BIOS-provided\|e820'
para ver la tabla de descriptores de rango de memoria devuelta. En mi sistema, se ve así:
[ 0.000000] BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x00000000000917ff] usable
[ 0.000000] BIOS-e820: [mem 0x0000000000091800-0x000000000009ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000000e0000-0x00000000000fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000d2982fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d2983000-0x00000000d2989fff] ACPI NVS
[ 0.000000] BIOS-e820: [mem 0x00000000d298a000-0x00000000d2db9fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d2dba000-0x00000000d323cfff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000d323d000-0x00000000d7eeafff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d7eeb000-0x00000000d7ffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000d8000000-0x00000000d875ffff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d8760000-0x00000000d87fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000d8800000-0x00000000d8fadfff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d8fae000-0x00000000d8ffffff] ACPI data
[ 0.000000] BIOS-e820: [mem 0x00000000d9000000-0x00000000da718fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000da719000-0x00000000da7fffff] ACPI NVS
[ 0.000000] BIOS-e820: [mem 0x00000000da800000-0x00000000dbe11fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000dbe12000-0x00000000dbffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000dd000000-0x00000000df1fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000f8000000-0x00000000fbffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fec00000-0x00000000fec00fff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fed00000-0x00000000fed03fff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fed1c000-0x00000000fed1ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fee00000-0x00000000fee00fff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000ff000000-0x00000000ffffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000041edfffff] usable
[ 0.002320] e820: update [mem 0x00000000-0x00000fff] usable ==> reserved
[ 0.002321] e820: remove [mem 0x000a0000-0x000fffff] usable
[ 0.002937] e820: update [mem 0xdd000000-0xffffffff] usable ==> reserved
[ 0.169287] e820: reserve RAM buffer [mem 0x00091800-0x0009ffff]
[ 0.169288] e820: reserve RAM buffer [mem 0xd2983000-0xd3ffffff]
[ 0.169289] e820: reserve RAM buffer [mem 0xd2dba000-0xd3ffffff]
[ 0.169289] e820: reserve RAM buffer [mem 0xd7eeb000-0xd7ffffff]
[ 0.169289] e820: reserve RAM buffer [mem 0xd8760000-0xdbffffff]
[ 0.169290] e820: reserve RAM buffer [mem 0xd8fae000-0xdbffffff]
[ 0.169291] e820: reserve RAM buffer [mem 0xda719000-0xdbffffff]
[ 0.169291] e820: reserve RAM buffer [mem 0xdbe12000-0xdbffffff]
[ 0.169292] e820: reserve RAM buffer [mem 0x41ee00000-0x41fffffff]
Los rangos de memoria que comienzan con "BIOS-e820" se describen en esa tabla. La primera línea le dice claramente la fuente de esta información. El formato exacto de esta información depende de la versión del kernel de Linux. En cualquier caso, verá un rango y un tipo en cada entrada. Las filas que comienzan con "e820" (sin la parte "BIOS-") son cambios que el propio núcleo ha realizado en la tabla. La implementación del E820h
puede tener errores o puede haber superposiciones entre los rangos obtenidos en diferentes entradas. El kernel realiza las comprobaciones y los cambios necesarios en consecuencia. Los rangos que están marcados como "utilizables" son en su mayoría libres para que los use el kernel con las excepciones discutidas en la especificación ACPI y de las cuales el kernel es consciente. La gran mayoría de las implementaciones de BIOS de PC devuelven como máximo 128 descriptores de rangos de memoria. Las versiones anteriores del kernel de Linux solo podían manejar hasta 128 rangos de memoria, por lo que E820h
se ignoran las entradas devueltas más allá del 128. A partir de la versión ?, esta limitación se relajó. Para obtener más información, consulte la serie de parches del kernel titulada "arranque x86: pasar más de 128 entradas del mapa de memoria E820 a través de una lista vinculada de datos de configuración".
Rangos de tipo usable
y ACPI data
. Los rangos de tipo reserved
están respaldados por DRAM DIMM o eliminados para MMIO por la CPU o el firmware de la plataforma. Los rangos de tipo ACPI NVS
están respaldados por la memoria del firmware. Todos los demás rangos no están respaldados por la memoria real por lo que el firmware puede decir. Tenga en cuenta que el firmware puede optar por no asignar todos los módulos DRAM DIMM o NVDIMM instalados. Esto puede suceder si la configuración de la memoria física no es compatible tal cual o si el firmware no puede obtener información de un DIMM instalado debido a un problema en el DIMM.
Puede calcular la cantidad de memoria de los DIMM DRAM y NVDIMM instalados que el firmware pone a disposición del kernel. En mi sistema, instalé 16 GB de DRAM DIMM. Entonces, a menos que algunos de los DIMM no estén instalados correctamente, no funcionen correctamente, haya un error en el firmware o no sean compatibles con la plataforma o el procesador, debería haber un poco menos de 16 GB disponibles para el kernel.
Todos los usable
rangos suman 0x3FA42B800 bytes. Tenga en cuenta que la última dirección de un rango es inclusiva, lo que significa que apunta a una ubicación de bytes que forma parte del rango. La cantidad total de DIMM instalados físicamente es de 16 GB o 0x400000000 bytes. Entonces, la cantidad total de memoria instalada que no estaba disponible para el kernel es 0x400000000 - 0x3FA42B800 o aproximadamente 92 MB del total de 16 GB. Esta memoria fue tomada por algunos de los reserved
rangos y todos los ACPI data
rangos. Si el firmware de la plataforma determinó que ciertas ubicaciones en una DRAM DIMM o NVDIMM no eran confiables, también se eliminarán como reserved
.
Tenga en cuenta que el rango 0x000a0000-0x000fffff no se describe en el E820
mapa de memoria según la especificación ACPI. Esta es el área de memoria superior de 640 KB-1 MB. El kernel imprime un mensaje que dice que ha eliminado este rango del área de memoria utilizable para mantener la compatibilidad con los sistemas antiguos.
En este punto, la memoria que se utilizará como MMIO para la mayoría de los dispositivos PCIe aún no está asignada. Mi procesador admite un espacio de direcciones físicas de 39 bits, lo que significa que las direcciones entre 0 y 2 ^ 39 están disponibles para el mapeo. Hasta ahora, solo la mayoría de los 16,5 GB inferiores de este espacio se han asignado a algo. Tenga en cuenta que todavía hay brechas no mapeadas en este rango. El kernel puede usar estos espacios (unos cientos de MB) y el resto del espacio de direcciones físicas (aproximadamente 495,5 GB) para asignar rangos de direcciones para dispositivos IO. El kernel eventualmente descubrirá dispositivos PCIe y para cada dispositivo, intentará cargar un controlador compatible si está disponible. Luego, el controlador determina cuánta memoria necesita el dispositivo y cualquier restricción en las direcciones de memoria impuestas por el dispositivo y solicita al kernel que asigne memoria para el dispositivo y la configure como una memoria MMIO propiedad del dispositivo. Puede ver el mapa de memoria final usando el comando sudo cat /proc/iomem
.
Hay situaciones en las que desearía cambiar manualmente el tipo de memoria de un rango de memoria existente (por ejemplo, para realizar pruebas), crear un nuevo rango (por ejemplo, para emular la memoria persistente en DRAM o si el firmware no puede descubrir todos los memoria disponible por cualquier motivo), reduzca la cantidad de memoria utilizable por el kernel (por ejemplo, para evitar que un hipervisor de metal completo use la memoria más allá de un límite y haga que el resto esté disponible para los invitados), o incluso anule por completo toda la tabla devuelta por E820h
. Los parámetros mem
y memmap
kernel se pueden utilizar para tales fines. Cuando uno o más de estos parámetros se especifican con valores válidos, el núcleo primero leerá el mapa de memoria proporcionado por el BIOS y realizará los cambios correspondientes. El núcleo imprimirá el mapa de memoria final como "mapa de RAM físico definido por el usuario". en el búfer de anillo de mensajes del kernel. Puede ver estos mensajes con dmesg | grep user:
(cada fila de rango de memoria comienza con "usuario:"). Estos mensajes se imprimirán después de los mensajes "BIOS-e820".
En una plataforma x86 iniciada con firmware UEFI que admite el Módulo de soporte de compatibilidad (consulte la especificación CSM para obtener más información, que es independiente de UEFI), la E820h
interfaz de modo real heredada es compatible y el kernel de Linux todavía la usa por defecto. Si el kernerl se ejecuta en una plataforma x86 con UEFI que no admite CSM, es posible que la E820h
interfaz no proporcione todos o ninguno de los rangos de memoria. Puede que sea necesario utilizar el add_efi_memmap
parámetro del kernel en dichas plataformas. Se puede encontrar un ejemplo en UEFI Memory V E820 Memory . Cuando se proporcionan uno o más de los rangos de memoria GetMemoryMap()
, el kernel fusiona estos rangos con los de la E820h
interfaz. El mapa de memoria resultante se puede ver usando dmesg | grep 'efi:'
Otro parámetro del kernel relacionado con UEFI que afecta el mapa de memoria es efi_fake_mem
.
La especificación ACPI (Sección 6.3) proporciona mecanismos de notificación para informar al kernel cuando un dispositivo IO o DIMM se ha insertado o eliminado del sistema en cualquier estado S. (Sin embargo, no sé si hay motherboads que admitan la eliminación de DIMM en cualquier estado S. Por lo general, esto solo es posible en el estado G3 y tal vez S4 y / o S5). Cuando ocurre tal evento, el kernel o el firmware realiza los cambios correspondientes en el mapa de memoria. Estos cambios se reflejan en sudo cat /proc/iomem
.
El direccionamiento relativo a PC se refiere a una técnica de programación en la que su programa puede operar en cualquier dirección. Dado que los registros de reubicación (por ejemplo, segmentos) se han vuelto obsoletos, la mayor parte de la programación relativa a PC se realiza explícitamente. Aquí hay un ejemplo en un tipo genérico de código de máquina:
.text
entry:
call reloc /* call is pc relative */
reloc:
pop %r0 /* r0 now contains physical address of reloc */
sub $reloc, %r0, %r14 /* r14 contains difference between link address of reloc */ /* At this point, r14 is a relocation register. A virtual address + r14 == the corresponding physical address. */ add $proot, %r14, %r0 /* physical address of page table root */
add $entry, %r14, %r1 /* entry is where we were loaded into ram */ test $0xfff, %r1 /* someone is being funny and not page aligning us */
jnz bad_alignment
or $0x7, %r1 /* put mythical page protection bits in r1 */ mov $1024, %r2 /* number of pages in r2 */
loop:
store %r1, (%r0) /* store a page table entry */
add $0x1000, %r1 /* setup next one 4096 bytes farther */ add $4, %r0 /* point to next page table entry */
sub $1, r2 /* are we done? */ cmp %0, r2 jne loop /* nope, setup next entry */ add $proot, %r14, %r0
loadsysreg %r0, page_table_base_register
mov $1, %r0 mov $v_entry, %r1
loadsysreg %r0, page_table_enabled
jmp %r1
v_entry:
/* now we are virtually addressed */
call main
1: jmp 1b /* main shouldn't return. */
.data
.align 12 /* 4096 byte pages */
proot:
.zero 4096
.text
Esta mítica máquina es muy simple, con una sola tabla de página plana, y el kernel está vinculado en la dirección 0, pero podría ejecutarse desde cualquier lugar en los primeros 4M (1024 * 4096). Las máquinas reales son solo versiones más detalladas de esto. En general, no puede confiar ni siquiera en los idiomas del sistema C
hasta que tenga la configuración inicial del espacio de direcciones. Una vez que lo esté, el código en él puede construir tablas de páginas mucho más complejas y consultar bases de datos como el árbol de dispositivos, o incluso monstruosidades como apic / uefi para obtener más información sobre el diseño de RAM, etc.
En arquitecturas de tabla de páginas mapeadas hacia adelante donde los nodos interiores están en un formato compatible como los nodos hoja (x86-classic, por ejemplo), puede usar una tabla de página única de forma recursiva para permitir una dirección de enlace más flexible. Por ejemplo, si apuntó la última entrada en proot (es decir, proot [1023]) de nuevo a proot, entonces podría vincular su sistema operativo en 0xffffc000, y este código simplemente funcionaría (una vez traducido a x86).