lunes, 11 de junio de 2007

init e Introducción a los Niveles de Ejecución en Linux

Orestes Leal R. [orestesleal13022@cha.jovenclub.cu]

Análisis del Stage ejecutado cuando el Núcleo "finaliza" ejecutando el proceso init::

El proceso de arranque del núcleo Linux tiene básicamente la misma distribución jerárquica en cuanto al proceso que se ejecuta después del arranque e inicialización del núcleo que los UNIX convencionales, lo que será descrito a continuación es una descripción de los entresijos que ocurren durante esa etapa de inicialización.

Desde su primera versión, y al ser un OS tipo UNIX, cuando el kernel "termina" su ejecución (realmente no lo hace, sólo cede el control a un proceso padre init), aunque continuará ejecutándose, planificando procesos, etc. Hace una búsqueda de un programa llamado init. Más adelante veremos qué es y para qué sirve, pero con el fin de ir dando un seguimiento correcto, mostremos a continuación lo que ejecuta el kernel para encontrar el ejecutable init:

// Definiciones en C para buscar / encontrar init
762 if (execute_command) {
763 run_init_process(execute_command);
764 printk(KERN_WARNING "Failed to execute %s. Attempting "
765 "defaults...\n", execute_command);
766 }
767 run_init_process("/sbin/init");
768 run_init_process("/etc/init");
769 run_init_process("/bin/init");
770 run_init_process("/bin/sh");
771
772 panic("No init found. Try passing init= option to kernel.");
773 }

Donde run_init_process se encuentra definida de esta manera en el mismo fichero:

727 static void run_init_process(char *init_filename)
728 {
729 argv_init[0] = init_filename;
730 kernel_execve(init_filename, argv_init, envp_init);
731 }

Donde argv_init se encuentra definida en el mismo fichero y donde se define que el argumento debe ser init:

184 static char * argv_init[MAX_INIT_ARGS+2] = { "init", NULL, };

Estos segmentos de código están definidos en $KERNELSOURCE/init/main.c ($KERNELSOURCE es el árbol de las fuentes). Las líneas a analizar en cuestión son desde 767 -> 770, estando todo esto basado en el núcleo 2.6.21.1. Esas definiciones en C quieren decir, básicamente, que en este momento el núcleo está haciendo una búsqueda de los posibles lugares donde el proceso init se aloja haciendo llamadas a varias funciones. Si la búsqueda es efectiva, ejecuta el proceso con permisos de super usuario (UID=0); de lo contrario, si el kernel no puede encontrar a init, el mensaje posterior en la línea 772 es bastante claro en ese sentido. A diferencia de algunas variantes de UNIX o BSD UNIX, en Linux, init se encuentra siempre en /sbin/init, definido así por el LFH, un estándar de la distribución de ejecutables, directorios, etc., en un sistema Linux.

Stage Post INIT::

Una vez que init es encontrado, Linux le cede el control de inicialización de procesos -los cuales serán descritos más adelante. Se puede visualizar el momento en el que init ha sido iniciado por el kernel, ya que cuando ésto ocurre se muestra un mensaje tipo "Startting Init 2.86", por poner un ejemplo. Nótese que el comando dmesg no muestra los mensajes pre-init, sino los posteriores.

A partir de ese momento, init toma control de la inicialización de TODOS los procesos post-kernel; es por eso que init es considerado el padre de todos los procesos. Podemos caer en cuenta de esto ejecutando el comando pstree y observar desde dónde viene cada proceso desde el árbol jerárquico superior. Un fragmento de salida podría ser el siguiente:

