Sei sulla pagina 1di 32

Tema 4

La inicialización del
sistema

Los programas residen en ficheros denominados ficheros ejecutables. El sistema operativo carga en
memoria el fichero ejecutable y le concede el procesador para convertirlo en un proceso. También el
sistema operativo reside en un fichero ejecutable, tradicionalmente denominado la imagen del
sistema. La pregunta que se plantea es ¿quién carga en memoria la imagen del sistema y le cede el
control del procesador? o, en otras palabras, ¿quién arranca el sistema? Lógicamente otro programa.
Pasaremos revista a toda una secuencia de programas que interviene desde que pulsamos el botón de
encendido de la máquina hasta que comienza a ejecutar MINIX. Una vez dentro de MINIX,
estudiaremos el código que conduce al momento en que aparece en la pantalla “login:”. A todo este
proceso lo hemos denominado la inicialización del sistema. A ella se dedica este capítulo.

4.1 La BIOS y la secuencia de arranque


La BIOS es un código denominado por IBM "sistema de entrada-salida básico". La BIOS es un
pequeño sistema operativo, una colección de rutinas y datos que el fabricante del PC proporciona como
memoria ROM. Ejecuta en modo real y contiene los manejadores de los dispositivos que componen el
PC tal y como sale de fábrica, como el teclado, el puerto USB o el disco duro. Como su nombre indica,
su objetivo es liberar al programador de la gestión de los dispositivos del sistema. Fue creado para un
procesador Intel 8086 y mantenido tal cual por razón de compatibilidad del software de aplicación,
especialmente el basado en el sistema operativo DOS.
La historia de la BIOS comienza a finales de los años setenta cuando Intel lanza dos nuevos tipos de
microprocesadores, el 8086 y el 8088. IBM decide construir un computador basado en el 8088,
probablemente basada en el éxito de Apple. Para ello, IBM contacta con una compañía joven y activa
de Seattle, Microsoft. Ambas acuerdan construir un sistema operativo para el nuevo Computador
Personal (PC) que consistiría en dos partes. Por un lado, el sistema de entrada-salida básico (BIOS),
añadido al hardware del sistema como memoria ROM. Por otro, el sistema operativo, disponible en
disquete. El sistema operativo estaría compuesto por una serie de programas que utilizarían los
servicios de la BIOS en ROM. Esta segunda parte recibió el nombre de Disk Operating System
(DOS).
El sistema se hizo realidad en agosto de 1981, cuando IBM lanzó al mercado su IBM PC. El éxito del
PC superó las expectativas más ambiciosas. No extraña que montones de compañías electrónicas de
todo el mundo tratasen de sacar partido de este éxito muy pronto. Enseguida se desarrolló una activa
industria alrededor del PC (controladoras de disco duro y flexible, monitores, software de sistema y

1
UEX. Diseño de Sistemas Operativos

aplicaciones, etc.) Por supuesto, algunas de estas compañías trataron de copiar el IBM PC con
impunidad y vender este "clónico" como producto propio.
No era difícil producirlo. Todos los componentes de hardware estaban disponibles en el mercado. El
único problema era la BIOS, de la que IBM tenía los derechos de autor y no licenciaba el producto.
Algunas compañías copiaron la BIOS, pero fueron condenadas con fuertes sanciones. Así, los
fabricantes de clónicos se vieron obligados a crear sus propias BIOS. La primera compañía en
conseguirlo fue Compaq Computer. Muchas todavía lo hacen, pero la mayoría tienden a utilizar el
código proporcionado por compañías terceras, entre las más importantes Phoenix, Award, Quadtel y
American Megatrends.
¿Cómo se organiza la BIOS? Está construida con tecnología EPROM. Ocupa generalmente el último
segmento de 64Kb del primer Mbyte de la memoria. La tecnología EPROM es un tipo de memoria
en la que no se puede escribir. Actualmente, la BIOS se implementa sobre memoria EEPROM,
también denominada flash. La tecnología EEPROM permite actualizar la BIOS. Además del espacio
en ROM, la BIOS toma un espacio de datos de 256 octetos de memora RAM baja previa a la carga
de DOS que contiene información sobre estado de bloqueo de mayúsculas del teclado, el buffer del
teclado, etc. Desde el punto de vista del software, la BIOS se estructura en tres partes:
1. La utilidad setup. La utilidad Setup de la BIOS puede utilizarse para introducir y modificar
datos de configuración del sistema tales como las características de los discos duros y flexibles,
el tiempo, estados de espera de las memorias, velocidad del teclado, cadena de discos de
arranque, etc.
2. La BIOS del sistema. Contiene las llamadas Rutinas de Servicio de Interrupción, disponibles
durante la operación del equipo. Proporcionan a las aplicaciones o al sistema operativo servicios
básicos de gestión de dispositivos como leer un número de sectores de un disco, detectar el tipo
de UCP o comprobar si se ha pulsado una tecla.
3. La secuencia de arranque. Comprueba los componentes del sistema, inicializa estructuras de
datos y, finalmente, carga el sistema operativo.
Cada placa base dispone de su propia BIOS, cada una con sus características propias. No obstante, a
pesar de las diferencias, existe un patrón de arranque común. A continuación se muestra la secuencia
de arranque paso por paso:
1. El procesador Intel, tras recibir la señal de reset, comienza a ejecutar en modo real la instrucción
situada en la posición de memoria EPROM F000:FFF0 (absoluta 0xFFFF0), es decir, 16 octetos
previos al Mbyte. Esta instrucción es un salto al comienzo del programa de arranque de la BIOS.
2. La BIOS realiza las pruebas del sistema "power-on self test (POST)". Si hay errores, el proceso
de arranque se detiene y el altavoz emite sonidos de diagnóstico.
3. La BIOS busca el adaptador de vídeo. Normalmente, el adaptador de vídeo dispone de su propia
ROM BIOS, situada en la posición 0xC000. El arranque salta a esta posición para inicializar el
vídeo y luego retorna. En ocasiones esta BIOS muestra información en el monitor. Esta es la
razón por la que la información del vídeo se muestra con anterioridad a la pantalla de arranque de
la BIOS.
4. La BIOS busca y ejecuta otras ROM BIOS en los adaptadores de dispositivo. Normalmente los
discos duros IDE/ATA tienen su BIOS en la dirección 0xC800.
5. La BIOS muestra su familiar pantalla de arranque y, tras ello, realiza más comprobaciones en el
sistema. Entre ellos el examen de la memoria RAM disponible. Los contenidos de la pantalla de
arranque son los siguientes:
• El fabricante de la BIOS y el número de versión
• La fecha de la BIOS, que ayuda para determinar sus capacidades.
• La tecla de inicio del programa de configuración de la BIOS.
• El logo del fabricante de la BIOS y en algunas ocasiones del fabricante del PC o de la
placa base.
• El logo "Energy Star" si la BIOS soporta el estándar Energy Star.

2
La inicialización del sistema

• El número de serie de la BIOS. Localizado en la parte inferior de la pantalla. Ya que la


BIOS está fuertemente ligada a la placa base, este número de serie sirve para determinar
las características de la placa base y la BIOS.
6. La BIOS realiza un inventario de los dispositivos existentes, entre ellos el teclado y los puertos
serie y paralelo.
7. Si la BIOS soporta el estándar "Plug and Play (PnP)", detecta y asigna recursos a los dispositivos
PnP. Muestra un mensaje por cada uno que encuentra.
8. La BIOS muestra en forma de tabla un resumen de la configuración del sistema. Esta tabla indica
los problemas surgidos en el arranque, si los hubiese.
9. La BIOS comienza a buscar un dispositivo desde el que arrancar el sistema. Normalmente
comenzando por los disquetes, los discos duros y los CDs, dependiendo de cómo se ha
parametrizado la cadena de arranque en el programa de configuración de la BIOS (Setup).
Entonces carga en memoria el primer sector del dispositivo en la dirección 0000:7C00H. A
continuación, la BIOS comprueba que los últimos dos octetos del sector son 0xAA55. Si no lo
son, significa que el primer sector del disquete o disco duro no es un sector de arranque. La BIOS
emite un mensaje, como el familiar “No boot disk or disk error, Replace and stroke a key”,
requiriendo la intervención del operador. Si lo son, la BIOS salta a la posición 0000:7C00H para
ejecutar el código contenido en el sector de arranque.
Un dispositivo de bloques que contiene la imagen de un sistema operativo se denomina disco de
arranque. Clásicamente, el sistema operativo se dispone en el disco de arranque según muestra la Fig.
4.1. El primer bloque del disco se denomina el bloque de arranque. El primer sector del bloque se
denomina sector de arranque y contiene un pequeño programa escrito en ensamblador. Este programa
conoce la ubicación del sistema operativo o, en su caso, el monitor de arranque en el disco. Un
dispositivo de arranque se distingue de otro que no lo es mediante un octeto del sector de arranque, que
contiene un código mágico. El segundo sector del bloque de arranque no contiene nada o bien datos
auxiliares de dicho programa.

Fig. 4.1 Disco de arranque.

4.2 Particiones y cargadores


Un disco duro es un dispositivo de almacenamiento de gran capacidad. A veces conviene dividir el
disco en compartimentos lógicos pero estancos, cada uno de ellos con un propósito distinto, por
ejemplo para almacenar tipos de datos o establecer sistemas operativos diferentes. Cada uno de estos
compartimentos se denomina partición.
Una partición es un conjunto de cilindros contiguos. Es costumbre en los discos actuales hacer cuatro
particiones. Cada partición actúa como un disco separado, con un nombre de dispositivo distinto, por
ejemplo /dev/hd1, /dev/hd2, etc. Se hace necesaria, así, la denominada tabla de partición. La
tabla de partición contiene tantas entradas como particiones tenga el disco duro. Cada entrada de esta
tabla indica en qué sector comienza y acaba la partición. El fichero include/ibm/
partition.h contiene la definición de una entrada de la tabla de partición:
struct part_entry {
unsigned char bootind; /* boot indicator 0/ACTIVE_FLAG */
unsigned char start_head; /* head value for first sector */
unsigned char start_sec; /* sector value + cyl bits for first sector */
unsigned char start_cyl; /* track value for first sector */
unsigned char sysind; /* system indicator */
unsigned char last_head; /* head value for last sector */

3
UEX. Diseño de Sistemas Operativos

unsigned char last_sec; /* sector value + cyl bits for last sector */
unsigned char last_cyl; /* track value for last sector */
unsigned long lowsec; /* logical first sector */
unsigned long size; /* size of partition in sectors */
};

