Está en la página 1de 14

Tema 1.

Arquitectura y
procesos

1.1 ¿Por qué una disciplina de diseño de sistemas operativos?


Tras haber adquirido cierta experiencia en la programación de aplicaciones y tras cursar una asignatura
clásica sobre sistemas operativos, el estudiante ve al sistema operativo como un conjunto de módulos y
rutinas que implementan servicios de alto nivel, las llamadas al sistema, que son bien comprendidos en
su uso y función. Ahora bien, es muy probable que considere que la organización interna de los mismos
sea tan compleja que resulte impenetrable tanto para él como para el propio profesor de teoría de
sistemas operativos. Es común la opinión de que un sistema operativo es construido por un pequeño
equipo de programadores privilegiados, gurús de la informática y únicos conocedores de técnicas
recursos que son la clave de la construcción del sistema. Lo que es peor, casi siempre estos arcanos
procedimientos son dependientes de la arquitectura concreta. El enorme desequilibrio que se percibe en
la literatura clásica sobre sistemas operativos entre teoría y práctica parece apoyar esta impresión y, lo
que es más, ha contribuido a extenderla.
El propósito de la asignatura de Diseño de Sistemas Operativos no es otro sino el de despejar esta
especie de tabú que es el código interno del sistema y mostrar que el proceso de diseño e
implementación de un sistema operativo puede ser -y debe ser- objeto de un tratamiento organizado y
sistemático como cualquier otra disciplina científica, sea o no sea del ámbito de la informática. A lo
largo del curso examinaremos cómo se implementan los conceptos que ya se conocen en teoría: las
interrupciones de los dispositivos, el planificador de procesos, los manejadores de dispositivos, la
gestión de la memoria o el sistema de ficheros. Y lo más importante, percibiremos cómo estas piezas
encajan entre sí para formar un sistema operativo de propósito general.
La clave del éxito estriba organizar todos estos componentes en una jerarquía de niveles de abstracción
con una interconexión entre ellos sencilla y clara ([1]). Hay que tener en cuenta que un sistema
operativo es un ingenio complejo. Construye una máquina abstracta que ofrece servicios de alto nivel a
partir de una máquina real con unas capacidades muchísimo más primitivas. Permite que muchos
usuarios compartan una sola de tales máquinas sin ser conscientes de ello y bajo medidas de protección
de cada usuario en particular. El sistema operativo gestiona y controla incluso cientos de dispositivos de
características muy diferentes. Históricamente, la consecución de estos objetivos no ha sido fácil. La
labor de investigación en sistemas operativos ha sido ingente y está plagada de sonoros fracasos. No
obstante, de todos ellos hemos aprendido lecciones. La asignatura de Diseño de Sistemas Operativos
tiene como objetivo el impartir algunas de ellas.

1
El diseño de cualquier sistema complejo como un sistema operativo tiene un componente muy
importante de arte e ingenio. No existe una metodología a seguir en la construcción de un sistema
operativo. La definición de una estructura por capas ayuda, pero no lo es todo. No existe, al menos hoy
día, una teoría establecida que nos asista en la tarea de crear un sistema operativo a partir de una
especificación de requisitos. En esta situación, llegar a un acuerdo ampliamente aceptado sobre cuáles
deben ser los contenidos de una asignatura como Diseño de Sistemas Operativos no resulta sencillo. Es
posible que la aproximación más adecuada sea estudiar un buen modelo, un caso de estudio. Para que
resulte útil en la docencia, este modelo de sistema operativo debe tener unos criterios de diseño bien
definidos y debe ser lo suficientemente sencillo para que permita ser comprendido en su totalidad por
una sola persona. Pero, ¿este sistema existe y es accesible? Afortunadamente sí. Es el sistema operativo
MINIX. Un sistema creado para ser enseñado. Del mismo modo que Niklaus Wirth diseñó el lenguaje
Pascal para mostrar lo en su opinión debía ser un lenguaje de programación, Andrew Tanenbaum ha
escrito MINIX para revelar sus ideas acerca de lo que debe ser el diseño y la implementación de un
sistema operativo. MINIX es el modelo sobre el que trabajaremos en la asignatura. Lo estudiaremos en
la clase de teoría y lo modificaremos en la clase de prácticas.

1.2 Historia de MINIX


El nombre de MINIX deriva del de UNIX. Hagamos un poco de historia. Los sistemas de tiempo
compartido nacen en el MIT, el Instituto Tecnológico de Massachussets, a principios de los 60, a través
de los proyectos BASIC, CTSS (Computer Timeshared System) y finalmente MULTICS
(MULTiplexed Information and Computing Service). MULTICS fue en principio un proyecto conjunto
del MIT, ATT y General Electric. Esta última se retiraría del mismo posteriormente.
Ken Thompson, un investigador del MIT que había trabajado en el proyecto MULTICS decidió escribir
una versión de éste en miniatura para un computador Digital PDP-7, una máquina que en aquel
momento nadie utilizaba en el MIT. Brian Kernigham, en tono jocoso, llamó al sistema de Ken
Thompsom UNICS (UNiplexed Information and Computing Service) porque era obra de un sólo
hombre frente a las docenas de programadores que participaron en el proyecto MULTICS. El éxito de
UNICS para el PDP-7 llevó a que se proporcionase a Ken Thompson una máquina más moderna, el
PDP-11. UNICS estaba escrito en ensamblador y el trabajo de reescritura para la nueva arquitectura
hizo pensar en que esta debía llevarse a cabo en un lenguaje de alto nivel. Por entonces, Dennis Ritchie
había diseñado e implementado un lenguaje denominado C. Thompson y Ritchie reescribieron UNICS
en C para el PDP-11 y decidieron que UNIX, que se pronunciaba igual que UNICS, era un nombre más
atractivo.
Un famoso artículo del año 1974 que describía UNIX ([2]) atrajo la atención de las Universidades,
que solicitaron de ATT el código fuente de UNIX a fin de estudiarlo y explicarlo en las aulas. Muy
pronto, UNIX logró una amplia aceptación en la comunidad científica, de modo que ATT se vio
obligada a restringir su uso haciendo valer sus derechos de propiedad. A pesar de todo, el interés por
UNIX siguió extendiéndose. La Universidad de California, mediante un contrato con ATT, portó la
versión siete de UNIX, UNIX V7, a la arquitectura Digital VAX. Este UNIX fue bautizado como
UNIX BSD. Las modificaciones de ATT sobre UNIX V7 dieron lugar a otro sistema UNIX, UNIX
SYSTEM V. Ambas versiones UNIX eran parcialmente incompatibles (tenían varias llamadas al
sistema diferentes). Fueron reconciliadas posteriormente por el estándar IEEE POSIX.
La consecuencia del éxito comercial de UNIX fue la prohibición de que su código fuente no pudiera ser
explicado y discutido en las aulas universitarias. El "saber hacer" en el ámbito de los sistemas
operativos volvía a ser monopolizado, como en el pasado, por un reducido grupo de personas y
empresas. Ante esta situación, un profesor de la Universidad de Vrije, en Amsterdam, Andrew
Tanenbaum, se hizo con un ordenador personal IBM PC y decidió emular a Ken Thompson escribiendo
un sistema operativo para la arquitectura PC. Este nuevo sistema operativo ofrecía la misma
funcionalidad, es decir, las mismas llamadas al sistema que UNIX V7 y era de un tamaño mucho más
reducido, por lo que su autor lo bautizo como MINIX (Mini UNIX). El código de MINIX, como una
vez lo fue UNIX, es público y es ahora ampliamente utilizado en las universidades en cursos de

2
sistemas operativos. La versión 1.2 de MINIX fue publicada como un apéndice de la referencia [3]. La
versión 1.5 de MINIX, más completa, trata de ser, al menos en parte, conforme al estándar POSIX, lo
que provocó, entre otras cosas, un aumento en cinco llamadas al sistema, de cuarenta y cuatro a
cuarenta y nueve. Las nuevas llamadas son fcntl, mkdir, rmdir, ptrace y rename, y un
aumento de tamaño de 12000 a 20000 líneas. La versión 2.0.0 ([4]) es conforme POSIX 1003.1a. La
Fig. 1.1 muestra la evolución de MINIX a lo largo del tiempo:

Fig. 1.1 La historia de MINIX

La migración a otros procesadores de MINIX 1.5 se abandonó. La rama Linux fue tomada por
alguien que se cansó de las limitaciones de MINIX-386 y comenzó a construir su propio sistema
UNIX: Linux.

1.3 La arquitectura del sistema operativo


¿Cuál es la mejor arquitectura de sistema operativo? Este debate produjo un enconado encuentro
([6]) en el grupo de noticias de MINIX, comp.os.minix, entre Andy Tanenbaum y Linus
Torvalds, padre de LINUX. A continuación examinamos las arquitecturas clásicas de sistema
operativo.

1.3.1 La arquitectura plana


Esta arquitectura ha sido adoptada tradicionalmente por muchos sistemas de tiempo real. Como
muestra la Fig. 1.2, integra todos los componentes de la aplicación en el mismo espacio de
direccionamiento que el sistema operativo.

3
Fig. 1.2 Arquitectura plana de sistema operativo

La arquitectura plana suele emplearse en sistemas empotrados pequeños o procesadores sin


facilidades de protección de memoria, como procesadores digitales de señal (DSPs). La carencia de
protección conlleva el que si un proceso falla, por ejemplo a causa de un puntero C erróneo, puede
escribir sobre código o datos del núcleo, de modo que todo el sistema se corrompe. Además, en
cuanto el sistema crece, la probabilidad del fallo se multiplica. Imaginemos que una nueva
funcionalidad (proceso de usuario o manejador de dispositivo) se incorpora al sistema. La fiabilidad
de todo él depende del nuevo componente. Por otra parte, cada fallo es difícil de localizar. Incluso las
mejores herramientas pueden ser incapaces de aislarlo y es preciso que una persona conozca bien
todo el sistema completo. Las limitaciones de la arquitectura plana, por lo tanto, son patentes.

1.3.2 La arquitectura monolítica


Es un intento de paliar los problemas de la arquitectura plana. La aportación de la arquitectura
monolítica estriba en que los procesos de usuario ejecutan en espacios de direccionamiento diferentes
al del sistema operativo, según muestra la Fig. 1.3.

Fig. 1.3 Arquitectura monolítica

Un sistema operativo monolítico es una colección de procedimientos compilados en un único


programa objeto donde cada procedimiento es visible a todos los demás y es libre de realizar
llamadas a los procedimientos que considere útiles sin restricción alguna. Las implementaciones
comerciales de UNIX han respondido tradicionalmente a un diseño de este patrón.
Este diseño parece resolver todos los problemas encontrados en la arquitectura plana. No es así. Es
cierto que hemos aislado al sistema de los errores de los procesos de usuario, pero nuevos
dispositivos aparecen en el mercado continuamente y es preciso escribir manejadores para
soportarlos. De nuevo el sistema crece y la probabilidad del fallo aumenta. El fallo, ahora también
compromete la operación del núcleo y, por lo tanto, del sistema completo.
Incluso en los sistemas monolíticos puede introducirse cierta estructura, formada por tres niveles
según se ilustra en la Fig. 1.4. En el nivel superior figura un procedimiento de entrada al núcleo,
denominado procedimiento principal. Este procedimiento llama a otro diferente denominado
procedimiento de servicio. El conjunto de los procedimientos de servicio constituyen el nivel
inmediatamente inferior. Hay tantos procedimientos de servicio como llamadas al sistema. Los

4
procedimientos de servicio disponen de una amplia colección de procedimientos de utilidad que se
sitúan en el nivel más bajo de los tres.

Fig. 1.4 Estructura interna de un sistema operativo monolítico

El programa de usuario lleva a cabo las llamadas al sistema mediante interrupciones software o traps.
Una interrupción software especifica un vector de interrupción que, como su nombre indica, apunta a
una rutina de interrupción. ¿Por qué es necesaria una instrucción de interrupción software y no una
instrucción de salto? Debido a que esta instrucción de interrupción software conmuta el procesador
de modo usuario a modo supervisor, según ilustra la Fig. 1.5. Sólo en modo supervisor se permite al
procesador ejecutar instrucciones privilegiadas como los accesos a las posiciones de memoria
asignadas a los adaptadores de dispositivo o la copia de datos entre espacios de direccionamiento
diferentes. Uno de los parámetros del trap identifica la llamada al sistema solicitada y el resto
contienen los parámetros de la misma.

Fig. 1.5 Modos de ejecución de un proceso en el contexto de un sistema operativo

1.3.3 La arquitectura micronúcleo: MINIX


Frente a los sistemas monolíticos, MINIX hace uso del paradigma cliente-servidor. La tendencia en
los sistemas operativos modernos es disponer de un núcleo lo más pequeño posible, procurando que
algunas de sus funciones -cuantas más mejor- sean desplazadas fuera del mismo y sean
implementadas como programas de usuario. Este nuevo núcleo reducido se viene denominando
microkernel. La Fig. 1.6 muestra la arquitectura de MINIX. MINIX extrae la gestión de memoria y el
servicio de ficheros del sistema operativo y los implementa como procesos de usuario con su propio
espacio de direccionamiento.

5
Fig. 1.6 Arquitectura microkernel de MINIX