init-+-abiword
|-4*[agetty]
|-bash---startx---xinit-+-X
|       `-fluxbox-+-gkrellm
|             `-konsole-+-bash---mplayer
|                   |-bash---vim
|                   |-bash
|                   |-bash---man---less
|                   `-bash---pstree

Claramente se puede observar desde dónde provienen cada uno de los procesos, y los procesos ejecutados a través de otros procesos; por ejemplo en este caso la siguiente línea:

|-bash---startx---xinit-+-X

Quiere decir que desde el shell actual (bash) se ha ejecutado el proceso startx que a su vez inicia a xinit y finaliza ejecutando el proceso X -que actualmente se encuentra en ejecución-, y donde bash es un proceso directo de init, como se pudo observar.

Habiendo visto esto vamos a entrar en detalles del por qué y cómo init funciona, dando paso a todos estos procesos. Init crea los procesos leyendo un fichero de configuración que se puede decir que es global, ya que siempre se encuentra en la misma ruta. El fichero /etc/inittab es leído e interpretado por init para la inicialización de los procesos en los denominados run-levels o niveles de ejecución. En inittab, entre otras cosas, se definen varias entradas que causan que init expanda gettys, getty y agetty (comúnmente usados en Linux para abrir un puerto tty, pidiendo login e invoca al ejecutable /bin/login en cada línea para que los usuarios puedan loguearse).

Runlevels o Niveles de Ejecución::

Un runlevel es una configuración del sistema que permite sólo a un grupo de procesos existir. Los procesos que han sido expandidos por init para cada runlevel son definidos en /etc/inittab. Init puede estar en uno de 8 runlevels: 0 al 6 y S, s. El runlevel puede ser cambiado corriendo como usuario privilegiado y corriendo el comando telinit, el cual envía señales apropiadas a init, indicándole a qué runlevel cambiar.

Los runlevels 0, 1 y 6 están reservados. El nivel de ejecución 0 es usado para detener el sistema, el 6 para reiniciarlo, y el 1 para el modo monousuario. El runlevel S no debe ser usado directamente, sólo para los scripts que son ejecutados cuando se está accediendo al runlevel 1.

Despues de que init es ejecutado como el último paso de la secuencia de arranque del kernel, se realiza una búsqueda del inittab en /etc para ver si encuentra una línea del tipo initdefault, la cual establece cuál es el runlevel por defecto o inicial del sistema. Si no existe la entrada o no se encuentra el inittab, un runlevel debe ser establecido en la consola del sistema.

En algunas líneas del inittab debemos tener en cuenta la siguiente sintaxis:

id:runlevels:accion:proceso

Por ejemplo:

1:2345:respawn:/sbin/agetty tty1 9600

En resumen, el ID aquí es 1, el 2345 quiere decir que se inicializará en cualquiera de esos runlevels (recordar que init siempre inicia por defecto en algún runlevel); la palabra respawn quiere decir que el proceso será reiniciado en el momento que sea terminado; /sbin/agetty, en este caso, es el proceso a ejecutar donde se le pasan 2 parámetros requeridos, el ttyX, donde X = número de tty, y la velocidad (en baudios).

Conociendo ya básicamente cómo funciona init, damos paso a la descripción del fichero inittab. Un fichero de ejemplo de este file es el siguiente:

id:3:initdefault:

si::sysinit:/etc/rc.d/init.d/rc sysinit
l0:0:wait:/etc/rc.d/init.d/rc 0
l1:S1:wait:/etc/rc.d/init.d/rc 1
l2:2:wait:/etc/rc.d/init.d/rc 2
l3:3:wait:/etc/rc.d/init.d/rc 3
l4:4:wait:/etc/rc.d/init.d/rc 4
l5:5:wait:/etc/rc.d/init.d/rc 5
l6:6:wait:/etc/rc.d/init.d/rc 6

ca:12345:ctrlaltdel:/sbin/shutdown -t1 -a -r now

1:2345:respawn:/sbin/agetty tty1 9600
2:2345:respawn:/sbin/agetty tty2 9600
3:2345:respawn:/sbin/agetty tty3 9600
4:2345:respawn:/sbin/agetty tty4 9600
5:2345:respawn:/sbin/agetty tty5 9600
6:2345:respawn:/sbin/agetty tty6 9600
7:2345:respawn:/sbin/agetty tty7 9600
8:2345:respawn:/sbin/agetty tty8 9600
9:2345:respawn:/sbin/agetty tty9 9600

id:3:initdefault:

Como se mencionaba anteriormente, la entrada initdefault establece el runlevel por defecto que init va a leer posteriormente en el inicio del sistema o con el comando telinit RUNLEVEL, donde RUNLEVEL es un número de 0 a 6 (ej: telinit 3). En este caso, si estuviésemos en las X pasaríamos automáticamente al modo en línea de comandos, ya que normalmente las X se ejecutan en el runlevel 5.

si::sysinit:/etc/rc.d/init.d/rc sysinit

Esta línea es de suma importancia (y varía de una distribución a otra con toda seguridad), es parte del mecanismo que se encarga de inicializar los scripts de arranque (S) y de apagado (K). Por ejemplo, unas líneas del script shell podrán ser las siguientes:

for i in $( ls -v ${rc_base}/rc${runlevel}.d/S* 2> /dev/null)
do
  if [ "${previous}" != "N" ]; then
    suffix=${i#$rc_base/rc$runlevel.d/S[0-9][0-9]}
    stop=$rc_base/rc$runlevel.d/K[0-9][0-9]$suffix
    prev_start=$rc_base/rc$previous.d/S[0-9][0-9]$suffix
    [ -f ${prev_start} ] && [ ! -f ${stop} ] && continue
    fi

Entre otras cosas, dichas líneas se encargan de inicializar lo antes mencionado, scripts de inicio y apagado en sus coincidencias S[números de 0 a 9] o K[lo mismo].

Un detalle a tener en cuenta es el parámetro del final, sysinit, que causara que se ejecute antes que cualquiera de las definiciones boot o bootwait durante el arranque del sistema.

Estas definiciones albergan el mismo significado, sólo que wait quiere decir que el proceso será iniciado cuando el runlevel especificado sea iniciado e init esperara por su finalización.

l0:0:wait:/etc/rc.d/init.d/rc 0
l1:S1:wait:/etc/rc.d/init.d/rc 1
l2:2:wait:/etc/rc.d/init.d/rc 2
l3:3:wait:/etc/rc.d/init.d/rc 3
l4:4:wait:/etc/rc.d/init.d/rc 4
l5:5:wait:/etc/rc.d/init.d/rc 5
l6:6:wait:/etc/rc.d/init.d/rc 6

La siguiente definición quiere decir que se hará cuando se presione la combinación mas conocida del planeta: CTRL + ALT + DEL. Cabe decir que los runlevels 0 y 6 no se incluyen, como es obvio, pues ya estos runlevels de por sí son apagado y reinicio, respectivamente.

ca:12345:ctrlaltdel:/sbin/shutdown -t1 -a -r now

Estas definiciones ya han sido explicadas, por lo cual no debe haber duda.

1:2345:respawn:/sbin/agetty tty1 9600
2:2345:respawn:/sbin/agetty tty2 9600
3:2345:respawn:/sbin/agetty tty3 9600
4:2345:respawn:/sbin/agetty tty4 9600
5:2345:respawn:/sbin/agetty tty5 9600
6:2345:respawn:/sbin/agetty tty6 9600
7:2345:respawn:/sbin/agetty tty7 9600
8:2345:respawn:/sbin/agetty tty8 9600
9:2345:respawn:/sbin/agetty tty9 9600

Normalmente, los inittab convencionales son algo elaborados, aunque un inittab podría ser lo siguiente y funcionar adecuadamente:

id:1:initdefault:
rc::bootwait:/etc/rc
1:1:respawn:/etc/getty 9600 tty1
2:1:respawn:/etc/getty 9600 tty2
3:1:respawn:/etc/getty 9600 tty3
4:1:respawn:/etc/getty 9600 tty4

Donde bootwait define que será ejecutado al arranque del sistema, mientras que init espera a que termine. El campo de los runlevels será ignorado.

En la parte final de toda esta ejecución, init lee las entradas de aggety o getty y es cuando se nos presenta el prompt de login que todos conocemos (en línea de comandos) algo así como:

========================================
Webdeveloper2 Linux2.6.21.1 tty1 i686
login:
========================================

Funcionamiento y distribución de los scripts de arranque::

Con las bases de todo lo mencionado en el tema anterior veamos cómo, por qué y cuándo serán ejecutados scripts de arranque más comúnmente llamados shells init o scripts de runlevels.

Para comenzar, hay que mencionar el directorio que contiene "casi todos" los scripts de inicio, los cuales serán iniciados en el runlevel al que pertenezcan. Es el dir /etc/init.d, comúnmente usado en distribuciones como Debian, no siendo así en Red Hat, que usa el estilo BSD (muy parecido al estilo del Debian, pero con otra ubicación en los ficheros shell y alguna que otra diferencia, aunque en el fondo se logra básicamente el mismo objetivo).

Se mencionaba "casi todos", pues hay scripts que son "fijos". Esos shells son los que se encargan de montar el sistema de ficheros, chequearlo, iniciar Udev, Limpiar el FS, establecer la consola de Linux, activar la swap, entre otras. Por ejemplo, en una distribución GNU/Linux con estilo de arranque System V al estilo BSD, el directorio /etc/init.d no existe propiamente en ese lugar, sino que se encuentra en /etc/rc.d/init, por lo que los scripts fijos de arranque se encuentran normalmente en el directorio /etc/rc.d/init.d/rcsysinit y los leídos por init antes que ningún otro. Es necesario explicar esto para que se comprenda en caso de confusión al tener contacto con alguna de las dos variantes.

Volviendo a los scripts de runlevel normales, éstos se alojan en /etc/rc.d/init.d o en /etc/init.d, habiendo enlaces simbólicos desde cada directorio, que es identificado por el número del runlevel. La estructura de los dir es la siguiente: /etc/rc.d/rcX.d, donde X es un número de 0 a 6. En el estilo que usa Debian es igual, sólo que se encuentran en /etc/rcX.d. Quedando así más o menos la siguiente estructura en los directorios /etc/ o /etc/rc.d:

- rc0.d
- rc1.d
- rc2.d
- rc3.d
- rc4.d
- rc5.d
- rc6.d

Como se había mencionado antes, los scripts se encuentran en init.d. Ejemplificado, vamos a ver un script real alojando en init.d:

/etc/rc.d/init.d/bind

Donde bind es un shell script de runlevel.

De por sí, bind alojado en init.d no será ejecutado por defecto, sino que tiene que ser enlazado a algún runlevel existente (rc0.d, rc1.d, etc.). Para enlazarlo es necesario conocer qué prioridades y acción tienen los enlaces simbólicos. Digamos que vamos a enlazar a bind al runlevel 3; para esto hacemos:

// ejemplo de enlazar un script a un runlevel para que sea iniciado en el arranque del sistema
cd /etc/rc.d/rc3.d
ln -sv ../init.d/bind S92bind

Esto quiere decir que estamos creando el enlace simbólico S92bind, que apunta al script shell ../init.d/bind.

La norma o sintaxis es muy fácil de comprender: los enlaces que comienzen con S (de start) serán iniciados dependiendo del número que prosiga a la letra, donde los números más bajos tienen mayor prioridad en la ejecución. Todo esto quiere decir que cuando init entre en el runlevel 3 (en el arranque del sistema o poco después) y ejecute todos los enlaces que comienzen con S, llegará el punto en que ejecutará el script del ejemplo anterior.

Como es lógico, los servicios que son iniciados también deben ser detenidos de manera automática cuando el sistema se vaya a reiniciar o apagar. Para ello, en este ejemplo vamos a enlazar a bind en los runlevels 0 y 6 para que se detenga en un reinicio o apagado:

// ejemplo de enlazar un script a un runlevel para que sea detenido
cd /etc/rc.d/rc6.d
ln -sv ../init.d/bind K12bind
cd /etc/rc.d/rc0.d
ln -sv ../init.d/bind K12bind

Aquí la norma es la misma, aunque con una diferencia: los que comiencen con K (de kill) serán detenidos de igual manera que lo antes explicado (la prioridad la tiene el número menor). Habiendo hecho esto aseguramos que si reiniciamos o apagamos se detenga el servicio que se había iniciado en el arranque del sistema.

A continuación un ejemplo del script bind. Algunos detalles sobre qué sucede cuando es S y K y qué tiene que ver con el script.

#!/bin/sh
# script bind.
# dns Bind9 {named}

case "$1" in
  start)
    echo "Iniciando Servidor DNS..."
    /usr/sbin/named -u named -t /srv/named -c /etc/named.conf
    ;;
  stop)
    echo "Deteniendo el Servidor DNS..."
    killall /usr/sbin/named
    ;;
  restart)
    $0 stop
    sleep 1
    $0 start
    ;;
  reload)
    echo "Recargando el servidor DNS..."
    /usr/sbin/rndc -c /etc/rndc.conf reload
    ;;
  *)
    echo "Uso: $0 {start|stop|restart|reload}"
    exit 1
    ;;
esac

Esta es la parte interesante. Cuando un enlace comienza con K, init sabe que tiene que matar ese proceso, por lo cual envía al script bind un parámetro de la siguiente manera:

/etc/rc.d/init.d/bind stop

Por el contrario, si comenzara con S sería así:

/etc/rc.d/init.d/bind start

Esto da la medida de cómo funcionan al nivel más básico los scripts de runlevels sin ser el presente documento un manual de programación shell.

La línea case "$1 in" es propia del intérprete de comandos (bash, sh, tcsh, ksh, etc). Lo que quiere decir es "en caso de darse el primer parámetro y que sea igual a los admitidos".

Los parámetros admitidos en este script son start, stop, restart, reload, (los parámetros start y stop son obligatorios). Si deseamos tener un script que sea un auténtico shell de runlevel, el parámetro start es enviado al script al arranque del sistema, así como el stop al apagado o reinicio. Los otros dos son muy usados para recargar la configuración del servicio y reiniciar el servicio, lo cual no se aplica en el inicio, apagado o reinicio por cuestiones lógicas.

Para saber más...



Artículos relacionados


No hay comentarios: