¿Cómo arranca el kernel de Linux?
Raul Vazquez CastilloHace algún tiempo leí por ahi esto en alguna página, la cual no recuerdo para poner aquí el vínculo, pero sí creo que resulta interesante saber cómo es que nuestro kernel se carga en nuestra caja Linux.
Claro que pueden irse a los fuentes y ya está, pero para entenderlo en cristiano creo los siguientes párrafos, si sigues leyendo, te serán de utilidad.
Comencemos con el caso común del procesador 80x86, como en mi caso personal. Alencender la PC, el procesador 80x86 se encuentra en modo real y ejecuta elcódigo que se encuentre en 0xFFFF0, que corresponde al ROM-BIOS. El BIOSejecuta algunas pruebas en el sistema (el famoso POST) e inicializa elvector de interrupciones en la dirección física 0x0000. Después carga el primer sector de un dispositivo arrancable directamente en 0x7C00 ycomienza a ejecutarlo.
La historia del booteo es quizás algo másviolenta, pero el preámbulo que anoté es la forma de entender en términos simples cómo comienza el booteo del kernel.
El primer bloque de código del kernel de Linux (y afortunadamente el único) está escrito en ensamblador 80x86. Todo comienza con el archivoboot/bootsect.S, que al correr, se coloca a si mismo en la dirección absoluta 0x90000, carga los siguientes 2 Kbytes de código del dispositivo dearrance a la dirección 0x90200, y el resto del kernel a la dirección 0x10000. El mensaje “Loading…” aparece durante la carga del sistema.
Posteriormente se pasa el control a boot/Setup.S, quien también es un archivo 100% en ensamblador. Esta parte del Setup.S identifica algunas características del sistemahuésped y el tipo de placa de gráficos, así que cuando compilan los driversde VESA y esas chunches en el kernel, generalmente es este señor el que los llama, no tan directamente, pero sí tiene que ver, porque esos drivers tienen que ver directamente con este programita (vean la documentación del kernelsobre framebuffer y similares). Este pedazo de código es también el que pregunta por cuál modo de video escoger cuando se le paso información alkernel sobre un modo de video equivocado o bien el parámetro “vga=ask”.
Luego de eso, el sistema completo se mueve de la dirección 0x10000, entra en modo protegido y brinca al resto del sistema (fork, fork, setsid para los que hayan programado daemons)
Ahora sí, a descomprimir el kernel. El código en 0x1000 viene de zBoot/head.S que inicializa los registros e invoca a decompress_kernel(),quien, en términos dados, esta formado por zBoot/inflate.c, zBoot/unzip.c yzBoot/misc.c., y todos esos archivos que fueron usados dependiendo del tipo de compresión del kernel. Ahora, la mayor parte de las distros lo traen compremido con block zip2, así que unzip.c puede ser bunzip2.c. Los datosdescomprimidos van a la direccion 0x100000 (1MB) y esta es la razón principal porque en las versiones 1.x y 2.x el kernel tiene que arrancar enal menos 2 megas de RAM. El directorio con estos archivos, según el último “estandar” fue en arch/i386/boot si mal no recuerdo 8-).
Ahora sí, ya tenemos todo listo para ejecución: el código descomprimido es ejecutado desde 0x1010000, donde todas las chunches de 32 bits soncompletadas: se carga el IDT, GDT y LDT, se identifica el tipo de procesador, el paginado de memoria y eventualmente se llama a start_kernel()todo esto esta en boot/head.S Uf!!! Gracias a Dios casi todo el kernel esta en C y no en ensamblador.Bueno, start_kernel() esta en init/main.c y de allí en adelante todo es puro C, claro dejando de lado el manejo de interrupciones y llamadas alsistema. Por supuesto, para optimizar las macros están embebidas en ensamblador también.
Despues de ver todo el relajo que fue el booteo inicial en ensamblador, comenzamos con start_kernel(). start_kernel() inicializa todas las partes del kernel, específicamente:
- Establece los linderos de la memoria y llama a paging_init()
- Inicializa los canales de irq, traps, y el scheduling.
- Hace el análisis gramático (parsing) de la linea de comandos que se le paso al kernel.
- Si se le pide, aloja un buffer para profiling.
- Inicializa todos los manejadores de dispositivos ( ¡no se porque MS les dice controladores, si los controladores sólo son fierros y los manejadores son el software! ) y el buffer para los dispositivos de almacenamiento, así como otras chunches menores.
- Calibra el bucle de demora (el famosísimo “BogoMips” que aparece en los mensajes).
- Verifica que la interrupción 16 funcione con el coprocesador, si es que lamáquina tiene uno (recordemos que los primeros 386 no tenían coprocesador matemático)
¡Uf! Finalmente, el kernel esta listo para moverse al modo usuario y cargar los scripts de inicio, así que ejecuta move_to_user_mode() para poder darle un fork al proceso init, cuyo código esta en el mismo archivo del códigofuente. En este momento, el proceso 0, también llamado idle task, se mantiene corriendo en un bucle idle infinito.
El proceso init busca en este momento la forma de ejecutar a /etc/init o /bin/init o /sbin/init. Si ninguno de los tres se encuentra, el código seencarga de ejecutar “/bin/sh /etc/rc” y hace un fork en un shell en la primera terminal. Este código permanece aún en la versión 2.5.x y existedesde Linux 0.01 cuando el SO fue hecho por únicamente el kernel y no habia proceso de login disponible.
Despues de hacer un exec() al programa init desde un lugar estándar, el kernel pierde control directo sobre el flujo del programa. Su rol principal, desde ahora, es proveer procesos con llamadas al sistema, asi como dar servicios para eventos asíncronos (como interrupciones de hardware). El entorno multitarea ha sido iniciado, y es ahora init quien maneja el acceso multiusuario haciendo fork() en los demonios y en los procesos de login.
Como ve el kernel los procesos.
Desde el punto de vista del kernel, un proceso es una entrada en la tabla de procesos. Nada más ni nada menos. La tabla de procesos, entonces, es una de las estructuras mas importantesdentro del sistema, junto con las tablas de administración de memoria y el caché para el buffer. En la tabla de procesos, cada individuo es una estructura task_struct, cuyo código pueden encontrar en include/linux/sched.h (por eso cuando andaba por allí del 2.5.4, el kernel tenía tantas broncas, ya que no eran tan compatibles las estucturas y bueno, cuando yo instalé esa cosa en mi vieja 486 tuve que corregir varias cosasallí). Dentro de la task_struct se encuentra tanto información de bajo como de alto nivel, yendo desde la copia de algunos registros de hardware hasta el i-nodo del directorio de trabajo del proceso.
La tabla de procesos es tanto un arreglo como una lista doblemente ligada como un árbol. La implementación fisica es un arreglo estático de punteros, cuya longitud es NR_TASKS, una constante definida en include/linux/tasks.h, y cada estructura reside en una página de memoria reservada. La lista de la estructura esta manejada a través de los punteros next_task y prev_task,mientras que el árbol es bastante complejo y por ende no lo describiré aquí.
Cuando termina el arranque, el kernel siemrpe esta trabajando en pro de uno de los procesos, y la variable global current, un puntero a un elemento de tipo task_struct, es usado para registrarlo. current sólo es cambiado por elscheduler, en kernel/sched.c. Mientras tanto, todos los procesos que deben ser vistos dependen de la macro for_each_task. Es considerablemente más rapida que un rastreo secuencia de el arreglo cuando el sistema esta muycargado. Un proceso siempre está corriendo en “modo usuario” o en “modo kernel”. El cuerpo principal de un programa de usuario es ejecutado en modo usario y lasllamadas al sistema se hacen en modo kernel.
La pila usada por el proceso en dos dos modos de ejecución es diferente – un segmento convencional de pila es usado para modo usuario, mientras un segmento de tamaño fijo (una página,perteneciente al proceso) es usado en modo kernel.
La página de pila del kernel nunca es intercambiada con el swap, porque SIEMPRE debe estar disponible para una llamada al sistema.Las llamadas al sistema, dentro del kernel, existen como funciones en C, su ‘nombre oficial’ comienza con ‘sys_’. Una llamada a sistema, por ejemplo, con el nombre burnout, invocaría la función del kernel sys_burnout().
Un sistema Unix crea un proceso a través del forkeo de una llamada al sistema, y la terminacion esta dada ya sea por exit() o por recibir unaseñal. La implementación de Linux para esto se encuentra en kernel/fork.c y en kernel/exit.c
El forkeo es sencillo y fork.c es muy fácil de entender. Su principal tarea es llenar la estructura de datos para un nuevo proceso. Los pasos relevantes, aparte llenar los campos, son:
- obtener una página libre para poder tener un task_struct allí.
- encontrar una ranura de proceso libre (find_empty_process()
- obtener otra pagina libre para el kernel_stack_page
- copiar el LDT padre al hijo
- duplicar la informacion mmap al padresys_fork() tambien maneja descriptores de archivo e i-nodos.
Salir de un proceso requiere unos “pases mágicos”, porque el proceso padre debe ser notificado del proceso hijo que sale. Más aún, un proceso puede salir siendo matado por otro proceso (esto es común en todos los Unix). El archivo exit.c es la casa de sys_kill() y los sabores existentes de sys_wait(), en adición con sys_exit().
El código perteneciente a exit.c no será discutido aquí, basicamente suobjetivo es lidiar con muchos detalles para poder mantener el sistema en estado consistente. El estandar POSIX, es muy demandante sobre señales ydebe lidiar con ello.
Ejecutando programas.
Después de forkear, dos copias del mismo programa están corriendo. Una de ellas usualmente hace un exec() a otro programa. La llamada al sistema exec() debe localizar la imagen binaria del archivo ejecutable, cargarla ycorrerla. La palabra “cargarla” no es necesariamente “copiar en memoria laimagen binaria”, porque Linux soporta carga sobre demanda, para más información, revisar las funciones dlopen() y similares :)
La implementación de Linux del exec() soporta diferentes formatos de binarios. Esto se da a través de la estructura linux_binfmt, que embebe dos punteros a funciones (una para cargar el ejecutable y la otra para cargar la librería), cada formato binario representa tanto al ejecutable como a la librería. La carga de librerías compartidas (shared objects) estaimplementada en el mismo archivo fuente donde este exec(), pero hablemos de exec() exclusivamente.
Los sistemas Unix proveen al programador seis sabores de la función exec(). Todos excepto uno pueden ser implementeados como funciones de librería, y además el kernel de Linux implementa sys_execve() sola. Se encarga de una tarea sencilla: cargar el encabezado del ejecutable, y tratar de ejecutarlo.
Si los primeros dos bytes son “#!”, entonces la primera línea es analizada gramáticamente y es invocado un intérprete, en caso contrario, se intentan secuencialmente los distintos formatos binarios.
El formato nativo de linux esta soportado directamente dentro de fs/exec.c y las funciones relevantes son load_elf_binary y load_elf_librart. Mientras para los binarios, la función que este cargando un ejecutable termina ya sea en un mmap() a la parte del disco donde reside el archivo o llamando read_exec().