You are on page 1of 62

DEFINICION Y CONTROL DE PROCESOS

Los sistemas operativos multiprogramados necesitan del concepto de proceso. El sistema operativo debe entremezclar la
ejecución de un número de procesos para maximizar la utilización de los recursos del ordenador. Al mismo tiempo, los
sistemas de tiempo compartido deben proporcionar un tiempo de respuesta razonable. El sistema operativo debe asignar
recursos a los procesos de acuerdo a una política específica (ciertas funciones o aplicaciones son de mayor prioridad), mientras
impide los interbloqueos. Por último, el sistema operativo debe ofrecer un soporte para llevar a cabo la comunicación entre
procesos.
El concepto de proceso es clave en los sistemas operativos modernos. La gestión del procesador mediante multiprogramación,
revolucionó la concepción de los sistemas operativos, e introdujo el término proceso como elemento necesario para realizar
dicha gestión. Por lo demás, este tema trata sobre la definición de proceso, el estudio de sus propiedades, y la gestión que
realiza el sistema operativo para crear la abstracción de proceso, aunque esto último se completará en el tema de
planificación. Por último, descubriremos que el concepto de proceso encierra, en realidad, dos características potencialmente
independientes: por un lado, es una unidad a la que se le asigna y posee recursos y, por otro, es una unidad planificable.
Basándonos en esta distinción emprenderemos el estudio de los threads (hebra o hilo), o también llamados procesos ligeros .

2.1 ¿ Qué es un proceso ?


Hasta ahora hemos utilizado siempre el término programa. A partir de ahora distinguiremos entre programa y proceso. Un
programa es una secuencia de instrucciones escrita en un lenguaje dado. Un proceso es una instancia de ejecución de un
programa, caracterizado por su contador de programa, su palabra de estado, sus registros del procesador, su segmento de
texto, pila y datos, etc. Un programa es un concepto estático, mientras que un proceso es un concepto dinámico. Es posible
que un programa sea ejecutado por varios usuarios en un sistema multiusuario, por cada una de estas ejecuciones existirá un
proceso, con su contador de programa, registros, etc. El sistema operativo necesita el concepto de proceso para poder
gestionar el procesador mediante la técnica de multiprogramación o de tiempo compartido, de hecho, el proceso es la unidad
planificable, o de asignación de la CPU.

2.2 Estados de un proceso y Transiciones de estado de los procesos


Durante su vida, un proceso puede pasar por una serie de estados discretos, algunos de ellos son:
En ejecución: El proceso ocupa la CPU actualmente, es decir, se está ejecutando.
Listo o preparado: El proceso dispone de todos los recursos para su ejecución, sólo le falta la CPU.
Bloqueado: Al proceso le falta algún recurso para poder seguir ejecutándose, además de la CPU. Por recurso se
pueden entender un dispositivo, un dato, etc. El proceso necesita que ocurra algún evento que le permita poder
proseguir su ejecución.
Hay otros estados de los procesos, pero en la presente exposición se tratarán estos tres. Por sencillez, se considera un sistema
con una sola CPU, aunque no es difícil la extensión a múltiples procesadores. Solamente puede haber un proceso en ejecución
a la vez, pero pueden existir varios listos y varios pueden estar bloqueados. Así pues, se forman una lista de procesos listos y
otra de procesos bloqueados. La lista de procesos listos se ordena por prioridad, de manera que el siguiente proceso que
reciba la CPU será el primero de la lista. La lista de procesos bloqueados normalmente no está ordenada; los procesos no se
desbloquean (es decir, no pasan a ser procesos listos) en orden de prioridad, sino que lo hacen en el orden de ocurrencia de
los eventos que están esperando. Como se verá más adelante, hay situaciones en las cuales varios procesos pueden
bloquearse esperando la ocurrencia del mismo evento; en tales casos es común asignar prioridades a los procesos que
esperan.

Transiciones de estado de los procesos


A continuación se dan ejemplos de eventos que pueden provocar transiciones de estado en un proceso en este modelo de tres
estados (ver figura 2.1). La mayoría de estos eventos se discutirán con profundidad a lo largo del curso:
De ejecución á Bloqueado: al iniciar una operación de E/S, al realizar una operación WAIT sobre un semáforo a cero
(en el tema de procesos concurrentes se estudiarán los semáforos).
De ejecución á Listo: por ejemplo, en un sistema de tiempo compartido, cuando el proceso que ocupa la CPU lleva
demasiado tiempo ejecutándose continuamente (agota su cuanto) el sistema operativo decide que otro proceso ocupe la
CPU, pasando el proceso que ocupaba la CPU a estado listo.
De Listo á en ejecución: cuando lo requiere el planificador de la CPU (veremos el planificador de la CPU en el tema de
planificación de procesos).
De Bloqueado á Listo: se dispone del recurso por el que se había bloqueado el proceso. Por ejemplo, termina la
operación de E/S, o se produce una operación SIGNAL sobre el semáforo en que se bloqueó el proceso, no habiendo
otros procesos bloqueados en el semáforo.
Obsérvese que de las cuatro transiciones de estado posibles, la única iniciada por el proceso de usuario es el bloqueo, las otras
tres son iniciadas por entidades externas al proceso.

1
Figura 2.1 Transiciones de estado de los procesos.
Interpretación de la figura. Como podemos observar en esta figura tenemos una serie de transiciones posibles entre estados
de proceso, representados a partir mediante una gama de colores. Estos colores hay que interpretarlos de forma que, el color
del borde de los estados representa a dichos estados, los colores dentro de los circulos nos dicen las posibles alternativas de
acceso hacia otro estado, y los colores de las flechas nos representan hacia que estado nos dirigimos si seguimos la misma.

2.3 Descripción de un proceso


De algún modo, debemos hacer una pregunta fundamental: ¿cuál es la manifestación física de un proceso? Como mínimo debe
incluir un programa o conjunto de programas que sean ejecutados. Asociados a estos programas hay un conjunto de
ubicaciones de datos para las variables locales y globales, y las constantes definidas. Así pues, un proceso constará, al menos,
de la memoria suficiente para albergar los programas y los datos del proceso. Además, en la ejecución de un programa entra
en juego normalmente una pila, que se utiliza para llevar la cuenta de las llamadas a procedimientos y de los parámetros que
se pasan entre los procedimientos. Por último, asociado a cada proceso hay una serie de atributos que utiliza el sistema
operativo para el control del proceso. Estos atributos se recogen en una estructura de datos que se conoce como bloque de
control de proceso (Process Control Block, PCB) o descriptor de proceso. A esta colección de programa, datos, pila y atributos
se le llama imagen o entorno del proceso.

2.3.1 El bloque de control de proceso


El bloque de control de proceso es la estructura de datos central y más importante de un sistema operativo. Cada bloque de
control de proceso contiene toda la información de un proceso que necesita un sistema operativo para su control. Estos
bloques son leídos y/o modificados por casi todos los módulos de un sistema operativo, incluyendo aquellos que tienen que
ver con la planificación, la asignación de recursos, el tratamiento de interrupciones y el análisis y supervisión del rendimiento.
Puede decirse que el conjunto de los bloques de control de procesos definen el estado del sistema operativo. El conjunto de
todos los PCB’s se guarda en una estructura del sistema operativo llamada tabla de procesos, la cual se puede implementar
como un vector o un lista enlazada. La tabla de procesos reside en memoria principal, debido a su alta frecuencia de consulta.
En un sistema de multiprogramación, se requiere una gran cantidad de información de cada proceso para su administración.
Sistemas distintos organizarán esta información de modo diferente. En general, se puede agrupar la información de los PCB’s
en tres categorías:
* Identificación del proceso.
* Información del estado del procesador.
* Información de control del proceso.
Con respecto a la identificación del proceso, en casi todos los sistemas operativos se le asigna a cada proceso un identificador
numérico único (ID). Este identificador nos servirá para localizarlo dentro de la tabla de procesos. Cuando se permite que los
procesos creen otros procesos, se utilizan identificadores para señalar al padre y a los descendientes de cada proceso. Además
de estos, un proceso también puede tener asignado un identificador de usuario que indica a quién pertenece el proceso (UID).
El siguiente conjunto de información es la información de estado del procesador. Básicamente, está formada por el contenido
de los registros del procesador. Por supuesto, mientras el proceso está ejecutándose, la información está en los registros.
Cuando se interrumpe el proceso, toda la información de los registros debe salvarse de forma que pueda restaurarse cuando el
proceso reanude su ejecución. La naturaleza y número de registros involucrados depende del diseño del procesador.
Normalmente, en el conjunto de registros se incluyen los registros visibles para el usuario, los registros de control y de estado
(contador de programa y palabra de estado) y los punteros a pila.
A la tercera categoría general de información del bloque de control de proceso se le podría llamar información de control del
proceso. Esta es la información adicional necesaria para que el sistema operativo controle y coordine los diferentes procesos
activos. Como, por ejemplo, información de planificación y estado (estado del proceso, su prioridad, información de
planificación, suceso), apuntadores(punteros) a estructuras de datos (los procesos que esperan en un semáforo), punteros a
zonas de memoria del proceso, recursos controlados por el proceso (ficheros abiertos), etc.
Así pues, el PCB es la entidad que define un proceso en el sistema operativo. Dado que los PCB necesitan ser manejados con
eficiencia por el sistema operativo, muchos ordenadores tienen un registro hardware que siempre apunta hacia el PCB del
proceso que se está ejecutando. A menudo existen instrucciones hardware que cargan en el PCB información sobre su
entorno, y la recuperan con rapidez.

2
Operaciones con procesos
Los sistemas que administran procesos deben ser capaces de realizar ciertas operaciones sobre y con los procesos. Tales
operaciones incluyen:
 crear y destruir un proceso
 suspender y reanudar un proceso
 cambiar la prioridad de un proceso
 bloquear y "desbloquear" un proceso
 planificar un proceso (asignarle la CPU)
 permitir que un proceso se comunique con otro (a esto se denomina comunicación entre procesos, y se estudiará en
el tema de procesos concurrentes).
Crear un proceso implica muchas operaciones, tales como:
 buscarle un identificador
 insertarlo en la tabla de procesos
 determinar la prioridad inicial del proceso
 crear el PCB
 asignar los recursos iniciales al proceso
Un proceso puede crear un nuevo proceso. Si lo hace, el proceso creador se denomina proceso padre, y el proceso creado,
proceso hijo. Sólo se necesita un padre para crear un hijo. Tal creación origina una estructura jerárquica de procesos, en la
cual cada hijo tiene sólo un padre, pero un padre puede tener muchos hijos. En el sistema operativo UNIX la llamada al sistema
‘fork’ crea un proceso hijo.
Destruir un proceso implica eliminarlo del sistema. Se le borra de las tablas o listas del sistema, sus recursos se devuelven al
sistema y su PCB se borra (es decir, el espacio de memoria ocupado por su PCB se devuelve al espacio de memoria disponible).
La destrucción de un proceso es más difícil cuando éste ha creado otros procesos. En algunos sistemas un proceso hijo se
destruye automáticamente cuando su padre es destruido; en otros sistemas, los procesos creados son independientes de su
padre y la destrucción de este último no tiene efecto sobre sus hijos.
Un proceso suspendido o bloqueado no puede proseguir sino hasta que lo reanuda otro proceso. La suspensión es una
operación importante, y ha sido puesta en práctica de diferentes formas en diversos sistemas. La suspensión dura por lo
normal sólo periodos breves. Muchas veces, el sistema efectúa las suspensiones para eliminar temporalmente ciertos
procesos, y así reducir la carga del sistema durante una situación de carga máxima. Cuando hay suspensiones largas se debe
liberar los recursos del proceso. La decisión de liberar o no los recursos depende mucho de la naturaleza de cada recurso. La
memoria principal debe ser liberada de inmediato cuando se suspenda un proceso; una unidad de cinta puede ser retenida
brevemente por un proceso suspendido, pero debe ser liberada si el proceso se suspende por un periodo largo o indefinido.
Reanudar (o activar) un proceso implica reiniciarlo a partir del punto en el que se suspendió.
Cambiar la prioridad de un proceso normalmente no implica más que modificar el valor de la prioridad en el PCB.

Suspensión y reanudación
Algunas líneas más arriba se presentaron los conceptos de suspensión y reanudación de un proceso. Estas operaciones son
importantes por diversas razones.
Si un sistema está funcionando mal, y es probable que falle, se puede suspender los procesos activos para reanudarlos
cuando se haya corregido el problema.
Un usuario que desconfíe de los resultados parciales de un proceso puede suspenderlo (en lugar de abortarlo) hasta
que verifique si el proceso funciona correctamente o no.
Algunos procesos se pueden suspender como respuesta a las fluctuaciones (bajas y altas) a corto plazo de la carga del
sistema, y reanudarse cuando las cargas vuelvan a niveles normales.
Interpretación de la figura. Como podemos observar en esta figura tenemos una serie de transiciones posibles entre estados
de proceso, representados mediante una gama de colores. Estos colores hay que interpretarlos de forma que, el color del
borde de los estados representa a dichos estados, los colores dentro de los circulos nos dicen las posibles alternativas de
acceso hacia otro estado, y los colores de las flechas nos representan hacia que estado nos dirigimos si seguimos la misma.

3
Figura 3.1. Transiciones de estado de los procesos.
La figura 3.1 muestra el diagrama de transiciones de estado de los procesos, modificado para incluir las operaciones de
suspensión y reanudación. Se han añadido dos estados nuevos, denominados suspendido_listo y suspendido_bloqueado; no
hay necesidad de un estado suspendido_en_ejecución. El rango de procesos en el azul mantiene los estados activos, y debajo
tenemos los estados suspendidos.
Una suspensión puede ser iniciada por el propio proceso o por otro. En un sistema con un sólo procesador, el proceso en
ejecución puede suspenderse a sí mismo; ningún otro proceso podría estar en ejecución al mismo tiempo para realizar la
suspensión (aunque otro proceso sí podría solicitar la suspensión cuando se ejecute). En un sistema de múltiples procesadores,
un proceso en ejecución puede suspender a otro que se esté ejecutando en ese mismo momento en un procesador diferente.
Solamente otro proceso puede suspender un proceso listo. Un proceso puede hacer que otro proceso que se encuentre en el
estado suspendido_listo pase al estado listo. Un proceso puede suspender a otro proceso que esté bloqueado, y hacerlo pasar
de suspendido_bloqueado a bloqueado. Se podría alegar que en lugar de suspender un proceso bloqueado, sería mejor
esperar hasta que ocurriera el evento que esperaba; entonces el proceso podría suspenderse y pasarse al estado
suspendido_listo. Por desgracia, puede ser que nunca ocurra el evento o que se postergue indefinidamente. Así pues, el
diseñador debe decidir si realiza la suspensión del proceso bloqueado o establece un mecanismo mediante el cual se realice la
suspensión desde el estado listo cuando ocurra la finalización de la operación. Como la suspensión es, por lo normal, una
actividad de alta prioridad, se debe realizar de inmediato. Cuando ocurre finalmente el evento, el proceso
suspendido_bloqueado pasa a suspendido_listo.
En el tema de planificación y de gestión de memoria se analiza la forma en que el sistema operativo utiliza las operaciones de
suspensión y reanudación para equilibrar la carga del sistema.

2.4 Control de un proceso


2.4.1 Modos de Ejecución
Antes de continuar la discusión sobre la forma en que el sistema operativo gestiona los procesos, hace falta distinguir entre el
modo de ejecución del procesador que normalmente se asocia con el sistema operativo y el modo que normalmente se asocia
con los programas de usuario. Ciertas instrucciones máquina pueden ejecutarse sólo en modo privilegiado. Entre éstas están
la lectura o modificación de registros de control (como la palabra de estado), instrucciones primitivas de E/S e instrucciones
relativas a la gestión de memoria. Y solamente se puede acceder a ciertas zonas de memoria en el modo privilegiado. El modo
de menor privilegio se conoce como modo usuario, y el de mayor privilegio como modo de sistema, supervisor o núcleo.
La razón por la que se usan dos modos debe quedar clara. Es necesario proteger al sistema operativo y a las estructuras de
datos importantes, tales como los bloques de control de procesos, de las inferencias de los programas de usuario. En el modo
núcleo o privilegiado, el software tiene control completo del procesador y de todas las instrucciones, registros y memoria.
Surgen dos preguntas: ¿cómo conoce el procesador en qué modo va a ejecutar?, ¿cómo se cambia de modo? Para la primera
pregunta, hay un bit en la PSW (palabra de estado), que indica el modo de ejecución. El bit se cambia como respuesta a ciertos
sucesos tales como una llamada al sistema y, así, se cambia de modo.

2.4.2 Cambio de Proceso


A primera vista, la función de cambio de proceso parece sencilla. En cierto momento, un proceso que se está ejecutando se
interrumpe, el sistema operativo pone a otro proceso en el estado de ejecución y pasa el control a dicho proceso. Sin embargo,
surgen diversas cuestiones de diseño. En primer lugar, ¿qué sucesos provocan un cambio de proceso? Otra cuestión es que se
debe hacer una distinción entre cambio de contexto y cambio de proceso. Por último, ¿qué debe hacer el sistema operativo
con las diferentes estructuras de datos bajo su control para llevar a cabo un cambio de proceso?

4
¿Qué eventos provocan el cambio de proceso?
Un cambio de proceso puede suceder en cualquier instante en el que el sistema operativo gana el control de la CPU.
En primer lugar, se van a tener en cuenta las interrupciones del sistema. Se pueden distinguir dos clases de interrupciones del
sistema. La primera es originada por algún tipo de suceso que es externo e independiente del proceso que se está ejecutando,
como la culminación de una E/S. La segunda tiene que ver con una condición de error o excepción generada dentro del
proceso que se está ejecutando, como un intento ilegal de acceso a un fichero, una división entre cero, una instrucción
máquina con código de operación no contemplado. En una interrupción ordinaria, el control se transfiere primero al gestor de
interrupciones, quien lleva a cabo algunas tareas básicas y, después, se salta a la rutina del sistema operativo que se ocupa del
tipo de interrupción que se ha producido. Algunos ejemplos de estas interrupciones son:
 Interrupción de reloj: Un reloj es un dispositivo que genera interrupciones periódicamente. Ante una interrupción de
este tipo, un sistema operativo de tiempo compartido, entre otras cosas, determina si el proceso en ejecución ha
alcanzado el máximo tiempo de ejecución que se le concedió. Si es así, el proceso pasará a estado listo, y se asignará
la CPU a otro proceso.
 Interrupción de E/S: El sistema operativo determina exactamente qué acción de E/S ha ocurrido. Si se trata de un
evento o suceso por el que esperaban uno o más procesos, entonces el sistema operativo traslada todos los procesos
bloqueados en dicho evento al estado listo, y determina (dependiendo de la política de planificación, que se verá en
el próximo tema) si reanuda la ejecución del proceso interrumpido o pasa a otro de mayor prioridad.
 Falta de memoria: Un proceso hace una referencia a una dirección que no se encuentra en memoria y que debe
traerse de memoria secundaria (esta posibilidad se estudiará en el módulo de gestión de la memoria). Después de
hacer la solicitud de E/S para traer esa o esas direcciones de memoria, el sistema operativo lleva a cabo un cambio de
contexto (próximo apartado) para reanudar la ejecución de otro proceso; el proceso que cometió la falta de memoria
se pasa al estado bloqueado. Después de que las direcciones aludidas se carguen en memoria, dicho proceso se
pondrá en estado listo.
En una interrupción del segundo tipo, el sistema operativo determina si el error es fatal. Si lo es, el proceso que se estaba
ejecutando es eliminado, y se produce un cambio de proceso. Si no es fatal, la acción del sistema operativo dependerá de la
naturaleza del error y del diseño del sistema operativo. Se puede hacer un cambio de proceso o, simplemente, reanudar el
mismo proceso que se estaba ejecutando.
Finalmente, el sistema operativo puede activarse mediante una llamada al sistema desde el programa que se está ejecutando.
Por ejemplo, está ejecutándose un proceso de usuario y se llega a una instrucción que solicita una operación de E/S, tal como
abrir un fichero. Esta llamada provoca la transferencia a una rutina que forma parte del código del sistema operativo. Por lo
general (aunque no siempre) el uso de una llamada al sistema hace que el proceso de usuario pase al estado bloqueado.

2.4.3 Cambio de Contexto


Si existe una interrupción pendiente es necesario:
- Salvar el contexto (PC, registros del procesador, información de la pila) del programa en ejecución.
- Poner en el PC la dirección del programa de tratamiento de la interrupción, que suele constar de unas pocas tareas
básicas.
Una pregunta que puede plantearse es: ¿qué es lo que constituye el contexto que se debe salvar? La respuesta es que se debe
incluir información que pueda ser necesaria para reanudar el programa interrumpido. Así pues, debe guardarse la parte del
bloque de control del proceso denominada información de estado del procesador. Esto incluye al contador de programa, otros
registros del procesador y la información de la pila.
¿Se tiene que hacer algo más? Ello dependerá de lo que ocurra a continuación. La rutina de tratamiento de la interrupción es
normalmente un programa corto que lleva a cabo unas pocas tareas básicas relacionadas con una interrupción. Por ejemplo, se
marca el indicador que señala la presencia de una interrupción, puede enviar un acuse de recibo a la entidad que produjo la
interrupción (como un módulo de E/S) y puede hacer algunas tareas básicas relacionadas con los efectos del suceso que causó
la interrupción. Por ejemplo, si la interrupción está relacionada con un suceso de E/S, el gestor de interrupciones comprobará
condiciones de error. Si se ha producido un error, la rutina de tratamiento puede enviar una señal al proceso que solicitó
originalmente la operación de E/S.
¿Hay que hacer algo más? Pues depende de si la interrupción va a venir seguida de un cambio de proceso o no. La ocurrencia
de una interrupción no siempre causa el cambio de proceso. Es posible que después de que el gestor de interrupciones se haya
ejecutado, el proceso que estaba ejecutándose reanude su ejecución. En tal caso, tan sólo hay que guardar la información de
estado del procesador y restaurarla para que pueda reanudarse correctamente el proceso interrumpido (estas funciones son
realizadas en hardware).
Por tanto, el cambio de contexto es un concepto distinto al cambio de un proceso. Puede ocurrir un cambio de contexto sin
cambiar el estado del proceso que está actualmente en estado de ejecución. En tal caso, salvar el contexto y restaurarlo
posteriormente involucra un pequeño coste extra. Sin embargo, si el proceso que estaba ejecutándose tiene que pasar a otro
estado (listo o bloqueado), el sistema operativo tiene que llevar a cabo cambios substanciales en su entorno( contexto ). Los
pasos involucrados en un cambio completo de proceso son los siguientes:
1.Salvar el contexto del procesador, incluyendo el contador de programa y otros registros.

5
2.Actualizar el PCB que estaba en estado de ejecución. Esto implica cambiar el estado del proceso a alguno de los otros
estados (listo, bloqueado, suspendido_listo). También se tienen que actualizar otros campos, como uno en el que se
guarde la razón por la que se abandona el estado de ejecución y otros con información de contabilidad.
3.Mover el PCB a la cola apropiada (listos, bloqueados por el suceso i, suspendido_listo).
4.Seleccionar otro proceso para ejecución (como veremos en el tema de Planificación de Procesos).
5.Actualizar el PCB seleccionado. Cambiar, por ejemplo, su estado a ‘en ejecución’.
6.Actualizar las estructuras de datos de gestión de la memoria. Esto puede hacer falta dependiendo de cómo se
gestione la traducción de direcciones (lo dejaremos para los temas sobre memoria).Restaurar el contexto del procesador
a aquél que existía en el momento en el que el proceso seleccionado dejó por última vez el estado de en ejecución,
cargando los valores previos del contador de programa y de otros registros.
Así pues, el cambio de proceso, que implica un cambio de contexto, requiere un esfuerzo considerablemente superior al de un
cambio de contexto.

2.5 Procesos y Threads [STAL95] [MILE94]


El concepto de proceso es más complejo y sutil que el presentado hasta ahora. Engloba dos conceptos separados y
potencialmente independientes: uno relativo a la propiedad de recursos y otro que hace referencia a la ejecución.
- Unidad que posee recursos: A un proceso se le asigna un espacio de memoria y, de tanto en tanto, se le puede asignar
otros recursos como dispositivos de E/S o ficheros.
- Unidad a la que se le asigna el procesador: Un proceso es un flujo de ejecución (una traza) a través de uno o más
programas. Esta ejecución se entremezcla con la de otros procesos. De tal forma, que un proceso tiene un estado (en
ejecución, listo, etc) y una prioridad de expedición u origen. La unidad planificada y expedida por el sistema operativo es
el proceso.
En la mayoría de los sistemas operativos, estas dos características son, de hecho, la esencia de un proceso. Sin embargo, son
independientes, y pueden ser tratadas como tales por el sistema operativo. Esta distinción ha conducido en los sistemas
operativos actuales a desarrollar la construcción conocida como thread, cuyas traducciones más frecuentes son hilo, hebra y
proceso ligero. Si se tiene esta división de características, la unidad de asignación de la CPU se conoce como hilo, mientras que
a la unidad que posee recursos se le llama proceso.
Dentro de un proceso puede haber uno o más hilos de control cada uno con:
 Un estado de ejecución (en ejecución, listo, bloqueado).
 Un contexto de procesador, que se salva cuando no esté ejecutándose.
 Una pila de ejecución.
 Algún almacenamiento estático para variables locales.
 Acceso a la memoria y a los recursos de ese trabajo que comparte con los otros hilos.
Los beneficios clave de los hilos se derivan de las implicaciones del rendimiento: se tarda menos tiempo en crear un nuevo hilo
de un proceso que ya existe, en terminarlo, y en hacer un cambio de contexto entre hilos de un mismo proceso. Al someter a
un mismo proceso a varios flujos de ejecución se mantiene una única copia en memoria del código, y no varias.
Un ejemplo de aplicación que podría hacer uso de los hilos es un servidor de ficheros de una red de área local. Cada vez que
llega una solicitud de una operación sobre un fichero, se puede generar un nuevo hilo para su gestión. El servidor gestiona
multitud de solicitudes, por tanto, se pueden crear y destruir muchos hilos en poco tiempo para dar servicio a estas peticiones.
Si el servidor es un multiprocesador, se pueden ejecutar varios hilos de un mismo proceso simultáneamente y en diferentes
procesadores.
A partir de aquí nos centraremos en la planificación a corto plazo o de la CPU. Discutiremos los principales objetivos y criterios
a tener en cuenta a la hora de decidirnos por una determinada política de planificación. A continuación realizaremos una
clasificación de estos criterios agrupándolos en apropiativos y no apropiativos. Hablaremos del reloj de interrupciones, con la
intención de aclarar cómo es posible la intervención del sistema operativo para evitar la monopolización de la CPU por parte
de los usuarios. Dedicaremos especial atención al mecanismo de planificación basado en prioridades. Terminaremos haciendo
un estudio y evaluación cualitativo de los algoritmos de planificaciónque se pueden emplear. Durante este repaso haremos una
reflexión sobre las repercusiones en cuanto a eficiencia y tiempo de respuesta del parámetro tamaño de cuanto.

6
PLANIFICACION

Durante este capítulo analizaremos todos los aspectos relacionados con el problema de cuándo asignar un procesador(CPU) y
a qué proceso. Distinguiremos entre tres niveles o tipos de planificación (a largo, medio y corto plazo).
A partir de aquí nos centraremos en la planificación a corto plazo o de la CPU. Discutiremos los principales objetivos y criterios
a tener en cuenta a la hora de decidirnos por una determinada política de planificación. A continuación realizaremos una
clasificación de estos criterios agrupándolos en apropiativos y no apropiativos. Hablaremos del reloj de interrupciones, con la
intención de aclarar cómo es posible la intervención del sistema operativo para evitar la monopolización de la CPU por parte
de los usuarios. Dedicaremos especial atención al mecanismo de planificación basado en prioridades. Terminaremos haciendo
un estudio y evaluación cualitativo de los algoritmos de planificaciónque se pueden emplear. Durante este repaso haremos una
reflexión sobre las repercusiones en cuanto a eficiencia y tiempo de respuesta del parámetro tamaño de cuanto.
3.1 Niveles de Planificación
La planificación de la CPU, en el sentido de conmutarla entre los distintos procesos, es una de las funciones del sistema
operativo. Este despacho es llevado a cabo por un pequeño programa llamado planificador a corto plazo o dispatcher
(despachador). La misión del dispatcher consiste en asignar la CPU a uno de los procesos ejecutables del sistema, para ello
sigue un determinado algoritmo. En secciones posteriores estudiaremos algunos algoritmos posibles. Para que el dispatcher
conmute el procesador entre dos procesos es necesario realizar un cambio de proceso.
Los acontecimientos que pueden provocar la llamada al dispatcher dependen del sistema (son un subconjunto de las
interrupciones), pero son alguno de estos:
 El proceso en ejecución acaba su ejecución o no puede seguir ejecutándose (por una E/S, operación WAIT, etc).
 Un elemento del sistema operativo ordena el bloqueo del proceso en ejecución (ver estados de un proceso).
 El proceso en ejecución agota su cuantum o cuanto de estancia en la CPU.
 Un proceso pasa a estado listo.
Hay que destacar el hecho de que cuanto menos se llame al dispatcher menos tiempo ocupa la CPU un programa del sistema
operativo, y, por tanto, se dedica más tiempo a los procesos del usuario (un cambio de proceso lleva bastante tiempo).
Así, si sólo se activa el dispatcher como consecuencia de los 2 primeros acontecimientos se estará haciendo un buen uso del
procesador. Este criterio es acertado en sistemas por lotes en los que los programas no son interactivos. Sin embargo, en un
sistema de tiempo compartido no es adecuado, pues un proceso que se dedicara a realizar cálculos, y no realizara E/S,
monopolizaría el uso de la CPU. En estos sistemas hay que tener en cuenta el conjunto de todos los procesos, activándose el
dispatcher con la circunstancia tercera y, posiblemente, la cuarta. Los sistema operativos en que las dos siguientes
circunstancias no provocan la activación del dispatcher muestran preferencia por el proceso en ejecución, si no ocurre esto se
tiene más en cuenta el conjunto de todos los procesos.

Se puede definir el scheduling -algunas veces traducido como -planificación- como el conjunto de políticas y mecanismos
construidos dentro del sistema operativo que gobiernan la forma de conseguir que los procesos a ejecutar lleguen a
ejecutarse.
El scheduling está asociado a las cuestiones de:
Cuándo introducir un nuevo proceso en el Sistema.
Determinar el orden de ejecución de los procesos del sistema.
El scheduling está muy relacionado con la gestión de los recursos. Existen tres niveles de scheduling, como se ilustra en la
figura 1.1, estos niveles son:
Planificador de la CPU o a corto plazo.

7
Planificador a medio plazo.
Planificador a largo plazo.
Ya hemos hablado del planificador de la CPU, y en los subapartados posteriores se comentan los dos restantes:

3.1.1 Planificación a largo plazo


Este planificador está presente en algunos sistemas que admiten además de procesos interactivos trabajos por lotes.
Usualmente , se les asigna una prioridad baja a los trabajos por lotes, utilizándose estos para mantener ocupados a los
recursos del sistema durante períodos de baja actividad de los procesos interactivos. Normalmente, los trabajos por lotes
realizan tareas rutinarias como el cálculo de nóminas; en este tipo de tareas el programador puede estimar su gasto en
recursos, indicándoselo al sistema. Esto facilita el funcionamiento del planificador a largo plazo.
El objetivo primordial del planificador a largo plazo es el de dar al planificador de la CPU una mezcla equilibrada de trabajos,
tales como los limitados por la CPU (utilizan mucho la CPU) o la E/S. Así, por ejemplo, cuando la utilización de la CPU es baja, el
planificador puede admitir más trabajos para aumentar el número de procesos listos y, con ello, la probabilidad de tener algún
trabajo útil en espera de que se le asigne la CPU. A la inversa, cuando la utilización de la CPU llega a ser alta, y el tiempo de
respuesta comienza a reflejarlo, el planificador a largo plazo puede optar por reducir la frecuencia de admisión de trabajos.
Normalmente, se invoca al planificador a largo plazo siempre que un proceso termina. La frecuencia de invocación depende,
pues, de la carga del sistema, pero generalmente es mucho menor que la de los otros dos planificadores. Esta baja frecuencia
de uso hace que este planificador pueda permitirse utilizar algoritmos complejos, basados en las estimaciones de los nuevos
trabajos.

3.1.2 Planificación a Medio Plazo


En los sistemas de multiprogramación y tiempo compartido varios procesos residen en la memoria principal. El tamaño
limitado de ésta hace que el número de procesos que residen en ella sea finito. Puede ocurrir que todos los procesos en
memoria estén bloqueados, desperdiciándose así la CPU. En algunos sistemas se intercambian procesos enteros (swap) entre
memoria principal y memoria secundaria (normalmente discos), con esto se aumenta el número de procesos, y, por tanto, la
probabilidad de una mayor utilización de la CPU.
El planificador a medio plazo es el encargado de regir las transiciones de procesos entre memoria principal y secundaria, actúa
intentando maximizar la utilización de los recursos. Por ejemplo, transfiriendo siempre a memoria secundaria procesos
bloqueados, o transfiriendo a memoria principal procesos bloqueados únicamente por no tener memoria.

3.2 Objetivos y Criterios de Planificación


El principal objetivo de la planificación a corto plazo es repartir el tiempo del procesador de forma que se optimicen algunos
puntos del comportamiento del sistema. Generalmente se fija un conjunto de criterios con los que evaluar las diversas
estrategias de planificación. El criterio más empleado establece dos clasificaciones. En primer lugar, se puede hacer una
distinción entre los criterios orientados a los usuarios y los orientados al sistema. Los criterios orientados al usuario se refieren
al comportamiento del sistema tal y como lo perciben los usuarios o los procesos. Uno de los parámetros es el tiempo de
respuesta. El tiempo de respuesta es el periodo de tiempo transcurrido desde que se emite una solicitud hasta que la
respuesta aparece en la salida. Sería conveniente disponer de una política de planificación que ofrezca un buen servicio a
diversos usuarios.
Otros criterios están orientados al sistema, esto es, se centran en el uso efectivo y eficiente del procesador. Un ejemplo puede
ser la productividad, es decir, el ritmo con el que los procesos terminan. La productividad es una medida muy válida del
rendimiento de un sistema y que sería deseable maximizar.
Otra forma de clasificación es considerar los criterios relativos al rendimiento del sistema y los que no lo son. Los criterios
relativos al rendimiento son cuantitativos y, en general, pueden evaluarse o ser analizados fácilmente. Algunos ejemplos son el
tiempo de respuesta y la productividad. Los criterios no relativos al rendimiento son, en cambio cualitativos y no pueden ser
evaluados fácilmente. Un ejemplo de estos criterios es la previsibilidad. Sería conveniente que el servicio ofrecido a los
usuarios tenga las mismas características en todo momento, independientemente de la existencia de otros trabajos ejecutados
por el sistema.
En particular, una disciplina de planificación debe:
 Ser equitativa: debe intentar hacer una planificación justa, esto es, se debe tratar a todos los procesos de la misma
forma y no aplazar indefinidamente ningún proceso. La mejor forma de evitarlo es emplear alguna técnica de
envejecimiento; es decir, mientras un proceso espera un recurso, su prioridad debe crecer.
 Ser eficiente: debe maximizar el uso de los recursos tales como intentar que la ocupación de la CPU sea máxima. Al
mismo tiempo se debe intentar reducir el gasto extra por considerar que es trabajo no productivo. Normalmente el
idear algoritmos eficientes supone invertir recursos en gestión del propio sistema.
 Lograr un tiempo bueno de respuesta, es decir, que los usuarios interactivos reciban respuesta en tiempos
aceptables.
 Lograr un tiempo de proceso global predecible. Esto quiere decir que un proceso debe ejecutarse aproximadamente
en el mismo tiempo y casi al mismo costo con independencia de la carga del sistema.
 Elevar al máximo la productividad o el rendimiento, esto es, maximizar el número de trabajos procesados por unidad
de tiempo. Eso supone, por un lado, dar preferencia a los procesos que ocupan recursos decisivos y, por otro,

8
favorecer a los procesos que muestran un comportamiento deseable. En el primer caso conseguimos liberar el recurso
cuanto antes para que esté disponible para un proceso de mayor prioridad. Con el segundo criterio escogemos a los
procesos que no consumen muchos recursos dejándole al sistema mayor capacidad de actuación.
Estos criterios son dependientes entre sí y es imposible optimizar todos de forma simultánea. Por ejemplo, obtener un buen
tiempo de respuesta puede exigir un algoritmo de planificación que alterne entre los procesos con frecuencia, lo que
incrementa la sobrecarga del sistema y reduce la productividad. Por tanto, en el diseño de un política de planificación entran
en juego compromisos entre requisitos opuestos; el peso relativo que reciben los distintos requisitos dependerá de la
naturaleza y empleo del sistema.

3.3 Planificación Apropiativa y No apropiativa


Una disciplina de planificación es no apropiativa si una vez que la CPU ha sido asignada al proceso, ya no se le puede arrebatar.
Y por el contrario, es apropiativa, si se le puede quitar la CPU.
La planificación apropiativa es útil en los sistemas en los cuales los procesos de alta prioridad requieren una atención rápida.
En los de tiempo real, por ejemplo, las consecuencias de perder una interrupción pueden ser desastrosas. En los sistemas de
tiempo compartido, la planificación apropiativa es importante para garantizar tiempos de respuesta aceptables.
La apropiación tiene un precio. El cambio de proceso implica gasto extra. Para que la técnica de apropiación sea efectiva deben
mantenerse muchos procesos en memoria principal de manera que el siguiente proceso se encuentre listo cuando quede
disponible la CPU. Conservar en memoria principal procesos que no están en ejecución implica gasto extra.
En los sistema no apropiativos, los trabajos largos retrasan a los cortos, pero el tratamiento para todos los procesos es más
justo. Los tiempos de respuesta son más predecibles porque los trabajos nuevos de alta prioridad no pueden desplazar a los
trabajos en espera.
Al diseñar mecanismos de planificación apropiativa no hay que perder de vista la arbitrariedad de casi todos los sistemas de
prioridades. Se puede construir un mecanismo complejo para implantar fielmente un esquema de apropiación por prioridades
sin que, de hecho, se hayan asignado prioridades de forma coherente.

3.4 El Reloj de Interrupciones


Se dice que un proceso está en ejecución cuando tiene asignada la CPU. Si el proceso pertenece al sistema operativo, se dice
que el sistema operativo está en ejecución y que puede tomar decisiones que afectan al sistema. Para evitar que los usuarios
monopolicen el sistema (deliberadamente o accidentalmente), el sistema operativo tiene mecanismos para arrebatar la CPU al
usuario.
El sistema operativo gestiona un reloj de interrupciones que genera interrupciones cada cierto tiempo. Un proceso mantiene
el control de la CPU hasta que la libera voluntariamente (acaba su ejecución, o se bloquea), hasta que el reloj interrumpe o
hasta que alguna otra interrupción desvía la atención de la CPU. Si el usuario se encuentra en ejecución y el reloj interrumpe,
el sistema operativo entra en ejecución para comprobar, por ejemplo, si ha pasado el cuanto de tiempo del proceso que estaba
en ejecución.
El reloj de interrupciones asegura que ningún proceso acapare la utilización del procesador. El sistema operativo, apoyándose
en él, intenta distribuir el tiempo de CPU entre los distintos procesos ya sean de E/S o de cálculo. Por tanto, ayuda a garantizar
tiempos de respuesta para los usuarios interactivos, evitando que el sistema quede bloqueado en un ciclo infinito de algún
usuario y permite que los procesos respondan a eventos dependientes de tiempo. Los procesos que deben ejecutarse
periódicamente dependen del reloj de interrupciones.

No se debe confundir en ningún caso al reloj de interrupciones con el reloj de la máquina o reloj hardware. Veamos con un
pequeño ejemplo como esto es imposible.
Como sabemos, todas las tareas de una computadora están sincronizadas por un reloj hardware. La velocidad de un
procesador determina la rapidez con la que ejecuta un paso elemental o cambio en el sistema. Por ejemplo, si decimos de una
máquina que tienen un microprocesador que va a una frecuencia de 100 MHz eso quiere decir que produce alrededor de 100
millones de pasos elementales o cambios en el sistema en un segundo. Pero una instrucción consume algunos de estos pasos
mínimos. Supongamos que en media una instrucción consume alrededor de 100 pasos elementales. No podemos interrumpir
al procesador a la misma velocidad a la que opera porque entonces no se podría llegar nunca a ejecutar ninguna instrucción.
Parece razonable que se elija una frecuencia menor para el reloj de interrupciones. Por ejemplo, se podría generar una
interrupción cada 0'02 segundos (tener una frecuencia de 50 Hz) esto significa que se estaría interrumpiendo al procesador
cada dos millones de ciclos. En ese tiempo bajo la suposición de que una instrucción consume 100 pasos se habría ejecutado
unas 20000 instrucciones. Esto sí es mucho más razonable. En resumen el reloj de interrupciones tiene una frecuencia inferior
al reloj hardware y superior al cuanto de tiempo o intervalos de tiempo en que se quiera controlar en el sistema.

9
3.5 Uso de Prioridades
La mayoría de los algoritmos de planificación apropiativos emplean el uso de prioridades de acuerdo con algún criterio. Cada
proceso tiene una prioridad asignada y el planificador seleccionará siempre un proceso de mayor prioridad antes que otro de
menor prioridad.
Las prioridades pueden ser asignadas de forma automática por el sistema, o bien se pueden asignar externamente. Pueden
ganarse o comprarse. Pueden ser estáticas o dinámicas. Pueden asignarse de forma racional, o de manera arbitraria en
situaciones en las que un mecanismo del sistema necesita distinguir entre procesos pero no le importa cuál de ellos es en
verdad más importante.
Las prioridades estáticas no cambian. Los mecanismos de prioridad estática son fáciles de llevar a la práctica e implican un
gasto extra relativamente bajo. Sin embargo, no responden a cambios en el entorno que podrían hacer necesario un ajuste de
prioridades.
Las prioridades dinámicas responden a los cambios. La prioridad inicial asignada a un proceso tiene una corta duración,
después se ajusta a un valor más apropiado, a veces deducido de su comportamiento. Los esquemas de prioridad dinámica son
más complejos e implican un mayor gasto extra que puede quedar justificado por el aumento en la sensibilidad del sistema.

3.6 Algoritmos de planificación


En los siguientes subapartados vamos a estudiar ciertos algoritmos utilizados para planificar la CPU, la elección de uno (o de
una mezcla de varios) depende de decisiones de diseño. Antes de exponer los algoritmos vamos a explicar ciertas medidas que
se utilizan para evaluarlos.
Porcentaje de utilización de la CPU por procesos de usuario. La CPU es un recurso caro que necesita ser
explotado, los valores reales suelen estar entre un 40% y un 90%.
Rendimiento (throughput) = nº de ráfagas por unidad de tiempo. Se define una ráfaga como el período de tiempo
en que un proceso necesita la CPU; un proceso, durante su vida, alterna ráfagas con bloqueos. Por extensión, también se
define como el nº de trabajos por unidad de tiempo.
Tiempo de espera (E) = tiempo que una ráfaga ha permanecido en estado listo.
Tiempo de finalización (F) = tiempo transcurrido desde que una ráfaga comienza a existir hasta que finaliza. F = E +
t (t = tiempo de CPU de la ráfaga).
Penalización (P) = E + t / t = F / t, es una medida adimensional que se puede aplicar homogéneamente a las ráfagas
independientemente de su longitud.
En general, hay que maximizar los dos primeros parámetros y minimizar los tres últimos. Sin embargo, estos objetivos son
contradictorios, el dedicar más tiempo de CPU a los usuarios se hace a costa de llamar menos al algoritmo de planificación
(menos cambios de proceso), y de simplificarlo. Esto provoca que la CPU se reparta menos equitativamente entre los procesos,
en detrimento de los últimos tres parámetros.
Así pues, dependiendo de los objetivos se elegirá cierto algoritmo. En los sistemas por lotes suele primar el rendimiento del
sistema, mientras que en los sistemas interactivos es preferible minimizar, por ejemplo, el tiempo de espera.

3.6.1 Planificación de Plazo Fijo


En la planificación de plazo fijo se programan ciertos trabajos para terminarse en un tiempo específico o plazo fijo. Estas tareas
pueden tener un gran valor si se entregan a tiempo, y carecer de él si se entregan después del plazo. Esta planificación es
compleja por varios motivos:
El usuario debe informar por adelantado de las necesidades precisas de recursos del proceso. Semejante información rara vez
está disponible.
El sistema debe ejecutar el proceso en un plazo fijo sin degradar demasiado el servicio a los otros usuarios y debe planificar
cuidadosamente sus necesidades de recursos dentro del plazo. Esto puede ser difícil por la llegada de nuevos procesos que
impongan demandas imprevistas al sistema.
Si hay muchas tareas a plazo fijo activas al mismo tiempo, la planificación puede ser tan compleja que se necesiten métodos de
optimización avanzados para cumplir los plazos.
La administración intensiva de recursos requerida por la planificación de plazo fijo puede producir un gasto extra substancial.

3.6.2 Planificación Primero en Entrar-Primero en Salir (FIFO)


Cuando se tiene que elegir a qué proceso asignar la CPU se escoge al que llevara más tiempo listo. El proceso se mantiene en la
CPU hasta que se bloquea voluntariamente.
La ventaja de este algoritmo es su fácil implementación, sin embargo, no es válido para entornos interactivos ya que un
proceso de mucho cálculo de CPU hace aumentar el tiempo de espera de los demás procesos . Para implementar el algoritmo
(ver figura 2) sólo se necesita mantener una cola con los procesos listos ordenada por tiempo de llegada. Cuando un proceso
pasa de bloqueado a listo se sitúa el último de la cola.
En a) el proceso P7 ocupa la CPU, los procesos P2, P4 y P8 se mantienen en la lista de preparados. En b) P7 se bloquea (ya sea
al realizar una E/S, una operación WAIT sobre un semáforo a cero u otra causa) y P2 pasa a ocupar la CPU. En c) ocurre un
evento (finalización de la operación de E/S, operación SIGNAL, ...) que desbloquea a P7, esto lo vuelve listo, pasando al final de
la cola de procesos listos.

10
ENLACE A LA SIMULACIÓN FIFO
Algunas de las características de este algoritmo es que es no apropiativo y justo en el sentido formal, aunque injusto en el
sentido de que: los trabajos largos hacen esperar a los cortos y los trabajos sin importancia hacen esperar a los importantes.
Por otro lado es predecible pero no garantiza buenos tiempos de respuesta y por ello se emplea como esquema secundario.

3.6.3 Planficación por Turno Rotatorio (Round Robin).


Este es uno de los algoritmos más antiguos, sencillos y equitativos en el reparto de la CPU entre los procesos, muy válido para
entornos de tiempo compartido. Cada proceso tiene asignado un intervalo de tiempo de ejecución, llamado cuantum o
cuanto. Si el proceso agota su cuantum de tiempo, se elige a otro proceso para ocupar la CPU. Si el proceso se bloquea o
termina antes de agotar su cuantum también se alterna el uso de la CPU. El round robin es muy fácil de implementar. Todo lo
que necesita el planificador es mantener una lista de los procesos listos, como se muestra en la figura 6.2.
En esta figura en a) el proceso P7 ocupa la CPU. En b) P7 se bloquea pasando P2 a ocupar la CPU. En c) P2 agota su cuantum
con lo que pasa al final de la lista y P4 ocupa la CPU. La figura 4 representa un ejemplo más largo de la ocupación de la CPU
utilizando el algoritmo round robin.

ENLACE A LA SIMULACIÓN COLAS RR


Este algoritmo presupone la existencia de un reloj en el sistema. Un reloj es un dispositivo que genera periódicamente
interrupciones. Esto es muy importante, pues garantiza que el sistema operativo (en concreto la rutina de servicio de
interrupción del reloj) coge el mando de la CPU periódicamente. El cuantum de un proceso equivale a un número fijo de pulsos
o ciclos de reloj. Al ocurrir una interrupción de reloj que coincide con la agotación del cuantum se llama al dispatcher.

3.6.4 Tamaño del Cuanto


La determinación del tamaño del cuanto es vital para la operación efectiva de un sistema de cómputo. ¿Debe el cuanto ser
pequeño o grande?, ¿fijo o variable?, ¿el mismo para todos los usuarios o debe determinarse por separado para cada uno?
Si el cuanto de tiempo es muy grande, cada proceso tendrá el tiempo necesario para terminar, de manera que el esquema de
planificación por turno rotatorio degenera en uno de primero-en-entrar-primero-en-salir. Si el cuanto es muy pequeño, el
gasto extra por cambio de proceso se convierte en el factor dominante y el rendimiento del sistema se degradará hasta el
punto en que la mayor parte del tiempo se invierte en la conmutación del procesador, con muy poco o ningún tiempo para
ejecutar los programas de los usuarios.
¿Exactamente dónde, entre cero e infinito, debe fijarse el tamaño del cuanto? La respuesta es, lo bastante grande como para
que la mayoría de las peticiones interactivas requieran menos tiempo que la duración del cuanto.
Pongamos un ejemplo, supongamos que el cambio de proceso tarda 5 mseg., y la duración del cuantum es de 20 mseg.. Con
estos parámetros, se utiliza un mínimo del 20% del tiempo de la CPU en la ejecución del sistema operativo. Para incrementar la

11
utilización de la CPU por parte de los procesos de usuario podríamos establecer un cuantum de 500 mseg., el tiempo
desperdiciado con este parámetro sería del 1%. Pero consideremos lo que ocurriría si diez usuarios interactivos oprimieran la
tecla enter casi al mismo tiempo. Diez procesos se colocarían en la lista de procesos listos. Si la CPU está inactiva, el primero de
los procesos comenzaría de inmediato, el segundo comenzaría medio segundo después, etc. Partiendo de la hipótesis de que
todos los procesos agoten su cuantum, el último proceso deberá de esperar 4'5 seg. para poder ejecutarse. Esperar 4'5 seg.
para la ejecución de una orden sencilla como pwd parece excesivo.
En conclusión, un cuantum pequeño disminuye el rendimiento de la CPU, mientras que un cuantum muy largo empobrece los
tiempos de respuesta y degenera en el algoritmo FIFO. La solución es adoptar un término medio como 100 mseg.

3.6.5 Planificación por Prioridad al más corto (SJF, Short Job First).
Al igual que en el algoritmo FIFO las ráfagas se ejecutan sin interrupción, por tanto, sólo es útil para entornos batch. Su
característica es que cuando se activa el planificador, éste elige la ráfaga de menor duración. Es decir, introduce una noción de
prioridad entre ráfagas. Hay que recordar que en los entornos batch se pueden hacer estimaciones del tiempo de ejecución de
los procesos.
La ventaja que presenta este algoritmo sobre el algoritmo FIFO es que minimiza el tiempo de finalización promedio, como
puede verse en el siguiente ejemplo:

Supongamos que en un momento dado existen tres ráfagas listos R1, R2 y R3, sus tiempos de ejecución respectivos son
24, 3 y 3 ms. El proceso al que pertenece la ráfaga R1 es la que lleva más tiempo ejecutable, seguido del proceso al que
pertenece R2 y del de R3. Veamos el tiempo medio de finalización (F) de las ráfagas aplicando FIFO y SJF:
FIFO F = (24 + 27 + 30) / 3 = 27 ms.
SJF F = (3 + 6 + 30) / 3 = 13 ms.
Se puede demostrar que este algoritmo es el óptimo. Para ello, consideremos el caso de cuatro ráfagas, con tiempos de
ejecución de a, b, c y d. La primera ráfaga termina en el tiempo a, la segunda termina en el tiempo a+b, etc. El tiempo
promedio de finalización es (4a+3b+2c+d)/4. Es evidente que a contribuye más al promedio que los demás tiempos, por lo
que debe ser la ráfaga más corta, b la siguiente, y así sucesivamente. El mismo razonamiento se aplica a un número
arbitrario de ráfagas.
ENLACE A LA SIMULACIÓN SJF
No obstante, este algoritmo sólo es óptimo cuando se tienen simultáneamente todas las ráfagas. Como contraejemplo,
considérense cinco ráfagas desde A hasta E, con tiempo se ejecución de 2, 4, 1, 1 y 1 respectivamente. Sus tiempos de llegada
son 0, 0, 3, 3 y 3. Primero se dispone de A y B, puesto que las demás ráfagas no han llegado aún. Con el algoritmo SJF las
ejecutaríamos en orden A, B, C, D, y E con un tiempo de finalización promedio de 4.6. Sin embargo, al ejecutarlas en orden B,
C, D, E y A se tiene un promedio de finalización de 4.4.

