sábado, 3 de octubre de 2009

Kernel boot process para novatos

El usuario de un PC pulsa el boton de alimentación del sistema (power) y espera su arranque hasta que se muestre en pantalla el interfaz de usuario gráfico donde el sistema solicita usuario/password para iniciar la sesion. Este post profundiza en ese proceso de arranque, con la intención de que cualquier lector con los conocimientos suficientes, entienda mejor todos los pasos involucrados, sin entrar en detalles que dificulten su comprensión.

En resumen, desde el power-on hasta el login del usuario, se pasa por 4 etapas:
1.- Programa en ROM carga el boot-loader
2.- El boot-loader carga el kernel
3.- El kernel inicializa el sistema y pasa el control al user-space
4.- El user-space finaliza la inicializacion.


Etapa 1. La BIOS

Tras el power-on, un pequeño programa cargado en ROM toma el control del sistema (la BIOS en x86).

Este pequeño programa localiza el "boot device" con el que arrancar el sistema, y para ello elige el primer dispositivo bootable disponible en la secuencia "boot order" (por ejemplo, primero con CD/DVD, despues con USB y despues con HD). Una vez conocido el dispositivo bootable, lee su MBR (sector 0). Este MBR almacena 3 cosas: la rutina bootstrap loader (en .asm), la tabla de particiones y la firma de dispositivo bootable (0x55AAh) como dos ultimos bytes de la partición. Por último, el pequeño programa cargado en ROM pasa el control de ejecución a la rutina bootstrap loader, hecha única y exclusivamente para pasar el control al boot-loader.


Etapa 2. El boot-loader

Los ejemplos mas comunes de boot-loader son grub y lilo (x86, amd) y u-boot (arm). Lo típico es que el boot-loader presente al usuario un menu con las distintas opciones de arranque (diferentes sistemas operativos, distintos kernels del mismo sistema operativo, o programas que no necesiten de sistema operativo como memtest86+). Normalmente tienen una opcion seleccionada por defecto mas un timeout. El usuario elige una de las opciones o el timeout elige la opcion por defecto.

El boot-loader carga el kernel seleccionado en RAM, y opcionalmente tambien la imagen initramfs. La pregunta es, ¿y como accede el boot-loader a la imagen del kernel guardada en el filesystem de una de las particiones...? ¡Sin soporte del kernel!. Cada boot-loader resuelve este problema de forma diferente. El lilo por ejemplo necesita conocer la localizacion exacta del sector del disco en el que esta la imagen del kernel. Grub, en cambio, entiende muchos sistemas de fichero (ffs, fat16, fat32, minic, ext2, reiserfs, jfs, xfs y vst), y puede leer ficheros en dichas particiones sin ayuda de nadie.

Finalmente, el boot-loader pasa el control de ejecución al kernel cargado en RAM, pasandole tambien su lista de argumentos argv (como se hace con cualquier otro programa escrito en C).

Arranque directo vs arranque encadenado

El grub boot-loader permite dos tipos de arranque: directo y encadenado. El arranque directo permite arrancar directamente un sistema operativo (Linux). En arranque encadenado (chain-loading), permite arrancar otro boot-loader distinto (el que usa Windows).

Para arrancar un sistema operativo, el grub necesita conocer el dispositivo de almacenamiento y la partición de ese dispositivo en el que se encuentra almacenado su kernel. Además, en modo directo tambien necesita el path al binario del kernel al que transfiere el control. Mientras que en modo encadenado, transfiere el control al PBR (Partition Boot Record) de la partición indicada; se trata de un bloque similar al del MBR pero almacenado en la partición. Esta dualidad posibilita a grub el arranque del cualquier sistema operativo, sea libre o propietario.

El fichero de configuración del grub

El fichero de configuracion del grub se llama menu.lst y generalmente se encuentra en el directorio /boot/grub/. Cuando grub arranca, lee su contenido y presenta al usuario un menu con las opciones de arranque disponibles en su interior.