Bajo la filosofía microkernel, un proceso de usuario que solicita un servicio del sistema se denomina
cliente. Un proceso de usuario que provee un servicio se denomina servidor. Entre estos últimos
figuran el sistema de ficheros y el gestor de memoria. Los manejadores de los dispositivos también
toman la forma de servidores; no obstante, se mantienen integrados en el núcleo para facilitar su
acceso a los dispositivos físicos. La labor del microkernel es soportar la comunicación entre clientes
y servidores, que se realiza mediante el envío de mensajes. Una de las ventajas de un sistema
microkernel es que las partes se dividen por fronteras bien definidas, por lo que resultan más
comprensibles y manejables. Una segunda ventaja es su idoneidad para construir sistemas
distribuidos, donde clientes y servidores ejecutan en máquinas diferentes y se intercambian los
mensajes a través de mecanismos de red.

1.3.4 La arquitectura micronúcleo: QNX


Un paso más allá en la evolución de la arquitectura microkernel es el sistema operativo QNX, que
muestra la Fig. 1.7. QNX, como MINIX, construye los manejadores de dispositivo como procesos,
pero con la ventaja adicional de que los desplaza fuera del núcleo en espacios de direccionamiento
propios. Este es el diseño más fiable de todos por que es el que proporciona mayor protección. La
versión 3.0 de MINIX ([5]) incorpora esta mejora.

Fig. 1.7 Arquitectura microkernel de QNX

1.4 Procesos y comunicación de procesos


A diferencia de un sistema monolítico como Linux, MINIX se estructura en cuatro niveles, tal y
como ilustra la Fig. 1.8.

6
Fig. 1.8 MINIX está estructurado en cuatro niveles de procesos

El nivel uno, el más profundo, que de aquí en adelante denominaremos "el núcleo" tiene dos
funciones fundamentales. La primera es proporcionar a los niveles superiores el concepto de proceso.
Se encarga de gestionar interrupciones y traps salvando el estado del proceso en ejecución en su
descriptor. La segunda es soportar el mecanismo de comunicación entre procesos a través de
mensajes. Sólo una mínima parte del nivel uno está escrita en ensamblador. El resto y los demás
niveles están escritos en lenguaje C. Los capítulos tres y cuatro están dedicados al estudio de este
nivel.
El nivel dos contiene los manejadores de los dispositivos de entrada-salida (teclado, disco, etc). Los
manejadores de dispositivos son conocidos como drivers pero, de aquí en adelante les llamaremos
tareas de entrada-salida o simplemente tareas. Existe una tarea para cada tipo de dispositivo. Si un
nuevo dispositivo se añade al sistema, es necesario incorporar una nueva tarea. El código del nivel
uno y las tareas se compilan conjuntamente en MINIX en un único espacio de direccionamiento. A
pesar de compartir un mismo espacio de direccionamiento, las tareas se comportan como procesos
independientes y se comunican entre sí mediante el mecanismo de paso mensajes del sistema, igual
que lo hacen los servidores y los procesos de usuario.
El nivel tres contiene tres procesos denominados servidores. Son el gestor de memoria, que sirve las
llamadas al sistema como fork, exec y brk, el sistema de ficheros, que sirve llamadas al sistema
como open, read, etc, y, finalmente el servidor de red, que implementa la pila TCP/IP. El sistema
de ficheros de MINIX ha sido diseñado como un servidor de ficheros, de manera que es sencillo
portarlo a una máquina diferente como servidor remoto. Lo mismo ocurre con el gestor de memoria.
En el nivel cuatro se sitúan los procesos de usuario, entre ellos init.
Cada tarea o proceso en un sistema MINIX tiene asociado un número de proceso. Cada proceso en el
sistema ocupa una entrada en la tabla de descriptores de proceso. El número de proceso es el índice del
proceso en la tabla (Fig. 1.9). Las primitivas de paso de mensajes send, receive y sendrec especifican
números de proceso en sus parámetros de dirección.

Fig. 1.9 Números de proceso en la tabla de descriptores del núcleo

Así, el manejador de la impresora tiene un número de proceso -7 y el proceso init un número de


proceso 1. El gestor de memoria siempre tiene el número de proceso 0. Si existen nueve tareas en el