3.6.6 Planificación por Prioridad al Tiempo Restante más Corto (SRTF, Short Remaining Time First).

Es similar al anterior, con la diferencia de que si un nuevo proceso pasa a listo se activa el dispatcher para ver si es más corto
que lo que queda por ejecutar del proceso en ejecución. Si es así el proceso en ejecución pasa a listo y su tiempo de
estimación se decrementa con el tiempo que ha estado ejecutándose.
En la figura 6.5 tenemos un ejemplo de funcionamiento del algoritmo en el que se observa cómo se penalizan las ráfagas largas
(como en SJF). Un punto débil de este algoritmo se evidencia cuando una ráfaga muy corta suspende a otra un poco más larga,
siendo más largo la ejecución en este orden al ser preciso un cambio adicional de proceso y la ejecución del código del
planificador.

3.6.7 Planificación a la Tasa de Respuesta más Alta

12
Brinch Hansen desarrolló la estrategia de prioridad a la tasa de respueta más alta (HRN, highest-response-ratio-next) que
corrige algunas deficiencias de SJF, particularmente el retraso excesivo de trabajos largos y el favoritismo excesivo para los
trabajos cortos. HRN es un disciplina de planificación no apropiativa en la cual la prioridad de cada proceso no sólo se calcula
en función del tiempo de servicio, sino también del tiempo que ha esperado para ser atendido. Cuando un trabajo obtiene el
procesador, se ejecuta hasta terminar. Las prioridades dinámicas en HRN se calculan de acuerdo con la siguiente expresión:
prioridad = (tiempo de espera + tiempo de servicio) / tiempo de servicio
Como el tiempo de servicio aparece en el denominador, los procesos cortos tendrán preferencia. Pero como el tiempo de
espera aparece en el numerador, los procesos largos que han esperado también tendrán un trato favorable. Obsérvese que la
suma tiempo de espera + tiempo de servicio es el tiempo de respuesta del sistema para el proceso si éste se inicia de
inmediato.

3.6.8 Planificación por el Comportamiento


Con este tipo de planificación se pretende garantizar al usuario cierta prestación del sistema y tratar de cumplirla. Si en un
sistema tenemos 'n' usuarios lo normal será garantizar a cada uno de ellos al menos 1/n de la potencia del procesador. Para
ello necesitamos del tiempo consumido por el procesador y el tiempo que lleva el proceso en el sistema. La cantidad de
procesador que tiene derecho a consumir el proceso será el cociente entre el tiempo que lleva en el sistema entre el número
de procesos que hay en el sistema. A esa cantidad se le puede asociar una prioridad que vendrá dada como el cociente entre
tiempo de procesador que ha consumido y el tiempo que se le prometió (el tiempo que tiene derecho a consumir). De tal
modo que si esa proporción es de 0'5 significa que tan sólo ha consumido la mitad del tiempo prometido pero si es de 2 quiere
decir que ha consumido más de lo debido, justamente el doble.
En sistemas de tiempo real se puede adoptar una variante de este algoritmo en el que se otorgue mayor prioridad al proceso
cuyo riesgo de no cumplir el plazo sea mayor.

3.6.9 Colas de Retroalimentación de Múltiples Niveles


Cuando un proceso obtiene la CPU, sobre todo cuando todavía no ha tenido oportunidad de establecer un patrón de
comportamiento, el planificador no tiene idea de la cantidad de tiempo de CPU que necesitará el proceso. Los procesos
limitados por la E/S normalmente usan la CPU sólo un momento antes de generar una solicitud de E/S; los procesos limitados
por la CPU pueden usar el procesador durante horas si está disponible en forma no apropiativa.
Un mecanismo de planificación debe:
- Favorecer a los trabajos cortos.
- Favorecer a los trabajos limitados por la E/S para lograr un mejor aprovechamiento de los dispositivos de E/S.
- Determinar la naturaleza de un trabajo lo más pronto posible y planificarlo de acuerdo con su naturaleza.

ENLACE A LA SIMULACIÓN COLAS MULTINIVEL


Las colas de retroalimentación de niveles múltiples (figura 6.6) ofrecen una estructura que cumple con estos objetivos. Un
proceso nuevo entra en la red de colas al final de la primera cola. Se desplaza en esa cola mediante Round Robin hasta que
obtiene la CPU. Si el trabajo termina o cede la CPU para esperar la terminación de una operación de E/S o de algún evento, el
trabajo abandona la red de colas. Si el cuanto expira antes de que el proceso ceda voluntariamente la CPU, el proceso se
colocará al final de la cola del siguiente nivel. El proceso será atendido otra vez cuando llegue a la cabeza de esa cola si está
vacía la primera. Mientras el proceso utilice todo el cuanto proporcionado en cada nivel, continuará desplazándose al final de
la siguiente cola inferior. Por lo general, existe una cola en el nivel más bajo en la cual el proceso circula por turno rotatorio
hasta que termina.

13
En muchos esquemas de retroalimentación de múltiples niveles, el cuanto asignado a un proceso cuando pasa a una cola de
nivel inferior alcanza un valor mayor. De esta forma, cuanto más tiempo se encuentre un proceso en la red de colas más grande
será el cuanto asignado cada vez que obtenga la CPU, pero tal vez no obtenga la CPU muy a menudo, porque los procesos de
las colas de nivel superior tienen mayor prioridad. Un proceso situado en una cola no puede ejecutarse a menos que estén
vacías las colas de nivel superior. Un proceso en ejecución será desposeído por un proceso que llegue a una cola superior.
Considérese ahora cómo responde un mecanismo de este tipo a diferentes tipos de procesos. El mecanismo debe favorecer a
los procesos limitados por la E/S para lograr un buen aprovechamiento de los dispositivos y una respuesta buena para los
usuarios interactivos; y de hecho lo hace porque los procesos limitados por la E/S entrarán en la red con prioridad alta y se les
asignará rápidamente la CPU. El tamaño del cuanto de la primera cola se elegirá lo suficientemente grande para que la gran
mayoría de los trabajos limitados por la E/S generen una petición de E/S antes de que expire el primer cuanto. Cuando el
proceso solicita E/S, abandona la red y ha obtenido un tratamiento favorable, tal como se deseaba.
Ahora considérese una tarea limitada por la CPU que necesita mucho tiempo de procesador. Esa tarea entra en la cola más alta
de la red con prioridad alta. Recibe rápidamente su primera asignación de la CPU, pero su cuanto expira y el proceso se coloca
en la cola del siguiente nivel inferior. En ese momento, el proceso tiene una prioridad menor que la de los procesos que llegan
al sistema, en particular los trabajos limitados por la E/S, que obtienen primero la CPU. El proceso limitado por la CPU acaba
recibiendo ésta, obtiene un cuanto mayor que en la cola más alta y vuelve a utilizar la totalidad de su cuanto. Luego es situado
al final de la siguiente cola inferior. El proceso sigue desplazándose a colas inferiores, espera más entre divisiones de tiempo y
utiliza todo su cuanto cada vez que obtiene la CPU ( a menos que sea arrebatada por un proceso entrante). En algún momento,
el proceso limitado por la CPU llega a la cola de nivel inferior, en donde entrará en una planificación por turno hasta terminar.
Las colas de retroalimentación de niveles múltiples son ideales para separar procesos en categorías basadas en su necesidad
de la CPU. En un sistema de tiempo compartido, cada vez que un proceso abandona la red de colas puede "marcarse" con la
identidad de la última cola en donde estuvo, y cuando el proceso entra de nuevo en la red de colas, puede enviarse
directamente a la cola en la cual terminó su operación por última vez. En este caso, el planificador está usando un
razonamiento heurístico, según el cual el comportamiento anterior del proceso es un buen indicador de su comportamiento en
un futuro inmediato. De esta forma, un proceso limitado por la CPU que regresa a la red de colas no se coloca en las colas de
nivel alto donde interferiría con el servicio a los procesos cortos de prioridad alta o con los limitados por la E/S.
Si los procesos se colocan siempre dentro de la red en la cola que ocuparon la última vez, será imposible que el sistema
responda a cambios de un proceso, por ejemplo, de estar limitado por la CPU, a estar limitado por la E/S. El problema puede
resolverse marcando al proceso también con su duración dentro de la red la última vez que estuvo en ella. Así, cuando el
proceso entra de nuevo en la red puede colocarse en la cola correcta. Entonces, si el proceso entra en una fase nueva en la
cual deja de estar limitado por la CPU y empieza a estar limitado por la E/S, el proceso experimentará en principio un
tratamiento lento mientras el sistema determina que la naturaleza del proceso está cambiando. Pero el mecanismo de
planificación responderá con rapidez a este cambio. Otra forma de hacer que el sistema responda a los cambios de
comportamiento de los procesos es permitir que un proceso ascienda un nivel en la red de colas cada vez que abandona
voluntariamente la CPU antes de que expire su cuanto.
El mecanismo de colas de retroalimentación de niveles múltiples es un buen ejemplo de mecanismo adaptativo, que responde
a los cambios en el comportamiento del sistema que controla. Los mecanismos adaptativos implican, en general, una carga
extra mayor que los no adaptativos, pero la sensibilidad ante los cambios en el sistema da como resultado una mejor
capacidad de respuesta, y justifica el aumento en el gasto extra.
Una variante común del mecanismo de colas de retroalimentación de múltiples niveles consiste en hacer que un proceso
circule por turno varias veces en cada cola antes de pasar a la siguiente cola inferior. El número de ciclos en cada cola crece por
lo regular cuando el proceso pasa a la siguiente cola inferior.

CONCURRENCIA: EXCLUSIÓN MUTUA Y SINCRONIZACIÓN


4.1 Comunicación y Sincronización de Procesos
Puede verse la concurrencia de procesos como la ejecución simultánea de varios procesos. Si tenemos un multiprocesador o
un sistema distribuido(tema 1) la concurrencia parece clara, en un momento dado cada procesador ejecuta un proceso. Se
puede ampliar el concepto de concurrencia si entendemos por procesado concurrente (o procesado paralelo) la circunstancia
en la que de tomar una instantánea del sistema en conjunto, varios procesos se vean en un estado intermedio entre su estado
inicial y final. Esta última definición incluye los sistemas multiprogramados de un único procesador que estudiamos en los
temas anteriores.
Los distintos procesos dentro de un ordenador no actúan de forma aislada. Por un lado, algunos procesos cooperan para lograr
un objetivo común. Por otro lado, los procesos compiten por el uso de unos recursos limitados, como el procesador, la
memoria o los ficheros. Estas dos actividades de cooperación y competición llevan asociada la necesidad de algún tipo de
comunicación entre los procesos. Parte de este tema lo dedicaremos a estudiar mecanismos de comunicación entre los
procesos.
Para aclarar un poco todo esto, supongamos que en un entorno UNIX se ejecuta la orden
cat tema1 tema2 tema3 | wc -l
Esta orden va a provocar que el intérprete de órdenes cree dos procesos concurrentes, el primero ejecutará el programa cat,
que concatenará el contenido de los ficheros de texto tema1, tema2 y tema3. El segundo ejecutará el programa wc, que
contará el número de líneas de la salida producida por cat. Estos dos procesos cooperan, y será preciso algún tipo de
sincronización entre ellos, concretamente hasta que cat no produzca alguna salida wc debería bloquearse.

14
4.1.1 Cooperación entre Procesos
La velocidad de un proceso con respecto a otro es impredecible ya que depende de la frecuencia de la interrupción asociada a
cada uno de ellos y cuán a menudo y de por cuánto tiempo tiene asignado cada proceso un procesador. Diremos, pues, que un
proceso se ejecuta asíncronamente con respecto a otro, es decir, sus ejecuciones son independientes. Sin embargo, hay ciertos
instantes en lo que los procesos deben sincronizar sus actividades. Son estos puntos a partir de los cuales un proceso no puede
progresar hasta que otro haya completado algún tipo de actividad.
Los procesos también necesitan comunicarse entre sí. Por ejemplo, para leer de un fichero, los procesos de usuario deben
decir al proceso que gestiona los ficheros lo que desean. Este, a su vez, debe pedirle al proceso de disco que lea los bloques
necesarios. Cuando el shell conecta dos procesos con un tubo, los datos de salida del primer proceso se transfieren al
segundo. Se necesita que los procesos se comuniquen entre sí, con un mecanismo bien estructurado y no por medio de
interrupciones.

4.1.2 Competencia entre los procesos


Los recursos de un sistema pueden clasificarse como compartibles, lo que significa que pueden ser utilizados por varios
procesos de forma concurrente, o no compartibles, lo que equivale a que su uso se restrinja a un sólo proceso a la vez. El
hecho de que un recurso no sea compartible deriva de una de las dos razones siguientes:
- La naturaleza física del recurso hace que sea imposible compartirlo. Un ejemplo lo constituye una impresora, si una
impresora fuera utilizada por varios procesos simultáneamente sus salidas se entremezclarían en el papel.
- El recurso es tal que si es usado por varios procesos de forma concurrente la acción de uno de ellos puede interferir
con la de otro. Un ejemplo común lo constituye un fichero que contiene unos datos accesibles desde más de un
proceso, y modificables al menos por uno de ellos. Por ejemplo, supongamos un fichero que contiene registros con la
siguiente información sobre una cuenta bancaria:

CLAVE SALDO

Supongamos también dos procesos que se ejecutan concurrentemente y que actualizan una misma cuenta, el proceso P1
añade al saldo el valor de la nómina y el proceso P2 descuenta del saldo una factura domiciliada. El código de los procesos es el
siguiente:

El resultado final de la ejecución de los dos procesos debería actualizar el saldo añadiéndole la nómina, y descontándole la
factura. Sin embargo, la secuencia de ejecución en el procesador de las instrucciones a1b1b2b3a2a3, que puede darse debido
a un cambio de proceso, hace que no se descuente la factura.
Dentro de la categoría de recursos no compartibles se incluirán la mayoría de los periféricos (los discos no), los ficheros de
escritura y las zonas de memoria sujetas a modificación. Los recursos compartibles incluirán la CPU (en los temas previos vimos
cómo compartir la CPU entre los procesos introduciendo el concepto de PCB), los ficheros de lectura, y las zonas de memoria
que contengan rutinas puras o bien datos que no están sujetos a modificación.

4.1.3 Requisitos para la Exclusión Mutua


Los recursos no compartibles, ya sean periféricos, ficheros, o datos en memoria, pueden protegerse del acceso simultáneo por
parte de varios procesos evitando que éstos ejecuten de forma concurrente sus fragmentos de código a través de los cuales
llevan a cabo este acceso. Estos trozos de código reciben el nombre de secciones o regiones críticas, pudiéndose asimilar el
concepto de exclusión mutua en el uso de estos recursos a la idea de exclusión mutua en la ejecución de las secciones críticas.
Así, por ejemplo, puede implementarse la exclusión mutua de varios procesos en el acceso a una tabla de datos mediante el
recurso de que todas las rutinas que lean o actualicen la tabla se escriban como secciones críticas, de forma que sólo pueda
ejecutarse una de ellas a la vez. En el ejemplo previo de la cuenta bancaria los fragmentos de código a1a2a3 y b1b2b3
constituyen dos secciones críticas mutuamente excluyentes, esto significa que una vez que se ha comenzado la ejecución de
una sección crítica, no se puede entrar en otra sección crítica mutuamente excluyente.
Idear soluciones que garanticen la exclusión mutua es uno de los problemas fundamentales de la programación concurrente.
Muchas son las alternativas y tipos de mecanismos que se pueden adoptar. A lo largo de este tema veremos diferentes
soluciones software y alguna hardware ; unas serán sencillas y otras complejas, algunas requieren la cooperación voluntaria de
los procesos y otras que exigen un estricto ajuste a rígidos protocolos. La selección de las operaciones primitivas adecuadas
para garantizar la exclusión mutua de las secciones críticas es una decisión primordial en el diseño de un sistema operativo. Al
menos, una solución apropiada debería cumplir las cuatro condiciones siguientes:

15
1. Que no haya en ningún momento dos procesos dentro de sus respectivas secciones críticas.
2. Que no hagan suposiciones a priori sobre las velocidades relativas de los procesos o el número de procesadores
disponibles.
3. Que ningún proceso que esté fuera de su sección crítica pueda bloquear a otros.
4. Que ningún proceso tenga que esperar un intervalo de tiempo arbitrariamente grande para entrar en su sección
crítica.

4.2 Utilizando Memoria Compartida


A partir de aquí, iremos haciendo un estudio progresivo sobre las posibles soluciones de todo tipo que intentarán resolver la
exclusión mutua y el sincronismo entre procesos. Muchas de estas soluciones fracasarán en su intento. Otras lo lograrán, pero
mostrarán ciertas deficiencias que nos llevarán a pensar en nuevas primitivas de concurrencia.
Nuestro viaje comenzará con las soluciones que utilizan memoria compartida. Dentro de este grupo, distinguiremos entre
mecanismos software y hardware que emplean espera ocupada, y los que usan espera bloqueada (semáforos y monitores).

4.2.1 Soluciones Software al Problema de la Exclusión Mutua


En esta sección se van a estudiar varios métodos que intentan resolver el problema de la ejecución en exclusión mutua de las
secciones críticas de los procesos. En primer lugar, introduciremos algunos métodos que no producen una solución aceptable,
sin embargo, nos ayudarán a comprender mejor el problema.

Desactivación de las interrupciones


La solución más sencilla es que cada proceso prohiba todas las interrupciones justo antes de entrar en su región crítica y las
permita de nuevo justo antes de salir. Esto funciona correctamente, pues el código del sistema operativo sólo se activa como
consecuencia de la ocurrencia de interrupciones. Si el sistema operativo no puede ejecutarse mientras se ejecuta la sección
crítica, es obvio que no podrá realizar un cambio de proceso en la CPU, por lo tanto, la sección crítica se ejecutará en exclusión
con todo proceso.
Pero con las interrupciones prohibidas no se servirá ni la interrupción de reloj ni ninguna otra. Dado que la asignación del
procesador a los diferentes procesos sólo cambia como consecuencia de una interrupción, sea de reloj o de cualquier otra
clase, el prohibir interrupciones hace que no pueda conmutarse el procesador a otro proceso.
Esta solución es poco atrayente porque le da a los procesos de usuario la capacidad de prohibir las interrupciones. Si no las
vuelve a habilitar, sería el fin del sistema. Además, si el computador tuviera dos o más procesadores, la desactivación de las
interrupciones sólo afectaría al procesador que ejecutó la instrucción. El resto de los procesadores continuarían normalmente
y podrían acceder a la memoria compartida sin mayor problema.
Sin embargo, a veces es conveniente que el propio sistema operativo prohiba las interrupciones mientras está actualizando sus
variables o listas. La prohibición de interrupciones es un mecanismo adecuado para garantizar la exclusión mutua dentro del
núcleo, pero poco recomendable para procesos de usuario.

Variable de cerradura
Analicemos una solución mediante software. Consiste en tener una variable de cerradura compartida por los procesos. Antes
de que un proceso entre en su sección crítica comprueba el valor de la cerradura. Si ésta vale 0, el proceso cambia el valor a 1 y
entra en la sección crítica. Si ésta ya vale 1, el proceso espera hasta que la variable valga 0. Por lo tanto, un 0 indica que ningún
proceso se encuentra en una sección crítica y un 1 indica que algún proceso está en una sección crítica. A continuación se
presenta el fragmento del código (en lenguaje C) de un proceso que quiere entrar en una sección crítica. A las instrucciones,
relativas a proporcionar la exclusión mutua, que se ejecutan antes de entrar en la sección crítica, se les llama protocolo de
entrada; a las que se ejecutan después de ejecutar el código de la sección crítica se les llama protocolo de salida. En este caso
el protocolo de entrada lo constituyen la conjunción de las instrucciones while y ‘cerrado = 1'. El protocolo de salida lo
conforma la instrucción ‘cerrado = 0'.

Esta solución no funciona. Suponga que un proceso lee la cerradura y comprueba que tiene un valor 0. Antes de que pueda
ponerla a 1 se planifica otro proceso. Éste comprueba el valor de cerrado, al ser 0 entra en su sección crítica. Si antes de que
este proceso termine la sección crítica se vuelve a planificar al primer proceso no se habrá logrado la exclusión mutua. El
problema proviene de que el protocolo de entrada es una sección crítica más, pues se dedica a modificar una variable
compartida (la variable cerrado). Obsérvese que si el protocolo de entrada se pudiera ejecutar sin cambios de proceso la
solución sería efectiva.

16
Algunos intentos para especificar el código de las primitivas de exclusión mutua
Acabamos de ver cómo el uso de una variable de cerradura fracasa en su intento. Analicemos otras posibles soluciones
software. Necesitamos unas primitivas de exclusión mutua que protejan el acceso a las secciones críticas. Utilizaremos el
siguiente esquema general (figura 4.2) : tendremos una pareja de construcciones entrar_exclusión_mutua y
salir_exclusión_mutua que encapsulan el código de cada proceso que tiene acceso a la variable compartida. La construcción
parbegin/parend hace que proceso_uno y proceso_dos operen como procesos concurrentes. Cada uno de estos procesos se
encuentra dentro de un ciclo infinito, entrando una y otra vez en sus secciones críticas.

Fig 4.2. Esquema general del uso de primitivas de exclusión mutua.

Primera tentativa
La variable compartida número_proceso, con un valor inicial 1, mantiene un registro del turno para entrar en la sección crítica.
El Proceso_uno ejecuta el ciclo while do. Como en un principio número_proceso vale 1, proceso_uno entra en la sección
crítica. Proceso_dos encuentra que número_proceso equivale a 1 y se mantiene dentro de su ciclo while do. Cada vez que el
proceso_dos obtiene el procesador, se limita a repetir el ciclo en espera de que número_proceso valga 2, de manera que
proceso_dos no entra en su sección crítica y la exclusión mutua está garantizada. El procesador está en uso mientras
proceso_dos no hace nada (excepto verificar el valor de número_proceso), esto se conoce como espera ocupada o activa. La
espera activa se considera una técnica aceptable cuando las esperas anticipadas son cortas; de lo contrario, la espera activa
puede resultar costosa.
La exclusión mutua está garantizada, pero a un precio alto. Los procesos deben entrar y salir de su sección crítica de manera
estrictamente alterna. Si un proceso es más corto que otro, la velocidad de uno de ellos condicionará al otro. Si uno de los
procesos termina, el otro no podrá continuar. Esta primera solución utiliza una variable global, y ella ocasiona el problema de la
sincronización con alternancia estricta.

17
Fig 4.3 Primera versión de las primitivas de exclusión mutua.

Segunda Tentativa
Por ello, en la segunda versión se usan dos variables: p1dentro, que es cierta si el proceso uno está en su sección crítica y,
p2dentro, que también será cierta si el proceso dos está en su sección crítica.
P1 y P2 son concurrentes e intentarán entrar simultáneamente en su sección crítica. No está garantizada la exclusión mutua. El
problema se debe a que, entre el momento en que un proceso determina que puede seguir y el momento en que se asigna
cierto a su bandera ( variable centinela), hay tiempo suficiente para que el otro verifique su bandera y entre en su sección
crítica.
Por tanto, en el momento en que realiza su verificación deberá estar seguro de que el otro proceso no podrá pasar su
verificación.

Fig 4.4. Segunda versión de las primitivas de exclusión mutua.

Tercera Tentativa
Ahora cambiamos la bandera antes de realizar la verificación, indicando su deseo de ejecutar la sección crítica. Se puede
producir un bloqueo mutuo, los dos procesos entrarán en un ciclo infinito. Esto se debe a que cada uno de los procesos se
puede bloquear en su verificación.

Fig 4.5. Tercera versión de las primitivas de exclusión mutua.

18
Cuarta Tentativa
En la anterior versión quedábamos atrapados en la verificación, por tanto, ahora intentaremos escapar de ella. Una manera de
hacerlo es retardando la verificación, para comprobar si el otro también desea entrar. La exclusión mutua está garantizada y no
ocurre bloqueo mutuo, pero puede ocurrir un aplazamiento indefinido. Un aplazamiento indefinido o inanición es una
situación en la que se posterga indefinidamente el progreso en la ejecución de uno o más procesos. Veamos cómo puede
suceder: puesto que no podemos hacer suposiciones sobre sus velocidades podría suceder que se ejecutasen en tándem, esto
es, uno ejecuta una instrucción y el otro, a continuación, la correspondiente. Luego, cada proceso puede asignar el valor cierto
a su bandera, realizar la verificación, entrar en el cuerpo del 'ciclo while', asignar el valor falso a su bandera, asignar el valor
cierto a su bandera y después repetir la secuencia comenzando con la verificación (esto no se puede consentir para ciertas
aplicaciones que deben ejecutar en un tiempo concreto como el software de control de vuelo, un marcapasos o un sistema de
control de tráfico aéreo...).

Fig 4.6. Cuarta versión de las primitivas de exclusión mutua.

Algoritmo de Dekker
Gestiona elegantemente la exclusión mutua. Utiliza, además de las banderas de intención de entrar, una variable turno
(proceso_favorecido) que resuelve en caso de conflicto. Da la posibilidad de que el otro proceso entre en su sección crítica una
segunda vez si lo hace antes que el proceso que acaba de dejar el bucle de espera, asigne el valor cierto a su intención de
entrar. Pero cuando de nuevo tiene el procesador, realiza la asignación y es su turno, así que se ejecuta. En consecuencia, no
produce aplazamiento indefinido (figura 4.7).

19
Fig 4.7. Algoritmo de Dekker

Algoritmo de Peterson
Simplifica el algoritmo de Dekker. El protocolo de entrada es más elegante con las mismas garantías de exclusión mutua,
imposibilidad de bloqueo mutuo y de aplazamiento indefinido.

20
Fig 4.8 Algoritmo de Peterson

4.2.2 Soluciones hardware a la exclusión mutua


Se estudia ahora una solución que requiere apoyo del hardware. Muchos ordenadores, en particular aquellos diseñados
teniendo en mente varios procesadores, tienen una instrucción TEST AND SET LOCK (TSL). Esta instrucción máquina provoca la
lectura del contenido de una palabra de la memoria en un registro, y el almacenamiento de un valor distinto de cero en dicha
palabra de memoria. Al ser una instrucción máquina, las operaciones de lectura y escritura de la palabra tienen la garantía de
ser indivisibles (atómicas). Además, ninguno de los demás procesadores puede tener acceso a la palabra hasta terminar la
instrucción. El procesador que ejecuta la instrucción TSL cierra el bus de la memoria para prohibir a los demás el acceso a la
memoria hasta el término de la instrucción.