Cada entrada ocupa 16 octetos. Una entrada de la tabla es válida cuando el campo sysind tiene un
valor distinto de cero. Al igual que el disco duro puede estar dividido en particiones, una partición
también puede estar particionada. A cada partición de una partición se le denomina subpartición.
Observemos la Fig. 4.2. Si una partición del disco duro contiene subparticiones, el primer sector de
la partición no es un sector de arranque, sino de nuevo un sector de arranque maestro, del que
después hablaremos. Esta jerarquización del disco duro puede continuar sin límite. Sysind indica el
tipo de partición, que puede ser primaria o extendida. El concepto de partición extendida fue
introducido con el sistema operativo MS-DOS y es soportado por MINIX.
Todas las particiones pueden comenzar por un sector de arranque, es decir, todas pueden ser capaces
de arrancar la máquina cargando un sistema operativo. Entonces ¿qué partición arrancar? MINIX
identifica una partición de arranque por defecto. Salvo indicación explícita del operador en el
momento del arranque, la partición que arranca es la primera con el campo bootind activo.
Bootind es un carácter con el bit 7 igual a uno si la partición es activa, es decir, contiene un sector
de arranque. Como a continuación veremos, el operador de MINIX puede "saltarse" este mecanismo
e iniciar el arranque a partir de la partición que desee.
Una partición puede comenzar y terminar en cualquier sector del disco, de modo que necesitamos
especificar dos direcciones tridimensionales [cilindro/pista/sector] en cada entrada de la tabla de
partición, una de sector comienzo y otra de sector final. El campo start_cyl indica el número del
cilindro de comienzo de la partición dentro del disco. El campo start_head indica la pista de
comienzo de la partición dentro del cilindro. El campo start_sec indica el sector de comienzo de
la partición dentro de la pista. Los campos que especifican el último sector de la partición son
análogos a estos. El campo size indica el número total de sectores de la partición.
Los sectores de un disco pueden numerarse de uno en uno desde el primero hasta el último. Un disco
duro de 1Gb contiene 220 Kb = 221 sectores, de modo que los sectores tienen una dirección lineal en
el rango [0-221-1]. MINIX denomina a esta dirección dirección lógica y también dirección absoluta
mientras que a la dirección tridimensional [cilindro/pista/sector] la denomina dirección física. De la
dirección física puede extraerse la lógica y viceversa. El campo lowsec es la dirección lógica del
sector de comienzo de la partición.
Observemos la Fig. 4.2. Si una partición contiene un sistema operativo, el primer sector de la
partición contiene un programa que carga el sistema operativo en memoria y le cede el control. A
este programa se le denomina cargador y a este primer sector sector de arranque de la partición.

Fig. 4.2 Particiones y sectores de arranque en un disco duro

La Fig. 4.3 muestra que el sector de arranque contiene el programa denominado cargador primario y
las direcciones del denominado cargador secundario o monitor de arranque.

4
La inicialización del sistema

Fig. 4.3 Composición de un sector de arranque

El primer sector del disco duro se denomina sector de arranque maestro o "Master Boot Record
(MBR)" y contiene la tabla de partición. Observemos que la tabla de partición sirve como índice a
los sectores de arranque. Además de la tabla de partición, el MBR contiene un ejecutable
denominado cargador maestro (Fig. 4.4).

Fig. 4.4 Composición del sector de arranque maestro (MBR) en octetos.

El cargador maestro es un cargador de cargadores. Su misión no es cargar ningún sistema operativo,


sino examinar la tabla de partición y, de acuerdo a sus instrucciones, localizar y cargar uno de los
sectores de arranque y dar control al cargador primario allí residente. La Fig. 4.5 muestra la
secuencia de programas que intervienen desde que la BIOS comienza a ejecutar hasta que lo hace el
MINIX. La BIOS juega el papel fundamental en el proceso de arranque, fundamentalmente porque
soporta el acceso al disco y permite la interacción con el operador.

Fig. 4.5 La inicialización de MINIX.

La figura Fig. 4.6 ilustra el procedimiento de arranque de MINIX. Un procesador Intel está diseñado de
tal forma que siempre comienza a direccionar una rutina residente en la BIOS (1). Esta rutina realiza un
escrutinio de los dispositivos de bloques. Para cada dispositivo, lee el primer sector a la posición de
memoria 0x7C00 (31 Kbytes). Ahora extrae el código mágico para determinar si el sector leído es de
arranque. Si es así, la BIOS le cede el control (2). El trabajo de la BIOS ha terminado.

5
UEX. Diseño de Sistemas Operativos

Fig. 4.6 El procedimiento de arranque.

Si el dispositivo de arranque es un disco duro, que es la situación más común, el programa cargado es
un cargador maestro. El cargador maestro de MINIX se replica en la posición 1.5 Kb (2a) y desde allí
continúa ejecutando (2b). Analiza la tabla de partición del disco para determinar la partición activa,
carga su sector de arranque en la posición 31 Kb y le cede el control. Comienza pues la ejecución del
cargador primario de la partición activa (3), que carga el monitor de arranque en la posición 64 Kb y le
cede el control. El monitor de arranque migra al final de la memoria baja (4a) para no ser destruido por
MINIX. A continuación interactúa con el operador para determinar la imagen de MINIX que se carga y
su entorno y, finalmente, carga MINIX (4b) y le cede el control.

4.3 El monitor de arranque


El monitor de arranque es un intérprete de mandatos inspirado en el shell de Bourne ([4]). Como tal,
consiste en un bucle infinito de lectura y ejecución de mandatos. Permite cargar MINIX con una
variedad de opciones extraordinariamente rica. Cuando la máquina arranca, el monitor toma el
control y su abanico de mandatos permite al operador decidir cuál es la imagen que de MINIX que se
carga, cuándo se carga y con qué nivel de servicio (con red o sin red, etc.), en modo real o en modo
protegido y un largo etcétera. La funcionalidad del intérprete viene dada por sus mandatos,
documentados por la entrada “monitor(8)” del manual. En general, el intérprete permite definir
variables de entorno, agrupar mandatos predefinidos como echo, boot y otros en nuevos mandatos
más potentes denominados funciones, desplegar un menú para elegir funciones y planificar la
ejecución de un mandato en un instante futuro. Todo ello a través de su interfaz. El mandato más
importante es boot, que carga la imagen identificada por la variable de entorno image y le da
control. Cuando MINIX termina al invocar shutdown, el flujo de control vuelve al intérprete, que
espera la emisión del mandato siguiente. Así, podemos considerar a MINIX como ¡un mandato del
monitor de arranque!
El monitor se extiende a lo largo de cuatro ficheros fuente del directorio src/boot:
1. boothead.s, (1011 líneas de ensamblador)
2. boot.c, (1514 líneas de C), es el fichero principal
3. bootimage.c, (659 líneas de C)
4. rawfs.c, (259 líneas de C),

6
La inicialización del sistema

lo que hace un total de 3443 líneas de código. La potencia del monitor tiene el precio del tamaño y de
la complejidad. El monitor es realmente es un programa grande, fundamentalmente porque la falta de
soporte contribuye a aumentarlo. El mismo autor, reconoce esta impresión. En el comentario inicial
de la función más importante, bootminix, la rutina que finalmente otorga el control a MINIX,
Kees Bot, en tono jocoso, dice así: -"Dado el tamaño de este programa, es sorprendente que llegue
aquí alguna vez". Tengamos en cuenta que un mandato del sistema o una aplicación ordinaria hacen
uso de los servicios del sistema operativo para realizar la entrada-salida. El monitor no dispone de
estos servicios. Sólo la BIOS ayuda (Fig. 4.5).
El inicio de monitor de entorno muestra las líneas de presentación
Minix boot monitor 2.12
Press ESC to enter the monitor
y a continuación ejecuta el mandato predefinido menu, que muestra en pantalla un menú con una
única opción, la de arrancar MINIX, como sigue:
Hit a key as follows:
= Start Minix
La pulsación de la tecla ESC aborta el mandato menu para entrar en la línea de mandatos del
monitor. El prompt es el dispositivo de arranque, es decir, la partición de disco del que procede el
monitor, en este caso la subpartición hd3a. Pues bien, la invocación del mandato set muestra las
variables de entorno definidas en el intérprete. Un subconjunto de estas variables son las variables
predefinidas, mostradas en el siguiente ejemplo:
hd3a> set
rootdev = hd3a
ramimagedev = hd3a
ramsize = 1024
processor = (586)
bus = (at)
video = (vga)
chrome = (color)
memory = (15F90:7BB30,100000:FEF0000)
image = minix
main() {menu}
delay = swap
hd3a>
Estas variables toman un valor por defecto. El significado de las variables predefinidas está bien
documentado en el manual del sistema, monitor(8). No obstante, vamos a completar al manual con
un poco más de perspectiva.
Rootdev. En la operación normal del sistema, el sistema de ficheros del dispositivo de arranque no
tiene por qué estar montado en la jerarquía de directorios. Esta comienza en sistema de ficheros raíz
"/", aquel del que cuelgan /dev, /etc, /usr, etc. Así, MINIX distingue entre dispositivo de
arranque y dispositivo raíz. Pues bien, la variable rootdev informa a MINIX de cuál es el
dispositivo que contiene el sistema de ficheros raíz. Esta variable es consultada por el sistema de
ficheros en su secuencia de inicio para montar el dispositivo raíz.
Ramimagedev. El ejemplo anterior muestra la disposición más común, donde el dispositivo de
arranque es también el dispositivo raíz, hd3a. Ahora bien, otras opciones son posibles. Por ejemplo,
también es común disponer como dispositivo raíz el disco ram, una sección de memoria ram que
actúa como si fuese una partición de disco duro. En este caso el contenido inicial de la memoria ram
es una imagen del dispositivo dado por la variable de entorno ramimagedev.
Processor. Cuando el monitor de arranque inicializa las variables de entorno predefinidas, recurre
a la BIOS para determinar el procesador en el que está ejecutando y lo almacena codificado en la
variable processor. El valor que toma es uno entre 86, 186, 286, 386, 486, 586, ... MINIX
dispone de dos ensambladores, uno de 16 bits y uno de 32 bits. El primero ensambla el juego de
instrucciones de los procesadores de 16 bits de Intel, a saber, 8088, 8086, 80186 y 80286. El segundo