Algunas etiquetas para definir parametros generales de su configuración:
- default - Establece la entrada (title) seleccionada por defecto
- timeout - Tiempo en segundos para arrancar la configuracion default
- splashimage - Imagen de fondo (14 colores, 640x480, .xpm.gz)

Por ejemplo:
default 1
timeout 3
splashimage=(hd0,0)/boot/grub/splash.xpm.gz

Por cada sistema operativo que grub puede arrancar, se define en menu.lst un grupo de etiquetas que especifican toda la información que grub necesita para poder arrancarlo. Cada grupo de etiquetas se inicia con la palabra clave "title", que especifica el texto mostrado para el sistema operativo arrancado, y a continuación una serie de etiquetas que dependen del modo de arranque (directo o encadenado). Veamos dos ejemplos para entenderlo mejor:

Ejemplo tipico Linux (carga directa)
title       Ubuntu, kernel 2.6.22.5-custom
root (hd0,0)
kernel /boot/vmlinuz-2.6.22.5-custom root=/dev/hda1 ro quiet splash
initrd /boot/initrd.img-2.6.22.5-custom
boot

Ejemplo tipico Windows (carga encadenada)
title       Windows Vista
root (hd0,1)
chainloader +1

Las etiquetas tipicas de estas entradas son:
- title - Texto mostrado para el sistema operativo arrancado
- root - Dispositivo de almacenamiento y particion donde esta disponible el kernel.
- kernel - (Carga directa) Path en root a la imagen del kernel arrancado
- initrd - (Carga directa) Path en root al fichero initramfs
- boot - (Carga directa) Arranca el sistema operativo seleccionado
- chainloader - (Carga encadenada) Pasa control al sector del PBR indicado

La etiqueta "root" confunde a mucha gente (y no es para menos). La sintaxis es propia de grub, no la confundais con la sintaxis de los ficheros dispositivo situados en /dev para los dispositivos de almacenamiento, que diferencia los dispositivos de almacenamiento IDE (hda) de los SCSI (sda). En cambio, grub usa siempre el prefijo hd. Por tanto, (hd0,0) significa primer bootable-device (0) de la primera partición (0), mientras que (hd1,3) significa segundo bootable-device (1) de la cuarta partición (3). Tampoco olvideis que el bootable-device depende del boot-order asignado en la BIOS.


Etapa 3. El kernel de Linux

El kernel toma el control del sistema: se descomprime a si mismo, inicializa el hardware (CPU, cache y configuración específica de cada board), los distintos subsistemas del kernel (el scheduler, el gestor de memoria, y los sistemas de ficheros virtuales /proc y /sys, sin llegar a montarlos). A continuación lee su lista de argumentos argv. Algunos parámetros tipicos de argv son estos:

- root=/dev/sda1 - Fichero dispositivo de la particion raiz ("/")
- rootfstype=ext2 - Sistema de ficheros de la particion raiz
- init=/sbin/init - Primer programa del user-space ejecutado
- Muchos mas en Documentation/kernel-parameters.txt

A continuación inicializa la consola (/dev/console) para poder mostrar los logs del arranque por pantalla. Y por fin monta su partición raiz (root). Este es el paso mas importante para los usuarios, y se explica despues con mayor detalle.

El kernel intenta ejecutar el primer programa del user-space (init). Para ello, crea un kernel-thread que se reemplaza a si mismo (syscall execve) con la imagen del binario del userspace apuntado por init (normalmente apuntando a /sbin/init). Si el programa indicado por init no se encuentra en la particion raiz, el kernel muestra un panic error indicando el error. Si lo encuentra, pasa el control a dicho programa.

Recordemos que este binario reside en la particion raiz (root). Se trata del primer programa del user-space ejecutado, y el primer programa compilado con la libreria de C estandar (libc).

Montaje de la partición raiz

Para montar la partición raiz (root), el kernel necesita el device driver del dispositivo que aloja la partición raiz y el driver del sistema de ficheros de esa partición (ext2, ext3, etc.).