A continuación se muestra una solución al problema de la exclusión mutua que se basa en la instrucción TSL. La solución viene
expresada en un lenguaje ensamblador ficticio (pero típico). Existen dos subrutinas que los procesos llamarán como
protocolos de entrada y salida:

21
La variable cerrado es una variable compartida que se utiliza para coordinar el acceso a las secciones críticas. Cuando vale 0 se
permite el acceso y cuando vale 1 no. Cuando un proceso quiera entrar a una sección crítica llamará a la subrutina
entrar_sección. Esta subrutina estará ejecutando un ciclo hasta que cerrado valga 0. Obsérvese que esta solución funciona,
mientras que la solución de la variable cerradura no funcionaba. La clave está en que la instrucción TSL realiza atómicamente el
comprobar que cerrado vale 0 y el asignarle el valor 1.

Fig 5.8. Instrucción TSL

4.2.3 Semáforos
Tanto la solución de Peterson como la que emplea la instrucción TSL son correctas, pero adolecen del defecto de tener que
utilizar espera ocupada. Esencialmente, lo que hacen estas soluciones es lo siguiente: inmediatamente antes de entrar en sus
regiones críticas, los procesos comprueban que les está permitido; si no es así, se quedan en un bucle de espera hasta que
puedan entrar.
Este mecanismo no sólo malgasta tiempo del procesador, sino que también puede producir efectos inesperados. Considérese
una computadora con dos procesos; uno, H, de alta prioridad; y otro, L, de prioridad inferior. Las reglas de planificación (ver
tema) hacen que se asigne el procesador a H siempre que esté listo para ejecución. En un instante dado, L estará dentro de su
región crítica y H pasará a estar listo para ejecución. H comenzará ahora a realizar una espera activa, pero, dado que L no
obtiene nunca el procesador mientras H esté ejecutándose, L nunca podrá salir de su región crítica, así que H se quedará para
siempre esperando.
Podemos emplear unas primitivas de comunicación entre procesos que, en lugar de malgastar tiempo de procesador cuando
un proceso no puede entrar en su región crítica, bloquean la ejecución del mismo. Una de las parejas más simples de
primitivas son DORMIR (sleep) y DESPERTAR (wakeup). DORMIR es una llamada al sistema que bloquea al llamador, es decir,
suspende su ejecución hasta que otro proceso lo despierta. La llamada DESPERTAR tiene como parámetro el proceso que se
debe despertar.
El problema del productor-consumidor (con un buffer finito) servirá para ilustrar el uso de estas primitivas. Especifiquemos el
problema. Un conjunto de procesos "productores" y un conjunto de procesos "consumidores" se comunican entre sí a través
de un buffer, en el que los productores van depositando elementos, y del cual, los consumidores los van sacando. Los
productores llevan a cabo de forma continua un ciclo "producir un elemento - guardar el elemento en el buffer", y los
consumidores repiten un ciclo parecido: "sacar un elemento del buffer - consumir el elemento". Un productor típico puede ser
un proceso de tipo computacional que vaya colocando líneas de resultados en un buffer, mientras que el consumidor
correspondiente puede ser el proceso que imprime estas líneas. El buffer tiene una capacidad limitada, pudiendo almacenar
sólo N elementos del mismo tamaño. La sincronización que se requiere en este caso es doble: en primer lugar, los productores
no pueden colocar elementos en el buffer si éste está lleno, y en segundo lugar, los consumidores no pueden sacar elementos
del buffer si éste está vacío.
Además, el buffer y las variables compartidas deben protegerse del acceso concurrente por parte de los distintos procesos,
pues son zonas compartidas de memoria modificables por los procesos. De ahí que el acceso al buffer y a las variables
compartidas se realice en exclusión mutua.
Los problemas comienzan cuando el productor quiere dejar algo en el buffer, pero éste ya está lleno. La solución es mandar a
dormir al productor y despertarlo sólo cuando el consumidor haya retirado uno o más elementos. Análogamente, debe

22
enviarse a dormir al consumidor cuando trate de retirar un elemento del buffer y éste esté vacío. Más tarde, cuando el
productor deje uno o más elementos en el buffer, despertará al consumidor (ver figura 4.1).
Sin darnos cuenta, nos hemos metido en la boca del lobo. Para llevar la cuenta del número de elementos en el buffer,
necesitamos una variable, cuenta. El acceso a la variable cuenta no está controlado. Así, se puede presentar la siguiente
situación: el buffer está vacío y el consumidor acaba de leer 0 de la variable cuenta. En este instante el planificador decide
pasar el control del consumidor al productor. El productor deja un elemento en el buffer, incrementa cuenta y observa que su
valor ahora es 1, por lo que deduce que su valor era 0 y que, por tanto, el consumidor está dormido, así que el productor llama
a despertar para despertar al consumidor.
Por desgracia, el consumidor no está todavía lógicamente dormido, de modo que la señal para despertarlo se pierde. Cuando
el consumidor se active de nuevo, comprobará el valor de cuenta que leyó anteriormente, verá que es 0 y se irá a dormir. Por
su parte, el productor acabará llenando el buffer y también se tendrá que ir a dormir. Ambos dormirán eternamente.

Fig 4.1 Solución equívoca al problema del productor-consumidor mediante dormir-despertar

Para solucionar este problema, Dijkstra, en 1965, tuvo la idea de utilizar una variable entera para contar el número de señales
de despertar puestas "en conserva" para uso futuro. Propuso el tipo de variable llamada semáforo junto con las operaciones
wait y signal definidas sobre él. En el artículo original sobre los semáforos Dijkstra utilizó las letras P y V, que son las iniciales de
estos términos en holandés. Nosotros utilizaremos wait y signal por ser más descriptivos.
Un semáforo es una zona de memoria compartida que almacena un entero no negativo sobre el cual sólo puede actuarse con
una de las operaciones siguientes:
- Inicialización: Un semáforo sólo puede inicializarse una vez, normalmente en el momento en que se define. Nosotros
representaremos la inicialización como 's  x', donde s es el nombre del semáforo y x es un entero no negativo.
- wait(s): Si s > 0 decrementa s en una unidad. Si s vale 0 provoca el bloqueo del proceso hasta que otro proceso realice
una operación signal sobre s. Si existen varios procesos bloqueados en un semáforo, sólo uno de ellos podrá pasar a
listo tras una operación signal sobre él. No se lleva a cabo suposición alguna sobre cuál de los procesos bloqueados
será el elegido.
- signal(s): Si no hay procesos bloqueados en s, provoca el incremento de s en una unidad. Si hay procesos bloqueados
en s (que valdrá cero), provoca la transición a listo de uno de estos procesos.
Además, las acciones que implica una operación wait o signal se ejecutan indivisiblemente. Esto es crucial para garantizar la
coherencia del semáforo, ya que éste no es más que una variable compartida. Como se vio, una zona compartida de memoria
no puede ser accedida concurrentemente por más de un proceso, si al menos uno de ellos la modifica. Volveremos sobre este
tema en breve.
Una posible implementación de los semáforos
Como se ha comentado, los mecanismos de comunicación entre procesos son servicios del sistema operativo. La
implementación de wait y signal se va a realizar como rutinas de servicio a interrupción. La operación wait lleva implícita la
idea de que los procesos se quedan bloqueados cuando un semáforo tiene el valor 0, y son liberados cuando se realiza una
operación signal. La forma natural de implementar todo esto es asociando a cada semáforo una cola. Cuando un proceso lleva
a cabo una operación wait sobre un semáforo a cero, se añade a la cola del semáforo y se le pasa a estado bloqueado.
Recíprocamente, cuando se ejecuta una operación signal sobre un semáforo determinado, se saca un proceso de la cola del
mismo (a menos que esté vacía) y se le hace listo. Un semáforo debe implementarse, pues, a base de dos elementos: un
entero y un puntero asociado a una cola (que puede ser nulo).

23
No hemos dicho nada aún acerca del proceso que será sacado de la cola del semáforo después de una operación signal.
Tampoco hemos dicho nada sobre el lugar donde hay que colocar un proceso que haya efectuado un wait sobre un semáforo a
cero. Para la mayoría de semáforos es adecuada una cola en la que se siga el criterio de que el primero en entrar sea el
primero en salir (FIFO), ya que con ello se garantizaría que todos los procesos bloqueados puedan eventualmente hacerse
listos. Lo que es importante tener en cuenta es que distintos semáforos pueden estar asociados a colas con criterios distintos.
Ello obliga a añadir un nuevo elemento en la implementación de un semáforo con el fin de especificar el criterio de gestión de
la cola. Este elemento puede ser un puntero a un pequeño programa que lleve a cabo las funciones de entrada y salida de la
cola.

Indivisibilidad
Como se apuntó más arriba tanto wait como signal deben ser operaciones indivisibles, en el sentido de que sólo un proceso
debe poder ejecutarlas en cualquier momento. Justifiquemos dicho requisito de indivisibilidad. Supóngase que dos procesos,
P0 y P1, desean realizar la operación signal(S) sobre el semáforo S que tiene el valor 3. Si el código expuesto más arriba no es
indivisible, podría darse la circunstancia de que el procesador se cambiara mientras que un proceso ejecuta la instrucción 's  s
+ 1'. Como esta instrucción de un lenguaje de alto nivel puede ser traducida por el compilador a las instrucciones en
ensamblador que se apuntan en la siguiente tabla (donde 'LOAD A, S' carga el contenido de la variable S, 'ADD A' incrementa el
acumulador en una unidad y 'STO S, A' almacena en la variable S el contenido del acumulador)

la ejecución de estas instrucciones en el orden a1b1b2b3a2a3 provocaría que S valiera 4, cuando debería valer 5. El problema
básico es el mismo del ejemplo de la cuenta bancaria, P0 lee S, pero después trabaja con un valor local (3) distinto al valor
actual (4).
Se presenta un problema análogo si dos procesos ejecutan simultáneamente una operación wait sobre un mismo semáforo, o
si se ejecuta simultáneamente un wait y un signal.
Pasemos ahora a ver cómo puede lograrse la exclusión mutua en el código de wait y signal. Ambas operaciones deben
implementarse como rutinas que empiecen con algún tipo de operación de bloqueo, y terminen con otra operación de
desbloqueo. En una configuración consistente en un sólo procesador, la operación de bloqueo puede implementarse
fácilmente inhibiendo el mecanismo de interrupción. Ello asegura que el proceso no puede perder el control del procesador
central mientras esté ejecutando el wait o el signal, ya que no hay forma de que pueda ser interrumpido. La operación de
desbloqueo se llevaría a cabo entonces, simplemente, volviendo a autorizar las interrupciones. En una máquina con varios
procesadores centrales, este procedimiento no es adecuado, ya que es posible que dos procesos ejecuten de forma simultánea
un wait o un signal trabajando sobre dos procesadores distintos. En este caso, el problema se solventa desactivando las
interrupciones y encerrando el código de las operaciones en una exclusión mutua basada en la instrucción TSL, tal como se vio
en el apartado de la exclusión mutua con espera ocupada. La utilización de la instrucción TSL implica una espera ocupada
potencial si dos procesos quieren ejecutar simultáneamente, en procesadores distintos, una operación sobre un mismo
semáforo. Sin embargo, como las rutinas wait y signal son muy simples, el tiempo de espera empleado en este ciclo de espera
es corto.
Es conveniente destacar que las operaciones de bloqueo y desbloqueo implementadas mediante la instrucción TSL no pueden
emplearse como sustitutas de los semáforos para implementar la exclusión mutua entre procesos. La espera ocupada no es
aceptable para el tiempo que podría pasar un proceso esperando que acabe la sección crítica de otro proceso. Sin embargo, sí
es aceptable para lograr la exclusión mutua de las operaciones sobre semáforos, pues estas tienen un código corto, esto se
ilustra en la siguiente tabla

24
Fig 4.3. Utilización de semáforos para la Exclusión Mutua

Ejemplos de sincronización utilizando semáforos


Al principio del tema ya explicamos en qué consistía la sincronización. Un tipo particular de sincronización lo constituye la
exclusión mutua, que puede ser lograda si los procesos colocan una operación wait justo antes de entrar en su sección crítica,
y una operación signal inmediatamente después de su sección crítica sobre un mismo semáforo exmut cuyo valor inicial sea 1.
Obsérvese que esta solución evita la espera ocupada.

Ejemplo 1
Dados los siguientes procesos, donde las letras representan pasos secuenciales:

25
vamos a sincronizarlos de forma que el paso f de P1 no se pueda realizar hasta que P0 haya realizado el paso b. La solución es:

Ejemplo 2

Dados los procesos del ejemplo anterior, vamos a sincronizarlos de forma que el paso f de P1 no pueda realizarse hasta que P0
haya realizado el paso b, y que el paso c de P0 no se pueda realizar hasta que P1 haya realizado el paso e. Este tipo de
sincronización doble, en el que los dos procesos se sincronizan en un determinado punto de cada proceso se llama cita.
Veamos como funciona el siguiente intento:
Esta solución no es correcta, si el proceso P1 llega a wait(s) antes de que P0 llegue a signal(s), la sincronización se produce, si
no ocurre esto, la sincronización falla. El siguiente planteamiento sí funciona:

Por último, supóngase que un proceso desea que se le avise si ocurre un evento específico, y que algún otro proceso es capaz
de detectar la ocurrencia de este evento. Este problema lo resolveríamos con la utilización de un semáforo tal y se muestra en
la figura 4.4.

26
Fig 4.5 Ejemplo de sincronización con semáforos.

Deadlock (Interbloqueo)
Cuando varios procesos compiten por recursos es posible que se dé una situación en la que ninguno de ellos pueda proseguir
debido a que los recursos que cada uno de ellos necesita estén asignados a otros procesos. Esta situación se conoce en inglés
con el nombre de deadlock, nosotros utilizaremos el término interbloqueo. Otra definición de interbloqueo es la siguiente: "un
conjunto de procesos se interbloquea si cada proceso del conjunto espera un evento que sólo puede ser provocado por otro
proceso del conjunto".
Una situación que provoca un interbloqueo es la de varios procesos que tienen secciones críticas mutuamente excluyentes,
para su ejecución en exclusión mutua adoptan la solución de utilizar un semáforo exmut inicializado a 1, tal como se vio un
poco más arriba. Si un proceso ejecuta un wait(exmut) dentro de una sección crítica interbloquearía a todos los procesos que
excluyen sus secciones críticas con exmut.

Veamos otro ejemplo, dos procesos, llamados P0 y P1, utilizan dos recursos no compartibles (tales como una impresora y un
fichero abierto en modo de escritura) X e Y. Para proteger su uso concurrente se utilizan los semáforos x e y, en el modo antes
descrito. Estúdiese los fragmentos de código de P0 y P1 mostrados más abajo. Ambos procesos quieren utilizar los dos
recursos a la vez. Si primero se ejecuta el código de un proceso y después el del otro no hay ningún problema. Pero supóngase
que tras realizar P0 el wait(x) se cambia el procesador a P1, si éste ejecuta wait(y) se producirá un interbloqueo. Es importante
darse cuenta de que este error se producirá sólo para muy pocas ejecuciones (especialmente si estos fragmentos de código
cálculos(2), cálculos(3), cálculos(5) y cálculos(6) son pequeños), pues va a depender del orden de asignación del procesador a
los procesos (algo impredecible). Por lo tanto, estos errores son difíciles de detectar, y cuando ocurren, difíciles de depurar,
pues para reproducir el error se debería dar una secuencia de planificación similar, y la secuencia de planificación dependerá
de la carga del sistema.
Volviendo al ejemplo anterior del productor-consumidor veamos cómo también una mala programación nos llevaría a un
interbloqueo. Supongamos que invertimos las operaciones wait en el código del productor, de forma que mutex se decrementa
antes que vacio. Si el buffer está lleno, el productor se bloquearía, con mutex igual a 0. La próxima vez que el consumidor
intenta acceder al buffer, realiza wait sobre mutex, y como vale 0, el consumidor también se bloquea.

27
Fig 4.6. Productor-Consumidor con semáforos (buffer de un sólo elemento)

Fig 4.7. Solución al problema productor_consumidor con semáforos (buffer con N elementos)
Se llega a la conclusión de que la programación con semáforos debe realizarse con mucho cuidado para evitar los
interbloqueos.

4.2.4 Monitores
La programación mediante semáforos tiene el inconveniente de que es fácil equivocarse al utilizarlos; además, una
equivocación de este tipo es difícil de detectar, pues sólo produce ejecuciones erróneas ocasionalmente.
Para facilitar la escritura de programas correctos, Hoare (1974) y Brinch Hansen (1975) propusieron una primitiva de
sincronización de alto nivel, llamada monitor. Sus propuestas tenían ligeras diferencias, las cuales señalaremos a continuación.
Un monitor es una colección de procedimientos, variables y estructuras de datos que se agrupan en cierto tipo de módulo o
paquete. Los procesos pueden llamar a los procedimientos de un monitor cuando lo deseen, pero no se tiene acceso directo a
las estructuras de datos internas del monitor desde un procedimiento declarado fuera de él. Para modificar las estructuras de
datos internas del monitor es preciso utilizar los procedimientos definidos en él. El siguiente ejemplo ilustra un monitor escrito
en un lenguaje similar a C.
Monitor ejemplo

28
{
int i;
condición c;
insertar(int item)
{
...
}
sacar(int *item)
{
...
}
}
Los monitores tienen una propiedad importante que los hace útiles para conseguir la exclusión mutua: sólo uno de los
procesos puede estar activo en un monitor en cada momento. Los monitores son construcciones del lenguaje de
programación, por lo que el compilador sabe que son especiales y puede controlar las llamadas a los procedimientos del
monitor de manera distinta a las llamadas a los demás procedimientos. Por lo general, cuando un proceso llama a un
procedimiento de un monitor, las primeras instrucciones de éste verificarán si hay otro proceso activo dentro del monitor. En
caso afirmativo, el proceso que hace la llamada será bloqueado hasta que el otro proceso salga del monitor. Si no hay otro
proceso que esté utilizando el monitor, el que hace la llamada podrá entrar.
El compilador es el responsable de conseguir la exclusión mutua sobre los datos del monitor, siendo usual que implemente
dicha exclusión con semáforos, tal como se vio en el apartado anterior. En todo caso, el programador no es consciente del
modo en que se logra tal exclusión, él sólo debe preocuparse de determinar los datos compartidos y las secciones críticas, y de
situar todo esto en el interior del monitor. La responsabilidad de lograr la exclusión mutua es del compilador.
Sin embargo, los monitores no son tan sencillos de programar como parece a primera vista. Existe un problema que aún no se
ha comentado. A veces es necesario que un proceso se bloquee dentro del monitor. Por ejemplo, en el problema de los
productores y consumidores es posible que un productor se deba bloquear al intentar insertar y descubrir que el buffer está
lleno. Si se bloqueara sin más, tendríamos un interbloqueo, pues ningún consumidor podrá entrar al monitor para consumir un
elemento y producir un espacio.
La solución a este problema reside en la introducción de variables de condición, junto con dos operaciones sobre ellas, wait y
signal. Si un procedimiento de un monitor descubre que no puede continuar (por ejemplo, cuando un productor encuentra el
buffer lleno), ejecuta una operación wait sobre alguna variable de condición, como por ejemplo espacio. Esto hace que el
proceso que entró en el monitor se bloquee. También permite que otro proceso que tenía prohibida la entrada al monitor
pueda hacerlo ahora.
Este otro proceso, como por ejemplo, un consumidor, puede despertar a su compañero dormido con una instrucción signal,
sobre la variable de condición que su compañero espera. Para evitar que existan dos procesos activos en el monitor al mismo
tiempo, es necesaria una regla que indique lo que debe ocurrir después de signal. Hoare propuso que se permitiera la
ejecución del proceso que ha despertado y que se suspendiera la ejecución del otro. Brinch Hansen propuso que un proceso
que realizara una instrucción signal debería salir inmediatamente del monitor. En otras palabras, una operación signal debería
aparecer sólo como última instrucción de un procedimiento del monitor. Nosotros utilizaremos la propuesta de Brinch Hansen.
Si se ejecuta un signal en una variable de condición en la que están esperando varios procesos, sólo uno de ellos revive, siendo
determinado éste por el planificador del sistema.
Las variables de condición no son contadores. No acumulan señales para su uso posterior, como hacen los semáforos. Así, si se
hace un signal sobre una variable de condición a la que nadie espera, la señal se pierde. Wait debe aparecer antes de signal.
En la práctica, esto no representa ningún problema puesto que un proceso puede verificar el valor de una variable de
condición (esta operación no es realizable sobre semáforos) para ver si es necesario realizar un signal.
A continuación se muestra una solución al problema de los productores y consumidores utilizando monitores. En esta solución
se utilizan los conceptos recién explicados. Está expresada en un lenguaje parecido a C.

29
Con la exclusión automática de las secciones críticas, los monitores hacen que la programación en paralelo tenga menos
tendencia a los errores que en el caso del uso de semáforos. Sin embargo, pocos lenguajes de programación incluyen
monitores, ejemplos de estos lenguajes son Concurrent Euclid y Modula 2.
Fig 4.8. Asignación de recursos mediante monitores

30
4.3 Sin Utilizar Memoria Compartida. Mensajes
Los monitores y los semáforos se diseñaron para resolver problemas de sincronización en un sistema mono o multiprocesador,
es decir, un sistema con una o varias CPU's que tienen acceso a una memoria compartida. La comunicación se logra
compartiendo una zona de memoria donde los procesos guardan información, los semáforos y monitores se utilizan para
sincronizar el acceso a dicha zona. Si pasamos a un sistema distribuido, con varias CPU's, cada una con su memoria privada,
unidas mediante una red, estos mecanismos de comunicación entre procesos ya no se pueden aplicar. Además, estos dos
mecanismos sirven para la sincronización, pero no para el intercambio de información entre procesos. En el siguiente apartado
veremos un mecanismo que solventa ambos problemas.
Transferencia de mensajes
Este mecanismo de comunicación entre procesos se basa en dos primitivas: send y receive, las cuales, al igual que los
semáforos, y a diferencia de los monitores, son servicios (llamadas al sistema) proporcionados por el sistema operativo. Los
lenguajes de programación tendrán rutinas de biblioteca para invocar tales servicios desde un programa. Dichas rutinas de
biblioteca tendrán un formato parecido al siguiente:
send(mensaje, destino) ,y
receive(mensaje, origen)
La primera función envía un mensaje a un destino dado, y la segunda recibe un mensaje desde cierto origen (o desde cualquier
origen, si al receptor no le importa el origen).
Los sistemas operativos suelen implementar dos tipos de comunicación basadas en transferencia de mensajes: la directa y la
indirecta. El comportamiento de los procesos en cuanto a la sincronización es distinto en los dos tipos de comunicación. A
continuación se exponen ambos.

Transferencia de mensajes con designación directa


En este sistema, tanto el proceso emisor como el proceso receptor deben especificar explícitamente el proceso destino y
origen respectivamente. Las funciones tendrá un formato similar al siguiente:
send(mensaje, identificador proceso destino)
receive(mensaje, identificador proceso origen)
En cuanto a la sincronización, viene determinada por la ausencia de un lugar donde almacenar el mensaje. Si se ejecuta send
antes de receive en el proceso destino, el proceso emisor se bloquea hasta la ejecución de receive, momento en el cual el
mensaje se puede copiar de manera directa desde el emisor al receptor sin almacenamiento intermedio. Análogamente, si se
ejecuta primero receive, el receptor se bloquea hasta que se ejecute una operación send en el proceso emisor. Este modo de
sincronización se conoce como cita (rendezvous).
Transferencia de mensajes con designación indirecta
En este sistema, los mensajes se envían o reciben de unos objetos proporcionados por el sistema operativo denominados
buzones. Un buzón es un lugar donde almacenar un número determinado de mensajes, la cantidad de mensajes que alberga el
buzón se especifica al crearlo. Al utilizar buzones, los parámetros de dirección en las llamadas send y receive son buzones, y no
procesos:
send(mensaje, buzón)
receive(mensaje, buzón)
El sistema operativo debe proporcionar servicios para la creación y destrucción de buzones, así como para la especificación de
los usuarios y/o procesos que pueden utilizar un buzón.

31
En cuanto a la sincronización, un proceso que haga una operación send sobre un buzón, sólo se bloquea si dicho buzón está
lleno. Un proceso que realice una operación receive sobre un buzón, sólo se bloquea si el buzón está vacío, es decir, no
contiene mensajes.
Los sistemas de transferencia de mensajes con designación directa son más fáciles de implementar. Sin embargo, son menos
flexibles, puesto que el emisor y el receptor deben estar ejecutándose simultáneamente. Si se utilizan buzones, el emisor
puede almacenar sus mensajes en un buzón, pudiendo el receptor recuperarlos posteriormente.
Ejemplos de utilización de transferencia de mensajes
En este apartado vamos a ver cómo resolver los problemas de la exclusión mutua y de los productores y consumidores
utilizando transferencia de mensajes con designación indirecta. En el apartado de Comunicación y Sincronización se emplea la
designación directa para crear una aplicación cliente-servidor.

Exclusión mutua

Se necesita un proceso de inicialización, en el que se crea el buzón exmut con un tamaño de un mensaje, y tamaño de
mensaje de un byte. A continuación se llena el buzón (el contenido del mensaje, el carácter '*', no es relevante). Para realizar la
exclusión mutua en la sección crítica del proceso Pi se tiene en cuenta que la operación receive es indivisible, y representa un
bloqueo potencial si el buzón está vacío. Obsérvese que la solución es análoga a la realizada mediante semáforos. En aquella se
definía un semáforo de valor incial 1, representando este valor la ausencia de procesos dentro de una sección crítica. El cero
representaba el que un proceso estaba en una sección crítica. En esta solución, se define un buzón de tamaño 1, la existencia
de un mensaje en ese buzón significa la no existencia de procesos en una sección crítica. La ausencia de mensajes en el buzón
implica que un proceso está en una sección crítica.

Productores y consumidores
Para la solución se crean dos buzones de tamaño N (longitud del buffer). El tamaño de un mensaje es sizeof(int), sizeof es una
macro de C que sirve para calcular el número de bytes que ocupa el tipo de dato que representa su único parámetro (en este
caso un entero). En el buzón esp habrá tantos mensajes (de valor insustancial 0) como espacios haya en el buzón ele. La
información almacenada en el buzón ele sí es importante, son los elementos producidos por los productores.

Llamadas a procedimientos remotos