7
UEX. Diseño de Sistemas Operativos

ensambla el juego de instrucciones de 32 bits del 80386, 80486 y Pentium. Igualmente, MINIX
dispone de dos compiladores, uno de 16 bits, que genera código ensamblador de 16 bits y otro de 32
bits, que genera código ensamblador de 32 bits. El monitor de arranque está escrito utilizando las
herramientas de 16 bits. Igualmente, existen dos versiones de MINIX, una de 16 bits y otra de 32
bits. La versión de 16 bits opera en modo real en todos los procesadores y es capaz de operar en el
modo protegido si detecta un procesador 80286 o superior. La versión de 32 bits está construida con
el compilador y el ensamblador de 32 bits. Si el monitor detecta que el indicador K_I386 está
activo, conmuta a modo protegido antes de invocar a MINIX. Si no está activo, el monitor continúa
en modo real e invoca a MINIX en modo real.
Video. Video toma cuatro valores posibles en función del adaptador de vídeo presente en el
equipo, "mda", si un adaptador monocromo, "cga", si un adaptador CGA, "ega", si un adaptador
EGA y "vga", si un adaptador VGA.
Chrome. Chrome toma dos valores posibles en función de si la combinación adaptador/monitor
permite color, "mono", si la combinación no permite el color y "color", si sí lo permite.
Memory. La arquitectura PC presenta una memoria discontinua. Generalmente un PC dispone de
640K de RAM, comúnmente denominada “memoria convencional” o “memoria baja”. El espacio
que resta hasta el Mb no está disponible a los procesos de usuario, ya que lo ocupa la memoria
destinada a los controladores de red y vídeo y a la ROM BIOS. A partir del Mbyte se dispone
generalmente de un bloque de memoria contigua. A este bloque se le conoce como “memoria
extendida”. Las primeras instrucciones del monitor de arranque están escritas en ensamblador en el
fichero boothead.s. Una de las primeras acciones es invocar a la BIOS para determinar la
cantidad de memoria del equipo, que almacena en el vector mem. La primera entrada de mem
contiene la base y el tamaño de la memoria convencional. La segunda entrada contiene la base y
tamaño de la memoria extendida. Cuando MINIX va a ejecutar en modo real sólo se dispone de la
memoria convencional, de modo que hay poco espacio para los procesos de usuario. Por esta razón,
el espacio del monitor será ocupado por estos. La destrucción del monitor impide el retorno este.
Cuando MINIX ejecuta en modo protegido se dispone de la memoria extendida. En este caso el
núcleo se carga en la memoria convencional, mientras que el resto de MINIX se carga en la memoria
extendida. El monitor permanece, de modo que es posible retornar al mismo. En cualquier caso, el
monitor modifica mem para que refleje sólo el espacio que queda para MINIX más los procesos de
usuario. Con este nuevo mem construye la variable de entorno memory. El primer componente
contiene el comienzo y el tamaño del área disponible en memoria convencional expresada en octetos
en hexadecimal. El segundo componente se refiere a la memoria extendida.
Image. MINIX se carga en memoria a partir de un fichero ejecutable que generalmente se denomina
la imagen del sistema operativo o simplemente la imagen. La imagen reside en el dispositivo de
arranque, que en el ejemplo anterior es la subpartición MINIX hd3a. La ventaja de este método es
que podemos disponer en el sistema de ficheros del dispositivo de arranque de todas las imágenes de
MINIX que consideremos convenientes (Fig. 4.9) y elegir una de ellas en tiempo de arranque. Pues
bien, el valor de image es la ruta absoluta en el sistema de ficheros del dispositivo de arranque de la
imagen que deseamos cargar. Así, en el ejemplo, el monitor carga la imagen residente en el fichero
/minix del sistema de ficheros residente en el dispositivo físico hd3a. Hemos descrito la situación
más común, aunque hay otra, ya que la imagen no tiene porqué residir en un sistema de ficheros.
Label. El valor de esta variable es una etiqueta. El monitor carga una imagen de MINIX formada
por los procesos que llevan esta etiqueta y los que no llevan etiqueta alguna. Por ejemplo,
supongamos que construimos la imagen denominada dual con dos núcleos, uno etiquetado como
AT y otro como XT:
! installboot -i dual AT:at_kernel XT:xt_kernel mm fs ... init
A continuación copiamos esta imagen en el sistema de ficheros del dispositivo hd3a:
! cp dual /minix/dual

8
La inicialización del sistema

Para cargar la imagen con el núcleo AT, actuaríamos como sigue:


hd3a> label = AT
hd3a> image = minix/dual
hd3a> boot
Excepto el cargador maestro y el cargador primario, en general, un programa en memoria procede de
un fichero. A estos ficheros se les denomina ejecutables. La Fig. 4.7 muestra el formato uno de ellos.
Se divide en cinco partes: encabezamiento, instrucciones, datos inicializados, bits de reubicación y
tabla de símbolos. Los 32 o 48 primeros octetos de un fichero ejecutable corresponden al
encabezado, que viene definido por la estructura exec en el fichero include/a.out.h.

Fig. 4.7 Formato de un fichero ejecutable en MINIX

El encabezado determina cómo se dispone el ejecutable en la memoria. Cuando la llamada al sistema


exec debe ejecutar un proceso, abre el fichero ejecutable correspondiente y extrae el encabezado.
Entonces exec determina el tamaño total del ejecutable en la memoria y establece el mapa de
memoria para el mismo. Examinemos el contenido del encabezado, campo a campo.
• A_magic. El encabezado comienza con el así denominado número mágico, 0x0301. Identifica al
fichero como ejecutable.
• A_syms es el tamaño de la tabla de símbolos.
• A_flags es un campo de indicadores que define características del programa. Un ejecutable
ocupa un sólo segmento si sigue el modelo de instrucciones y datos conjuntos y dos segmentos si
sigue el modelo de instrucciones y datos separados. El indicador A_SEP distingue entre ambos.
Observemos la Fig. 4.8. En el modelo de datos conjunto el área de código no dispone de un
segmento propio y utiliza el segmento de datos. En consecuencia, el campo a_text vale cero. En
el modelo separado texto y datos se asignan a segmentos distintos. En un 8086 un ejecutable
puede ocupar un máximo de 64 Kbytes en el modelo conjunto y hasta 128 Kbytes en el separado.

9
UEX. Diseño de Sistemas Operativos

Fig. 4.8 Los modelos de ejecutable

• A_text es el tamaño en octetos de la sección de instrucciones del ejecutable.


• A_data es el espacio en octetos que ocupará la sección de de datos inicializados. Por ejemplo, la
variable cantidad de la sentencia C “static int cantidad = 23;” se almacena en la
sección de datos inicializados
• A_bss es el tamaño en octetos de la sección de datos no inicializados. Son las variables globales
no inicializadas. No ocupa espacio alguno en el fichero ejecutable, sino que se crea cuando el
fichero se carga en memoria para formar el espacio de direccionamiento. En MINIX esta área
reside entre el último dato inicializado y el espacio de heap.
• A_entry es la dirección de comienzo de ejecución del programa.
• A_total es el número total de octetos consumidos por el ejecutable en memoria después de
restarle a_text. Cuidado aquí. En a_total se incluye el espacio de heap. A_total puede
cambiarse a voluntad por el propietario del ejecutable mediante el mandato chmem.
• A_hdrlen almacena el tamaño del encabezado, que puede ser 32 o 48.
La Fig. 4.9 muestra la ubicación de la imagen de MINIX en el sistema de ficheros de la partición de
arranque. Por defecto, /minix es el fichero que contiene esta imagen, pero puede ser un directorio
con distintas imágenes. En ese caso, por defecto, se carga la más moderna.

Fig. 4.9 Ubicación de la imagen de MINIX en el dispositivo de arranque

La Fig. 4.10 muestra la composición de la imagen. El mandato del sistema installboot genera la
imagen a partir de los ejecutables del núcleo, de los servidores y de init. Luego se copia a la partición
de arranque. En la figura la tabla de símbolos del ejecutable no forma parte de la imagen. El
encabezado se inserta de modo que ocupe un sector completo. El espacio entre el final del
encabezado y el sector se rellena con ceros hasta completar los 512 octetos.

Fig. 4.10 Composición de la imagen de MINIX en el dispositivo de arranque

La variable de entorno image es una ruta absoluta en el sistema de ficheros del dispositivo de
arranque. Dada image, el monitor:

10
La inicialización del sistema

1. Carga, uno tras otro, todos los ejecutables que componen la imagen de MINIX
2. Construye la estadística de la carga en el vector local process
3. Escribe los encabezados de todos los ejecutables encima del monitor
4. Invoca MINIX
La Fig. 4.11 muestra que el monitor de arranque carga la imagen de MINIX en la posición 2K.
Limit, una variable del monitor, guarda la posición de comienzo del monitor de arranque. Éste ha
de comprobar que una imagen de MINIX demasiado grande no invada su espacio de
direccionamiento, lo que abortaría la secuencia de arranque. Obsérvese, no obstante, que MINIX
sobrescribe el cargador primario, ya código muerto. El monitor copia el encabezado de cada
ejecutable según muestra la Fig. 4.11, pero, tras la copia, modifica el campo a_syms con la posición
absoluta de memoria donde se ha cargado el segmento de texto.

Fig. 4.11 Ubicación de MINIX en la memoria

