Ya hemos hecho una introducción al desarrollo de módulos del Kernel para Linux y hemos explicado como configurar un entorno de desarrollo con Eclipse con el que trabajar. Hoy en Programando módulos para el Kernel de Linux vamos a hablar sobre el proceso de arranque del Kernel y los sistemas implicados.
Como todo sistema basado en arquitectura x86, Linux necesita que la BIOS cargue el MBR (Master Boot Record) desde el primer dispositivo de almacenamiento del sistema. El código residente en el MBR procesa la tabla de particiones del dispositivo y carga un gestor de arranque de Linux, generalmente GRUB o SYSLINUX desde la partición activa.
Finalmente, el gestor de arranque carga la imagen comprimida del Kernel en memoria y le pasa el control de la ejecución. El Kernel se descomprime a sí mismo y comienza el proceso de arranque. Esto por supuesto, partiendo del supuesto de que estamos arrancando un sistema simple sin RAIDS ni volúmenes lógicos ni nada por el estilo.
El proceso de arranque
Los procesadores basados en arquitectura x86 disponen de dos modos de operación, el modo real y el modo protegido. En modo real podemos acceder solamente al primer megabyte de memoria sin ningún tipo de protección.
El modo protegido es un modo más sofisticado y nos permite hacer uso de muchas características avanzadas del procesador como la paginación. El procesador tiene que pasar de forma obligada por el modo real en su camino hacia el modo protegido. No se puede pasar de modo protegido otra vez a modo real.
Las primeras inicializaciones del kernel se producen en el modo real. Posteriormente el resto del arranque tiene lugar en modo protegido por la función start_kernel()
del archivo init/main.c. Esta función comienza la inicialización del subsistema de la CPU. La gestión de memoria y de procesos tienen lugar un poco después. Los buses periféricos y los dispositivos de E/S son iniciados a continuación.
En último lugar en la secuencia de arranque se inicia el programa init que es el padre de todos los procesos en Linux. Init ejecuta los scripts de espacio de usuario necesarios para levantar diferentes servicios del Kernel y arranca los terminales y muestra el prompt de login en ellos.
Memoria
El Kernel ensambla el mapa de memoria del sistema desde la BIOS utilizando la interrupción int 0x15
con la función número 0xe820
para obtener el mapa de la memoria. El mapa indica los rangos de memoria disponible para su uso y reservada que es usada por el Kernel para crear el memory pool.
La región que puede ser direccionada de forma normal por el Kernel se llama low memory y es de 896MB. El asignador de memoria del kernel, kmalloc()
, devuelve memoria desde esa región. La memoria por encima de los 896MB es llamada high memory solo puede ser accedida usando mapeos especiales.
El Kernel organiza la memoria física en páginas. El tamaño de la página depende de la arquitectura, en las máquinas basadas en x86 es de 4096 bytes (4k). Cada página física tiene un struct page
(que está definido en include/linux/mm_type.h) asociado a ella.
En sistemas x86 de 32 bits la configuración por defecto del Kernel separa los 4GB disponibles de memoria (232 bytes de direccionamiento sin PAE (Physical Address Extension)) en 3GB de espacio de memoria virtual conocido como espacio de usuario y 1GB de espacio para el Kernel conocido como espacio del Kernel. En sistemas x86 de 64 bits la máxima memoria direccionable por Linux es de 247 bytes o lo que es lo mismo 128 Terabytes (los procesadores AMD64 pueden direccionar hasta 248, 256TB)
En realidad, el límite es 896MB en el Kernel Space porque 128MB de ese espacio de direcciones está ocupado por estructuras de datos del propio Kernel. Esto nos deja las siguientes zonas de memoria del Kernel:
ZONE_DMA (<16MB), esta es la zona usada para acceso directo a la memoria (DMA). Esto es necesario para dispositivos ISA que solo pueden acceder a los primeros 16MB de la memoria.
ZONE_NORMAL (16MB a 896MB), la región normal de la memoria también conocida como memoria baja
ZONE_HIGH (>896MB), espacio al que el Kernel solo puede acceder después de mapear páginas a regiones en la ZONE_NORMAL usando
kmap()
ykunmap()
. Las direcciones correspondientes son virtuales y no lógicas.
La función kmalloc()
asigna segmentos contiguos de memoria desde la ZONE_NORMAL. La memoria devuelta por esta función retiene el contenido de llamadas previas, por lo que exponerla al espacio de usuario puede incurrir en un problema de seguridad, para devolver la memoria a cero es necesario usar el método kzalloc()
Jiffies y BogoMIPS, calibración de delays
Durante el arranque, el Kernel calcula el número de veces que el procesador puede ejecutar un bucle de delay interno en un jiffy, que es el intervalo de tiempo entre dos ticks consecutivos del reloj del sistema. La calibración del tiempo de demora se calcula en base a la velocidad de procesamiento de la CPU. El resultado de la calibración se almacena en una variable llamada loops_per_jiffy
.
Algunos drivers utilizan loops_per_jiffy
para demorar ejecuciones en el orden de los microsegundos. Como se calculan los loops_per_jiffy
excede de largo el cometido de este post, si quieres saber más al respecto, puedes leer el código del archivo init/calibrate.c. Los BogoMIPS (millones de instrucciones por segundo) que la CPU puede procesar se calculan con la siguiente fórmula:
loops_per_jiffy * número de jiffies en un segundo (Herzios) * número de instrucciones consumidas por el bucle de delay interno / un millonEn mi caso:
2785450 * 1000 * 2 = 5570900000
5570900000 / 1000000 = 5570,9 BogoMIPS
Que se corresponde con;$ cat /proc/cpuinfo | grep bogomips | head -n 1
bogomips : 5570.90
Modo Kernel y Modo Usuario
Algunos sistemas operativos como MS-DOS siempre se ejecutaban en un único modo de la CPU, pero los sistemas operativos basados en UNIX usan modos duales para implementar mecanismos de tiempo compartido de forma efectiva.
En un sistema Linux, la CPU puede estar en un modo Kernel o seguro, o en modo usuario o restringido. Todos los procesos de usuario se ejecutan en modo usuario, mientras que el Kernel en sí se ejecuta en modo Kernel. Con la introducción en el Kernel 2.6 del sistemas de preferencia en el Kernel (kernel preemption) mucho código del modo Kernel pude cambiar de contexto al igual que el código ejecutado en el modo usuario.
A estos dos modos también se les conoce como Ring-0 para el Modo Kernel y Ring-3 para el modo usuario. Si te estás preguntando para que son el Ring-1 y el Ring-2, tan solo decir que son supuéstamente para el código ejecutado por drivers de dispositivos, pero en la implementación, éstos se ejecutan en el Ring-0.
Las CPUs modernas que soportan instrucciones de virtualización por hardware, lo que hacen es precisamente ofrecer instrucciones a un hypervisor para que controle el acceso al hardware del Ring-0. El hypervisor (o host) crea un nuevo Ring-1 donde las máquinas virtuales alojadas pueden ejecutar código del Ring-0 de forma nativa sin afectar a otras máquinas virtuales o al propio hypervisor.
Conclusión
Hoy nos hemos adentrado en las entrañas del funcionamiento del Kernel de Linux en las etapas de arranque del sistema, nos hemos introducido en la gestión de memoria y hemos aprendido como se calibran los delays, por último, hemos indagado en los modos de ejecución de código del Kernel.
Todos estos sistemas son básicos a la hora de comprender como funciona el Kernel y necesitamos conocerlos si pretendemos desarrollar drivers de dispositivos para el mismo. En la próxima entrega hablaremos sobre contextos e interrupciones de procesos, temporizadores en el Kernel, indagaremos un poco más en los Jiffies y los Delays y aprenderemos algo sobre el RTC (Real Time Clock).
Happy Kernel hacking.
Más en Genbeta Dev | Programación a pecho descubierto (Linux Kernel)