La noción de llamadas a procedimientos remotos (remote procedure calls, RPC) se introdujo con el objeto de ofrecer un
mecanismo estructurado de alto nivel para realizar la comunicación entre procesos en sistemas distribuidos. Con una llamada
a un procedimiento remoto, un proceso de un sistema puede llamar a un procedimiento de un proceso de otro sistema. El
proceso que llama se bloquea esperando el retorno desde el procedimiento llamado en el sistema remoto y después continúa
su ejecución desde el punto que sigue a la llamada.
El procedimiento llamado y el que llama residen en máquinas distintas, con espacios de direcciones distintos, no existe la
noción de variables globales compartidas, como en los procedimientos normales dentro de un proceso. Las RPC transfieren la
información a través de parámetros de la llamada. Una RPC se puede realizar con los mecanismo del tipo enviar/recibir.
send(proceso_remoto,parámetros_entrada);
receive(proceso_remoto, parámetros_salida);
Una llamada a procedimiento remoto debería ser igual a una llamada a un procedimiento local, desde el punto de vista del
usuario. Una llamada por valor es más fácil enviando copias de los datos mediante mensajes. Las llamadas por referencia son

32
más difíciles, porque una RPC cruza a otro espacio de direcciones. Los formatos internos de los datos pueden cambiar en una
red de máquinas heterogéneas; requieren conversiones complejas y un considerable trabajo adicional. Deben tenerse en
cuenta la posibilidad de transmisiones defectuosas y la pérdida de mensajes en la red.

4.4 Problemas Clásicos de Comunicación entre Procesos

4.4.1 El problema de la cena de los filósofos


En 1965 Dijkstra planteó y resolvió un problema de sincronización llamado el problema de la cena de los filósofos, que se
puede enunciar como sigue. Cinco filósofos se sientan a la mesa, cada uno con un plato de espaghetti. El espaghetti es tan
escurridizo que un filósofo necesita dos tenedores para comerlo. Entre cada dos platos hay un tenedor. En la figura 4.15 se
muestra la mesa.

Fig 4.15. Los filósofos se disponen a comer

La vida de un filósofo consta de periodos alternos de comer y pensar. Cuando un filósofo tiene hambre, intenta obtener un
tenedor para su mano derecha, y otro para su mano izquierda, cogiendo uno a la vez y en cualquier orden. Si logra obtener los
dos tenedores, come un rato y después deja los tenedores y continúa pensando. La pregunta clave es: ¿ puede el lector escribir
un programa para cada filósofo que permita comer equitativamente a los filósofos y no se interbloquee ?

Figura 4.16. Una no-solución al problema de la cena de los filósofos

La figura 4.16 muestra una solución obvia. El procedimiento coger_tenedor espera hasta que el tenedor especificado esté
disponible y lo coge. Por desgracia la solución obvia es incorrecta. Supongamos que los cinco filósofos cogen sus tenedores
izquierdos de forma simultánea. Ninguno podría coger su tenedor derecho, lo que produciría un interbloqueo.

33
Se podría modificar el programa de forma que después de coger el tenedor izquierdo, el programa verificara si el tenedor
derecho está disponible. Si no lo está, el filósofo deja el izquierdo, espera cierto tiempo y vuelve a repetir el proceso. Esta
propuesta también falla, aunque por razones distintas. Con un poco de mala suerte todos los filósofos podrían empezar el
algoritmo de forma simultánea, por lo que cogerían sus tenedores izquierdos, verían que los derechos no están disponibles,
esperarían, volverían a coger sus tenedores izquierdos simultáneamente, etc. eternamente. Esto implica un aplazamiento
indefinido.

Figura 4.17. Una solución al problema de la cena de los filósofos

El lector podría pensar: "si los filósofos esperaran un tiempo arbitrario, en vez del mismo tiempo, después de que no pudieran
coger el tenedor derecho, la probabilidad de que todo quedara bloqueado, incluso una hora, sería muy pequeña". Esta
observación es correcta, pero en ciertas aplicaciones se desea una solución que funcione siempre, y no que pueda funcionar
bien con gran probabilidad. (Piense en el control de seguridad de una planta nuclear).
Una mejora a la figura 4.16, que no tiene interbloqueos ni aplazamiento indefinido, es la protección de los cinco enunciados
siguientes a la llamada al procedimiento pensar mediante un semáforo binario exmut. Antes de empezar a coger los tenedores,
un filósofo haría un wait sobre exmut. Desde el punto de vista teórico esta solución es adecuada. Desde el punto de vista
práctico presenta un error de eficiencia: en todo instante existirá a lo sumo un filósofo comiendo. Si se dispone de cinco
tenedores, se debería permitir que dos filósofos comieran al mismo tiempo.
La solución que aparece en la figura 4.17 es correcta, y permite el máximo paralelismo para un número arbitrario de filósofos.
Utiliza un vector, estado, para llevar un registro de la actividad de un filósofo: si está comiento, pensando o hambriento (estado
que indica que quiere coger los tenedores). Un filósofo puede comer únicamente si los vecinos no están comiendo. Los vecinos
del i-ésimo filósofo se definen en las macros IZQ y DER. En otras palabras, si i= 2, entonces IZQ = 1, y DER = 3.
El programa utiliza un vector de semáforos, uno por filósofo, de forma que los filósofos hambrientos puedan bloquearse si los
tenedores necesarios están ocupados. Observe que cada proceso ejecuta el procedimiento filósofo como programa principal,
pero los demás procedimientos, coger_tenedores, dejar_tenedores y prueba, son procedimientos ordinarios y no procesos
separados.

4.2 El problema de los lectores y los escritores


El problema de la cena de los filósofos es útil para modelar procesos que compiten por el acceso exclusivo a un número
limitado de recursos, como una unidad de cinta u otro dispositivo de E/S. Otro problema famoso es el de los lectores y
escritores (Courtois et al., 1971), que modela el acceso a una base de datos. Supóngase una base de datos, con muchos
procesos que compiten por leer y escribir en ella. Se puede permitir que varios procesos lean de la base de datos al mismo
tiempo, pero si uno de los procesos está escribiendo (es decir, modificando) la base de datos, ninguno de los demás debería
tener acceso a ésta, ni siquiera los lectores. La pregunta es ¿ cómo programaría los lectores y escritores ? La solución se
muestra en la figura 4.18.

34
En esta solución, el primer lector que obtiene el acceso a la base de datos realiza un wait sobre el semáforo bd. Los lectores
siguientes sólo incrementan un contador, nl. Al salir los lectores, éstos decrementan el contador, y el último en salir realiza un
signal sobre el semáforo, lo que permite entrar a un escritor bloqueado, si existe.
Una hipótesis implícita en esta solución es que los lectores tienen prioridad sobre los escritores. Si surge un escritor mientras
varios lectores se encuentran en la base de datos el escritor debe esperar. Pero si aparecen nuevos lectores, y queda al menos
un lector accediendo a la base de datos, el escritor deberá esperar hasta que no haya más lectores interesados en la base de
datos.
ENLACE A LA SIMULACIÓN LECTORES-ESCRITORES (Productor-Consumidor)

Figura 4.18. Una solución al problema de los lectores y escritores

INTERBLOQUEOS
5.1 Definiciones Previas
Cuando un proceso de un sistema de multiprogramación espera en balde a que se presente un evento específico, se dice que
se encuentra en un estado de interbloqueo o bloqueo mutuo. Los procesos que pueden encontrase en esta situación pueden
ser uno o varios.
En los sistemas de multiprogramación, compartir recursos es uno de los principales objetivos del sistema operativo. Cuando
se comparten recursos entre una población de usuarios o procesos, cada uno de los cuales mantiene un control exclusivo
sobre ciertos recursos asignados a él, es posible que se produzcan bloqueos mutuos que impedirán la terminación de algunos
de los procesos del sistema.

35
Todos los interbloqueos suponen demandas contradictorias de recursos por parte de dos o más procesos. La figura 5.1 ilustra
este conflicto de forma abstracta en el caso de dos procesos y dos recursos. Los dos ejes del diagrama representan el avance
de los dos procesos en términos de instrucciones ejecutadas. El avance conjunto de los dos procesos se representa entonces
con una secuencia discreta de puntos en el espacio. Las líneas horizontales o verticales representan el intervalo de tiempo en
el que sólo uno de los procesos está ejecutándose (intercalado); una línea diagonal significa ejecución simultánea
(solapamiento). Supóngase que existe un punto en la ejecución de cada proceso en el que se requiere el uso exclusivo de
ambos recursos, R1 y R2, para continuar. En el ejemplo, llega un punto en el que el proceso P1 ha adquirido el recurso R1 y el
proceso P2 ha adquirido el recurso R2, y cada proceso necesita el otro recurso. Este es el punto de interbloqueo.

En este tema se analizará el problema del interbloqueo y las distintas alternativas de solución que se pueden adoptar
clasificadas en las siguientes cuatro áreas : prevención, evitación, detección y recuperación del bloqueo mutuo. Para cada una
de las estrategias adoptadas, se analizará el equilibrio entre la sobrecarga debida a los mecanismos de corrección del
interbloqueo y los beneficios que reportan. En algunos casos es excesivo el precio (gasto extra) que hay que pagar para
conseguir a toda costa que no se produzcan interbloqueos. Sin embargo, en algunos casos, como en los sistemas de tiempo
real, no hay más alternativa que pagar el precio, ya que puede resultar catastrófico permitir la posibilidad de un bloqueo
mutuo.
Un problema afín : el aplazamiento indefinido
En cualquier sistema que mantenga los procesos en espera mientras se les asigna un recurso o se toman decisiones de
planificación, la programación de un proceso puede postergarse indefinidamente mientras otro recibe la atención del sistema.
Tal situación se conoce con varios nombres, entre los que se incluyen aplazamiento indefinido, bloqueo indefinido e inanición,
y puede resultar tan peligrosa como el interbloqueo.
El aplazamiento indefinido puede ocurrir debido a predisposiciones en las políticas de planificación de recursos del sistema.
Cuando los recursos se planifican por prioridad, es posible que un proceso dado espere de forma indefinida un recurso porque
siguen llegando otros procesos con mayor prioridad. Los sistemas deben diseñarse para administrar los procesos en espera de
manera justa además de eficiente. En algunos sistemas, el aplazamiento indefinido se evita aumentando la prioridad del
proceso mientras espera (técnica de envejecimiento). En algún momento la prioridad de ese proceso superará la prioridad de
los entrantes y el proceso en espera será atendido.

5.2 Casos de Interbloqueos


El caso más simple de interbloqueo sería el de un sólo proceso que espera la ocurrencia de un evento y, sin embargo, el
sistema no incluye la posibilidad de señalar dicha ocurrencia. Es muy difícil detectar los bloqueos mutuos de esta naturaleza. La
mayor parte de los bloqueos mutuos implican una competencia entre varios procesos por varios recursos.
Holt (1972) utilizó grafos dirigidos para representar situaciones de interbloqueo. Estos grafos tienen dos tipos de nodos :
procesos, que se representan con círculos, y recursos, representados por cuadrados. Si in proceso está utilizando un recurso,
previamente solicitado y concedido, se traza un arco desde el nodo del recurso (cuadrado) hasta el proceso (círculo). En la
figura 2, el recurso R está en ese momento asignado al proceso A. En b), el proceso B está solicitando el recurso s. Por último
en c) se representa un situación de interbloqueo : el proceso C está a la espera del recurso T, que está asignado al proceso D. El
proceso D no ha dejado T, porque está esperando a que quede libre el recurso U, que, a su vez, está siendo utilizado por C.
Ambos esperarán indefinidamente.

36
El siguiente ejemplo servirá para ilustrar el empleo de grafos de recursos. Supongamos que tenemos tres procesos, A, B y C, y
tres recursos, R, S, y T. La figura 5.2 representa las secuencias de petición y liberación que realizan los tres procesos. El sistema
operativo tiene en todo momento completa libertad para ejecutar cualquiera de los procesos que no estén bloqueados, así
que, por ejemplo, podría decidirse a ejecutar A hasta que éste terminara su trabajo, después B hasta que acabe y, finalmente
C.
Este secuenciamiento no produce interbloqueo, ( ya que no se compite por los recursos), pero suprime completamente el
paralelismo. Además de pedir y liberar recursos, los procesos también realizan E/S y procesamiento de datos. Si se ejecutan
uno tras otro, se elimina completamente la posibilidad de que, mientras uno de ellos está esperando que acabe una E/S, otro
pueda utilizar el procesador.
Supongamos, sin embargo, que los procesos realizan tanto E/S como procesamiento de datos, de forma que la planificación
por turno rotatorio es la más adecuada. En este caso, la secuencia de petición de recursos podría ser la representada en la
figura 3 (d). Si las seis peticiones se llevan a cabo en ese orden, se producirían los seis grafos de los casos (e)-(j). Después de la
petición 4, A está bloqueado en espera de captar S, como se muestra en (h). Los procesos B y C se bloquean en las dos etapas
siguientes, lo que conduce finalmente a un bucle cerrado y al correspondiente interbloqueo representado en (j).

Figura 5.3 Ejemplo de Interbloqueo y como evitarlo.

El sistema operativo no está obligado a ejecutar los procesos en ningún orden en particular. En concreto, si la concesión de un
recurso a un proceso determinado puede provocar interbloqueo, el sistema operativo es muy libre de suspender al proceso y
no atender su petición hasta que esté seguro de que esto no conduce a una situación problemática. En la figura 5.3, por
ejemplo, si el sistema operativo supiera que se avecinaba un interbloqueo, podría decidir suspender al proceso B antes de
concederle el recurso S. La ejecución sólo de los procesos A y C produciría las secuencias de petición y liberación de la figura
5.3 (k), en lugar de las de la figura 5.3 (d). Esta secuencia de ejecución produce los grafos de recursos (l)-(q), y no produce
interbloqueo.
Después de la etapa (q), no hay ningún problema en conceder S a B, ya que A ha terminado y C tiene todo lo que necesita.
Aunque B se bloqueara al solicitar T, no se produciría interbloqueo; B simplemente esperaría hasta que terminara C.

5.3 Condiciones Necesarias para Producir un Interbloqueo


Coffman, Elphick y Shoshani (71) establecen que deben darse las siguientes cuatro condiciones necesarias para que ocurra un
bloqueo mutuo.
Condición de exclusión mutua : los procesos exigen un control exclusivo de los recursos que necesitan.

37
Condición de espera : los procesos mantienen la posesión de los recursos ya asignados a ellos mientras esperan
recursos adicionales.
Condición de no apropiación : los recursos no pueden arrebatarse a los procesos a los cuales están asignados hasta
que termine su utilización.
Condición de espera circular : existe una cadena circular de procesos en la que cada proceso tiene uno o más recursos
que son requeridos por el siguiente proceso en la cadena.
Como dichas condiciones son necesarias para que se presente un interbloqueo, la existencia de un bloqueo mutuo implica que
se han dado todas y cada una de las cuatro condiciones. Como se verá más adelante, tener en mente semejante observación
será de gran ayuda para desarrollar esquemas que eviten los interbloqueos.
5.4 Estrategias para Resolver Interbloqueos
Los resultados de la investigación sobre el bloqueo mutuo han sido satisfactorios en cuanto a que se han encontrado métodos
limpios y rápidos para manejar la mayoría de los problemas más comunes. Existen cuatro áreas de interés relacionadas con los
interbloqueos que pueden resumirse como prevención, técnicas para evitarlos, detección y recuperación de los mismos.
En la prevención del interbloqueo interesa ajustar el sistema para eliminar toda posibilidad de que ocurra un bloqueo mutuo.
La prevención suele funcionar pero sus métodos ocasionan, en general, un aprovechamiento pobre de los recursos. No
obstante, estos métodos se utilizan con frecuencia.
Las técnicas que tienen como objetivo evitar el interbloqueo imponen condiciones menos atractivas que en la prevención,
para tratar de obtener un aprovechamiento de los recursos. No elimina como las técnicas de prevención todas las posibilidades
de que se produzca un bloqueo mutuo, pero se esquiva cuanto está a punto de suceder (algoritmo del banquero de Dijkstra).
Los métodos de detección del interbloqueo es utilizan en sistemas que permiten la ocurrencia de los mismos, ya sea de
manera voluntaria o involuntaria. Su objetivo es determinar si ha ocurrido un bloqueo mutuo y saber exactamente cuáles son
los procesos y recursos implicados en él.
Los métodos de recuperación están íntimamente ligados a los de detección. Sirven para eliminar los interbloqueos detectados
en un sistema para poder seguir trabajando y para que los procesos implicados puedan terminar su ejecución y liberen sus
recursos. La recuperación es un problema complejo, en el mejor de los casos, los sistemas se recuperan de un bloqueo mutuo
eliminando completamente uno o varios de los procesos implicados. Después, se inician de nuevo los procesos eliminados,
perdiéndose la mayor parte o todo el trabajo previo realizado por el proceso.

5.4.1 Desentenderse. El Algoritmo de la Avestruz


La estrategia más sencilla es el algoritmo del avestruz : esconder la cabeza bajo tierra y pretender que el problema no existe. La
gente reacciona a esta estrategia de distintos modos según su formación. Los matemáticos consideran que es inaceptable y
argumentan que los interbloqueos se deben evitar a toda costa. Los ingenieros se interrogan sobre la frecuencia del problema,
la frecuencia con el que el sistema se para por otras causas y la importancia de los interbloqueos. Si éstos se presentan de una
vez cada cinco años, y los sistemas se paran una vez al mes por errores en el hardware, en el compilador o en el sistema
operativo, a casi ningún ingeniero le gustaría tener que sufrir una degradación seria de las prestaciones del sistema para
garantizar la eliminación de los interbloqueos.
Por ejemplo, Unix pude sufrir interbloqueos que ni siquiera se detectan, y que, por supuesto, no se eliminan automáticamente.
El número total de procesos en el sistema viene determinado por el número de posiciones de la tabla de procesos, que, en
definitiva, constituye un recurso limitado. Supongamos ahora que un sistema Unix con 100 posiciones en la tabla de procesos
tiene ejecutándose diez programas, cada uno de los cuales ha de crear 12 subprocesos. Después de que cada proceso haya
creado otros 9, los 10 procesos originales y los 90 nuevos llenarán por completo la tabla. Los 10 procesos originales se
encontrarán ahora en un bucle infinito intentando crear un nuevo proceso sin poder : se ha producido un interbloqueo. Otros
ejemplos de recursos que suelen ser limitados son : el número máximo de ficheros que pueden estar abiertos está limitado, el
área en el disco para intercambio con memoria principal. En realidad, casi todas las tablas del sistema operativo representan
recursos limitados, ¿deberíamos, por tanto, limitar estos recursos para no producir un interbloqueo?
La estrategia UNIX es simplemente desentenderse del problema, suponiendo que la mayoría de los usuarios preferirán un
interbloqueo ocasional antes que la imposición de que cada usuario pueda crear un solo proceso, abrir un solo fichero y usar
sólo una unidad de lo que sea. Veremos a continuación que se puede adoptar alguna estrategia adecuada que nos permitirá
prevenir, evitar o detectar y recuperar situaciones de interbloqueo.

5.4.2 Prevención de Interbloqueos


La estrategia empleada con más frecuencia por los diseñadores para tratar el bloqueo mutuo es la prevención. En esta sección
se examinan los métodos de prevención, junto con los efectos que tienen sobre los usuarios y los sistemas, sobre todo desde la
perspectiva del rendimiento.
Havender (68) llegó a la conclusión de que si falta alguna de las cuatro condiciones necesarias no puede haber un
interbloqueo. Este autor sugiere las siguientes estrategias para negar varias de esas condiciones :
Cada proceso deberá pedir todos sus recursos al mismo tiempo y no podrá seguir la ejecución hasta haberlos
recibido todos.
Si a un proceso que tiene recursos se le niegan los demás, ese proceso deberá liberar sus recursos y, en caso
necesario, pedirlos de nuevo junto con los recursos adicionales.

38
Se impondrá un ordenamiento lineal de los tipos de recursos en todos los procesos ; es decir, si a un proceso le han
sido asignados recursos de un tipo específico, en lo sucesivo sólo podrá pedir aquellos recursos que siguen en el
ordenamiento.
Como vemos Havender presenta tres estrategias y no cuatro. Cada una de ellas, está diseñada para negar una de las
condiciones necesarias. La primera de estas condiciones, esto es, que los procesos exijan el uso exclusivo de los recursos que
requieren, es una condición que no es deseable impedir, porque específicamente queremos permitir la existencia de recursos
no compartibles o dedicados.

Negación de la condición de espera


La primera de las estrategias requiere que los recursos que necesita un proceso sean pedidos de una sola vez. El sistema debe
proporcionarlos según el principio de todo o nada. Si está disponible el conjunto de los recursos que necesita un proceso,
entonces el sistema puede asignarle todos los recursos y éste seguir su ejecución. Si no está disponible alguno de ellos, el
proceso debe esperar. Mientras espera no puede tener ningún recurso. Con esto se elimina la condición de espera y no puede
ocurrir un interbloqueo.
Todo esto suena bien, pero puede llevar a un grave desperdicio de recursos. Supongamos que un proceso necesita diez
unidades de un determinado recurso para su ejecución. Como debe solicitarlas todas antes de comenzar, los mantendrá en su
poder durante toda su ejecución. Pudiera suceder, que el programa únicamente utilice estos recursos al principio de su
ejecución, por tanto, los recursos están ociosos el resto del tiempo.
Dividir el programa en varios pasos que se ejecuten de manera relativamente independiente es una técnica empleada con
frecuencia para conseguir una mejor utilización de los recursos en estas circunstancias. La asignación de recursos se controla
por etapas. Esta solución reduce el desperdicio pero implica mucho trabajo extra tanto en el diseño de las aplicaciones como
en al ejecución.
Por otro lado, esta estrategia puede provocar un aplazamiento indefinido, pues los recursos requeridos pueden no estar
disponibles todos al tiempo. El sistema podría, entonces, permitir que se fueran acumulando recursos hasta conseguir todos
los que necesita un proceso. Pero mientras se acumulan no se pueden asignar a otros procesos y volvemos a infrautilizarlos.

Negación de la condición de no apropiación


La segunda estrategia de Havender consiste en liberar los recursos que un proceso tiene asignados cuando se le niegan
peticiones de recursos adicionales. De esta forma, se anula la condición de no apropiación. Los recursos se pueden quitar al
proceso que los tiene antes de que termine su ejecución.
En este caso también existe un costo excesivo. Cuando un proceso libera recursos puede perder todo el trabajo realizado hasta
ese momento. El costo puede parecer muy alto, pero la pregunta es : ¿con qué frecuencia ha de pagarse ese precio ? Si ocurre
de tarde en tarde, entonces éste parece ser un buen método para prevenir el interbloqueo. Si, por el contrario, es muy
frecuente, entonces el costo es sustancial y sus efectos demasiado perjudiciales (por ejemplo, para procesos de alta prioridad o
plazo fijo).
Esta estrategia también adolece de aplazamiento indefinido. Un proceso puede aplazarse continuamente mientras pide y
libera muchas veces los mismo recursos. Si esto ocurre, el sistema puede verse obligado a eliminar el proceso para que otros
puedan ejecutarse.

Negación de la condición de espera circular


La tercera estrategia de Havender anula la posibilidad de un espera circular. Como todos los recursos tienen una numeración
única y como los procesos deben pedir los recursos en un orden lineal ascendente, es imposible que se presente una espera
circular (figura 5.4). Esta estrategia presenta las siguientes dificultades :
Los recursos deben pedirse en un orden ascendente por número de recursos. El número de recurso es asignado por
la instalación y debe tener un tiempo de vida largo (meses o años) . Si se agregan nuevos tipos de recursos, puede ser
necesario reescribir los programas y los sistemas.
Lógicamente, cuando se asignan los números de recursos, éstos deben reflejar el orden normal en que los usan la
mayoría de las tareas. Pero los procesos que necesiten los recursos en un orden diferente que el previsto por el sistema,
los deberán adquirir y conservar, quizá durante tiempo antes de utilizarlos realmente, lo que significa un desperdicio
considerable.
Una de las metas más importantes de los sistemas operativos actuales es crear ambientes amables con el usuario.
Los usuarios deben ser capaces de desarrollar sus aplicaciones sin tener en cuenta molestas restricciones de hardware y
software. El ordenamiento lineal impide al usuario escribir sus códigos libremente.

39
5.4.3 Evitación de Interbloqueos
Aún presentándose las condiciones para un interbloqueo, todavía es posible evitarlo mediante una asignación cuidadosa de los
recursos. Tal vez el algoritmo más famoso para evitar el interbloqueo sea el algoritmo del banquero de Dijkstra (73), cuyo
interesante nombre se debe a que atañe a un banquero que otorga préstamos y recibe pagos a partir de una determinada
fuente de capital.

Algoritmo del Banquero


En principio, estudiaremos este algoritmo suponiendo que todos los recursos del mismo tipo. Considérese la asignación de una
cantidad t, de unidades de cintas idénticas.
Un sistema operativo comparte un número fijo, t, de unidades de cinta entre un número fijo de, p, de procesos. Cada proceso
especifica por adelantado el número máximo de unidades de cinta que necesitará durante su ejecución. El sistema operativo
aceptará la petición de un usuario si la necesidad máxima de ese proceso no es mayor que t. Un proceso puede obtener o
liberar unidades de cinta una a una. Algunas veces un usuario puede verse obligado a esperar para obtener una unidad
adicional, pero el sistema operativo garantiza una espera finita. El número real de unidades asignadas a un proceso nunca será
superior a la necesidad máxima declarada por ese usuario. Si el sistema operativo es capaz de satisfacer la necesidad máxima
del proceso, entonces éste debe garantizar al sistema operativo que las unidades de cinta serán utilizadas y liberadas en un
tiempo finito.
Se dice que el estado del sistema es seguro si el sistema operativo puede garantizar que todos los procesos terminan en un
tiempo finito. En otro caso, el sistema está en un estado inseguro.
Sea préstamo (i) la representación del préstamo actual de unidades de cinta para el proceso i. Sea máx(i) la necesidad máxima
de cintas de un proceso y, por último, sea petición (i) la petición actual del usuario, que es igual a su necesidad máxima menos
el préstamo actual. Por ejemplo, el proceso 7 tiene una necesidad máxima de 6 unidades y un préstamo actual de 5, entonces
tiene:
petición(7) = máx(7) - préstamo(7) = 6 - 5 = 2
El sistema operativo controla t unidades de cinta. Sea a el número de unidades de cinta todavía disponibles para asignar.
Entonces a es igual a t menos la suma de los préstamos de los usuarios.
El algoritmo del banquero permite la asignación de unidades de cinta a los usuarios solamente cuando la asignación conduzca
a estados seguros, y no a estados inseguros. Un estado seguro es una situación tal en la que todos los procesos son capaces de
terminar en algún momento. Un estado inseguro es aquel en el cual puede presentarse un bloqueo mutuo.