Process es una tabla que guarda la estadística final del proceso de carga. Cada entrada se
corresponde con uno de los ejecutables y básicamente describe las ubicaciones en la memoria de los
distintos segmentos.
struct process {
char name[IM_NAME_MAX + 1]; /* Nice to have a name for the thing. */
u32_t entry; /* Entry point. */
u32_t cs; /* Code segment. */
u32_t ds; /* Data segment. */
u32_t data; /* To access the data segment. */
u32_t end; /* End of this process, size = (end - cs). */
} process[PROCESS_MAX];

Código 4.1 La estadística de carga de cada ejecutable de MINIX

La descripción de cada entrada es la que sigue:


• Name. Consiste en el par (etiqueta:nombre) del ejecutable.
• Entry. La dirección absoluta de la primera instrucción del ejecutable. Se copia directamente del
encabezado del ejecutable.
• Cs. Dirección absoluta de comienzo del segmento de texto del ejecutable cargado en memoria.
• Ds. Dirección absoluta de comienzo del segmento de datos del ejecutable cargado en memoria.
En el caso de un ejecutable con instrucciones y datos conjuntos, los campos ds y cs coinciden,
apuntando al comienzo del único segmento. No es así en el caso de instrucciones y datos
separados, en que cada uno apunta a su segmento correspondiente.
• Data. Dirección absoluta de comienzo del segmento de datos del ejecutable cargado en
memoria. En el caso de instrucciones y datos conjuntos cobra verdadero sentido, apuntando al

11
UEX. Diseño de Sistemas Operativos

comienzo de los datos. En el caso de caso de instrucciones y datos separados, apunta al comienzo
del segmento de datos.
• End. Dirección absoluta de la posición siguiente al segmento de datos

Casi se puede decir que el monitor realiza la función inversa a installboot. Si este empaqueta
los ejecutables en un fichero imagen, el monitor toma ese fichero imagen y extrae estos ejecutables,
para cargarlos en memoria, si bien despojados de los encabezados. Estos se copian encima del
monitor de entorno.
El número de pares [variable, valor] que el operador puede asignar al entorno es variable e ilimitado,
de modo que estos se almacenan como una lista de nodos. Cada nodo almacena un par [variable,
valor] y se define como un objeto environment:
typedef struct environment {
struct environment *next;
char flags;
char *name; /* name = value */
char *arg; /* name(arg) {value} */
char *value;
char *defval; /* Safehouse for default values. */
} environment;

Como podemos observar, cada estructura environment contiene más información que los meros
campos nombre y valor. La razón es que existen varios tipos de variable, pero las que ahora interesan
son las variables propiamente dichas, que llevan aparejado un valor. La variable global env contiene
un puntero al primer nodo de la lista de entorno y, por lo tanto, es la referencia estable a la lista:
environment *env; /* Lists the environment. */

Fig. 4.12 La lista de variables de entorno que construye el monitor

Una vez cargada la imagen, pero antes de darle el control, una función del monitor recorre la lista de
entorno env buscando nodos de tipo E_VAR y forma una cadena de caracteres en memoria dinámica
en la que concatena los pares (nombre, valor) en el formato “nombre=valor”. La función devuelve
esta cadena en la variable params y su tamaño en la variable paramsize. A continuación cede el
control a MINIX invocando la función minix, escrita en ensamblador en boothead.s, como
sigue:
reboot_code= minix(process[KERNEL].entry, process[KERNEL].cs,
process[KERNEL].ds, params, paramsize, aout);

Los tres primeros parámetros los extrae de la estructura process y los dos siguientes son la lista de
entorno en forma de cadena y su tamaño. El último parámetro es la dirección de los encabezados. El
Código 4.2 no es parte del monitor, sino el punto de entrada al núcleo, en mpx88.s.
1 MINIX: ! this is the entry point for the MINIX kernel
2 jmp over_kernel_ds ! skip over the next few bytes
3 .data2 CLICK_SHIFT ! for the monitor: memory granularity
4 kernel_ds:
5 .data2 0x00B4 ! boot monitor flags: (later kernel DS)
6 ! call in 8086 mode, make bss, make stack,
7 ! load low, don`t patch, will return,
8 ! (has own INT calls), memory vector
9 over_kernel_ds:
10 ...

Código 4.2 El punto de entrada al núcleo

12
La inicialización del sistema

Cuando el monitor salta a la etiqueta MINIX, los octetos quinto y sexto del segmento de código del
núcleo, etiquetados como kernel_ds, no contienen una instrucción, sino unos indicadores de
configuración del núcleo. Estos informan al monitor de arranque sobre algunos aspectos de la
disposición en memoria y el modo de ejecución de la imagen. Los indicadores vienen documentados
por el apartado extensions de la entrada del manual monitor(8). Nunca son utilizados por el
núcleo. Son para uso exclusivo del monitor de arranque. Tras cargar MINIX, el monitor copia los
indicadores en la variable global k_flags, que es inspeccionada por la rutina minix. El indicador
K_I386, si está activado, indica que el núcleo ha sido compilado para operar en modo protegido y,
si no, en modo real. El modo de operación articula la rutina minix según muestra Código 4.3.
1 _minix:
2 push bp
3 mov bp, sp ! Pointer to arguments
4 ...
5 test _k_flags, #K_I386 ! Switch to 386 mode?
6 jnz minix386
7
8 ! Call Minix in real mode.
9 minix86:
10 ...
11 retf
12
13 ! Call Minix in 386 mode.
14 minix386:
15 ...
16 retf

Código 4.3 La rutina minix y el bit k_i386.

Una vez que el monitor se apercibe de que la imagen ha sido construida para ejecutar en modo real,
salta a la etiqueta minix86, que muestra el Código 4.4.
1 ! Call Minix in real mode.
2 minix86:
3 test _k_flags, #K_MEML ! New memory arrangements?
4 jz 0f
5 push 22(bp) ! Address of a.out headers
6 push 20(bp)
7 0:
8 push 18(bp) ! # bytes of boot parameters
9 push 16(bp) ! Address of boot parameters
10
11 test _k_flags, #K_RET ! Can the kernel return?
12 jz noret86
13 mov dx, cs ! Monitor far return address
14 mov ax, #ret86
15 cmp _mem+14, #0 ! Any extended memory? (mem[1].size > 0 ?)
16 jnz 0f
17 xor dx, dx ! If no ext mem then monitor not preserved
18 xor ax, ax
19 0: push dx ! Push monitor far return address or zero
20 push ax
21 noret86:
22
23 mov ax, 8(bp)
24 mov dx, 10(bp)
25 call abs2seg
26 push dx ! Kernel code segment
27 push 4(bp) ! Kernel code offset
28 mov ax, 12(bp)
29 mov dx, 14(bp)
30 call abs2seg
31 mov ds, dx ! Kernel data segment
32 mov es, dx ! Set es to kernel data too
33 retf ! Make a far call to the kernel

13
UEX. Diseño de Sistemas Operativos

Código 4.4 La secuencia de código minix86.

Esta secuencia de código dispone la pila que utilizarán las primeras instrucciones de MINIX. La Fig.
4.13 muestra la composición de la pila justo antes de que minix salte vía retf a la etiqueta MINIX
del núcleo, su punto de entrada (línea 29 del Código 4.4).

Fig. 4.13 El marco de pila de la rutina minix.

La instrucción retf es un “retorno lejano”, es decir, al código de otro segmento de código. La


ejecución de retf es el salto al sistema operativo, con los registros de segmento que muestra la Fig.
4.14. Como se observa MINIX comienza a ejecutar en la pila del monitor de arranque. La discusión
se traslada ahora a la sección siguiente, que trata sobre la inicialización de MINIX.

Fig. 4.14 Los registros de segmento tras el salto a MINIX.

4.4 La inicialización de MINIX


El salto retf es realmente una llamada a función por parte del monitor. La función comienza en la
etiqueta MINIX (Código 4.5) y así podemos llamarla. Como toda función, construye su marco de pila,
que muestra Fig. 4.15. Está compuesto por dos parámetros que son el vector de encabezados y la
cadena de entorno, y por la dirección de retorno al monitor. Observemos que el marco de pila se
encuentra aún en el segmento de datos del monitor. El trabajo de la función es “salvar” los parámetros
antes de conmutar a la pila del núcleo. La dirección de los encabezados se salva en la variable del
núcleo aout (líneas 26-29) y la cadena con las variables de entorno en los registros bx y dx (líneas
24-25).

14
La inicialización del sistema

Fig. 4.15 Las primeras instrucciones del núcleo salvan la información proporcionada por el monitor.

1 MINIX: ! this is the entry point for the MINIX kernel


2 jmp over_kernel_ds ! skip over the next few bytes
3 .data2 CLICK_SHIFT ! for the monitor: memory granularity
4 kernel_ds:
5 .data2 0x00B4 ! boot monitor flags: (later kernel DS)
6 ! call in 8086 mode, make bss, make stack,
7 ! load low, don`t patch, will return,
8 ! (has own INT calls), memory vector
9 over_kernel_ds:
10
11 ! Set up a C stack frame on the monitor stack. (The monitor sets cs and ds
12 ! right. The ss register still references the monitor data segment.)
13 push bp
14 mov bp, sp
15 push si
16 push di
17 cmp 4(bp), #0 ! monitor code segment is
18 jz noret ! nonzero if return possible
19 inc _mon_return
20 noret: mov _mon_ss, ss ! save stack location for later return
21 mov _mon_sp, sp
22
23 ! Locate boot parameters, set up kernel segment registers and stack.
24 mov bx, 6(bp) ! boot parameters offset
25 mov dx, 8(bp) ! boot parameters length
26 mov ax, 10(bp) ! address of a.out headers
27 mov _aout+0, ax
28 mov ax, 12(bp)
29 mov _aout+2, ax
30 mov ax, ds ! kernel data
31 mov es, ax
32 mov ss, ax
33 mov sp, #k_stktop ! set sp to point to the top of kernel stack

Código 4.5 El arranque de MINIX (I).