7
sistema, la tarea del terminal tiene siempre el número de proceso -9 y ocupa la primera entrada. Si se
introduce una nueva tarea, como un controlador de CD ROM, entonces esta pasa a tener número de
proceso -9 y el manejador del terminal número de proceso -10. Todos los demás procesos son
desplazados un lugar en la tabla, pero su número de proceso se mantiene.
La tarea -1 (HARDWARE) es una tarea ficticia. En MINIX, las rutinas de interrupción de los dispositivos
son cortas. Se limitan a construir un mensaje, que envían a la tarea que controla tal dispositivo,
indicándoles que hay trabajo que hacer. Estos mensajes se consideran procedentes de una tarea virtual,
la tarea -1. Lo importante de este truco es que consigue una abstracción del hardware como una tarea
que envía mensajes al resto de ellas, no recibe ninguno y no puede bloquearse.
El descriptor de proceso está partido en tres partes, de modo que realmente existen tres tablas de
descriptores. Una reside en el núcleo, otra en el gestor de memoria y otra en el sistema de ficheros.
Estas últimas no tienen entradas para las tareas, de modo que el primer descriptor corresponde al
gestor de memoria, que conserva el número de proceso 0.

Fig. 1.10 En MINIX un mensaje es una porción de memoria de tamaño fijo que se copia entre dos
espacios de direccionamiento

Observemos la Fig. 1.10. P1 y P2 son procesos. Como tales tienen espacios de direccionamiento
distintos. En un sistema UNIX, un intento de P1 de escribir en datos de P2 provoca que el proceso
muere y se reciba en pantalla un mensaje parecido a "segmentation fault". El paso de mensajes
resuelve el problema de la copia entre espacios de direccionamientos distintos. Para copiar el
mensaje msg1 en P2, P1 invoca el procedimiento send. Más que un procedimiento, send es una
llamada al sistema. El núcleo bloquea P1 hasta que realiza la copia. En P2 ocurren las cosas de forma
simétrica. P2 invoca la llamada al sistema receive para hacer saber al sistema que la copia ha de
realizarse en msg2. P2 queda entonces bloqueado hasta que la copia se produce.
El núcleo de MINIX realmente soporta únicamente tres llamadas al sistema, encargadas de llevar a cabo
el paso de mensajes. Son send, receive y sendrec. Los prototipos de estas funciones son:
int send (int dst, message *msg);
int receive(int src, message *msg);
int sendrec(int dst, message *msg);
Send envía el mensaje msg al proceso destinatario cuyo número de proceso es dst. Bloquea al
proceso invocante hasta que el receptor llaga a la cita. Receive bloquea al proceso invocante hasta
que en msg se recibe un mensaje procedente de el mensaje msg al proceso destinatario cuyo número
de proceso es src. Sendrec envía el mensaje msg al proceso destinatario cuyo número de proceso es
dst. Y bloquea al proceso invocante hasta que llega la réplica, que se almacena en msg,
sobrescribiendo su contenido. A los procesos de usuario sólo les está permitido invocar sendrec. El
autor de MINIX considera que el uso de send y receive en los programas de usuario les hace difícil
de entender y mantener, dando lugar a lo que denomina programación spaghetti. Considera tan nocivo
su uso en comunicación de procesos como la sentencia goto en programación estructurada.
Un proceso de usuario usa sendrec para solicitar los servicios del gestor de memoria o el sistema de
ficheros. No puede comunicarse directamente con las tareas de entrada-salida. Las funciones UNIX
read, fork, etc. están implementadas como funciones de biblioteca sobre sendrec, tal y como
veremos más adelante.

8
La Fig. 1.11 muestra un esquema simplificado de una operación hipotética de lectura por parte de un
proceso de usuario MINIX. En ella pueden apreciarse los actores implicados y los mensajes
intercambiados por estos.

Fig. 1.11 Actores implicados en la E/S y mensajes intercambiados

El proceso de usuario realiza una llamada al sistema read. Su implementación invoca la llamada al
sistema sendrec, que envía un mensaje al sistema de ficheros y bloquea al proceso esperando la
réplica.
1. El sistema de ficheros comprueba que los parámetros del mensaje, los permisos, etc., son
correctos y envía (también vía sendrec) un mensaje al manejador de dispositivo pertinente, por
ejemplo el del terminal.
2. Si el manejador establece que los datos no están disponibles, envía un mensaje al sistema de
ficheros para hacérselo saber y desbloquearlo. El proceso de usuario continúa bloqueado en
sendrec.
3. El dispositivo proporciona los datos e interrumpe. La rutina de interrupción construye un
mensaje que envía a la tarea.
4. La tarea recoge los datos recién llegados y los copia al espacio de direccionamiento del programa
de usuario que espera por ellos.
5. La tarea envía un mensaje al sistema de ficheros comunicándole que puede despertar al proceso
de usuario.
6. El sistema de ficheros envía un mensaje al programa de usuario con el resultado de la operación
de lectura. El programa lo recibe y sale de sendrec.
Los manejadores de dispositivo son procesos de pleno derecho, con su contador de programa,
descriptor de proceso, pila, etc. Todos los manejadores en MINIX siguen el mismo patrón. El Fig. 1.12
muestra el esqueleto de una tarea de entrada-salida.
1 mensaje mens;
2 tarea()
3 {
4 int r, emisor;
5
6 inicializar();
7 while(TRUE)
8 {
9 receive(ANY, &mens);
10 emisor = mens.fuente;
11 switch(mens.tipo)
12 {
13 case READ: r = do_read(); break;
14 case WRITE: r = do_write(); break;
15 case INTERRUP: r = do_interrup(); break;
16 case OTRO: r = do_otro(); break;
17 default: r = ERROR;
18 }
19 mens.tipo = TASK_REPLAY;

9
20 mens.REP_STATUS = r;
21 send(emisor, &mens);
22 }
23 }