Ejemplo de estado seguro


Supóngase que un sistema tiene doce unidades de cinta y tres procesos que las comparten.

40
La tabla anterior representa un estado seguro porque el proceso 2 tiene un préstamo de 4 unidades y necesita como máximo
6, o sea, 2 más. El sistema tiene 12 de las cuales 10 están en uso y mantiene 2 disponibles. Si las que están disponible se
asignan al proceso 2, cubriendo su demanda máxima, este proceso podrá terminar. Al acabar, devolverá todos los recursos, 6
unidades de cinta, y el sistema podrá asignarlas al proceso 1 y al 3. De esta forma, la clave de un estado seguro es que exista al
menos una forma en la que terminen todos los procesos.
Ejemplo de estado inseguro

Ahora 11 de las 12 unidades de cinta están asignadas y solamente hay una disponible. En este momento, no se puede
garantizar que terminen los tres procesos. Si el proceso 1 pide y obtiene la última unidad de cinta y los tres vuelven a solicitar
una unidad de cinta más se produciría un bloqueo triple.
Es importante señalar, que un estado inseguro no implica la existencia, ni siquiera eventual, de un interbloqueo. Lo que sí
implica un estado inseguro es la posibilidad de que ocurra por una desafortunada secuencia de eventos.

Ejemplo de transición de estado seguro a estado inseguro


Saber que un estado es seguro no implica que serán seguros todos los estados futuros. La política de asignación de recursos
debe considerar cuidadosamente todas las peticiones antes de satisfacerlas. Por ejemplo supongamos la situación que se
muestra en la siguiente tabla.

Ahora supóngase que el proceso 3 pide un recurso más. Si el sistema satisface esta petición, el nuevo estado será el que se
muestra en la tabla de abajo.

Según vemos, aunque, en principio el sistema no estaba bloqueado, ha pasado de un estado seguro a uno inseguro. La última
de las tablas caracteriza un sistema en el cual no puede garantizarse la terminación de todos los procesos. Solamente hay un
recurso disponible, pero deben estarlo al menos dos para asegurar que el proceso 2 o el 3 puedan terminar, devolver sus
recursos al sistema y permitir que los otros procesos acaben.
Algoritmo del banquero para múltiples recursos

Figura 5.5 Algoritmo del banquero para múltiples recursos

La figura 5.5 representa dos matrices. La de la izquierda muestra el número de recursos asignados en ese instante (préstamo
actual) a cada uno de los cinco procesos. En la derecha aparece el número de recursos que todavía necesita cada proceso para
llevar a cabo su función (petición). Al igual que el caso de un único recurso, los procesos deben comunicar sus necesidades
máximas antes de empezar a ejecutarse, de forma que en todo momento el sistema pueda calcular la matriz de la derecha.
Para describir los recursos existentes en el sistema, los que están en posesión y los que están disponibles, emplearemos tres
vectores como los siguientes : E =(6342), P=(5322) y D=(1020). E indica que el sistema tiene 6 unidades de cinta, 3 trazadores, 4
impresoras y 2 cdrom. De ellos se están utilizando 5 unidades de cinta, tres trazadores gráficos, dos impresoras y dos cdrom.
Esto se puede deducir sin más que sumar las cuatro columnas de recursos en préstamo de la matriz izquierda. El vector de
recursos disponibles es simplemente la diferencia ente lo que el sistema tiene y lo que está usando en ese momento.
Ahora estamos en condiciones de describir en qué consiste el algoritmo de comprobación de estado seguro :

41
1.-Buscar una fila, F, cuyas necesidades de recursos sean menores o iguales a D. Si no existe tal fila, el sistema puede
interbloquearse, ya que ningún proceso puede ejecutarse hasta el final.
2.-Suponer que el proceso de la fila seleccionada solicita todos los recursos que necesita, y termina. Marcar el proceso
como terminado y añadir sus recursos al vector D.
3.-Repetir las etapas 1 y 2 hasta que se vayan marcando todos los procesos como terminados (en cuyo caso el estado
inicial era seguro) o hasta que se llegue a una situación de interbloqueo (en cuyo caso no lo era).
En el caso de que se pueda seleccionar más de un proceso en el paso 1, la elección sería indiferente: en cualquier caso, el
conjunto de recursos disponibles aumenta o, en el peor de los casos, se queda igual.
Volviendo al ejemplo de la figura 5.5 . El estado actual es seguro. Supongamos que el proceso B solicita la impresora. Dado que
el estado resultante es todavía seguro, esta petición puede concederse (el proceso D puede terminar, seguido por los procesos
A o E y a continuación el resto).
Imaginemos ahora que, después de que B obtiene una de las dos impresoras restantes, E solicita la última impresora. Si se le
concede esta petición, el vector de recursos disponibles se reduciría a (1 0 0 0), situación que produce interbloqueo. Por lo
tanto, la petición de E debe posponerse de momento.
Ahora debe estar claro cómo opera el algoritmo del banquero de Dijkstra cuando asigna recursos. Están permitidas las
condiciones de espera circular, espera y no apropiación, pero los procesos sí exigen el uso exclusivo de los recursos que
requieren. Los procesos pueden conservar recursos mientras piden y esperan recursos adicionales y los recursos no pueden
arrebatarse a los procesos que los tienen. Los procesos facilitan el trabajo al sistema pidiendo un solo recurso a la vez. El
sistema puede satisfacer o rechazar cada petición. Si una petición es rechazada, el proceso conserva los recursos que ya tiene
asignados y espera un tiempo finito a que se satisfaga la petición. El sistema sólo satisface peticiones que llevan a estados
seguros. Una petición que condujese a un estado inseguro se rechazaría repetidamente hasta que pueda quedar satisfecha.
Como el sistema se mantiene en un estado seguro, tarde o temprano (en un tiempo finito) todas las peticiones podrán ser
atendidas y los procesos terminarán.

Defectos del algoritmo del banquero


El algoritmo del banquero es interesante porque ofrece una forma de asignar los recursos que evita el interbloqueo. Permite
ejecutar procesos que tendrían que esperar seguramente con alguna de las estrategias de prevención. Sin embargo, tiene
varios defectos importantes :
El algoritmo requiere un número fijo de recursos asignables. Como los recursos a menudo requieren servicio, ya sea
por algún fallo o por mantenimiento preventivo, no se puede contar con que será siempre constante.
El algoritmo requiere una población de usuarios constantes. En los sistemas multiprogramables y más en los de
tiempo compartido, la población de usuarios cambia constantemente, incluso en cuestión de segundos.
El algoritmo requiere que el banquero satisfaga todas las peticiones en un tiempo finito. Es evidente que en los
sistemas reales esto no es una garantía suficiente.
De manera similar, el algoritmo requiere que los procesos salden sus préstamos (es decir, devuelvan sus recursos) en
un tiempo finito. Una vez más, esto es insuficiente para un sistema de tiempo real.
El algoritmo requiere que los usuarios declaren por anticipado sus necesidades máximas. A medida que la asignación
de recursos se hace más dinámica, conocer las necesidades máximas de un usuario presenta mayor dificultad. De
hecho, ahora que los sistemas ofrecen interfaces gráficas, cada vez es más común que los usuarios no tengan la menor
idea de los recursos que necesitan.
En resumen, los métodos descritos anteriormente en el apartado de prevención son demasiado restrictivos, mientras que los
de evitación que acabas de estudiar requieren información de la que, generalmente, no se dispone.

5.4.4 Detección y Recuperación de Interbloqueos


La detección del bloqueo mutuo es el proceso de determinar si realmente existe un interbloqueo e identificar los procesos y
recursos implicados en él. Los algoritmos de detección determinan por lo general si existe una espera circular.
El empleo de algoritmos de detección del interbloqueo implica cierto gasto extra durante la ejecución. Así pues, se presenta de
nuevo la cuestión de costeabilidad, tan habitual en los sistemas operativos, ¿el gasto extra debido a los algoritmos de
detección del bloqueo mutuo se justifica con los ahorros potenciales debidos a la localización y solución de los interbloqueos?
Para facilitar la detección de interbloqueos, se utilizará una notación en la que un grafo dirigido indica las asignaciones y
peticiones de recursos. Los cuadrados representan procesos; los círculos grandes, clases de dispositivos idénticos; los círculos
pequeños de color rojo en el interior de los grandes indican el número de dispositivos de cada clase. Por ejemplo, si un círculo
grande etiquetado como R1 contiene tres círculos pequeños, significa que ya tres recursos del tipo R1 disponibles para
asignación en este sistema.

42
Figura 5.6 Gráfica de Asignación y Petición de recursos.
La figura 5.6 muestra las relaciones que pueden indicarse en una gráfica de asignación y petición de recursos. Por ejemplo, en
(a) el proceso P1 está pidiendo un recurso del tipo R1. La flecha que parte de P1 toca solamente el extremo del círculo grande,
lo cual implica que se está estudiando la petición. En (b), el proceso P2 tiene asignado un recurso del tipo R2 (del cual existen
dos unidades). La flecha va del círculo pequeño que se encuentra dentro del círculo grande R2 al cuadrado P2. En (c), el recurso
R3 ha sido solicitado por el proceso P3, pero ya se ha asignado al proceso P4. Por último, en (d), se representa un interbloqueo.
El proceso P5 tiene el recurso R5 que está siendo solicitado por el proceso P6, que tiene el recurso R4 que está siendo
solicitado por el proceso P5 (una espera circular).

Reducción de las gráficas de asignación de recursos


Una técnica útil para detectar los interbloqueos consiste en ir reduciendo una gráfica determinando los procesos que pueden
completar su ejecución. Si pueden atenderse las peticiones de recursos de un proceso, se dice que la gráfica puede ser
reducida por ese proceso. Esta reducción es equivalente a mostrar la gráfica como si el proceso hubiese acabado y hubiera
devuelto los recursos al sistema. Si una gráfica puede ser reducida por todos sus procesos, entonces no hay interbloqueo. Si
una gráfica no puede ser reducida por todos sus procesos, los procesos irreductibles constituyen el conjunto de procesos en
bloqueo mutuo de la gráfica (figura 5.7).
Cuando se ha bloqueado un sistema, el interbloqueo debe romperse mediante la eliminación de una o más de las condiciones
necesarias. Por lo general, varios procesos perderán una parte o la totalidad del trabajo efectuado, pero el precio pagado
puede ser pequeño, en comparación con las consecuencias de permitir que el sistema siga bloqueado. La recuperación
después de un bloqueo mutuo se complica por varias razones :

Puede no estar claro que el sistema se haya bloqueado.


La mayor parte de los sistemas tienen medios muy deficientes para suspender indefinidamente un proceso, eliminarlo
del sistema y reanudarlo más tarde. De hecho, algunos procesos como los de tiempo real, que deben funcionar
continuamente, sencillamente no se pueden suspender y reanudar.
Aún cuando existieran medios efectivos de suspensión/reanudación, con toda seguridad implicarían un gasto extra
considerable.
La recuperación después de un bloqueo mutuo de dimensiones modestas puede significar una cantidad razonable de
trabajo, un interbloqueo a gran escala puede requerir una cantidad enorme de trabajo.

43
En los sistemas actuales la recuperación se suele realizar eliminado un proceso y arrebatándole sus recursos. Por lo general, el
proceso eliminado se pierde pero ahora es posible concluir los procesos restantes. Algunas veces es necesario eliminar varios
procesos hasta que se hayan liberado los recursos suficientes para que terminen los procesos restantes.
Los procesos pueden eliminarse de acuerdo con algún orden de prioridad. También existen dificultades para ello :
Es posible que no existan prioridades entre los procesos bloqueados, de modo que se tiene que adoptar una decisión
arbitraria.
Las prioridades pueden ser incorrectas o un poco confusas debido a consideraciones especiales, como la planificación
a plazo fijo, en la cual un proceso de prioridad relativamente baja tiene una prioridad temporal alta a causa de un fin de
plazo inminente.
La determinación de una decisión óptima sobre los procesos que se deben eliminar puede requerir un esfuerzo
considerable.
Parece ser que el enfoque más deseable para la recuperación después de un bloqueo mutuo sería un mecanismo efectivo de
suspensión/reanudación. Ello implicaría suspender temporalmente los procesos y reanudarlos después sin pérdida de trabajo
productivo. Para ello sería deseable la posibilidad de especificar puntos de verificación/reinicio. De este modo, se facilita la
suspensión/reanudación, que se hará desde el último punto de verificación (es decir, la última grabación del estado del
sistema). Pero muchos sistemas de aplicación se diseñan sin aprovechar las ventajas de las funciones de punto de
verificación/reinicio. Por lo general, se requiere un esfuerzo consciente por parte de los diseñadores para incorporar la
verificación/reinicio, y a menos que las tareas requieran muchas horas de ejecución, su uso es poco común.

ADMINISTRACION DE MEMORIA
Durante este nuevo tema nos enfrentaremos con el problema de la gestión de la memoria. Haremos un breve estudio
preliminar de las posibles alternativas y variantes a la hora de organizar y administrar el espacio de direcciones de un sistema.
Esta primera toma de contacto nos servirá de excusa para introducir algunos conceptos generales, que se irán desarrollando
luego.
Empezaremos por el tipo de gestión más básico, el de los sistemas de monoprogramación que apenas necesitan de ninguna
organización. La irrupción de los sistemas multiprogramados hace necesario tomar decisiones sobre aspectos tan diversos
como cuánto espacio se dedica a cada proceso, de qué modo se le asigna, en qué lugar se ubica, durante cuánto tiempo
permanece en memoria, qué sucede si no existe suficiente espacio o cómo se protege frente a accesos ajenos. Todos estos
factores serán valorados primero para técnicas de asignación contigua(particiones estáticasy dinámicas) y para métodos de
asignación no contigua (paginación, segmentacióny segmentación paginada). También se discutirá el soporte hardware, y el
grado de protección y compartición que es posible con cada uno de los esquemas. Dentro del segundo paquete de estrategias
de administración de la memoria tendrán un particular interés los esquemas de traducción de direcciones, por su repercusión
en el tiempo efectivo de acceso a memoria y, por tanto, en el rendimiento del sistema.

6.1 La organización y gestión de la memoria. Conceptos generales


Para que un proceso pueda ejecutarse debe estar ubicado en la memoria principal del ordenador. Una parte del sistema
operativo se va a encargar de gestionar la memoria principal, de forma que los procesos puedan residir en la memoria sin
conflictos. La gestión de la memoria implica varias tareas, una de ellas es llevar un registro de qué zonas están libres (es decir,
no están siendo utilizadas por ningún proceso), y qué zonas están ocupadas por qué procesos. Otra tarea importante surge en
sistemas en los que no todos los procesos, o no todo el código y datos de un proceso, se ubican en la memoria principal. En
estos sistemas, a menudo se debe pasar parte, o la totalidad del código y datos de un proceso, de memoria a disco, o
viceversa; siendo el sistema operativo responsable de esta tarea. De esta forma se libera al usuario de realizar estas
transferencias de información, de las cuales no es consciente.

44
Otros dos temas importantes en la gestión de la memoria son el de la carga de los programas de disco a memoria y el de la
protección. Desde el momento en que varios procesos deben compartir la memoria del ordenador surge el problema de la
protección. En general, se pretende que un proceso no pueda modificar las direcciones de memoria en las que no reside. Esto
es así ya que en las direcciones de memoria donde no está ubicado el proceso pueden residir otros procesos, o código o
estructuras de datos del S.O. Si un proceso puede modificar indiscriminadamente la memoria, podría, por ejemplo, cambiar el
valor de una dirección de memoria donde residiera una variable de otro proceso, con la consecuente ejecución incorrecta del
proceso propietario de la variable. Algunos sistemas ni siquiera permiten que un proceso pueda leer las direcciones de
memoria en las que no reside, con esto se consigue privacidad sobre el código y datos de los procesos. Conforme avance este
tema y el siguiente se profundizará en todos estos aspectos.
Existen varias formas de gestionar la memoria. Por lo común, la forma de gestión dependerá de la máquina virtual que se
quiera proporcionar y del hardware subyacente. Con independencia de la forma de gestión es necesario decidir qué
estrategias se deben utilizar para obtener un rendimiento óptimo. Las estrategias de administración de la memoria determinan
el comportamiento de una organización de memoria determinada cuando se siguen diferentes políticas: ¿Cuándo se coge un
nuevo programa para colocarlo en la memoria? ¿Se coge el programa cuando el sistema lo necesita, o se intenta anticiparse a
las peticiones del sistema? ¿En qué lugar de la memoria principal se coloca el siguiente programa por ejecutar? ¿Se colocan los
programas lo más cerca posible unos de otros en los espacios disponibles de la memoria principal para reducir al mínimo el
desperdicio de espacio, o se colocan lo más rápido posible para reducir el tiempo empleado en tomar la decisión?
Los sistemas actuales son en su mayor parte sistemas con almacenamiento virtual, muchas de la formas de gestión estudiadas
en este tema tienen principalmente valor histórico, pero sientan las bases de los sistemas actuales.

Jerarquía de la memoria
Los programas y datos necesitan estar en la memoria principal para ser ejecutados, o para poder ser referenciados. Los
programas o datos que no se necesitan de inmediato pueden guardarse en la memoria secundaria hasta que se necesiten, y en
ese momento se transfieren a la memoria principal para ser ejecutados o referenciados. Los soportes de memoria secundaria,
como cintas o discos, son en general menos caros que la memoria principal, y su capacidad es mucho mayor. Normalmente, es
mucho más rápido el acceso a la memoria principal que a la secundaria.
En los sistemas con varios niveles de memoria hay muchas transferencias constantes de programas y datos entre los distintos
niveles. Estas transferencias consumen recursos del sistema, como tiempo de la CPU, que de otro modo podrían utilizarse
provechosamente.

En los años sesenta se hizo evidente que la jerarquía de la memoria podía extenderse un nivel más, con una clara mejora del
rendimiento. Este nivel adicional, la memoria caché, es una memoria de alta velocidad, mucho más rápida que la memoria
principal. La memoria caché es extremadamente cara, si se compara con la principal, por lo que sólo se utilizan memorias
caché relativamente pequeñas. La figura 6.1 muestra la relación que existe entre la memoria caché, la principal y la
secundaria.
La memoria caché introduce un nivel adicional de transferencia de información en el sistema. Los programas en memoria
principal se pasan a la memoria caché antes de ejecutarse. En la memoria caché se pueden ejecutar mucho más rápido que en
la principal. La esperanza de los diseñadores es que el trabajo extra requerido por la transferencia de programas sea mucho
menor que el incremento del rendimiento obtenido por la ejecución más rápida en la caché.

6.2 Gestión de la memoria en los sistemas monoprogramados


En los sistemas de monoprogramación sólo existe un proceso de usuario, que disfruta de todos los recursos del ordenador.
Esto va a simplificar notablemente la gestión de la memoria, ya que ésta sólo debe ser compartida por los programas del
sistema operativo, y por el único proceso de usuario existente. Esto se muestra en la figura 6.2. Dependiendo de detalles de
diseño, el sistema operativo ocupará la parte baja de la memoria RAM, como se muestra en la figura 6.2 (a); o la parte alta de

45
la memoria ROM, como se muestra en la figura 6.2 (b). El PC de IBM ubica parte del sistema operativo en RAM, y los gestores
de dispositivos en ROM; a esta última parte se le llama BIOS (Basic Input/Output System, sistema básico de entrada/salida),
esto último se ilustra en la figura 6.2 (c).

Si el usuario conoce la ubicación en la memoria del sistema operativo, entonces puede escribir programas en términos de
direcciones absolutas de memoria. Una dirección absoluta de memoria es una dirección física (es decir, real) de la memoria.
En contraposición se tienen las direcciones relativas. Un programa está escrito en término de direcciones relativas cuando se
escribe suponiendo que empieza a cargarse en la dirección cero de la memoria. Por lo general, los usuarios escriben programas
en lenguajes de alto nivel, por lo que son los traductores los encargados de generar las direcciones que ocupan las variables,
procedimientos, etc, en la memoria. Los compiladores no generan direcciones absolutas de memoria, pues no saben dónde se
almacenarán los procesos.
Por lo común, los sistemas operativos monousuario de monoprogramación (muy comunes en las microcomputadoras) no
tienen protección de la memoria. Por lo tanto, el único proceso de usuario que existe en la memoria, puede modificar
posiciones de memoria pertenecientes al sistema operativo, esto provocaría errores al ejecutarse la zona modificada. La
protección se puede realizar mediante un registro de límite integrado en la CPU. Si se tiene un esquema como el de la figura
6.2 (b) el registro de límite contendrá la dirección de inicio de carga del S.O. El hardware, en tiempo de ejecución, verifica que
las direcciones generadas por el proceso de usuario no son superiores al valor del registro de límite. En caso de ser superior, el
proceso de usuario intenta acceder al S.O., esto provoca una interrupción hardware que gestiona el S.O., normalmente
eliminando al proceso.

6.3 Gestión de la memoria en los sistemas multiprogramados


En un sistema de multiprogramación la memoria debe ser compartida por varios procesos de cara a obtener una mayor
utilización de los recursos del ordenador. Esto provoca que la gestión de la memoria se complique sustancialmente. En primer
lugar, hay que llevar un recuento de las zonas de memoria ocupadas por los procesos. Así, cuando un nuevo proceso entre en
la memoria se le asignará una zona que estaba libre. Otro problema a resolver viene dado por el hecho de que en el momento
de escribir un programa no se sabe en qué zona de memoria se ubicará, siendo posible que durante la vida de un proceso éste
cambie varias veces de emplazamiento. Habrá que tener en cuenta, también, la protección de las zonas de memoria ocupadas
por los procesos, máxime en sistemas multiusuario donde los procesos pueden pertenecer a distintos usuarios.
En lo que resta de este tema, y en el tema siguiente, se estudiarán varias formas de gestión de la memoria utilizadas en
sistemas de multiprogramación.

6.4 Asignación de memoria contigua


En un esquema de asignación de memoria contigua un proceso se ubica en su totalidad en posiciones consecutivas de
memoria. Un ejemplo de este tipo de asignación es el utilizado en los sistemas de monoprogramación vistos previamente. En
este apartados se estudian dos métodos de asignación contigua empleados históricamente en sistemas multiprogramados.

6.4.1 Particiones estáticas


Esta forma de gestión consiste en dividir la memoria en varias zonas, pudiendo ser cada zona de un tamaño diferente. Esto se
ilustra en la figura 6.3. El tamaño de las zonas podrá ser modificado eventualmente por algún usuario responsable de la
administración del ordenador.
Los trabajos se traducían mediante compiladores y ensambladores absolutos, para ejecutarse en una partición específica. Una
vez introducido un proceso en una partición, permanece en ella hasta su finalización. Si un trabajo se iniciaba, y la partición
para la que estaba compilado estaba ocupada, tenía que esperar, aunque estuvieran libres otras particiones. Esto provoca una
pérdida de eficiencia.

46
De cara a mejorar el rendimiento es preciso que un proceso se pueda cargar en cualquier partición. Para ello, los programas se
escriben en términos de direcciones relativas a la dirección de comienzo de carga cero. Sin embargo, los programas no se
cargan a partir de la dirección cero, esta circunstancia se debe resolver mediante la reasignación o reubicación, ¿cómo se
realiza ésta ?. Una posible implantación viene proporcionada por el hardware. Existe un registro, denominado registro base,
en el que el sistema operativo, dentro de la operación de cambio de proceso, escribe la dirección de memoria a partir de la
cual se almacenó el proceso. Esta dirección coincidirá con la de comienzo de la partición en que reside, y forma parte de su
descriptor(PCB). Cuando la CPU genera una dirección de memoria, ésta es transformada por el hardware antes de ser
introducida en el bus del sistema. La transformación consiste en sumarle a la dirección el registro base.
Para clarificar esto, supóngase que la instrucción que actualmente ejecuta la CPU guarda el contenido del acumulador en la
dirección relativa 100 de la memoria. Esta dirección, 100, (que podría, por ejemplo, guardar el contenido de una variable de
otro proceso) es relativa a una dirección 0 de comienzo del programa. Si el programa se ha cargado en la posición 10000 de la
memoria, el registro base contendrá el valor 10000. Cuando la CPU ejecuta la instrucción, genera la dirección 100 de memoria
(la cual físicamente pertenece a otro proceso). Sin embargo, el hardware suma 10000 a este valor, introduciéndose 10100 en el
bus del sistema, dirección que realmente ocupa la variable. Observe que con este esquema, si se quiere reubicar el proceso a
otro lugar de la memoria, sólo hay que desplazarlo de lugar, y modificar el registro base con la nueva dirección de comienzo de
carga.
Una solución software al problema de la reasignación consiste en modificar las instrucciones cuando el programa se carga en la
memoria. Para que esto ocurra es preciso que el enlazador (el programa que a partir de los ficheros objeto genera un único
objeto) incluya en el programa binario una lista que indique qué partes del programa son direcciones a reasignar, y cuáles no
(constantes, códigos de operadores u otros elementos que no deban ser reasignados). Con esta solución, cada reubicación en
la memoria implica una reasignación de las direcciones de memoria del programa.

Protección
Si se tiene el esquema hardware del registro base, para lograr la protección de las zonas de memoria basta con añadir un
nuevo registro, denominado registro límite. Este registro guarda la última dirección de la partición, y forma también parte del
PCB del proceso. El hardware, después de sumar el registro base a la dirección relativa, comprueba que la dirección obtenida
no supere el valor del registro límite. Si se supera el valor, se está intentando acceder a una zona que no corresponde al
proceso; en esta situación, el hardware genera una interrupción. El sistema operativo sirve a la interrupción, lo normal es que
mande una señal al proceso por violación de memoria. Si el proceso no tiene definido atrapar esa señal, lo cual es lo más
probable, se eliminará al proceso.
Cuando un proceso quiera ejecutar código del sistema operativo, por ejemplo, para realizar una E/S, no tiene acceso directo a
las rutinas que tiene el sistema operativo en memoria para implementar dicha función, sino que debe realizar una llamada al
sistema para no violar la protección. Este esquema es necesario, pues los programas de usuario tienen que avisar al sistema
operativo de que le solicitan servicios (al hacer una llamada al sistema), el sistema operativo atenderá las peticiones si son
correctas, o si pueden ser factibles en dicho momento (por ejemplo, no se asignará una impresora que está siendo utilizada
por otro proceso). Los procesos de usuario no pueden llamar directamente al sistema operativo gracias a la protección de
memoria.
Una última observación, al margen de la protección: cuando un proceso se introduce en una partición, lo más probable es que
su tamaño no sea el mismo (es decir, sea algo menor) que el de la partición. Esto origina un problema de desperdicio de
memoria conocido como fragmentación interna.