El Código 4.6 continúa con la inicialización. Observemos (línea 37) que la variable kernel_ds
(línea 4 del Código 4.2), que originalmente contenía los indicadores de descripción del núcleo, es
sobrescrita ahora con la ubicación del segmento de datos del núcleo.
34 ! Real mode needs to get kernel DS from the code segment. Protected mode
35 ! needs CS in the jump back to real mode.
36 cseg mov kernel_cs, cs
37 cseg mov kernel_ds, ds
38
39 ! Call C startup code to set up a proper environment to run main().
40 push dx
41 push bx
42 push _mon_ss
43 push ds
44 push cs
45 call _cstart ! cstart(cs, ds, mds, parmoff, parmlen)
46 add sp, #5*2
47

15
UEX. Diseño de Sistemas Operativos

48 cmp _protected_mode, #0
49 jz nosw ! ok to switch to protected mode?
50
51 call klib_init_prot ! initialize klib functions for protected mode
52 call real2prot ! switch to protected mode
53
54 push #0 ! set flags to known good state
55 popf ! especially, clear nested task and int enable
56 nosw:
57 jmp _main ! main()

Código 4.6 El arranque de MINIX (II).

En los programas escritos en C el punto de entrada del ejecutable es una función que
tradicionalmente se ha venido llamando crtsr, de “C run-time start-up rutine”, que se encarga de
llamar a la función main del proceso recién creado. El núcleo de MINIX también dispone de esta
función, cstart. Las líneas 40-45 invocan cstart. Cstart copia el entorno del monitor al núcleo.
En función del entorno registra si se conmuta a modo protegido en la variable booleana
protected_mode. Las líneas entre la invocación cstart y main se ocupan de conmutar a modo
protegido en su caso. Las particularidades de la arquitectura Intel no dejan de ser interesantes, pero,
quizás, se salen un tanto el ámbito de la asignatura, de modo que el modo protegido no va a ser
estudiado. Cstart está escrita en C en el fichero fuente del núcleo cstart.c según muestra el
Código 4.7.
1 PUBLIC void cstart(cs, ds, mds, parmoff, parmsize)
2 U16_t cs, ds; /* Kernel code and data segment */
3 U16_t mds; /* Monitor data segment */
4 U16_t parmoff, parmsize; /* boot parameters offset and length */
5 {
6 /* Perform system initializations prior to calling main(). */
7
8 register char *envp;
9 unsigned mon_start;
10
11 /* Record where the kernel and the monitor are. */
12 code_base = seg2phys(cs);
13 data_base = seg2phys(ds);
14
15 /* Initialize protected mode descriptors. */
16 prot_init();
17
18 /* Copy the boot parameters to kernel memory. */
19 if (parmsize > sizeof k_environ - 2) parmsize = sizeof k_environ - 2;
20 phys_copy(seg2phys(mds)+parmoff, vir2phys(k_environ), (phys_bytes) parmsize);
21
22 /* Convert important boot environment variables. */
23 boot_parameters.bp_rootdev = k_atoi(k_getenv("rootdev"));
24 boot_parameters.bp_ramimagedev = k_atoi(k_getenv("ramimagedev"));
25 boot_parameters.bp_ramsize = k_atoi(k_getenv("ramsize"));
26 boot_parameters.bp_processor = k_atoi(k_getenv("processor"));
27
28 /* Type of VDU: */
29 envp = k_getenv("video");
30 if (strcmp(envp, "ega") == 0) ega = TRUE;
31 if (strcmp(envp, "vga") == 0) vga = ega = TRUE;
32
33 /* Processor? */
34 processor = boot_parameters.bp_processor; /* 86, 186, 286, 386, ... */
35
36 /* XT, AT or MCA bus? */
37 envp = k_getenv("bus");
38 if (envp == NIL_PTR || strcmp(envp, "at") == 0) {
39 pc_at = TRUE;
40 } else
41 if (strcmp(envp, "mca") == 0) {

16
La inicialización del sistema

42 pc_at = ps_mca = TRUE;


43 }
44
45 /* Decide if mode is protected. */
46 #if _WORD_SIZE == 2
47 protected_mode = processor >= 286;
48 if (!protected_mode) mon_return = 0;
49 #endif
50
51 /* Return to assembler code to switch to protected mode (if 286), reload
52 selectors and call main().
53 */
54 }

Código 4.7 La rutina cstart.

La función seg2phys devuelve la dirección absoluta a la que apunta un registro de segmento. Las
líneas 12 y 13 disponen en las variables globales del núcleo code_base y data_base como las
posiciones absolutas de los segmentos de código y datos del núcleo. La línea 16 se puede ignorar, ya
que pasa a modo protegido, en su caso, en un procesador 286.
Un puntero en C, o lo que es lo mismo, la dirección de una variable, es realmente una dirección virtual,
la posición que ocupa la variable en el segmento de datos del proceso. En C se obtiene el puntero de la
variable var anteponiendo el carácter “&” a la variable. Así, la dirección virtual de var es &var. En
un 8086 el segmento de datos tiene como máximo 64Kb, de modo que los punteros se almacenan en
variables de 16 bits.
Un proceso trabaja con direcciones virtuales, pero el procesador lo hace con direcciones absolutas. Un
8086 obtiene la dirección absoluta de una variable sumando su dirección relativa a la dirección de
comienzo del segmento de datos, almacenado en el registro ds (Fig. 2.4).
El núcleo de MINIX necesita en ocasiones conocer la dirección absoluta de una variable. Las copias
entre segmentos de datos diferentes es una de ellas. Las direcciones absolutas tienen 20 bits, de modo
que se almacenan en variables de 32 bits. Así, el núcleo reserva los tipos vir_bytes y phys_bytes
para distinguir entre direcciones relativas y absolutas. Están definidos en /usr/include/minix/type.h
como int y long respectivamente. Para llevar a cabo las copias inter-segmento el núcleo dispone del
procedimiento de utilidad phys_copy. El primer parámetro es la dirección absoluta fuente, el segundo
la dirección absoluta destino y el tercero la cantidad de octetos que se copian. Todos los parámetros son
de tipo phys_bytes. Está escrita en klib88.s. La macro vir2phys transforma una dirección
relativa del núcleo en una absoluta. Está definida en const.h.
Las líneas 19-20 copian la cadena de variables de entorno desde el segmento de datos del monitor al del
núcleo. La copia se hace en concreto a la cadena k_environ, definida en cstart.c. Las funciones
k_getenv y k_atoi son métodos sobre k_environ. La primera devuelve el valor del nombre que
se pasa como parámetro. La segunda convierte a entero una cadena. Ambas están definidas en
cstart.c. Las últimas líneas de cstart comprueban el tipo de máquina establecido por el operador
(Este puede decidir ejecutar MINIX en un Pentium como si este fuese un 8086). Sólo si estableció un
286 o superior, MINIX ejecutará en modo protegido.
Main, en main.c, realiza cinco tareas bien definidas, que son:
1. Inicializar los vectores de interrupción.
2. Inicializar el mapa de memoria libre para los procesos de usuario.
3. Inicializar los descriptores de proceso de las tareas, los servidores e init.
4. Insertar estos descriptores en las colas de planificación.
5. Invocar restart para que ejecute la primera tarea dispuesta.
Una tras otra, las tareas ejecutan su código de inicialización y entran en su bucle de servicio donde se
bloquean en receive. Lo mismo les ocurre a los servidores. Finalmente, init, toma el procesador,

17
UEX. Diseño de Sistemas Operativos

detecta los terminales conectados al sistema y abre un proceso login para cada uno de ellos. El
Código 4.8 muestra las primeras sentencias de main.

1 PUBLIC void main()


2 {
3 /* Start the ball rolling. */
4
5 register struct proc *rp;
6 register int t;
7 int hdrindex;
8 phys_clicks text_base;
9 vir_clicks text_clicks;
10 vir_clicks data_clicks;
11 phys_bytes phys_b;
12 reg_t ktsb; /* kernel task stack base */
13 struct memory *memp;
14 struct tasktab *ttp;
15 struct exec e_hdr;
16
17 /* Initialize the interrupt controller. */
18 intr_init(1);
19
20 /* Interpret memory sizes. */
21 mem_init();
22 ...
23 }

Código 4.8 La rutina main (I).

La línea 21 invoca mem_init, en misc.c. Mem_init examina la variable de entorno memory y


construye un vector mem, idéntico al del monitor, con entradas de base y tamaño. La línea 18 invoca
intr_init, en i8259.c para inicializar los PIC 8259A y los vectores de interrupción.
1 typedef _PROTOTYPE( void (*vecaddr_t), (void) );
2
3 PRIVATE vecaddr_t int_vec[] = {
4 int00, int01, int02, int03, int04, int05, int06, int07,
5 };
6 PRIVATE vecaddr_t irq_vec[] = {
7 hwint00, hwint01, hwint02, hwint03, hwint04, hwint05, hwint06, hwint07,
8 hwint08, hwint09, hwint10, hwint11, hwint12, hwint13, hwint14, hwint15,
9 };
10
11 PUBLIC void intr_init(mine)
12 int mine;
13 {
14 /* Initialize the 8259s, finishing with all interrupts disabled. This is
15 * only done in protected mode, in real mode we don't touch the 8259s, but
16 * use the BIOS locations instead. The flag "mine" is set if the 8259s are
17 * to be programmed for Minix, or to be reset to what the BIOS expects.
18 */
19
20 int i;
21
22 lock();
23 if (protected_mode) {
24 ...
25 } else {
26 for (i = 0; i < 8; i++) set_vec(i, int_vec[i]);
27 set_vec(SYS_VECTOR, s_call);
28 }
29
30 /* Initialize the table of interrupt handlers. */
31 for (i = 0; i < NR_IRQ_VECTORS; i++) irq_table[i] = spurious_irq;
32 }

Código 4.9 La rutina intr_init.

18
La inicialización del sistema

Como podemos observar MINIX no reinicializa el PIC en modo real, sino que respeta la inicialización
previa hecha por la BIOS. La línea 26 inicializa los ocho primeros vectores de interrupción, que
corresponden a las excepciones que genera un procesador 8086. Las rutinas de excepción vienen dadas
por la tabla int_vec (línea 3) y están programadas en el fichero fuente mpx88.s. Set_vec es una
utilidad que asigna una rutina a un vector de interrupción. La línea 27 inicializa el vector de
interrupción 32, SYS_VECTOR, el correspondiente a la interrupción software de las llamadas al
sistema. La tabla irq_vec aglutina las direcciones de las rutinas de interrupción. Es cada manejador
de dispositivo el que establece su vector de interrupción.