Fig. 1.12 Esqueleto de una tarea de entrada-salida en MINIX

Como puede apreciarse, tras una inicialización de sus estructuras de datos, una tarea de entrada-
salida realiza un bucle infinito de espera por el mensaje, despacho del mismo y envío de la réplica. El
mensaje contiene un campo que especifica el procedimiento que lo sirve y otro campo que especifica
el remitente. Cuando se ha despachado el mensaje, la tarea replica al emisor con un código de retorno
en otro de los campos del mensaje.

1.5 El planificador
La función por excelencia del núcleo de un sistema operativo es la conmutación de procesos. En un
instante dado, un número determinado de procesos están dispuestos para ejecutar y el resto no lo está,
por estar suspendidos en el envío o recepción de un mensaje. En un sistema con un solo procesador, de
todos los procesos dispuestos para ejecutar sólo uno puede hacerlo. El planificador es un algoritmo que
decide cuál de ellos.
Una estructura de datos fundamental en el núcleo es la cola de los descriptores de procesos dispuestos.
En MINIX, esta cola está estructurada en tres niveles (Fig. 1.13) según su arquitectura. Una cola de
tareas de E/S, la más prioritaria, una cola de servidores y una cola de procesos de usuario, la menos
prioritaria. Es una variable global del núcleo definida en el fichero /usr/src/kernel/proc.h
según
struct proc *rdy_head[NQ]; /* pointers to ready list headers */
struct proc *rdy_tail[NQ]; /* pointers to ready list tails */

Fig. 1.13 Colas de procesos mantenida por el planificador de MINIX

Rdy_head apunta al comienzo de las colas. Dentro de cada cola se utiliza el algoritmo Round Robin,
de modo que cuando un proceso se reanuda, pasa al final de la cola. También pasa al final de la cola un
proceso de usuario que ha completado su quantum, de modo que rdy_tail es útil para acelerar este
trabajo.
Conocida la estructura de datos sobre la que opera, el algoritmo de planificación de MINIX resulta
extremadamente sencillo: encontrar cuál es la cola más prioritaria que no está vacía y, una vez
identificada, escoger el proceso que está a la cabeza de la misma. Si todas las colas están vacías se
ejecuta la tarea IDLE. Admirablemente, toda la labor de planificación del procesador se realiza en
¡cuatro rutinas! que ocupan alrededor de ciento cincuenta líneas de código C. La mayoría de los
textos sobre sistemas operativos dedican un capítulo completo a la planificación. El lector puede
sacar de esto sus propias conclusiones. Estas rutinas son pick_proc, ready, unready y sched.
1 PRIVATE void pick_proc()
2 {
3 /* Decide who to run now. A new process is selected by setting 'proc_ptr'.
4 * When a fresh user (or idle) process is selected, record it in 'bill_ptr',
5 * so the clock task can tell who to bill for system time.
6 */
7
8 register struct proc *rp; /* process to run */
9

10
10 if ( (rp = rdy_head[TASK_Q]) != NIL_PROC) {
11 proc_ptr = rp;
12 return;
13 }
14 if ( (rp = rdy_head[SERVER_Q]) != NIL_PROC) {
15 proc_ptr = rp;
16 return;
17 }
18 if ( (rp = rdy_head[USER_Q]) != NIL_PROC) {
19 proc_ptr = rp;
20 bill_ptr = rp;
21 return;
22 }
23 /* No one is ready. Run the idle task. The idle task might be made an
24 * always-ready user task to avoid this special case.
25 */
26 bill_ptr = proc_ptr = proc_addr(IDLE);
27 }