6.4.2 Particiones dinámicas


En este método se va asignando la memoria dinámicamente a los procesos, conforme se introducen en la memoria. A cada
proceso se le asigna exactamente la memoria que necesita.

47
En la figura 6.4 se ilustra cómo evoluciona la ocupación de la memoria en un sistema de este tipo. Al principio sólo se
encuentra el proceso A en la memoria. Después, se insertan los procesos B y C . En la figura 6.4-d A concluye. Luego, D entra y
B sale. Por último E entra.
Con este método de gestión de la memoria se evita el problema de la fragmentación interna. Sin embargo, aparece el
problema de la fragmentación externa entre particiones, el cual se aprecia en la figura 6.4-f. El problema consiste en que se
creen huecos libres demasiado pequeños como para que quepan procesos, aunque la unión de todos esos huecos produciría
un hueco considerable, lo que acarrea el desperdicio de la memoria. Una posible solución es la compactación de la memoria,
que consiste en desplazar todos los procesos hacia la parte inferior de la memoria mientras sea posible. Como la compactación
lleva mucho tiempo, a veces no se realiza, o se hace por la noche, en horas de poco uso del ordenador. Hay que tener en
cuenta que el sistema debe detener todas sus actividades mientras realiza la compactación. Ello puede ocasionar tiempos de
respuesta irregulares para usuarios interactivos, y podría ser devastador en un sistema de tiempo real. Además, con una
combinación normal de trabajos que cambia rápidamente, es necesario compactar a menudo. En este caso, los recursos del
sistema que se consumen quizá no justifiquen las ventajas de la compactación.

El esquema de los registro base y límite sigue siendo válido para la reasignación y la protección. Otro tema a tener en cuenta es
la cantidad de memoria por asignar a un proceso recién creado. Si los procesos se crean con un tamaño fijo invariante, la
asignación es muy sencilla, se asigna exactamente lo que se necesite.
Si, por el contrario, los segmentos de datos de los procesos pueden crecer, como es el caso de la asignación dinámica de
memoria a partir de una pila, que ocurre en muchos lenguajes de programación, aparece un problema cuando un proceso
intenta crecer. Si hay un hueco adyacente al proceso, éste puede ser asignado, y el proceso podrá crecer hacia el hueco. Sin
embargo, si el proceso es adyacente a otro proceso, el proceso de crecimiento deberá ser desplazado a un hueco de la
memoria lo suficientemente grande; o bien, habrá que eliminarlo.
Si es de esperar que la mayoría de los procesos crezcan conforme se ejecuten, sería una buena idea asignar un poco de
memoria adicional siempre que un proceso pase a la memoria, con el fin de reducir el gasto excesivo asociado con el traslado
de procesos que ya no caben en su memoria asignada. En la figura 5-a vemos una configuración de la memoria en la que se
asignó a dos procesos el espacio adicional para el crecimiento.
Si los procesos pueden tener dos segmentos de crecimiento, como por ejemplo, el segmento de datos, que se utiliza como una
pila, y el stack, se sugiere un método alternativo, el de la figura 5-b. En esta figura se puede ver que cada proceso tiene un
stack de crecimiento hacia abajo, en la parte superior de la memoria asignada a él; además, tiene un segmento de datos justo
encima del programa, el cual crece hacia arriba. La memoria entre ellos se puede utilizar para cualquiera de los segmentos. Si
el espacio se agota, puede ocurrir que el proceso sea desplazado a un hueco con el espacio suficiente; o bien, ser aniquilado.
Registro de la ocupación de la memoria

48
En el sistema de particiones estáticas es sencillo llevar el registro de la ocupación de la memoria, basta con guardar sobre cada
partición si está libre, u ocupada por qué proceso, así como sus direcciones de comienzo y fin de partición. Por contra, con las
particiones dinámicas, el número de éstas varía con el tiempo, así como su tamaño. Una forma posible de registrar la
ocupación de la memoria es utilizar una lista enlazada de los segmentos de la memoria asignados o libres. La memoria de la
figura 6.6-a se presenta en la figura 6-c como una lista enlazada de segmentos. Cada entrada de la lista especifica un hueco (H)
o un proceso (P), la dirección donde comienza, su longitud, y un puntero a la siguiente entrada.
En este ejemplo, la lista de segmentos está ordenada por direcciones. Este orden tiene la ventaja de que al terminar un
proceso la actualización de la lista es directa. Un proceso que termina, tiene dos vecinos (a menos que se encuentre en la parte
superior o inferior de la memoria). Estos pueden ser procesos o huecos, lo que produce las cuatro combinaciones de la figura
6.7. En la figura 6.7-a, la actualización de la lista requiere el reemplazo de una P por una H. En la figura 6.6 (b) y (c), dos
entradas se funden en una, y la lista se acorta en una entrada. En la figura 6.6-d tres entradas se fusionan en una. Puesto que
en el descriptor del proceso que termina se guardará un puntero a la entrada de la lista enlazada que ocupa dicho proceso,
sería conveniente que la lista fuera doblemente enlazada. Esta estructura facilita la búsqueda de la entrada anterior y, por
tanto, la verificación de si es posible una fusión.

Figura 6.6. (a) Una parte de la memoria, con cinco procesos y 3 huecos. La marca muestra las unidades de asignación de la
memoria. Las regiones sombreadas ( 0 en el mapa de bits) están libres. (b) El mapa de bits correspondiente. (c) La misma
información como lista enlazada.

6.4.3 Estrategias de colocación


Cuando en un sistema de particiones dinámicas se debe asignar memoria principal para un nuevo proceso, y los procesos y
huecos se mantienen en una lista ordenada por direcciones, se pueden utilizar diversos algoritmos para la elección del hueco
de memoria donde ubicar al proceso. Supongamos que se conoce la cantidad de memoria por asignar.
El algoritmo más simple es el primero en ajustarse (first fit). Se revisa la lista de huecos hasta encontrar un espacio lo
suficientemente grande. El espacio se divide entonces en dos partes, una para el proceso, y otra para la memoria no utilizada,
excepto en el caso poco probable de un ajuste perfecto. Este algoritmo es rápido, ya que busca lo menos posible.

49
Otro algoritmo es el mejor en ajustarse (best fit), el cual busca en toda la lista, y elige el mínimo hueco suficientemente grande
como para ubicar al proceso. Este algoritmo intenta que los huecos que se creen en la memoria sean lo más pequeños posible.

Como ejemplo de los algoritmos, retomemos la figura 6.6. Si se necesita un bloque de tamaño 2, el primero en ajustarse
asignará el espacio en 5, mientras que el mejor en ajustarse asignará el espacio en 18.
Realizando simulaciones se ha demostrado que el algoritmo del mejor ajuste desperdicia más la memoria, pues al crear huecos
demasiado pequeños, éstos no pueden ser utilizados por procesos. Un algoritmo que enfrenta el problema de la manera
contraria es el del peor ajuste (worst fit). En este algoritmo se elige el hueco más grande disponible. De esta forma aumenta la
probabilidad de que el nuevo hueco creado sea lo suficientemente grande como para albergar un proceso.
ENLACE A LA SIMULACIÓN ESTRATEGIAS DE COLOCACIÓN

6.4.4 Intercambio (swapping)


En un sistema con particiones estáticas el número de procesos con posibilidades de estar en estado listo viene determinado
por el número de particiones, y en uno de particiones dinámicas por el tamaño de la memoria principal y el tamaño de los
procesos, ya que en ambos métodos un proceso permanece en una partición hasta que finaliza. Supongamos un sistema en
que dicho número es cinco, es muy probable que en un momento dado los cinco procesos que ocupan las particiones estén
bloqueados (por ejemplo, porque esperan la finalización de una operación de E/S). Mientras los cinco procesos permanezcan
bloqueados se desperdicia la CPU. Para remediar este inconveniente muchos sistemas operativos optaron por permitir
ejecutar concurrentemente más procesos de los que pueden entrar físicamente en la memoria principal del ordenador. Para
ello se utiliza la memoria secundaria (generalmente los discos) que, aunque más lenta, tiene mayor capacidad de
almacenamiento que la principal. La solución consiste en tener en disco una copia de la parte de la memoria que ocupa todo
proceso. En el disco se encuentran todos los procesos, en la memoria sólo unos cuantos. Para que un proceso se pueda
ejecutar debe residir en memoria principal.
La razón por la que se aumenta el número de procesos con posibilidades de tener instrucciones en memoria principal es
porque cuanto mayor sea este número, es menos probable que se dé la circunstancia de que todos estén bloqueados y, por lo
tanto, es menor la posibilidad de que la CPU permanezca inactiva.
Algunos sistemas UNIX utilizaban el intercambio en un sistema de particiones dinámicas. El movimiento de procesos entre la
memoria principal y el disco lo realizaba el planificador de nivel medio, conocido como el intercambiador (swapper a medio
plazo). El intercambio de la memoria principal al disco (swapping out) se iniciaba cuando el S.O. precisa memoria libre y estaba
toda ocupa debido a alguno de los siguientes eventos:
- Una llamada al sistema fork que necesitaba memoria para un proceso hijo.
- Una llamada al sistema brk de solicitud de memoria dinámica en un proceso que no tiene suficiente memoria libre como
para aceptar la petición.
- Una pila que se agranda, y ocupa un espacio mayor al asignado.
Además, cuando había que recuperar un proceso presente en el disco (swapping in) desde hace mucho tiempo con frecuencia
se necesitaba sacar a otro proceso de memoria a disco para disponer de espacio para el primero.
El intercambiador elegía una víctima al examinar los procesos bloqueados en espera de algo (por ejemplo, una entrada del
terminal). Es mejor sacar a un proceso bloqueado (pasará a estado suspendido_bloqueado) que sacar a uno listo. Si existían
varios procesos bloqueados ubicados en la memoria principal se elegía a uno cuya combinación de prioridad y tiempo de
residencia en memoria principal fuera más desfavorable. Así, un buen candidato era un proceso que hubiera consumido
mucho tiempo de CPU recientemente, al igual que uno que hubiera permanecido en la memoria durante mucho tiempo, aun
cuando durante este tiempo hubiera realizado E/S. Si no se dispone de procesos bloqueados, entonces se elegía a un proceso
listo en base a los mismos criterios.
Cada pocos segundos el intercambiador examinaba la lista de procesos intercambiados para ver si alguno estaba listo para su
ejecución. En caso de que existiera alguno, se seleccionaba a aquel que hubiese permanecido en el disco durante mucho
tiempo. A continuación el intercambiador verificaba si el intercambio sería fácil o difícil. Un intercambio fácil era aquel en el
que existiera la suficiente memoria libre, de forma que no había necesidad de sacar a un proceso para hacer espacio para el
nuevo. Un intercambio difícil precisaba la eliminación de uno o más procesos. La implantación de un intercambio fácil se

50
llevaba a cabo al traer el proceso a la memoria. Un intercambio difícil se implantaba liberando primero la memoria suficiente
sacando a disco a uno o más procesos, y después, cargando en la memoria el proceso deseado.
Este algoritmo se repetía hasta que se cumpliera alguna de estas condiciones: (1) ningún proceso en el disco está
suspendido_listo; o (2) la memoria está tan ocupada por procesos recién traídos que no hay espacio para más. Para evitar un
trasiego excesivo entre memoria y disco que pudiera afectar al rendimiento no se intercambiaba hacia el disco a un proceso
hasta que hubiera permanecido en la memoria durante 2 segundos.
El espacio libre en la memoria principal y en el dispositivo de intercambio se registraba mediante una lista enlazada de huecos.
Si se necesitaba espacio en alguno de los dos, el algoritmo del primero en ajustarse leía la lista adecuada de huecos, y devolvía
el primer hueco que encontrara y que fuese lo bastante grande, a la vez que eliminaba este espacio de la lista de huecos.

6.5 Asignación de memoria no contigua.


Hasta ahora se han estudiado esquemas de administración de la memoria en los que los procesos se almacenan en posiciones
contiguas (consecutivas) de memoria. Sin embargo, un proceso puede dividirse en bloques, y estos bloques pueden situarse en
posiciones no contiguas de memoria principal. Es más, no es preciso que se encuentren en la memoria todos los bloques de un
proceso para que se pueda ejecutar, basta con que se encuentren los bloques que contienen código o datos actualmente
referenciados, el resto puede permanecer en memoria secundaria.

La memoria virtual
La clave del concepto de memoria virtual es la disociación de las direcciones a las que hace referencia un proceso en ejecución
de las direcciones disponibles en la memoria principal.
Las direcciones a las que hace referencia un proceso en ejecución, en este esquema, se llaman direcciones virtuales. El
intervalo de direcciones virtuales a las que puede hacer referencia un proceso en ejecución se llama espacio de direcciones
virtuales, V, del proceso. El intervalo de direcciones reales de la memoria principal de un ordenador concreto se llama espacio
de direcciones reales, R. El número de direcciones de V se denota |V|, y el número de direcciones de R, |R|. En los sistemas
de almacenamiento virtual ya implantados lo normal es que |V| >> |R|, aunque se han construido sistemas en los que |V| < |
R|.
La memoria virtual es una técnica de gestión de la memoria que posibilita que el espacio de direcciones virtuales sea mayor al
espacio de direcciones reales. En otras palabras, se permite hacer programas de tamaño mayor al de la memoria principal.
Para lograr esto, el sistema operativo se encarga de mantener en la memoria principal solamente aquellas partes del espacio
de direcciones del proceso que actualmente están siendo referenciadas, el resto permanece en disco.
La memoria virtual se basa en el hecho de que muchos programas presentan un comportamiento conocido como operación en
contexto, según el cual, en cualquier intervalo pequeño de tiempo un programa tiende a operar dentro de un módulo lógico
en particular, sacando sus instrucciones de una sóla rutina y sus datos de una sóla zona de datos. De esta forma, las referencias
de memoria de los programas tienden a agruparse en pequeñas zonas del espacio de direcciones. La localidad de estas
referencias viene reforzada por la frecuente existencia de bucles: cuanto más pequeño sea el bucle, menor será la dispersión
de las referencias. La observación de este comportamiento conduce al postulado (Denning, 1970) del llamado principio de
localidad: "Las referencias de un programa tienden a agruparse en pequeñas zonas del espacio de direcciones. Estas zonas,
además, tienden a cambiar sólo de forma intermitente".
La validez del principio de localidad varía de programa en programa: será, por ejemplo, más válido en programas que lleven a
cabo accesos secuenciales a vectores que en programas que accedan a estructuras complejas de datos.
La memoria virtual se compagina con la multiprogramación. Al no tener que almacenar los procesos enteros en la memoria,
pueden entrar más en la memoria principal, con lo que es más probable que siempre exista un proceso en estado listo. Por
otro lado, cuando un proceso espera a que se cargue en la memoria principal parte de su código o datos, se inicia una E/S con
el disco. Mientras dura dicha E/S la CPU puede ejecutar otro proceso.
Aunque los procesos sólo hacen referencia a direcciones virtuales, deben ejecutarse en la memoria real. Por lo tanto, durante
la ejecución de un proceso es preciso establecer la correspondencia entre las direcciones virtuales y las reales. Como se verá
más adelante esta correspondencia debe realizarse de una manera rápida, pues si no, se ralentizaría demasiado el tiempo de
ejecución de los procesos.

51
Se han desarrollado varios métodos para asociar las direcciones virtuales con las reales. Los mecanismos de traducción
dinámica de direcciones convierten la direcciones virtuales en direcciones reales en tiempo de ejecución. Todos estos sistemas
tienen la propiedad de que las direcciones contiguas en el espacio de direcciones virtuales de un proceso no son
necesariamente contiguas en la memoria principal. Esto se conoce como contigüidad artificial (fig. 6.8). Debe quedar claro que
toda esta correspondencia es transparente al programador, que escribe sus programas en términos de direcciones
consecutivas de memoria virtual.

6.5.1 Esquema general de traducción


Los mecanismos de traducción dinámica de direcciones deben mantener mapas de correspondencia de traducción de
direcciones que indiquen qué localidades de la memoria virtual están en memoria principal en un momento dado y dónde se
encuentran.
Existen dudas en cuanto a si los bloques en que se dividen los procesos deben ser del mismo tamaño o de tamaños diferentes.
Cuando los bloques son del mismo tamaño, se llaman páginas, y la organización de la memoria virtual correspondiente se
conoce como paginación. Cuando los bloques pueden tener tamaños diferentes se llaman segmentos, y la organización de la
memoria virtual correspondiente se llama segmentación. Algunos sistemas combinan ambas técnicas, con segmentos, que son
entidades de tamaño variable, compuestas de páginas de tamaño fijo ( Segmentación paginada ).
Las direcciones en un sistema de correspondencia son bidimensionales. Para referirse a un elemento en particular, el programa
especifica el bloque en el que se encuentra el elemento, y su desplazamiento a partir del inicio del bloque. Una dirección
virtual, v, se denota por un par ordenado (b,d), donde b es el número de bloque en el que se encuentra el elemento al que se
hace referencia, y d es el desplazamiento a partir del inicio del bloque.
La traducción de una dirección virtual v = (b,d) a una dirección real, r, se ejecuta de la siguiente forma (fig. 9). Cada proceso
tiene su propia tabla de correspondencia de bloques, mantenida por el sistema operativo dentro de la memoria principal. Un
registro especial dentro de la CPU, denominado registro de origen de la tabla de correspondencia de bloques, se carga con la
dirección real, a, de la tabla de correspondencia de bloques del proceso durante el cambio de proceso. La tabla contiene una
entrada por cada bloque del proceso, y las entradas siguen un orden secuencial para el bloque 0, el bloque 1, etcétera. Ahora
se suma el número de bloque, b, a la dirección base, a, de la tabla de bloques, para formar la dirección real de la entrada del
bloque b en la tabla de correspondencia de bloques. Esta entrada contiene la dirección real, b', de inicio del bloque b. El
desplazamiento, d, se suma a la dirección de inicio del bloque, b', para formar la dirección real
deseada, r = b' + d.

52
Todas las técnicas de correspondencia de bloques empleadas en los sistemas de segmentación, paginación, y paginación y
segmentación combinada son similares a la correspondencia mostrada en la figura 6.9.
Es importante señalar que la traducción de una dirección virtual a real la realiza una unidad hardware, que transforma todas
las direcciones generadas por la CPU antes de que pasen al bus del sistema. Es esencial que esta transformación la realice el
hardware, y no el sistema operativo, pues muchas instrucciones máquina incluyen referencias a memoria, y la
correspondencia debe realizarse rápidamente, para no ralentizar en exceso el tiempo de ejecución de los procesos. Por
ejemplo, las dos sumas indicadas en la figura 6.9 deben ser más rápidas que las sumas convencionales del lenguaje máquina.
Aunque el hardware consulta las tablas de correspondencia de bloques para la transformación de direcciones, es el sistema
operativo el encargado de rellenar y gestionar dichas tablas. Un proceso no tiene por qué tener todos sus bloques en memoria
principal, recuérdese que el espacio de direcciones virtuales puede ser muy superior al espacio de direcciones reales, esto
hace que a veces un proceso referencie a una dirección de un bloque que no se encuentra en la memoria principal. Para
detectar esto, las tablas de correspondencias tienen un "bit de presencia" por entrada (cada entrada representa un bloque),
que indica si el bloque se encuentra presente en la memoria principal o no. El hardware de traducción debe verificar este bit
en cada referencia a memoria.
Si el bloque no está en memoria principal, el hardware produce una interrupción. Esta interrupción provoca que el control
pase al software (sistema operativo), para que éste inicie la transferencia del bloque que falta desde la memoria secundaria a
la memoria principal, y actualice de acuerdo con ello la tabla de correspondencias. El proceso en ejecución se hará no listo
hasta que se haya completado esta transferencia. La posición de los bloques en la memoria secundaria puede guardarse en la
misma tabla de correspondencias.

6.5.2 Paginación
El concepto de almacenamiento a un sólo nivel, en el que la memoria secundaria aparece como una extensión de la memoria
principal, se introdujo por primera vez en el ordenador Atlas de la Universidad de Manchester alrededor de 1960, y desde
entonces ha ejercido una profunda influencia en el diseño de los ordenadores.
El almacenamiento a un sólo nivel puede llevarse a cabo mediante una técnica llamada paginación, según la cual el espacio de
direcciones virtuales se divide en páginas del mismo tamaño (en el Atlas eran de 512 palabras). La memoria principal se divide
también en marcos o páginas físicasdel mismo tamaño. Estos marcos son compartidos entre los distintos procesos que haya
en el sistema, de forma que en cualquier momento un proceso dado tendrá unas cuantas páginas residentes en la memoria
principal (sus páginas activas) y el resto en la memoria secundaria (sus páginas inactivas). El mecanismo de paginación cumple
dos funciones:
- Llevar a cabo la transformación de una dirección virtual a física, o sea, la determinación de la página a la que corresponde
una determinada dirección de un programa, así como del marco, si lo hay, que ocupa esta página;
- Transferir, cuando haga falta, páginas de la memoria secundaria a la memoria principal, y de la memoria principal a la
memoria secundaria cuando ya no sean necesarias.
La primera función se aborda a continuación, y se deja para el siguiente tema la segunda función.
Con el fin de determinar la página a la que hace referencia un programa, los bits de mayor peso de la dirección se interpretan
como el número de página, y los bits de menor peso como el número de palabra dentro de esta página. De ahí que si el
tamaño de página es 2n, los n bits finales de la dirección representarán el número de palabra y los bits restantes del principio el
número de página. El número total de bits en la dirección es suficiente para direccionar la totalidad de la memoria virtual. Así,

53
por ejemplo, en el Atlas las direcciones de programa tenían 20 bits de longitud, proporcionando una memoria virtual de 2 20
palabras; el tamaño de la página era de 512 palabras (2 9), y de ahí que los 9 bits inferiores representasen el número de palabra
y los 11 superiores representasen el número de la página. El número total de páginas en la memoria virtual era por tanto de 2 11
(en contraposición a las 32 páginas físicas de que disponía la memoria principal).
Es de destacar el hecho de que la división de la dirección en número de palabra y número de página, es tarea del hardware, y
es transparente al programador: por lo que a Jl concierne está programando en un espacio secuencial de direcciones muy
grande.
La transformación de número de página y de palabra en la dirección física de memoria se realiza a través de una tabla de
páginas, cuyo p-ésimo elemento contiene la posición p' del marco que contiene a la página p (la posibilidad de que la p-ésima
página no se encuentre en la memoria principal se abordará dentro de un momento). El número de palabra, w, se suma a p'
para obtener la dirección buscada (ver la figura 10).
La transformación de direcciones consiste, pues, en:
f(a) = f(p, w) = p' + w
donde la dirección de programa, a, el número de página, p, y el número de palabra, w, están relacionados con el tamaño de
página Z a través de:
p = parte entera de (a/Z)
w = resto de (a/Z)

ENLACE A LA SIMULACIÓN DE MEMORIA PAGINADA


Así pues, cada vez que la CPU genere una dirección de memoria ésta es transformada por una unidad hardware, de forma que
en el bus del sistema se introduce la dirección física correspondiente. Es importante observar que la paginación es en sí misma
una forma de reubicación dinámica. Cada dirección lógica es transformada en alguna dirección física por el hardware de
paginación. Observe también que si el tamaño de página (como es lo usual) es una potencia de dos, el hardware no precisa
realizar ninguna división, simplemente sabe que los últimos n bits, si el tamaño de página es de 2n , representan el
desplazamiento, y los primeros bits la página.
Cada proceso debe tener su propia tabla de páginas, y su dirección de comienzo en la memoria principal forma parte de la
porción del PCB utilizada para realizar un cambio de proceso.
Como el número de marcos (cantidad de memoria real) asignados a un proceso será normalmente menor que el número de
páginas que éste utiliza, es muy posible que una dirección del programa haga referencia a una página que no se encuentre en
aquel momento en la memoria principal. En este caso el elemento correspondiente de la tabla de páginas estará vacío,
provocando el hardware una interrupción de "fallo de página" si se intenta acceder a ella. Esta interrupción provoca que el
control pase al software (al sistema operativo), para que éste inicie la transferencia de la página que falta desde la memoria
secundaria a la memoria principal, y actualice de acuerdo con ello la tabla de páginas. El proceso en ejecución se hará no listo
hasta que se haya completado esta transferencia. La posición de las páginas en la memoria secundaria puede guardarse en una
tabla separada o en la misma tabla de páginas. En este último caso, es necesario un "bit de presencia" en cada elemento de la
tabla de páginas, para indicar si la página se encuentra presente o no en la memoria principal, y si el campo de direcciones
debe interpretarse como una dirección de marco, o bien como una dirección de la memoria secundaria.
Si no existe ningún marco vacío en el momento en que ocurre un fallo de página, hay que guardar en la memoria secundaria
alguna otra página con el fin de hacer sitio a la nueva. La elección de la página que habrá que sacar es el resultado de un
algoritmo de reemplazo de página, del cual veremos varios ejemplos en el tema siguiente. Por el momento, vamos a destacar
tan sólo el hecho de que la información que necesita el algoritmo de cambio de página, puede estar contenida en algunos bits
adicionales que se añaden a cada elemento de la tabla de páginas.

54
Quizás habría que aclarar, que toda la operación de transformaciones de direcciones la lleva a cabo el hardware, excepto en el
caso en que haya que traer una página de la memoria secundaria. En este caso, la aplicación del algoritmo de cambio de
página, así como la actualización de la tabla de páginas, las lleva a cabo el software.
La anterior discusión proporciona una visión general de cómo funciona la paginación. En la práctica hay que hacer una serie de
modificaciones para llegar a una implementación viable. Una de ellas es que la transformación de dirección virtual a física debe
ser rápida.

6.5.2.1 Memoria asociativa