Si esos 2 drivers fueron compilados built-in (no como modulos), el kernel dispone de todo lo necesario para montar la partición raiz. Por tanto la monta y completa su trabajo.

Si los drivers fueron compilados como modulos (no built-in), el kernel necesita extraer esos modulos de algun sitio. Para ello se usa la imagen initramfs de la que ya hemos hecho algun comentario en este post (profundizaremos sobre ella despues). El kernel lee la imagen initramfs cargada en RAM por el bootloader, la descomprime y monta el sistema de ficheros almacenado en su interior. Continua ejecutando el script /init almacenado en el initramfs. Desde ese script podemos lanzar el código que mas nos interese:

- Una opcion es que /init monte la partición raiz indicada en (root). Para ello lee de initramfs los modulos necesarios para montar el dispositivo indicado en root, carga dichos modulos y reemplaza el montaje del initramfs por el de la partición raiz.
- Otra opcion, mas empleada en sistemas embedded con sistemas de ficheros muy pequeños, es que el initramfs se convierta en la partición raiz del kernel y el su interior se aloje la aplicación del user-space que va a correr el sistema embedded.

La imagen initramfs

Introducido en el kernel 2.6 para conseguir un proceso de inicialización del kernel mucho mas limpio, ya que parte del código de inicialización del kernel se mueve al user-space. Solo esta soportado en kernels >= 2.6.17 configurados adecuadamente.

Como ya hemos visto, puede usarse en sistemas embedded como sistema de ficheros root del kernel, y en sistemas mas grandes, como sistema de ficheros intermedio para montar la partición raiz.

Para construir una imagen initramfs para el boot-loader brug, se hace esto:
$ mkinitramfs -o /boot/initrd.img-2.6.22.5-custom 2.6.22.5-custom

Para crear una imagen initramfs custom (casi nadie hace esto):
$ find  -print -depth | cpio -ov | gzip  -c  > initramfs.img

Para extraer el contenido de una imagen initramfs una vez generada:
$ gzip -dc initramfs.img | cpio -id



Etapa 4. El user-space

No vamos a entrar en muchos detalles, ya que el arranque del user-space es dependiente de cada distribución de Linux. Para no extendernos demasiado, explicaremos aqui el modelo genérico de linux, asumiendo que init=/sbin/init. Por tanto, el primer programa del user-space que tomará el control será /sbin/init, quien leerá el script /etc/inittab y lo ejecuta en secuencia.

Cada linea del fichero /etc/inittab tiene este formato:
id:runlevels:action:process

- id - Secuencia de 4 caracteres. Nosotros lo vamos a ignorar
- runlevels - Niveles de ejecucion. Nosotros lo vamos a ignorar
- action - Accion llevada a cabo cuando se ejecute la entrada
--> sysinit: proceso ejecutado durante el boot del sistema
--> restart: proceso se reinicia cuando termina
--> shutdown: proceso ejecutado con el shutdown del sistema
- process - Especifica linea de comandos del proceso ejecutado

Un ejemplo de fichero /etc/inittab típico de cualquier sistema Linux:
# Script de inicializacion del arranque del sistema
::sysinit:/etc/init.d/rcS
# El proceso /sbin/init se reinicia cada vez que termina
::restart:/sbin/init
# Proceso ejecutado cuando hacemos un shutdown
::shutdown:/bin/umount -a -r

Como vemos, la inicialización del user-space la completa el script apuntado por sysinit en inittab (/etc/init.d/rcS). Desde este script se configuran los dispositivos de red (ifconfig), se montan los virtual filesystems (/proc, /sys), se montan las particiones de datos (recordemos que la partición root ya esta montada), se inicializan los distintos servicios del sistema (http, smtp, ...).

Finalmente se muestra en pantalla el interfaz de usuario gráfico donde el sistema solicita usuario/password para iniciar la sesion.

No hay comentarios:

Visitas:

Seguidores