Código 1.1 La rutina pick_proc

Pick_proc examina las colas y decide quién toma el procesador. La rutina se autocomenta. Es un
ejemplo de lo sencillo y conciso que es el código cuando las estructuras de datos están bien definidas. Sí
cabe resaltar el papel de dos variables globales importantes, proc_ptr y bill_ptr. Pick_proc
examine las colas de descriptores, escoja un nuevo proceso dispuesto y hace que proc_ptr apunte a
este. Cuando el elegido es un proceso de usuario, su dirección también se hace apuntar por
bill_proc a fin de que la generar la contabilidad de uso de procesador que hace el proceso. Esta
información la necesita el planificador y el servicio de la llamada al sistema times.
La línea final es interesante. Cuando ningún proceso está dispuesto, es decir, no hay ningún proceso en
las colas, se elige la tarea ociosa, IDDLE. La tarea IDDLE se define en el fichero mpx88.x a
diferencia del resto de las tareas, que se definen en ficheros C.
1 idle_task: ! executed when there is no work
2 jmp _idle_task

Código 1.2 La tarea IDLE

También se diferencia del resto de tareas en que es una entrada adicional del núcleo, como puede ser
una interrupción o una llamada al sistema. Así, cuando el sistema está ocioso, el núcleo ejecuta este
bucle infinito en espera de una interrupción de dispositivo. No obstante, independientemente del
módulo dónde se encuentra su código fuente, IDLE se comporta como el resto de las tareas, ya que
tiene su propia pila y su propio código y dispone de su propio descriptor de tarea. IDLE no invoca a
ninguna rutina, de modo que su pila es más pequeña que el resto. La pila de IDLE tiene tan sólo 20
octetos, el tamaño necesario y suficiente para soportar la interrupción que reactive el sistema.
Cuando un proceso queda dispuesto, por ejemplo por haber recibido un mensaje que estaba esperando,
su descriptor ha de ser insertado en las colas de la Fig. 1.13 a fin de que el planificador pueda otorgarle
el procesador. El procedimiento ready (Código 1.3) añade un descriptor a la estructura de colas.
Ready está estructurada como pick_proc. Toma un parámetro que es un puntero al descriptor que
va a instalarse en las colas y comprueba si el descriptor es una tarea, un servidor o un proceso de
usuario. Una vez determinada la cola apropiada, añade a esta el descriptor.
1 PRIVATE void ready(rp)
2 register struct proc *rp; /* this process is now runnable */
3 {
4 /* Add 'rp' to the end of one of the queues of runnable processes. Three
5 * queues are maintained:
6 * TASK_Q - (highest priority) for runnable tasks
7 * SERVER_Q - (middle priority) for MM and FS only
8 * USER_Q - (lowest priority) for user processes
9 */
10
11 if (istaskp(rp)) {
12 if (rdy_head[TASK_Q] != NIL_PROC)
13 /* Add to tail of nonempty queue. */

11
14 rdy_tail[TASK_Q]->p_nextready = rp;
15 else {
16 proc_ptr = /* run fresh task next */
17 rdy_head[TASK_Q] = rp; /* add to empty queue */
18 }
19 rdy_tail[TASK_Q] = rp;
20 rp->p_nextready = NIL_PROC; /* new entry has no successor */
21 return;
22 }
23 if (!isuserp(rp)) { /* others are similar */
24 if (rdy_head[SERVER_Q] != NIL_PROC)
25 rdy_tail[SERVER_Q]->p_nextready = rp;
26 else
27 rdy_head[SERVER_Q] = rp;
28 rdy_tail[SERVER_Q] = rp;
29 rp->p_nextready = NIL_PROC;
30 return;
31 }
32 if (rdy_head[USER_Q] == NIL_PROC)
33 rdy_tail[USER_Q] = rp;
34 rp->p_nextready = rdy_head[USER_Q];
35 rdy_head[USER_Q] = rp;
36 }

Código 1.3 La rutina ready