En el sistema que hemos descrito el tiempo necesario para cada referencia a memoria queda doblado, debido a la necesidad
de acceder primero a la tabla de páginas. Una forma de evitarlo podría ser la de tener guardada la tabla de páginas en un
conjunto de registros rápidos en lugar de sobre la memoria ordinaria. Sin embargo, el tamaño de la tabla de páginas es
proporcional al tamaño del espacio de direcciones virtuales; de ahí que el número de registros necesarios sea demasiado
grande para que esta alternativa resulte económicamente viable. La solución al problema consiste en adoptar una técnica
diferente para acceder a las páginas activas. Esta técnica representa el tener que incorporar a la máquina una memoria
asociativa, que consistirá en un pequeño conjunto de registros de dirección de página (PARs, del inglés page address registers),
cada uno de los cuales contiene el número de página de una página activa. Los PARs presentan la propiedad de poderse buscar
en ellos de forma simultánea el número de página asociado a una dirección de programa en particular.
Por ejemplo, en la figura 6.11, la dirección de programa 3243 se divide en el número de página 3 y el número de palabra 243
(vamos a suponer, por comodidad, que el tamaño de la página sea 1000). El número de página se compara entonces de forma
simultánea con el contenido de todos los PARs, y se encuentra que coincide con el valor del PAR 5. Ello indica que la página 3
ocupa en la actualidad la página física número 5, de forma que la dirección buscada será la 5243.
El empleo de un almacenamiento de tipo asociativo reduce el tiempo empleado en la transformación de direcciones en un
orden de magnitud con respecto al caso en el que se guardaba la tabla de páginas sobre memoria principal.

Con el fin de que se pueda hacer referencia a todas las páginas activas a través de un PAR, hay que disponer de tantos como
marcos haya en la memoria. Ello es posible en sistemas con una memoria principal reducida (como por ejemplo, el Atlas), pero
en sistemas mayores no es viable, desde el punto de vista económico, disponer de todos los PARs necesarios para ello (aunque
es de esperar que estos argumentos de tipo económico cambien a medida que se desarrolla la tecnología). Se puede llegar a
una solución de compromiso, guardando para cada proceso una tabla de páginas completa en la memoria, y utilizando una
pequeña memoria asociativa para referenciar unas pocas páginas asociadas a los procesos activos más recientes. En este caso,
el marco al que hará referencia cada PAR, no vendrá implícito por la situación de éste, sino que deberá incluirse como un
campo adicional en el mismo PAR. El hardware de direccionamiento de la memoria lleva a cabo, entonces, la operación de
transformación de direcciones que se muestra en la figura 6.12. Como antes, sólo se requiere la intervención del software en
el caso de que haya que sustituir una página.
Un problema que no ilustra la figura 6.11 es el de distinguir en la memoria asociativa las páginas asociadas al proceso en
ejecución de las páginas correspondientes a otros procesos. Una solución consistiría en ampliar los PARs para incluir la
identificación de los procesos, junto con el número de la página. Cada dirección que se presente a la memoria asociativa
deberá incluir, según esto, el identificador del proceso junto con los bits de la página.
Evidentemente, es de desear que la memoria asociativa contenga los números de las páginas a las que haya mayores
posibilidades de acceder. Lamentablemente, no existe ningún algoritmo general que nos asegure que así suceda (véase el
siguiente tema). En la práctica se cargan cíclicamente en la memoria asociativa las direcciones de las páginas a las que se ha
hecho referencia con más frecuencia recientemente. Este algoritmo, más bien primitivo, es, de hecho, bastante eficaz.
El porcentaje de veces que se encuentra un número de página entre los registros asociativos está relacionado claramente con
el número de registros asociativos. Con 8 o 16 registros asociativos, puede obtenerse un porcentaje del 80 al 90%. Un
porcentaje del 80% significa que el 80% de las veces encontramos el número de página deseado entre los registros asociativos.
Si explorar los registros asociativos lleva 50 nanosegundos, y 750 nanosegundos acceder a memoria, entonces un acceso a
memoria "mapeada" lleva 800 nanosegundos cuando el número de página se encuentra en los registros asociativos. Si no

55
conseguimos encontrar el número de página (50 ns), entonces tenemos que acceder a la memoria en primer lugar para buscar
el número de marco en la tabla de páginas (750 ns) y entonces acceder a la palabra deseada en memoria (750 ns), dando en
total 1550 nanosegundos. Para encontrar el tiempo efectivo de acceso a memoria, tenemos que ponderar cada caso con su
probabilidad:
t. efectivo de acceso a memoria = 0,80 x 800 + 0,20 x 1550 = 950 ns
En este ejemplo, sufrimos un 26,6% de retardo en el tiempo de acceso a memoria (de 750 a 950 nanosegundos).

6.5.5.2 Páginas compartidas


Otra ventaja de la paginación es la posibilidad de compartir programas de uso corriente. Esto es particularmente importante
en un entorno de tiempo compartido. Consideremos un sistema que soporta 40 usuarios, cada uno de los cuales ejecuta un
editor de textos. Si el editor de textos consta de 30K de código y 5K de espacio para datos, necesitaríamos 1400K para permitir
a los 40 usuarios. No obstante, si el programa es reentrante, podría compartirse como se muestra en la figura 6.13. Aquí
vemos un editor de tres páginas que es compartido por tres procesos. Cada proceso tiene su propia página de datos
El código reentrante (también llamado código puro) es un código no automodificable. Si el código es reentrante, entonces
nunca cambia durante la ejecución. Así, dos o más procesos pueden ejecutar el mismo código al mismo tiempo. Cada proceso,
para su ejecución, tiene su PCB y su memoria para mantener los datos. Por supuesto, los datos de todos esos procesos
diferentes varían para cada uno de ellos.
Tan sólo hace falta mantener una copia del editor en la memoria física. Cada tabla de páginas de usuario-proceso hace
referencia a la misma copia física del editor, pero las páginas de datos lo hacen a marcos diferentes. Así, para permitir 40
usuarios, precisamos solamente una copia del editor, 30K, más 40 copias del espacio de 5K por usuario. El espacio total
requerido es ahora de 230K, en lugar de 1400K, un ahorro significativo.

56
También pueden compartirse otros programas muy utilizados: compiladores, ensambladores, sistemas de bases de datos, etc.
Para que sea compartible, el código tiene que ser reentrante (no automodificable). Este término significa que nunca debería
darse una tentativa de almacenar algo en el código, que es de sólo búsqueda o sólo lectura. Obviamente, es crucial que las
páginas compartidas sean inamovibles. Si un usuario cambiara una posición, cambiaría para todos los usuarios. La naturaleza
de sólo lectura del código compartido no debería dejarse a merced de la corrección del código. El sistema operativo debe
reforzar esa propiedad.

6.5.5.3 Protección
La protección de la memoria en un entorno paginado se consigue por medio de unos bits de protección asociados a cada
página. Normalmente estos bits se mantienen en la tabla de páginas. Un bit puede definir que una página sea de
lectura/escritura o de sólo lectura. Cada referencia a memoria pasa a través de la tabla de páginas para encontrar el número de
marco correcto. Al tiempo que se calcula la dirección física, pueden verificarse los bits de protección para asegurar que no se
escribe sobre una página de sólo lectura. Una tentativa de escribir sobre una página de sólo lectura ocasiona una excepción
hardware al sistema operativo (por violación de acceso a una zona de la memoria principal).
Esta concepción de la protección puede ser extendida fácilmente para obtener una protección más detallada. Podemos
disponer de hardware que ofrezca protección de sólo lectura, lectura-escritura o sólo ejecución. O bien, por medio de bits de
protección independientes para cada tipo de acceso, puede permitirse cualquier combinación de estos accesos, al tiempo que
las tentativas ilegales generan una excepción al sistema operativo.

6.5.5.4 Dos visiones de la memoria


Un aspecto muy importante de la paginación es la clara separación entre la visión de la memoria que tiene el usuario y la
memoria física real. El programa de usuario cree que la memoria es un espacio contiguo, conteniendo solamente ese único
programa. En realidad, el programa de usuario está disperso por la memoria física, que también contiene otros programas. La
diferencia que existe entre la visión que el usuario tiene de la memoria y la memoria física real se salva por medio del
hardware de traducción de direcciones o de transformación (mapping). El hardware de transformación traduce las direcciones
lógicas en direcciones físicas. Esta operación permanece oculta al usuario, y la controla el sistema operativo.
Un resultado de la distinción entre direcciones físicas y lógicas es que de hecho pueden no ser iguales. Por ejemplo, en el XDS-
940, una dirección lógica es de 14 bits y una dirección física es de 16 bits. Un número de página de 3 bits se usa como índice en
la tabla de páginas para seleccionar un número de marco de 5 bits. Así, puede haber hasta 4 veces más memoria física que la
que un usuario puede direccionar.
Esta técnica fue adoptada en particular por los fabricantes de miniordenadores. Muchos miniordenadores fueron diseñados
en los años 60, cuando la memoria era cara y los programas tenían que ser pequeños. De esta manera las direcciones estaban
limitadas a 15 o 16 bits. Con la disponibilidad de la memoria de semiconductores, más barata, se hizo factible aumentar la
memoria física de estos miniordenadores. Pero incrementar el tamaño de la dirección para obtener direcciones de 17 o 18 bits,
precisas para la memoria física aumentada, significaba, o bien rediseñar el conjunto de instrucciones, o bien extender el
tamaño de palabra para acomodar los bits extra. Cualquier solución implicaría un cambio importante, invalidando todos los
programas y documentación existentes. La solución que adoptaron la mayoría de los fabricantes fue el mapping de memoria.
Las direcciones lógicas (15 o 16 bits), se transforman en direcciones físicas (17 o 18 bits). Multiprogramando el sistema, puede
utilizarse toda la memoria. Sin embargo, los usuarios individuales no pueden emplear más memoria que antes, puesto que el
espacio para la dirección lógica no ha sido incrementado. No obstante, hay que tener claro que en los sistemas actuales el
rango de direcciones virtuales suele ser muy superior al rango de direcciones reales.

57
El sistema operativo controla esta transformación, y puede activarla para el usuario y desactivarla para el sistema operativo.
Puesto que el sistema operativo gestiona memoria física, tiene que estar al tanto de la memoria física: qué marcos están
asignados, qué marcos están disponibles, cuántos marcos hay en total, etc. Esta información se mantiene generalmente en una
estructura denominada tabla de marcos. La tabla de marcos tiene una entrada para cada marco, que indica si está libre o
asignado y, si está asignado, a qué página de qué proceso.
Además, el sistema operativo tiene que conocer qué procesos de usuario operan en el espacio de usuario, y tiene que
transformar todas las direcciones lógicas para generar direcciones físicas. Si un usuario realiza una llamada al sistema (para
realizar E/S) y da una dirección como parámetro (por ejemplo, un buffer), esa dirección tiene que ser traducida para generar la
dirección física correcta. El sistema operativo puede utilizar la dirección de la tabla de páginas del proceso (que se guarda en su
descriptor o PCB) para traducir las direcciones lógicas en físicas siempre que tenga que realizar por él mismo la operación.

6.5.3 Segmentación
Un aspecto importante de la gestión de la memoria que la paginación convierte en inevitable es la separación de la visión que
el usuario tiene de la memoria y la memoria física real. La visión del usuario no coincide con la memoria física real. La visión
del usuario se transforma en la memoria física. La traducción de direcciones permite esta diferencia entre la memoria lógica y
la física.

6.5.3.1 Visión del usuario de la memoria

¿Cuál es la visión de la memoria que tiene el usuario? Concibe el usuario la memoria como una tabla lineal de palabras,
algunas de las cuales contienen instrucciones mientras que otras contienen datos, o bien se prefiere alguna otra visión de la
memoria? Hay un acuerdo general en que el usuario o programador de un sistema no piensa en la memoria como una tabla
lineal de palabras. Más bien prefieren concebirla como una colección de segmentos de longitud variable, no necesariamente
ordenados (fig. 6.14).
Consideremos cómo ve usted un programa cuando lo está escribiendo. Piensa en él como un programa principal, con un
conjunto de subrutinas, procedimientos, funciones o módulos. También puede haber diversas estructuras de datos: tablas,
matrices, pilas, variables, etc. Cada uno de estos módulos o elementos de datos se referencian por un nombre. Usted habla de
la "tabla de símbolos", A "la función Sqrt", "el programa principal", sin tener en cuenta qué direcciones de memoria ocupan
estos elementos. Usted no se preocupa de si la tabla de símbolos se almacena antes o después de la función Sqrt. Cada uno de
estos elementos es de longitud variable; la longitud está definida intrínsecamente por el propósito del segmento en el
programa. Los elementos dentro de un segmento están identificados por su desplazamiento desde el principio del segmento:
la primera instrucción del programa, la decimoséptima entrada de la tabla de símbolos la quinta función Sqrt, etc.
La segmentación es un esquema de administración de la memoria que soporta la visión que el usuario tiene de la misma. Un
espacio de direcciones lógicas es una colección de segmentos. Cada segmento tiene un nombre y una longitud. Las direcciones
especifican tanto el nombre del segmento como el desplazamiento dentro del segmento. Por lo tanto, el usuario especifica
cada dirección mediante dos cantidades: un nombre de segmento y un desplazamiento. (Compárese este esquema con la
paginación, donde el usuario especificaba solamente una única dirección, que el hardware particionaba en número de página
y desplazamiento, siendo todo ello invisible al programador).
Por simplicidad de implementación, los segmentos están numerados y se referencian por un número de segmento en lugar de
por un nombre. Normalmente el programa de usuario se ensambla (o compila), y el ensamblador (o el compilador) construye
automáticamente segmentos que reflejan el programa de entrada. Un compilador de Pascal podría crear segmentos separados
para (1) las variables globales, (2) la pila de llamada de procedimientos, para almacenar parámetros y devolver direcciones, (3)
el código de cada procedimiento o función, y (4) las variables locales de cada procedimiento y función. El cargador tomaría
todos esos segmentos y les asignaría números de segmento.

6.5.3.2 Hardware
Aunque el usuario ahora puede referenciar los objetos del programa por medio de una dirección de dos dimensiones, la
memoria física real es todavía, por supuesto, una secuencia unidimensional de palabras. La transformación se efectúa por
medio de una tabla de segmentos.

58
El empleo de una tabla de segmentos se muestra en la figura 6.15. Una dirección lógica consta de dos partes: un número de
segmento s y un desplazamiento dentro de ese segmento, d. El número de segmento se utiliza como un índice en la tabla de
segmentos. Cada entrada de la tabla de segmentos tiene una base de segmento y un límite. El desplazamiento d de la dirección
lógica tiene que estar comprendido entre 0 y el límite de segmento. En caso contrario se produce una excepción al sistema
operativo (tentativa de direccionamiento lógico más allá del fin de segmento). Si este desplazamiento es legal, se añade a la
base para producir la dirección de la tabla deseada en la memoria física. La tabla de segmentos es así esencialmente una
matriz de pares registros base/límite.

ENLACE A LA SIMULACIÓN DE MEMORIA SEGMENTADA

6.5.3.3 Implementación de tablas de segmentos


Al igual que la tabla de páginas, la tabla de segmentos puede situarse bien en registros rápidos o bien en memoria. Una tabla
de segmentos mantenida en registros puede ser referenciada muy rápidamente: la adición a la base y la comparación con el
límite pueden realizarse simultáneamente para ahorrar tiempo. El PDP-11/45 utiliza este método; tiene 8 registros de
segmento. Una dirección de 16 bits se forma a partir de un número de segmento de 3 bits y de un desplazamiento de 13 bits.
Esta disposición permite hasta 8 segmentos; cada segmento puede ser de hasta 8 K-bytes. Cada entrada en la tabla de
segmentos tiene una dirección base, una longitud y un conjunto de bits de control de acceso que especifican acceso denegado,
acceso de sólo lectura, o acceso de lectura/escritura al segmento.
El Burroughs B5500 permitía 32 segmentos de hasta 1024 palabras cada uno. Estas especificaciones definían un número de
segmento de 5 bits y un desplazamiento de 10 bits. Sin embargo, la experiencia con este sistema mostró que los segmentos
eran pocos y que el límite del tamaño del segmento era demasiado pequeño (las tablas mayores de 1K tenían que
fragmentarse entre varios segmentos). Por ello, el GE 645 utilizado por Multics permite hasta 256 K-segmentos de hasta 64 K-
palabras.
Con tantos segmentos no es factible mantener la tabla de segmentos en registros, de modo que tiene que mantenerse en
memoria. Un registro de base de tabla de segmentos (STBR) apunta a la tabla de segmentos. Puesto que el número de
segmentos utilizado por un programa puede variar ampliamente, también se utiliza un registro de longitud de tabla de
segmentos (STLR). En el caso de una dirección lógica (s, d) verificamos primero que el número de segmento s es legal (s <
STLR), Entonces, añadimos el número de segmento al STBR resultando la dirección en memoria de la entrada de la tabla de
segmentos (STBR + s). Esta entrada se lee en la memoria y actuamos igual que antes: se verifica el desplazamiento frente a la
longitud de segmento, y se calcula la dirección física de la palabra deseada como la suma de la base del segmento y el
desplazamiento.
Igual que con la paginación, esta transformación requiere dos referencias a memoria por dirección lógica, el ordenador
disminuirá su velocidad en un factor de 2, a menos que se haga algo para evitarlo. La solución normal consiste en utilizar un
conjunto de registros asociativos para mantener las entradas utilizadas más recientemente en la tabla de segmentos. Un
conjunto de registros asociativos relativamente pequeño (8 \ 16) puede reducir generalmente el retardo a los accesos a
memoria hasta no más allá de un 10% o 15% más lentos que los accesos a memoria "mapeada".

6.5.3.4 Compartición y protección


Una ventaja importante de la segmentación es la asociación de la protección con los segmentos. Puesto que los segmentos
representan una porción del programa definida semánticamente, es probable que todas las entradas en el segmento se
utilicen de la misma manera. De ahí que tengamos algunos segmentos que son instrucciones, mientras que otros son datos. En
una arquitectura moderna las instrucciones son no automodificables, de modo que los segmentos de instrucciones pueden
definirse como de sólo lectura o sólo ejecución. El hardware verificará los bits de protección asociados a cada entrada en la
tabla de segmentos para impedir accesos ilegales a memoria, tales como tentativas de escribir en un segmento de sólo lectura

59
o de utilizar un segmento de sólo ejecución como datos. Situando una tabla en un segmento propio, el hardware verificará
automáticamente que toda indexación en la tabla es legal, y no sobrepasa los límites de la misma. Así, muchos errores
frecuentes en programas serán detectados por hardware antes de que puedan ocasionar un daño serio.
Otra ventaja de la segmentación está relacionada con la compartición de código y datos. Los segmentos se comparten cuando
las entradas en las tablas de segmentos de dos procesos diferentes apuntan a las mismas posiciones físicas.
La compartición se produce a nivel de segmento. Por lo tanto, cualquier información puede compartirse definiéndole un
segmento. Pueden compartirse varios segmentos, de modo que es posible compartir un programa compuesto de más de un
segmento.
Por ejemplo, consideremos el uso de un editor de textos en un sistema de tiempo compartido. Un editor completo podría
resultar bastante largo, y formado por muchos segmentos. Estos segmentos pueden compartirse entre todos los usuarios,
limitando la memoria física necesaria para soportar las tareas de edición. En lugar de necesitar n copias del editor, precisamos
solamente una. Aún necesitamos segmentos únicos e independientes para almacenar las variables locales de cada usuario.
Estos segmentos, por supuesto, no deben ser compartidos.
También es posible compartir solo partes de programas. Por ejemplo, subrutinas de uso frecuente pueden compartirse entre
muchos usuarios definiéndolas como segmentos de sólo lectura compartibles. Por ejemplo, dos programas Fortran pueden
utilizar la misma subrutina Sqrt, pero sólo será precisa una copia física de la rutina Sqrt.
Aunque esta compartición parece ser bastante sencilla, tiene algunas sutilezas. Típicamente, los segmentos de código tienen
referencias a sí mismos. Por ejemplo, un salto condicional tiene normalmente una dirección de transferencia. La dirección de
transferencia es un nombre de segmento y un desplazamiento. El número de segmento de la dirección de transferencia será el
del segmento de código. Si tratamos de compartir este segmento, todos los procesos que lo compartan tienen que definir el
segmento de código compartido con el mismo número de segmento.
Por ejemplo, si queremos compartir la rutina Sqrt y un proceso quiere definirla como segmento 4 y otro lo hace como
segmento 17, ¿cómo podría la subrutina Sqrt referenciarse a sí misma? Puesto que solamente hay una copia física de Sqrt,
tiene que referenciarse a sí misma de la misma manera para ambos usuarios: tiene que tener un número de segmento único. A
medida que crece el número de usuarios que comparten el segmento, también crece la dificultad de encontrar un número de
segmento aceptable.
Los segmentos de datos de sólo lectura (sin punteros) pueden compartirse aún usando números de segmento diferentes; lo
mismo puede hacerse con segmentos de código que no se referencian directamente a sí mismos, sino sólo indirectamente. Por
ejemplo, la bifurcación condicional que especifica la dirección de desplazamiento a partir del valor actual del contador de
programa o respecto a un registro que contiene el número de segmento actual, permite que el código no tenga que realizar
una referencia al número de segmento actual.
El ordenador GE 645 utilizado con Multics tenía 4 registros que contenían los números de segmento del segmento actual, del
segmento de pila, del segmento de enlace y de un segmento de datos. Los programas pocas veces hacen referencia
directamente a un número de segmento, sino siempre indirectamente a través de estos cuatro registros de segmento. Esto
permite que el código pueda compartirse libremente.

6.5.3.5 Fragmentación
El sistema operativo tiene que encontrar y asignar memoria para todos los segmentos de un programa de usuario. Esta
situación es similar a la paginación, excepto en el hecho de que los segmentos son de longitud variable; las páginas son todas
del mismo tamaño. Por tanto, como en el caso de las particiones dinámicas, la asignación de memoria es un problema de
asignación dinámica de almacenamiento, resuelto probablemente mediante un algoritmo del mejor o primer ajuste.
La segmentación puede ocasionar entonces fragmentación externa, cuando todos los bloques libres de memoria son
demasiado pequeños para acomodar a un segmento. En este caso, el proceso puede simplemente verse obligado a esperar
hasta que haya disponible más memoria (o al menos huecos más grandes), o puede utilizarse la compactación para crear
huecos mayores. Puesto que la segmentación es por naturaleza un algoritmo de reubicación dinámica, podemos compactar la
memoria siempre que queramos.
¿En qué medida es mala la fragmentación externa en un esquema de segmentación ? La respuesta a estas preguntas depende
principalmente del tamaño medio de segmento. En un extremo, se podría definir cada proceso como un segmento; este
esquema es el de las particiones dinámicas. En el otro extremo, cada palabra podría situarse en su propio segmento y
reubicarse por separado. Esta disposición elimina la fragmentación externa. Si el tamaño medio de segmento es pequeño, la
fragmentación externa también será pequeña. (Por analogía, consideremos la colocación de las maletas en el maletero de un
coche; parece que nunca encajan bien. Sin embargo, si se abren las maletas y se colocan en el maletero los objetos sueltos,
todo encaja). Puesto que los segmentos individuales son más pequeños que el proceso en conjunto, es más probable que
encajen en los bloques de memoria disponibles.

60
6.5.4 Segmentación paginada

ENLACE A LA SIMULACIÓN DE SEGMENTACIÓN PAGINADA

Tanto la paginación como la segmentación tienen sus ventajas y desventajas. También es posible combinar estos dos esquemas
para mejorar ambos. Veamos como ejemplo el esquema del ordenador GE 645 con el sistema operativo Multics. Las
direcciones lógicas estaban formadas a partir de un número de segmento de 18 bits y un desplazamiento de 16 bits. Aunque
este esquema crea un espacio de direcciones correspondiente a una dirección de 34 bits, la tabla de segmentos tiene un
tamaño tolerable, puesto que el número variable de segmentos conduce naturalmente al uso de un Registro de Longitud de
Tabla de Segmentos. Necesitamos tan solo el mismo número de entradas en la tabla de segmentos que segmentos; no
tenemos por qué tener entradas vacías en la tabla de segmentos.
No obstante, con segmentos de 64 K-palabras, el tamaño medio de segmento podría resultar bastante grande y la
fragmentación externa constituir un problema. Incluso si la fragmentación externa no es significativa, el tiempo de búsqueda
para asignar un segmento, utilizando un primer o mejor ajuste, podría ser grande. De esta manera se podría desperdiciar
memoria a causa de la fragmentación externa o bien desperdiciar tiempo debido a la búsqueda larga, o bien ambas cosas.
La solución adoptada fue paginar los segmentos. La paginación elimina la fragmentación interna y convierte en trivial el
problema de la asignación: cualquier marco vacío puede utilizarse para una página. Obsérvese que la diferencia entre esta
solución y la segmentación pura es que la entrada en la tabla de segmentos no contiene la dirección de la base del segmento,
sino la dirección de la base de una tabla de páginas para ese segmento. El desplazamiento del segmento se fragmenta
entonces en un número de página de 6 bits y un desplazamiento de página de 10 bits. El número de página indexa en la tabla
de páginas para dar el número de marco. Finalmente, el número de marco se combina con el desplazamiento de página para
formar la dirección física.

Fig.: Acceso a un sistema de memoria Segmentado Paginado.

61
Ahora debemos tener una tabla de páginas independiente para cada segmento. No obstante, puesto que cada segmento tiene
una longitud limitada por su entrada en la tabla de segmentos, la tabla de páginas no tiene por qué tener su tamaño máximo.
Sólo precisa tantas entradas como se necesiten realmente. Además, generalmente la última página de cada segmento no
estará totalmente llena. De este modo tendremos, por término medio, media página de fragmentación interna por segmento.
Consecuentemente, aunque hemos eliminado la fragmentación externa, hemos introducido fragmentación interna e
incrementado la sobrecarga de espacio de la tabla.
A decir verdad, incluso la visión de paginación segmentada de Multics que acabamos de presentar es simplista. Puesto que el
número de segmento es una cantidad de 18 bits, podríamos tener 262144 segmentos, con lo que precisaríamos una tabla de
segmentos muy larga. Para simplificar este problema, Multics pagina la tabla de segmentos. De esta manera, en general, una
dirección en Multics utiliza un número de segmento para definir un índice de página en una tabla de páginas para la tabla de
segmentos. A partir de esta entrada, localiza la parte de la tabla de segmentos que tiene la entrada para ese segmento. La
entrada en la tabla de segmentos apunta a una tabla de páginas para ese segmento, que especifica el marco que contiene la
palabra deseada.

http://wwwdi.ujaen.es/~lina/TemasSO/MEMORIAVIRTUAL/1y2Motivaciones,ventajasyEstrategiasdeadministracion.htm

62

You might also like