4.5 La creación de los procesos del sistema


La imagen de MINIX (Fig. 4.11) contiene varios programas que son las tareas de entrada-salida, los
servidores e init. MINIX convierte estos programas en procesos inicializando un descriptor de
proceso para cada uno de ellos. Esta sección se ocupa de examinar dicha inicialización. La
inicialización se hace en un bucle de main donde cada iteración inicializa un descriptor (Código
4.11).
La pila
El punto de entrada de las tareas de entrada-salida se conoce en tiempo de compilación ya que es un
desplazamiento relativo en el segmento de código del núcleo. El tamaño de la pila también. Ambos
se definen en una tabla del núcleo denominada tasktab, en table.c (Código 4.10). Así, la tarea
del terminal empieza a ejecutar en la función tty_task y utiliza una pila de TTY_STACK octetos.
Los servidores e init son programas independientes de los que el compilador desconoce
absolutamente todo cuando compila el núcleo, de modo que sus campos se rellenan con cero.
1 PUBLIC struct tasktab tasktab[] = {
2 { tty_task, TTY_STACK, "TTY" },
3 #if ENABLE_DOSDSK
4 { dosdsk_task, DOSDSK_STACK, "DOSDSK" },
5 #endif
6 #if ENABLE_WINI
7 { winchester_task, WINCH_STACK, "WINCH" },
8 #endif
9 { syn_alrm_task, SYN_ALRM_STACK, "SYN_AL" },
10 { idle_task, IDLE_STACK, "IDLE" },
11 { printer_task, PRINTER_STACK, "PRINTER" },
12 { floppy_task, FLOP_STACK, "FLOPPY" },
13 { mem_task, MEM_STACK, "MEMORY" },
14 { clock_task, CLOCK_STACK, "CLOCK" },
15 { sys_task, SYS_STACK, "SYS" },
16 { 0, HARDWARE_STACK, "HARDWAR" },
17 { 0, 0, "MM" },
18 { 0, 0, "FS" },
19 { 0, 0, "INIT" },
20 };

Código 4.10 La tabla tasktab

El espacio para las pilas de las tareas se toma del buffer t_stack, definido en table.c, según
ilustra la Fig. 4.16.

19
UEX. Diseño de Sistemas Operativos

Fig. 4.16 Inicialización del puntero de pila de los descriptores de las tareas

Las líneas 50-60 del Código 4.11 establecen los campos del descriptor de una tarea de entrada-salida
relativos a la pila. La inicialización de las pilas de los servidores e init se estudia después.
1 PUBLIC void main()
2 {
3 ...
35 /* Task stacks. */
36 ktsb = (reg_t) t_stack;
37
38 for (t = -NR_TASKS; t <= LOW_USER; ++t) {
39 rp = proc_addr(t); /* t's process slot */
40 ttp = &tasktab[t + NR_TASKS]; /* t's task attributes */
41 strcpy(rp->p_name, ttp->name);
42 if (t < 0) {
43 if (ttp->stksize > 0) {
44 rp->p_stguard = (reg_t *) ktsb;
45 *rp->p_stguard = STACK_GUARD;
46 }
47 ktsb += ttp->stksize;
48 rp->p_reg.sp = ktsb;
49 text_base = code_base >> CLICK_SHIFT;
50 /* tasks are all in the kernel */
51 hdrindex = 0; /* and use the first a.out header */
52 } else {
53 hdrindex = 1 + t; /* MM, FS, INIT follow the kernel */
54 }
55 ...
107 }
108 ...
115 }

Código 4.11 La rutina main (II). La pila de las tareas

El mapa de memoria
El mapa de memoria de un proceso registra la posición y el tamaño de las áreas de código, datos y
pila del proceso. MINIX almacena estos datos en términos de clicks para economizar
almacenamiento. Esto significa que un área comienza en un múltiplo de 256 y contiene un número
entero de bloques de 256 octetos. El mapa de memoria de un proceso se almacena en el campo
p_map de su descriptor. P_map es un vector de tres entradas, una para el área de código, otra para el
área de datos y otra para el área de pila. Cada entrada es una estructura de tipo mem_map, definida
en include/minix/type.h. La Fig. 4.17 muestra un proceso en memoria y, a su izquierda, su
mapa de memoria correspondiente. Las unidades del mapa son clicks.

20
La inicialización del sistema

Fig. 4.17 Mapa de un proceso MINIX con código y datos separados.

¿De dónde extraemos el comienzo de los segmentos de código y datos del núcleo, los servidores e
init? Observemos la Fig. 4.18. El monitor pasa al núcleo la posición del vector de encabezados de los
ejecutables que componen MINIX y el núcleo almacena esta posición en la variable aout. Cada
encabezado registra la posición absoluta del segmento de código en el campo a_syms, así como el
tamaño de los segmentos de código y datos.

Fig. 4.18 Las posiciones de los ejecutables se obtienen del vector de encabezados

El mapa de memoria de todas las tareas es el mismo, el correspondiente al núcleo, según muestra la
Fig. 4.19. Nótese que las tareas no tienen el área de pila ni el espacio de heap que muestra el proceso
de usuario genérico de la Fig. 4.17. La razón es que la pila de una tarea es una estructura de datos del
segmento de código.

Fig. 4.19 El mapa de memoria de una tarea de entrada-salida

21
UEX. Diseño de Sistemas Operativos

El Código 4.12 establece el mapa de memoria de los procesos del sistema. Las líneas 67-68 copian el
encabezado del ejecutable en curso a la variable local e_hdr. El campo syms contiene la posición
absoluta del ejecutable, que se almacena en la variable local text_base en términos de clicks. El
campo a_text contiene el tamaño en octetos del ejecutable, que se almacena en la variable local
text_size en términos de clicks.

1 PUBLIC void main()


2 {
3 ...
46 for (t = -NR_TASKS; t <= LOW_USER; ++t) {
47 ...
64 /* The bootstrap loader has created an array of the a.out headers at
65 * absolute address 'aout'.
66 */
67 phys_copy(aout + hdrindex * A_MINHDR, vir2phys(&e_hdr),
68 (phys_bytes) A_MINHDR);
69 text_base = e_hdr.a_syms >> CLICK_SHIFT;
70 text_clicks = (e_hdr.a_text + CLICK_SIZE-1) >> CLICK_SHIFT;
71 if (!(e_hdr.a_flags & A_SEP)) text_clicks = 0; /* Common I&D */
72 data_clicks = (e_hdr.a_total + CLICK_SIZE-1) >> CLICK_SHIFT;
73 rp->p_map[T].mem_phys = text_base;
74 rp->p_map[T].mem_len = text_clicks;
75 rp->p_map[D].mem_phys = text_base + text_clicks;
76 rp->p_map[D].mem_len = data_clicks;
77 rp->p_map[S].mem_phys = text_base + text_clicks + data_clicks;
78 rp->p_map[S].mem_vir = data_clicks; /* empty - stack is in data */
79 ...
107 }
108 ...
116 }

Código 4.12 La rutina main (III). El mapa de memoria del proceso

A continuación (Código 4.13) el mapa de memoria del proceso se resta de la memoria libre
disponible para los procesos de usuario, registrada en el vector mem (Fig. 4.20).

1 PUBLIC void main()


2 {
3 ...
48 for (t = -NR_TASKS; t <= LOW_USER; ++t) {
49 ...
80 /* Remove server memory from the free memory list. The boot monitor
81 * promises to put processes at the start of memory chunks.
82 */
83 for (memp = mem; memp < &mem[NR_MEMS]; memp++) {
84 if (memp->base == text_base) {
85 memp->base += text_clicks + data_clicks;
86 memp->size -= text_clicks + data_clicks;
87 }
88 }
89 ...
109 }
110 ...
117 }

Código 4.13 La rutina main (IV). El mapa de memoria libre

Un 8086 no dispone de memoria extendida. Como el espacio para los procesos de usuario es escaso,
el que ocupa el monitor se aprovecha para estos. De esta manera, el control no vuelve al monitor
cuando MINIX acaba. Para procesadores superiores, hay espacio de sobra en la memoria extendida,
de modo que el monitor coexiste con MINIX y este retorna al monitor.

22
La inicialización del sistema

Fig. 4.20 Disposición de MINIX en memoria.

El contador de programa y el registro de estado


De lo que se trata ahora es de asignar al campo p_reg.pc del descriptor el contador de programa, la
dirección de la primera instrucción que ejecutará el proceso. En cuanto a las tareas, esta es
precisamente la información contenida en la estructura tasktab (Código 4.10). Los servidores e
init son procesos independientes, de modo que la primera instrucción que ejecutan se encuentra en la
dirección cero de su segmento de código.
1 PUBLIC void main()
2 {
3 ...
50 for (t = -NR_TASKS; t <= LOW_USER; ++t) {
51 ...
111 /* Set initial register values. */
112 rp->p_reg.pc = (reg_t) ttp->initial_pc;
113 rp->p_reg.psw = istaskp(rp) ? INIT_TASK_PSW : INIT_PSW;
114 ...
115 }
116 ...
118 }

Código 4.14 La rutina main (V). El contador de programa y la palabra de estado

El registro o palabra de estado se inicializa en la línea 113. El registro de estado del 8086 se muestra en
la Fig. 4.21. MINIX define un registro de estado inicial para las tareas y otro para los servidores e init
en el fichero de constantes del núcleo const.h:
#define INIT_PSW 0x0200 /* initial psw */
#define INIT_TASK_PSW 0x1200 /* initial psw for tasks (with IOPL 1) */
Como los cuatro bits más significativos no tienen función asignada en un 8086, no hay diferencia
entre ambas en modo real. Como puede apreciarse, este registro inicial hace que el procesador
habilite las interrupciones. De este modo, en cuanto al proceso se le planifica puede ser interrumpido.

Fig. 4.21 Registro de estado del procesador 8086.