Cuando una tarea de entrada-salida queda dispuesta, pasa al último lugar de la cola y ready retorna,
pero cuando la cola estaba vacía, además de la inserción en la cola, a la tarea se le asigna el procesador
(línea 17) sin tener en cuenta a pick_proc, que es quien normalmente toma esta decisión. La cola
más prioritaria es la de las tareas de dispositivos, de modo que con una única tarea, pick_proc
tomaría la decisión de ejecutarla. Si esta decisión ya ha sido tomada por ready, se produce el ahorro
de la llamada a pick_proc que, aunque sencilla, consume ciclos preciosos. La estrategia va en
menoscabo de la estructura del código pero aumenta la eficiencia de la conmutación de procesos. Hay
que tener en cuenta que el planificador se ejecuta cada vez que se produce una transición al núcleo, de
modo que consume una parte apreciable de los recursos del sistema. Un diseño eficiente del mismo
puede suponer algún sacrificio en su estructura.
Unready (Código 1.4) se invoca cuando un proceso cesa en su estado de dispuesto al quedar
bloqueado en una llamada al sistema y es preciso sacarle de la cola de procesos activos. También se
invoca para sacar de la cola un proceso de usuario a quien mata una señal. Como ready, unready
toma un único parámetro que es la dirección del descriptor del proceso implicado.
1 PRIVATE void unready(rp)
2 register struct proc *rp; /* this process is no longer runnable */
3 {
4 /* A process has blocked. */
5
6 register struct proc *xp;
7 register struct proc **qtail; /* TASK_Q, SERVER_Q, or USER_Q rdy_tail */
8
9 if (istaskp(rp)) {
10 if ( (xp = rdy_head[TASK_Q]) == NIL_PROC) return;
11 if (xp == rp) {
12 /* Remove head of queue */
13 rdy_head[TASK_Q] = xp->p_nextready;
14 if (rp == proc_ptr) pick_proc();
15 return;
16 }
17 qtail = &rdy_tail[TASK_Q];
18 }
19 else if (!isuserp(rp)) {
20 if ( (xp = rdy_head[SERVER_Q]) == NIL_PROC) return;
21 if (xp == rp) {
22 rdy_head[SERVER_Q] = xp->p_nextready;
23 pick_proc();
24 return;
25 }
26 qtail = &rdy_tail[SERVER_Q];
27 } else {
28 if ( (xp = rdy_head[USER_Q]) == NIL_PROC) return;
29 if (xp == rp) {
30 rdy_head[USER_Q] = xp->p_nextready;

12
31 pick_proc();
32 return;
33 }
34 qtail = &rdy_tail[USER_Q];
35 }
36
37 /* Search body of queue. A process can be made unready even if it is
38 * not running by being sent a signal that kills it. */
39 while (xp->p_nextready != rp)
40 if ( (xp = xp->p_nextready) == NIL_PROC) return;
41 xp->p_nextready = xp->p_nextready->p_nextready;
42 if (*qtail == rp) *qtail = xp;
43 }

Código 1.4 La rutina unready

Sched pasa el proceso activo al último lugar de la cola de dispuestos (Código 1.5). Es invocada en la
tarea del reloj cuando esta se apercibe que el proceso de usuario en curso ha completado su quantum de
tiempo. Por lo tanto, el algoritmo de planificación en la cola de procesos de usuario es Round-Robin.
Las tareas y los servidores nunca son puestas al final de sus colas respectivas, ya que, una vez que
reciben un mensaje lo despachan rápidamente y se bloquean. Se confía en que operan correctamente y
no acaparan el procesador.
1 PRIVATE void sched()
2 {
3 /* The current process has run too long. If another low priority (user)
4 * process is runnable, put the current process on the end of the user queue,
5 * possibly promoting another user to head of the queue.
6 */
7
8 if (rdy_head[USER_Q] == NIL_PROC) return;
9
10 /* One or more user processes queued. */
11 rdy_tail[USER_Q]->p_nextready = rdy_head[USER_Q];
12 rdy_tail[USER_Q] = rdy_head[USER_Q];
13 rdy_head[USER_Q] = rdy_head[USER_Q]->p_nextready;
14 rdy_tail[USER_Q]->p_nextready = NIL_PROC;
15 pick_proc();
16 }

Código 1.5 La rutina sched

1.6 Referencias
[1] Comer, D., "Operating Systems Design, The XINU approach", Prentice-Hall, 1984.
[2] Ritchie, D. M. and Thompson, K. "The UNIX time-shared system". Communications of the
ACM, vol. 17, número 7, pp. 365-75. 1974.
[3] Tanenbaum, A. S., "Operating Systems, Design and Implementation", Prentice-Hall, 1986.
[4] Tanenbaum, A. S., "Operating Systems, Design and Implementation", 2nd edition, Prentice-
Hall, 1997.
[5] Tanenbaum, A. S., "Operating Systems, Design and Implementation", 3nd edition, Prentice-
Hall, 2006.
[6] http://www.educ.umu.se/%7Ebjorn/mhonarc-files/obsolete/index.html

13

También podría gustarte