23
UEX. Diseño de Sistemas Operativos

Las pilas de los servidores


El puntero de pila incial de una tarea se dispone según la tabla estática t_stack. En contraste, el
puntero de pila de un servidor y el de init apunta al final de su segmento de datos (Fig. 4.22).

Fig. 4.22 El mapa de memoria de un servidor.

1 PUBLIC void main()


2 {
3 ...
46 for (t = -NR_TASKS; t <= LOW_USER; ++t) {
47 ...
94 if (t >= 0) {
95 /* Initialize the server stack pointer. Take it down one word
96 * to give crtso.s something to use as "argc".
97 */
98 rp->p_reg.sp = (rp->p_map[S].mem_vir +
99 rp->p_map[S].mem_len) << CLICK_SHIFT;
100 rp->p_reg.sp -= sizeof(reg_t);
101 }
102
103 if (!isidlehardware(t)) lock_ready(rp); /* IDLE, HARDWARE neveready */
104 rp->p_flags = 0;
105
106 alloc_segments(rp);
107 }
108 ...
115 }

Código 4.15 La rutina main (VI). La pila de los servidores

Los registros de código y datos


Una vez conocido el mapa de memoria, los registros de código cs y datos ds se obtienen a partir del
campo de dirección física de la estructura mem_map (Fig. 4.17). El mapa de memoria tiene entradas
dadas en clicks, mientras que los registros de la máquina cs, ds, es, y ss apuntan a bloques 16
octetos o hclicks. La función alloc_segments, en system.c, toma la dirección en clicks y la
transforma en una dirección dada en hclicks. La conversión se reduce a un desplazamiento a la
izquierda de cuatro bits. Obsérvese que previamente main introduce el proceso en la cola de
procesos activos a través de lock_ready 1 .
El Código 4.16 muestra las últimas líneas de main. Dispuestos los procesos en sus colas de
planificación y las interrupciones preparadas, sólo falta una cosa, dar el banderazo de salida. Main
establece proc_ptr en la línea 110, invoca a restart y ... ¡Comienza la función!

1 PUBLIC void main()


2 {
3 ...
4 for (t = -NR_TASKS; t <= LOW_USER; ++t) {

1
No se introducen las tareas instrumentales IDLE y HARDWARE.

24
La inicialización del sistema

5 ...
6 }
7 proc[NR_TASKS+INIT_PROC_NR].p_pid = 1; /* INIT of course has pid 1 */
8 bill_ptr = proc_addr(IDLE); /* it has to point somewhere */
9 lock_pick_proc();
10
11 /* Now go to the assembly code to start running the current process. */
12 restart();
13 }

Código 4.16 La rutina main (VI). ¡Comienza la función!

4.6 Init
Las tareas y los servidores implementan un bucle de servicio. Tras el procedimiento de inicialización,
todos ellos se suspenden en la llamada receive. Entonces, init toma el procesador. La entrada del
manual init(8) documenta init.

Fig. 4.23 Los conceptos “grupo de procesos” y “terminal de control”.

La llamada al sistema setsid permite al proceso invocante constituirse como un “líder de sesión”
(Fig. 4.23). Es el líder de un grupo formado por él y todos sus descendientes. El grupo tiene un
identificador que coincide con el identificador del líder. La llamada al sistema getpgrp devuelve el
identificador del grupo de procesos al que pertenece el invocante. Cuando un líder de sesión abre un
terminal, este se convierte en el terminal de control del grupo. Por ejemplo, la pulsación de CTRL-C
en este terminal ocasiona que se envíe la señal SIGINT a todos ellos. Lo más común es que la
entrada y salida estándar de un proceso coincidan con su terminal de control, pero no tiene por qué
ser así, ya que el proceso puede redirigir su entrada y salida a una tubería, por ejemplo.
Stty es un programa que muestra o cambia los parámetros del terminal que actúa como entrada
estándar. Getty es un programa que muestra un mensaje de identificación del sistema en su salida
estándar, lee un nombre de usuario de su entrada estándar y ejecuta login con ese nombre como
argumento. Los parámetros de getty forman el mensaje de identificación.
El fichero /etc/ttytab (tabla de terminales) contiene la lista de los dispositivos terminales del sistema
que permiten a un usuario iniciar una sesión vía login. Login conmuta en un shell que permite
ejecutar procesos que crean otros procesos. Veremos que tal terminal se convierte en el “terminal de
control” del grupo. Cada línea de este fichero describe el nombre y el tipo de terminal, el programa
que hay que ejecutar para que muestre login (generalmente getty) y el programa (generalmente
stty) que inicializa el terminal con los parámetros de velocidad, caracteres de control, etc. Estos

25
UEX. Diseño de Sistemas Operativos

campos pueden omitirse para indicar que un terminal está deshabilitado o que no es necesaria la
inicialización. La entrada del manual ttytab(5) documenta el formato del fichero. A continuación se
muestra un ejemplo:
# Device Type Program Init
console minix getty
ttyc1 minix getty
ttyc2 minix getty
ttyc3 minix getty
tty00 vt100 getty "stty 9600"
tty01 dialup getty "stty 38400"
Console es el terminal compuesto por la pantalla gráfica del PC y el teclado. Tttyc1 a ttyc3
son las “consolas virtuales”, otras áreas de la memoria de vídeo disponibles mediante la pulsación de
ALT-F1, ALT-F2, etc. Ttt00, tty01, etc, son los terminales de línea conectados al rs-232. El
dispositivo asociado a cada terminal se halla anteponiendo el prefijo /dev al campo “device” de cada
entrada. Así, el dispositivo asociado a la consola es /dev/console.
El programador dispone de una interfaz para recorrer y leer las entradas de ttytab. Está
documentada en la entrada del manual getttyent(3) y se compone de las funciones getttyent,
setttyent, endttyent y getttyname. Getttyent sirve para recorrer la tabla. La primera
invocación de getttyent lee la primera entrada de ttytab y devuelve una estructura ttyent,
la segunda invocación lee la segunda entrada y así sucesivamente. Cuando no quedan entradas
devuelve NULL. Setttyent abre o rebobina la tabla y endttyent la cierra. Getttyname
devuelve la entrada dada por su parámetro.
La función de init es explorar la tabla de terminales y arrancar un proceso login en cada terminal
descrito (Fig. 4.24).

Fig. 4.24 Init despliega login en los terminales del sistema.

Init realiza además algunas tareas de administración. Los ficheros /etc/utmp y /usr/adm/wtmp
contienen los usuarios actualmente conectados y la historia de logins y logouts respectivamente.
Cada fichero es un vector de estructuras utmp, definida en <utmp.h> como sigue:
struct utmp {
char ut_user[8]; /* user name */
char ut_line[12]; /* terminal name */
char ut_host[16]; /* host name, when remote */
time_t ut_time; /* login/logout time */
};
Init no es creado por una llamada al sistema exec, de modo que no hereda ficheros abiertos de
ningún proceso padre. La entrada, salida y error estándar no están abiertos cuando init comienza.
Normalmente la acción fstat sobre la entrada estándar (línea 6 del Código 4.17) falla e init
establece como entrada estándar /dev/null y como salida y error estándar /dev/log. Este último es un
dispositivo especial siempre accesible concebido para enviar mensajes de depuración al operador.
1 int main(void)
2 {
3 ...
4 struct stat stb;

26
La inicialización del sistema

5
6 if (fstat(0, &stb) < 0) {
7 /* Open standard input, output & error. */
8 (void) open("/dev/null", O_RDONLY);
9 (void) open("/dev/log", O_WRONLY);
10 dup(1);
11 }
12 ...
28 }

Código 4.17 La rutina main (I). Inicialización.

Init establece a continuación los manejadores de las señales SIGUP, SIGTERM y SIGABRT (Código
4.18). El administrador del sistema envía SIGUP a init tras insertar una nueva terminal en ttytab.
También cuando la borra para dar de baja un terminal. En este caso debe matar su proceso login a
mano. SIGTERM la envían procesos que paran o reinician MINIX para dejar de abrir nuevos login.
SIGABRT la envía la tarea del terminal cuando se pulsa la combinación CTRL-ALT-DEL. Causa que
init ejecute el mandato shutdown. Una segunda señal SIGABRT hace que init para directamente el
sistema mediante la invocación de la llamada al sistema reboot. Una tercera pulsación de CTRL-
ALT-DEL hace que la tarea del terminal para el sistema sin ni siquiera volcar la caché de disco duro.
1 ...
2 int gothup = 0; /* flag, showing signal 1 was received */
3 int gotabrt = 0; /* flag, showing signal 6 was received */
4 int spawn = 1; /* flag, spawn processes only when set */
5 ...
6 int main(void)
7 {
8 ...
9 struct sigaction sa;
10 ...
11 sigemptyset(&sa.sa_mask);
12 sa.sa_flags = 0;
13
14 /* Hangup: Reexamine /etc/ttytab for newly enabled terminal lines. */
15 sa.sa_handler = onhup;
16 sigaction(SIGHUP, &sa, NULL);
17
18 /* Terminate: Stop spawning login processes, shutdown is near. */
19 sa.sa_handler = onterm;
20 sigaction(SIGTERM, &sa, NULL);
21
22 /* Abort: Sent by the kernel on CTRL-ALT-DEL; shut the system down. */
23 sa.sa_handler = onabrt;
24 sigaction(SIGABRT, &sa, NULL);
25 ...
26 }
27
28 void onhup(int sig)
29 {
30 gothup = 1;
31 spawn = 1;
32 }
33
34 void onterm(int sig)
35 {
36 spawn = 0;
37 }
38
39 void onabrt(int sig)
40 {
41 static int count;
42
43 if (++count == 2) reboot(RBT_HALT);
44 gotabrt = 1;
45 }

27
UEX. Diseño de Sistemas Operativos

Código 4.18 La rutina main (I). Inicialización de señales.

Tras la inicialización de las señales, init crea un hijo y espera por él suspendido en wait (Código
4.19). Es posible que el padre reciba una señal mientras espera (línea 8). En ese caso wait vuelve
con error. Si la señal fue SIGABRT, MINIX ejecuta la llamada al sistema reboot.

1 int main(void)
2 {
3 pid_t pid; /* pid of child process *
4 ...
5 /* Execute the /etc/rc file. */
6 if ((pid = fork()) != 0) {
7 /* Parent just waits. */
8 while (wait(NULL) != pid) {
9 if (gotabrt) reboot(RBT_HALT);
10 }
11 } else {
12 static char *rc_command[] = { "sh", "/etc/rc", NULL, NULL };
13 /* Minix: Input from the console. */
14 close(0);
15 (void) open("/dev/console", O_RDONLY);
16 execute(rc_command);
17 report(2, "sh /etc/rc");
18 _exit(1); /* impossible, we hope */
19 }
20 ...
21 }

Código 4.19 La rutina main (II). Ejecutando /etc/rc.

El hijo adquiere el terminal (el teclado) como entrada estándar e invoca execute antes mutar a un
shell que ejecuta el script /etc/rc, que realiza funciones básicas de inicialización y
administración. El Código 4.20 muestra execute. Su trabajo es en esencia determinar en qué
directorio está el shell para construir su ruta completa, que es el primer parámetro de execve. Lo
busca en las cuatro rutas de path (línea 7).
1 int execute(char **cmd)
2 {
3 /* Execute a command with a path search along /sbin:/bin:/usr/sbin:/usr/bin.
4 */
5 static char *nullenv[] = { NULL };
6 char command[128];
7 char *path[] = { "/sbin", "/bin", "/usr/sbin", "/usr/bin" };
8 int i;
9
10 if (cmd[0][0] == '/') {
11 /* A full path. */
12 return execve(cmd[0], cmd, nullenv);
13 }
14
15 /* Path search. */
16 for (i = 0; i < 4; i++) {
17 if (strlen(path[i]) + 1 + strlen(cmd[0]) + 1 > sizeof(command)) {
18 errno= ENAMETOOLONG;
19 return -1;
20 }
21 strcpy(command, path[i]);
22 strcat(command, "/");
23 strcat(command, cmd[0]);
24 execve(command, cmd, nullenv);
25 if (errno != ENOENT) break;
26 }
27 return -1;
28 }

Código 4.20 La rutina execute.

28
La inicialización del sistema

Una vez que /etc/rc ha sido ejecutado por el hijo, init continúa su labor. Entonces entra en un bucle
infinito en el que abre un hijo login por cada terminal en /etc/ttytab y espera a que alguno termine
para volverlo a crear (Código 4.21).
1 #define PIDSLOTS 32 /* first this many ttys can be on */
2
3 struct slotent {
4 int errct; /* error count */
5 pid_t pid; /* pid of login process for this tty line */
6 };
7
8 #define ERRCT_DISABLE 10 /* disable after this many errors */
9 #define NO_PID 0 /* pid value indicating no process */
10
11 struct slotent slots[PIDSLOTS]; /* init table of ttys and pids */
12
13 int main(void)
14 {
15 pid_t pid; /* pid of child process */
16 int fd; /* generally useful */
17 int linenr; /* loop variable */
18 int check; /* check if a new process must be spawned */
19 struct slotent *slotp; /* slots[] pointer */
20 struct ttyent *ttyp; /* ttytab entry */
21 ...
22 check = 1;
23 while (1) {
24 while ((pid = waitpid(-1, NULL, check ? WNOHANG : 0)) > 0) {
25 /* Search to see which line terminated. */
26 for (linenr = 0; linenr < PIDSLOTS; linenr++) {
27 slotp = &slots[linenr];
28 if (slotp->pid == pid) {
29 /* Record process exiting. */
30 wtmp(DEAD_PROCESS, linenr, NULL, pid);
31 slotp->pid = NO_PID;
32 check = 1;
33 }
34 }
35 }
36
37 /* If a signal 1 (SIGHUP) is received, simply reset error counts. */
38 if (gothup) {
39 gothup = 0;
40 for (linenr = 0; linenr < PIDSLOTS; linenr++) {
41 slots[linenr].errct = 0;
42 }
43 check = 1;
44 }
45
46 /* Shut down on signal 6 (SIGABRT). */
47 if (gotabrt) {
48 gotabrt = 0;
49 startup(0, &TT_REBOOT);
50 }
51
52 if (spawn && check) {
53 /* See which lines need a login process started up. */
54 for (linenr = 0; linenr < PIDSLOTS; linenr++) {
55 slotp = &slots[linenr];
56 if ((ttyp = getttyent()) == NULL) break;
57 if (ttyp->ty_getty != NULL
58 && ttyp->ty_getty[0] != NULL
59 && slotp->pid == NO_PID
60 && slotp->errct < ERRCT_DISABLE)
61 {
62 startup(linenr, ttyp);
63 }
64 }

29
UEX. Diseño de Sistemas Operativos

65 endttyent();
66 }
67 check = 0;
68 }
69 }

Código 4.21 La rutina main (III). El bucle principal.

La primera iteración del bucle abre los login por primera vez. Veamos cómo. En esta ocasión (línea
24) waitpid no suspende init, ya que vuelve inmediatamente con error debido a que init no tiene
ningún hijo por el que esperar y, a continuación, explora /etc/ttytab haciendo uso de su interfaz (bucle
54-65). Para cada entrada se abre un login vía startup (Fig. 4.25).

Fig. 4.25 Los login son registrados por su pid en el vector slots.

La segunda iteración del bucle principal y las siguientes sí se bloquean en waitpid. Esta llamada
vuelve cuando algún hijo termina o cuando init recibe una señal. Cuando un hijo ha terminado, se
realiza un escrutinio (línea 26) de todas las entradas de slots para detectar cuál es el terminal
asociado al hijo muerto. Entonces se marca esa entrada como libre (línea 31) y se registra el logout
en el fichero /etc/adm/wtmp. A continuación se entra de nuevo en el bucle 54-65, que detectará un
terminal libre (línea 59) en el que arrancar de nuevo login. Esta es la razón de que al teclear Control-
D en el proceso login aparezca el prompt "login: " de nuevo y recurrentemente.
Startup
Init delega en startup la creación los procesos hijos login. Su primer parámetro es el índice del
objeto slot en que va a registrar el proceso login. El segundo parámetro es la descripción del terminal
asociado. Startup abre un hijo login, pero no espera por él. El Código 4.22 muestra el trabajo del
padre, que simplemente almacena el pid del hijo en la entrada linenr del vector slots (línea 27) y
registra el login en /etc/adm/wtmp (línea 29).
1 void startup(int linenr, struct ttyent *ttyp)
2 {
3 /* Fork off a process for the indicated line. */
4
5 struct slotent *slotp; /* pointer to ttyslot */
6 pid_t pid; /* new pid */
7 int err[2]; /* error reporting pipe */
8 char line[32]; /* tty device name */
9 int status;
10
11 slotp = &slots[linenr];
12
13 /* Error channel for between fork and exec. */
14 if (pipe(err) < 0) err[0] = err[1] = -1;
15
16 if ((pid = fork()) == -1 ) {
17 report(2, "fork()");
18 sleep(10);
19 return;
20 }

30
La inicialización del sistema

21
22 if (pid == 0) {
23 ...
24 }
25
26 /* Parent */
27 if (ttyp != &TT_REBOOT) slotp->pid = pid;
28 ...
29 if (ttyp != &TT_REBOOT) wtmp(LOGIN_PROCESS, linenr, ttyp->ty_name, pid);
30 slotp->errct = 0;
31 }

Código 4.22 La rutina startup. El padre.

El Código 4.22 muestra el trabajo del hijo.


1 void startup(int linenr, struct ttyent *ttyp)
2 {
3 ...
4 char line[32]; /* tty device name */
5 ...
6 if ((pid = fork()) == -1 ) {
7 ...
8 }
9
10 if (pid == 0) {
11 /* Child */
12 ...
13
14 /* A new session. */
15 setsid();
16
17 /* Construct device name. */
18 strcpy(line, "/dev/");
19 strncat(line, ttyp->ty_name, sizeof(line) - 6);
20
21 /* Open the line for standard input and output. */
22 close(0);
23 close(1);
24 if (open(line, O_RDWR) < 0 || dup(0) < 0) {
25 write(err[1], &errno, sizeof(errno));
26 _exit(1);
27 }
28
29 if (ttyp->ty_init != NULL && ttyp->ty_init[0] != NULL) {
30 /* Execute a command to initialize the terminal line. */
31 if ((pid = fork()) == -1) {
32 report(2, "fork()");
33 errno= 0;
34 write(err[1], &errno, sizeof(errno));
35 _exit(1);
36 }
37
38 if (pid == 0) {
39 alarm(10);
40 execute(ttyp->ty_init);
41 report(2, ttyp->ty_init[0]);
42 _exit(1);
43 }
44
45 while (waitpid(pid, &status, 0) != pid) {}
46 ...
47 }
48
49 /* Redirect standard error too. */
50 dup2(0, 2);
51
52 /* Execute the getty process. */

31
UEX. Diseño de Sistemas Operativos

53 execute(ttyp->ty_getty);
54 ...
55 }
56
57 /* Parent */
58 ...
59 }

Código 4.23 La rutina startup. El hijo.

Lo primero que hace es configurarse como líder de sesión y cerrar la entrada y salida estándar
heredada de init, /dev/null y /dev/log respectivamente. Como a continuación abre un terminal de
/etc/ttytab como entrada y salida estándar (líneas 17-27), este se convierte en su terminal de control.
Como líder de sesión, este terminal también será el terminal de control de todo el grupo de procesos
formado por sus descendientes. Las señales que genere la tarea del terminal de control serán enviadas
a todo el grupo. A continuación se configura el terminal de control ejecutando en un proceso hijo el
mandato stty de su entrada en /etc/ttytab (líneas 29-47). Finalmente, init muta a getty. La figura
Fig. 4.26 ilustra startup.

Fig. 4.26 La rutina startup.

4.7 Referencias
[1] Tanenbaum, A. S., "Operating Systems, Design and Implementation", Second Edition, Prentice-
Hall, 1997.

32

Potrebbero piacerti anche