Sie sind auf Seite 1von 87

Sintesis : Sistemas Operativos

por Kondrasky Alejandro

11/02/09

Índice general

Índice general 1

I Introducción 4
1 Introducción 5
2 Estructuras de SO 6

II Gestión de Procesos 7
3 Procesos 8
3.1. Concepto de proceso . . . . . . . . . . . . . . . . . . . . . . . . 8
3.2. Planicación de Procesos . . . . . . . . . . . . . . . . . . . . . 9
3.3. Operaciones sobre los Procesos . . . . . . . . . . . . . . . . . . 11
3.4. Comunicación interprocesos . . . . . . . . . . . . . . . . . . . . 13
3.5. Ejemplos de sistemas IPC . . . . . . . . . . . . . . . . . . . . . 16
3.6. Comunicación en SO's cliente-servidor . . . . . . . . . . . . . . 18

4 Hebras (Threads) 21
4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
4.2. Modelos multihebras . . . . . . . . . . . . . . . . . . . . . . . . 22
4.3. Bibliotecas de Threads . . . . . . . . . . . . . . . . . . . . . . . 22
4.4. Consideraciones sobre Threads . . . . . . . . . . . . . . . . . . 24
4.5. Ejemplo de SO : Linux . . . . . . . . . . . . . . . . . . . . . . . 26

1
ÍNDICE GENERAL 2

5 Planicación de CPU 28
5.1. Conceptos básicos . . . . . . . . . . . . . . . . . . . . . . . . . 28
5.2. Criterios de Planicación . . . . . . . . . . . . . . . . . . . . . 29
5.3. Algoritmos de Planicación . . . . . . . . . . . . . . . . . . . . 30
5.4. Planicación de Sistemas Multiprocesador . . . . . . . . . . . . 33
5.5. Planicación de threads . . . . . . . . . . . . . . . . . . . . . . 34
5.6. Ejemplo de SO : Planicación en Linux . . . . . . . . . . . . . 35
5.7. Evaluación de Algoritmos . . . . . . . . . . . . . . . . . . . . . 35

6 Sincronización de Procesos 38
6.1. Fundamentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
6.2. Sección crítica . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6.3. Hardware de sincronización . . . . . . . . . . . . . . . . . . . . 40
6.4. Semáforos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.5. Problemas clásicos de sincronización . . . . . . . . . . . . . . . 44
6.6. Monitores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47

7 Interbloqueos 51
7.1. Modelo de sistema . . . . . . . . . . . . . . . . . . . . . . . . . 51
7.2. Caracterización de los interbloqueos . . . . . . . . . . . . . . . 51
7.3. Metodos para tratar los interbloqueos . . . . . . . . . . . . . . 52
7.4. Prevención de interbloqueos . . . . . . . . . . . . . . . . . . . . 53
7.5. Evasión de Interbloqueos . . . . . . . . . . . . . . . . . . . . . . 54
7.6. Detección de Interbloqueos . . . . . . . . . . . . . . . . . . . . 56
7.7. Recuperación de un interbloqueo . . . . . . . . . . . . . . . . . 58

III Gestión de Memoria 60


8 Memoria Principal 61
8.1. Fundamentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8.2. Intercambio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.3. Asignación de memoria contigua . . . . . . . . . . . . . . . . . 64
8.4. Paginación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
8.5. Estructura de la tabla de paginas . . . . . . . . . . . . . . . . . 68
8.6. Segmentación . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
8.7. Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70

9 Memoria Virtual 72
9.1. Fundamentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
9.2. Paginación bajo demanda . . . . . . . . . . . . . . . . . . . . . 73
9.3. Copia durante la Escritura ( Copy-on-Write , CoW ) . . . . . . 75
9.4. Sustitución de Páginas . . . . . . . . . . . . . . . . . . . . . . . 76
9.5. Asignación de Marcos . . . . . . . . . . . . . . . . . . . . . . . 81
9.6. Sobrepaginación . . . . . . . . . . . . . . . . . . . . . . . . . . 81
9.7. Archivos Mapeados en Memoria . . . . . . . . . . . . . . . . . . 83
9.8. Asignación de la Memoria del Kernel . . . . . . . . . . . . . . . 84
9.9. Otras consideraciones . . . . . . . . . . . . . . . . . . . . . . . 85
¡Advertencia!

Esta sintesis fue realizado en forma personal por el Alumno Kondrasky


Alejandro y no es considerado de forma alguna material ocial de la materia
Sistemas Operativos de la Carrera Licenciatura en Ciencias de la Computación
, Fa.M.A.F.
La causa de su existencia es la necesidad personal del autor dentro de sus
metodos de estudio , teniendo también como objetivo tener una guía para
refrescar conceptos y temas.
Esta fue basada en el libro Fundamentos de Sistemas Operativos ; Séptima
edición de Abraham Silberschatz.
Esta sintesis contiene pocos o casi ninguno de los ejemplos presentados
en cada parte del libro , solo contiene los conceptos básicos de cada tema.
Tampoco contiene muchos ejemplos de los distintos sistemas operativos , solo
mantuvemayoritariamente los de Linux por considerar a este mas importantes
ya que es el sistema que utilizamos durante la carrera la mayoría de las veces.
Para aclarar esta sintesis fue realizada con una vision personal de cada
tema , si bien esta basada en el libro pueden haberse realizado omisiones de
información que podrían considerarse inapropiadas. Cualquier comentario de
pie de pagina que contenga la leyenda ¨nota de autor¨ no son datos conrmados
en la mayoría de los casos , sino mas bien anotaciones de pensamientos del
propio autor1 .
Por ultimo , invito a que cualquiera que desee realizar versiones propias de
esta sintesis o colaborar en su desarrollo y corrección para así generar un mejor
material de estudio adicional.

1 Las notas inutiles son defectos mentales del propio autor , disfrutenlos o sepan per-
donarlo. Si , esta es tambien una nota inutil.

3
Parte I

Introducción

4
Capítulo 1

Introducción

5
Capítulo 2

Estructuras de SO

6
Parte II

Gestión de Procesos

7
Capítulo 3

Procesos

3.1. Concepto de proceso


Utilizaremos el termino procesos cuando nos reramos tanto a procesos
como a trabajos , ya que considero mas adecuado la idea de procesar datos
para un programa que de trabajar sobre ellos.

3.1.1. El Proceso
Un proceso es un programa en ejecución. El proceso incluye :

El código del programa ( conocido con text-sección).

La actividad actual , representada por el valor del program counter y el


contenido de los registros del procesador.

La pila del proceso que contiene datos temporales.

La sección de datos que contiene variables globales.

Un cumulo de memoria , que es la memoria que se asigna dinámicamente.

La pila y el cumulo de memoria crecen uno hacia abajo y el otro hacia arriba
, consumiendo la misma sección de espacio adicional del memoria1 .
Un programa es una entidad pasiva , mientras que un proceso es una entidad
activa. Un programa se convierte en proceso cuando se carga en memoria.
Los procesos generados por otros procesos ( child-process) son considerados
secuencias separadas de sus predecesores. Esto sucede habitualmente para la
mayoría de los procesos.

3.1.2. Estado del Proceso


Cada proceso puede estar en uno de lo siguientes estados2 :

Nuevo Esta siendo creado.


En ejecución Se están ejecutando las instrucciones.
1 Ver Fig 3.1
2 Ver Fig 3.2

8
CAPÍTULO 3. PROCESOS 9

En espera Esta esperando que se produzca un suceso.


Preparado Esta a la espera de que se le asigne un procesador.
Terminado Ha terminado su ejecución.
Solo pueden ejecutar a la vez tantos procesos como procesadores se tengan. En
cola de espera y preparados esta restricción no existe.

3.1.3. Bloque de Control de Proceso (PCB)


Un PCB contiene3 :
Estado del proceso : Los estados vistos antes.
Program-Counter: Indica la dirección de la siguiente instrucción a eje-
cutar.
Registros de la CPU : Los registros varían en cuanto a numero y tipo
, dependiendo de la arquitectura. Esta información debe guardarse junto
con el program-counter cuando se produce una interrupción , para luego
poder continuarlo correctamente.
Información de planicación de la CPU : La prioridad del proceso,
los punteros a las colas de planicación y cualesquiera otros parámetros
de planicación.
Información de gestión de memoria : Los registros base y lim-
ite , tablas de paginas o de segmentos ( dependiendo del mecanismo de
gestión).
Información contable : Quantum de CPU y tiempo real empleado ,
limites de tiempo asignados , números de cuenta , numero de proceso,
etc.
Información de recursos : La lista de los recursos , físicos y abstractos
, solcitados y en posesión.

3.1.4. Hebras (Threads)


Ciertos procesos generan subprocesos llamados hebras con los cuales com-
parten ciertos elementos del PCB en común , mediante el cual se comunican
internamente. Ampliaremos esto en el siguiente capitulo.

3.2. Planicación de Procesos


El objetivo de los sistemas de tiempo compartido es conmutar la CPU entre
los distintos procesos con tanta frecuencia que los usuarios tengan la sensación
de tener una maquina dedicada4 . El planicador de procesos selecciona un
proceso disponible para ejecutarlo.
3 Ver Fig 3.3
4 Ver Fig 3.4
CAPÍTULO 3. PROCESOS 10

3.2.1. Colas de Planicación


Una cola de procesos contiene todos los procesos del sistema, los cuales es-
tán preparados para ser ejecutados están en la sub cola de procesos prepara-
dos la cual se almacena en forma de lista enlazada con punteros entre los PCB.
El sistema también incluye otras colas , tales como la cola de dispositivo en los
cuales cada uno tiene su propia cola5 .El diagrama de colas 6 cada rectángulo
representa una cola y cada circulo un recurso.
Luego de que un proceso es despachado de la cpu pueden producirse :

Una solicitud de E/S , colocandolo en la cola de E/S.

Crear un child-process y esperar a que este termine.

Desalojo de la CPU por que se termino su quantum.

Desalojo de la CPU por otro tipo de interrupción.

3.2.2. Planicadores
El proceso de selección de procesos se realiza mediante planicadores. El
planicador a largo plazo seleccionan de la cola principal y los cargan en
memoria. El planicador de corto plazo o planicador de CPU selec-
ciona de entre los procesos listos para ejecutarse y lo asigna a un procesador.
La principal diferencia entre ellos es la frecuencia con la que se ejecutan.
Por lo tanto el de corto plazo debe ser muy rápido mientras el de largo plazo
puede durar mas tiempo.
El de largo plazo controla la cantidad de procesos en memoria ( multipro-
gramación) mientras el de corto plazo controla el tiempo que cada proceso
utiliza la cpu ( shared-time).
Existen procesos que son limitados por E/S ( I/O Bound) y otros por CPU
(CPU Bound), el de largo plazo es el encargado de seleccionar una adecuada
mezcla de ellos.
En algunos sistemas el de largo plazo no es necesario, sino que todo el
proceso es realizado por el de corto plazo. Mientras que en algunos sistemas
shared-time pueden introducir uno de medio plazo 7 . Este se encargaría de
eliminar ciertos procesos de memoria y reducir el grado de multiprogramación
, considerado ventajoso en ciertos casos. Luego estos se volverían a cargar , a
esto se le llama esquema de intercambio.

3.2.3. Cambio de Contexto


La conmutación de la CPU a otro proceso requiere una salvaguarda del
estado del proceso actual y la restauración del estado de otro proceso diferente.
Esta tarea se conoce como cambio de contexto (context-switch). Cuando
se produce , el kernel guarda el contexto del proceso antiguo en su PCB y carga
el del nuevo proceso a ejecutar. Es importante que esta acción se realice lo mas
rápido posible , ya que en un context-switch la CPU no realiza ningún trabajo
5 Ver Fig 3.6
6 Ver Fig 3.7
7 Ver Fig 3.8
CAPÍTULO 3. PROCESOS 11

útil. Este tiempo depende mucho del soporte de hardware. También a mayor
complejidad tenga el SO , mayor sera el tiempo que tardara el context-switch.

3.3. Operaciones sobre los Procesos


3.3.1. Creación de Procesos
El proceso creador se denomina padre y los nuevos hijos, los cuales son
creados mediante una SYSCall especica generando un árbol de procesos ,
numerados cada uno por un identicador del procesos (pid).
Cuando un proceso crea un child-process , este puede obtener recursos del
SO o compartir los recursos de con su padre. En este caso el padre puede decidir
compartir todos o algunos de ellos únicamente. Este método es útil para que
los programas no abusen de la creación de subprocesos , ya que no obtendrá
mayor cantidad de recursos solo con ejecutar mas subprocesos.
Adicionalmente el proceso padre puede pasar datos de inicialización a sus
hijos. Para ejemplicar un proceso hijo podría encargarse de la carga de un
mp3 , siendo su padre el reproductor de mp3 8 . A este el padre se encargaría
de pasarle la información del mp3 a reproducir obtenida por el administrador
de colecciones y también entregarle el dispositivo de audio reservado para re-
producirle ( junto a codec's , etc).
Existen 2 posibilidades en términos de ejecución cuando un proceso crea
otro nuevo :
1. El padre continua ejecutándose concurrentemente con el hijo.
2. El padre espera hasta que alguno o todos los hijos hayan terminado de
ejecutarse.
También existen 2 posibilidades en función al espacio de direcciones del nuevo
proceso :
1. El proceso hijo es un duplicado del proceso padre ( usa mismo programas
y datos del padre).
2. El proceso hijo carga un nuevo programa.
En UNIX cada proceso se identica mediante su identicador de proceso. Los
nuevos procesos se crean mediante la SYSCall fork , el cual crea un duplicado
del proceso padre. Este mecanismo permite la comunicación entre el proceso
padre y el hijo , continuando cada uno su ejecución en forma concurrente. La
única diferencia es que el código de retorno del fork que recibirá el hijo sera 0
, mientras que el que recibirá el padre sera el pid de su hijo9 .
Luego podría suceder que alguno de los 2 procesos utilice la SYSCall exec
( y derivados ) para sustituirse por un nuevo programa el cual es un binario
pasado como parametro a exec , luego de ser reemplazado se inicia su ejecu-
ción. Para esperar a sus hijos se utiliza la SYSCall wait , la cual auto-excluye
al proceso de la cola de procesos preparados hasta que sus hijos se completen.
8 Nota del Autor :Compuesto por muchos procesos hijos encargados de podcast , busqueda
de biografías en wiki's , i'tunes , revisar las collecciones y todo eso en simultaneo
9 Nota del Autor : Esto tiene mucho sentido si lo vemos del punto de vista de que el hijo
no hizo el fork , y por lo tanto no tiene hijos , así que no que no debería recibir ningún pid.
CAPÍTULO 3. PROCESOS 12

La Listing 3.1 ilustra las SYSCall's descriptas.

Listing 3.1: Programa C que bifurca un proceso distinto.


#include < s y s / t y p e s . h>
#include < s t d i o . h>
#include < u n i s t d . h>

int main ( )
{

pid . t pid ;

/* b i f u r c a un p r o c e s o h i j o */
pid = fork () ;

if ( pid < 0 ) /* e r r o r */
{

fprintf ( strderr , " Fork Failed !") ;


exit ( − 1) ;

}
else i f ( p i d == 0 ) /* p r o c e s o h i j o */
{

execlp ( "/ b i n / l s /" , "ls" , NULL ) ;

}
else /* p r o c e s o p a d r e */
{

/* E s p e r a a que s u h i j o t e r m i n e */
w a i t ( NULL ) ;
p r i n t f (" Hijo Completado " ) ;

3.3.2. Terminación de Procesos


Un proceso termina cuando se ejecuta su ultima instrucción y pide al SO
que lo elimine usando la SYSCall exit . El proceso puede devolver un valor
de estado a su padre a través de wait. El SO libera la asignación de todos los
recursos de dicho proceso. Un únicamente un proceso padre puede utilizar una
SYSCall para matar a sus hijos, esto se puede realizar únicamente si el padre
conoce la identidad de ellos10 .
Algunas de las razones por las que un padre puede querer matar a sus hijos :

El hijo ha excedido el uso de alguno de los recursos que se le han asignado.


Para esto el padre debe disponer de un mecanismo para inspeccionar el
estado de sus hijos.

La tarea asignada al proceso hijo ya no es necesaria.

El padre abandona el SO y este no permite que los hijos estén vivos sin
su padre. Esto es conocido como terminación en cascada.
10 Nota
del autor :Esta ultima como vimos antes puede ser obtenida mediante el valor
devuelto por fork , cada vez que creamos un hijo.
CAPÍTULO 3. PROCESOS 13

Algunos SO'S le asignan como padre el proceso principal , en el caso de UNIX


es init.

3.4. Comunicación interprocesos


Un proceso es independiente si no puede afectar o verse afectado por los
restantes proceso que se ejecutan en el SO, en caso contrario es cooperativo.
Algunas razones para la cooperación entre procesos :
Compartir información : Dado que varios usuarios pueden interesarse
en la misma información debemos proporcionar un entorno que permita
el acceso concurrente a la misma.
Acelerar los cálculos : Para que una tarea se ejecute rápidamente ,
dividiéndola en subtareas y ejecutándose en paralelo cada una de ellas
mediante threads. Esto solo surte verdadero efecto en maquinas con varios
procesadores.
Modularidad : Podemos querer construir el SO de forma modular ,
dividiendo las funciones del SO en diferentes threads.
Conveniencia : Un solo usuario puede querer trabajar en muchas tareas
al mismo tiempo.
La cooperación requiere mecanismos de comunicación interprocesos , para
lo cual existen 2 modelos : Memoria compartida y mensajería 11 .

3.4.1. Memoria compartida


Esta requiere que los procesos establescan una región de memoria compar-
tida. Todos los procesos que deseen comunicarse a través de estos segmentos
de memoria , deben acordar en eleminar la restricción de acceso que el SO
impone, luego quedara a responsabilidad de los procesos que intervengan en la
comunicación.
Consideremos el Productor-Consumidor, el cual también es aplicable co-
mo metáfora para el paradigma cliente-servidor 12 . Debemos tener disponible
un Buer para ser rellenado por el productor y vaciado por el consumidor.
Esta región de memoria sera compartida por ambos procesos. Deben estar sin-
cronizados de modo que el consumidor no intente consumir un elemento que
todavía no haya sido producido.
Pueden emplearse dos tipos de buferes :
Buer no limitado : No establece un tamaño jo del Buer. Este
puede producir mayor tiempo de espera para que el consumidor obtenga
elementos nuevos , pero el productor no tiene limite para generar nuevos
elementos ( lo cual le permite generar sin tener que esperar ).
Buer limitado : Establece un tamaño jo del Buer. El consumidor
espera si el Buer esta vació y el productor espera si el Buer esta lleno.
11 Ver Fig 3.13
12 Nota del Autor : Esto no es realmente así ya que los clientes también producen infor-
mación que debe leer el servidor , aunque esto sucede en menor medida.
CAPÍTULO 3. PROCESOS 14

Veamos como emplear un Buer limitado. Las siguientes variables residen en


una zona de la memoria compartida por ambos procesos.
#define BUFFER_SIZE 10

typedef struct {

. . .

} item ;

item buffer [ BUFFER_SIZE ] ;

int in = 0;

int out = 0;

Es una matriz circular con 2 punteros lógicos : in y out . in apunta a la sigu-


iente posición libre en el Buer mientras out apunta a la primera ocupada. El
Buer esta vació cuando in==out y esta lleno cuando (in+1) %BUFFER_SIZE
==out .

3.4.2. Mensajería
Proporciona un mecanismo que permite a los procesos comunicarse y sin-
cronizar sus acciones , siendo especialmente útil en entornos distribuidos. Los
mensajes enviados por un proceso pueden tener tamaño jo o variable , siendo
la primera es fácil de implementar a nivel de sistema pero complica la tarea de
programación mientras la segunda es la inversa , por lo cual es un elemento a
tener en cuenta en el diseño de SO's.
SI los procesos P y Q desean comunicarse debe existir un enlace de comu-
nicaciones ( conexión) entre ellos , el cual puede implementarse de la siguiente
manera :
Comunicación directa o indirecta.
Comunicación sincrónica o asincrónica.
Almacenamiento en Buer explicito o automatico.

3.4.2.1. Nombrado
Comunicación directa Cada proceso que desea comunicarse debe nombrar
de forma explicita al receptor o transmisor de la comunicación. Esto se
realiza de las siguiente forma :

send( P , mensaje ) : Envía a P.


recieve( P , mensaje ) : Recibe de P.
En este caso un enlace tiene las siguientes propiedades :
Se establecen de forma automática con solo nombrar el proceso.
Cada enlace se asocia con exactamente 2 procesos.
Entre cada par de procesos existe exactamente 1 enlace.
Este primer esquema presenta simetría en cuanto a direccionamiento. Una
variante de este esquema emplea asimetría , en la cual solo el transmisor debe
nombrar al proceso.Esto se realiza de las siguientes formas :
CAPÍTULO 3. PROCESOS 15

send( P , mensaje ) : Envía a P.


recieve( id , mensaje ) : Recibe de cualquier proceso , en id se obtiene
pid del proceso que envió el mensaje.

La desventaja de estos dos esquemas es la limitada modularidad de las deni-


ciones resultantes . Cambiar el identicador de un proceso puede requerir que
se modiquen todas las restantes deniciones de procesos. Deben localizarse to-
das las referencias al identicador antiguo para poder sustituirlas por el nuevo
identicador.
Como solución a este problema se presenta el modelo de comunicación
indirecta , los mensajes se envían y reciben en buzones de correo o puertos.
Visto de forma abstracta , se pueden en el colocar y quitar mensajes. Cada
buzón tiene una identicación univoca. Los procesos solo podrán comunicarse
si tienen un buzón de correo compartido.
Las primitivas send y recieve se denen del siguiente modo :

send( B , mensaje ) : Envía al buzón B.


recieve( B , mensaje ) : Recibe del buzón B.
Este esquema tiene las siguientes propiedades :

Establecerse un enlace entre un par de procesos si ambos tienen un buzón


de correo compartido.

Un enlace puede asociarse con varios procesos.

Entre cada proceso puede haber enlaces diferentes , correspondiendo cada


uno a un buzón de correo distinto.

Para saber que proceso recibirá que mensaje dependerá de cual de los siguientes
metodos se utilice :

Permitir que cada enlace asocie solo 2 procesos.

Permitir que solo un proceso como máximo , ejecute la operación de


recepción en cada momento.

Permitir que el SO seleccione arbitrariamente el proceso que lo recibirá.


El SO podría identicar el receptor ante el transmisor.

Un buzón de correo puede ser propiedad de un proceso o del SO. En el primer


caso podemos diferenciar entre el propietario ( el que solo recibe) y el usuario
( el que solo envía ) del buzón , lo cual es muy útil ya que solo existe un
propietario del buzón. En caso de que desparezca dicho buzón debe noticarle
a los procesos que intenten enviar el mensaje que este ya no existe. Si es el
segundo caso , entonces el buzón es independiente , el SO proporciona las
funcionalidades :

Crear un buzón nuevo.

Enviar y recibir a través de un buzón.

Eliminar un buzón.
CAPÍTULO 3. PROCESOS 16

El proceso que crea el buzón se convierte en el propietario, siendo la propiedad


y el privilegio de recepción transferibles a otros procesos mediante SYSCall's
logrando que haya múltiples receptores para cada buzón.

3.4.2.2. Sincronización
Veamos el envió y recepción desde el punto de vista de la sincronía :
Envió sincrónico : El proceso que envió se bloquea hasta que no se
reciba su mensaje.
Envió asincrónica : El proceso continua enviando.
Recepción sincrónica : Se bloquea hasta que hay un mensaje disponible.
Recepción asíncrona : Extrae continuamente mensajes validos o , en
su defecto , nulos.
Estos metodos son combinables entre si.

3.4.2.3. Almacenamiento en Buer


El almacenamiento de mensaje puede implementarse de 3 maneras :
Capacidad cero : No puede haber ningún mensaje esperando en el
enlace , osea que los 2 son sincrónicos.
Capacidad limitada : Una cantidad predenida , al llenarse el trans-
misor se bloqueara hasta que se libere espacio.
Capacidad ilimitada : No se bloquea el transmisor.

3.5. Ejemplos de sistemas IPC


3.5.1. Memoria compartida en POSIX
Los procesos crean los segmentos de memoria compartida mediante la SYSCall
shmget , la cual se utiliza de la siguiente manera :
segment_id = shmget (IPC_PRIVATE, s i z e , S_IRUSR |S_IWUSR) ;

El primer parametro especica la clave del segmento ; con IPC_PRIVATE


creo un nuevo segmento. El segundo es el tamaño del segmento. El tercero
permite establecer los permisos para lectura y/o escritura. Si es ejecutada con
éxito , devolverá el identicador entero para el segmento.
Para acceder a un segmento ya creado se utiliza la SYSCall shmat, la cual
se utiliza de la siguiente manera :
shared_memory = ( char * ) shmat ( id , NULL, 0 ) ;

El primer parametro especica la clave del segmento al que queremos ac-


ceder. El segundo es la ubicación de un puntero que indica donde se asociara
el segmento; si pasamos NULL el SO seleccionara la ubicación. El tercero per-
mite establecer que acceda para lectura y/o escritura , utilizando 0 permito
ambos. Si es ejecutada con éxito , devolverá un puntero a la posición inicial
del segmento. Luego de esto el proceso puede acceder a la memoria de forma
CAPÍTULO 3. PROCESOS 17

normal. Para esto primero el proceso asocia la región de memoria compartida


a su espacio de direcciones.
Para desconectar la memoria compartida utiliza la SYScall shmdt , a la
cual solo requiere el puntero al segmento que se obtuvo con shmat.
Y nalmente para eliminar un segmento se utiliza la SYScall shmctl , a la
cual se le pasa el id del segmento y el parametro IPC_RMID.
Veamos un ejemplo del uso de esta API en el Listing 3.2 :

Listing 3.2: Programa C que ilustra la API de shared-memory de POSIX


#include < s t i d i o . h>
#include <s y s /shm . h>
#include <s y s / s t a t . h>
int main ( ) {
int segment_id ; /* i d e n t i f i c a d o r d e l segmento */
char * shared_memory = NULL ; /* puntero a l segmento */
const int s i z e = 4 0 9 6 ; /* tamaño d e l segmento */
segment_id = shmget (IPC_PRIVATE, s i z e , S_IRUSR |S_IWUSR) ; /* Asigno e l
segmento a un p r o p i e t a r i o */
shared_memory = ( char * ) shmat ( id , NULL, 0 ) ; /* Lo a s o c i o */

s p r i n t f ( shared_memory , " H e l l o World" ) ; /* Escribo en e l */

p r i n t f ( " %s \n" , shared_memory ) ; /* Imprimo en Stdout l a i n f o que


c o n t i e n e */
shmdt ( shared_memory ) ; /* Lo desconecto */

s h m c t l ( segment_id , IPC_RMID , NULL ) ; /* Lo e l i m i n o */

return 0;

3.5.2. Mach
SO basado en mensajes , El kernel de Mach permite la creación y destrucción
de múltiples tareas , similares a los procesos , pero tienen múltiples threads
de control. Todos los tipos de comunicación se realizan mediante mensajes ,
incluidas las SYSCall's , utilizando buzones denominados puertos. Al crearse
una tarea se crean 2 buzones para ella :

Kernel : Para que el kernel reciba los mensajes de la tarea.


Notify : Para noticar de sucesos a la tarea.
Las SYSCall's básicas son msg_send y msg_recieve, y para llamadas a proced-
imientos remotos (RPC) se ejecutan mediante msg_rpc , que envía mensaje y
espera a recibir un solo mensaje como respuesta.
La SYSCall port_allocate crea un buzón nuevo y asigna espacio para su
cola de mensajes , la cual tiene un máximo de 8 mensajes.
La tarea que crea el buzón es su propietaria , lo cual le permite recibir
mensajes del buzón. Esta propiedad puede ser transferida a otras tareas , pero
solo una tarea puede tener la propiedad.
Todos los mensajes tienen la misma prioridad. Los mensajes de un mismo
emisor se colocan en la cola utilizando un algoritmo FIFO , aunque el orden
CAPÍTULO 3. PROCESOS 18

solo se garantiza solo entre los del mismo usuario, puede haber intercalados
mensajes de otros usuarios.
Los mensajes contan de una cabecera de longitud ja , seguida de unos datos
de longitud variable. La cabecera indica la longitud del mensaje e incluye dos
nombres de buzón, uno de ellos es el del buzón al que se esta enviando. La
hebra emisora espera una respuesta , la hebra receptora se le pasa el nombre
del buzón del emisor ( el segundo nombre ); la hebra emisora utiliza este como
dirección de retorno.
La parte variable de un mensaje es una lista de elementos de datos con tipo.
El tipo de objeto es importan ya que e puede enviarse objetos denidos por el
sistema tales como derechos de propiedad o acceso de recepción , etc.
Si el buzón esta lleno , la hebra emisora tiene cuatro opciones :

Esperar indenidamente hasta que se libere.

Esperar un tiempo.

No esperar y volver inmediatamente.

Almacenar el mensaje en cache. Puede entregárselo al SO para que lo


guarde, pero solo puede guardarle a cada proceso un para un determinado
receptor. Despues de terminar una solicitud , la tarea que recibe puede
necesitar enviar una única respuesta a la tarea que solicito el servicio,
también deben continuar con otras solicitudes de servicio , incluso aunque
el buzón de respuesta de un cliente este lleno.

La operación de recepción debe especicar el buzón o conjunto de buzones


13 de los cuales recibir.
La SYSCall port_status devuelve la cantidad de mensajes en un buzón. En
la recepción puede recibirse de cualquier buzón del conjunto o uno especico.
Uno de los principales problemas del sistema de mensajes es la doble copia14
la cual lo convierte en un sistema lento.
La solución de Mach es asignar el espacio de memoria del mensaje al recep-
tor , sin necesidad de copias. Esto proporciona mayor rendimiento pero solo
para intercambios dentro del sistema no entre procesos15 .

3.6. Comunicación en SO's cliente-servidor


Las técnicas de comunicación descriptas pueden emplearse en sistemas cliente-
servidor. Otras estrategias serán las que veremos a continuación.

3.6.1. Sockets (IPC)


Se dene como un punto terminal de una comunicación. Una pareja de
procesos que se comunican a través de una pareja de sockets. Cada socket se
identica mediante una dirección IP concatenada con un número de puerto. El
13 Nota del Autor : Estos pueden agruparse y tratarse como uno solo
14 Una al enviar y otra al recibir
15 Nota del Autor : Si el SO es propietario de los buzones de todos losprocesos , podría
este realizar la reubicación de las direcciones ya que tendría conciencia de los mensajes que
son enviados , mientras solo el proceso dueño del buzón puede recibir mensajes , no el SO.
CAPÍTULO 3. PROCESOS 19

servidor espera solicitudes poniendose a la escucha de un determinado puerto.


Una vez recibida , el servidor acepta la conexión del socket cliente para que
esta quede establecida. Cuando un cliente solicita conexión , el host le asigna
un puerto arbitrario mayor a 102416 . Los números de puerto asignados para
un cliente no pueden ser reusados para otros , ya que estos se utilizan para
identicar el cliente17 . La dirección 127.0.0.1 se utiliza para referirse a uno
mismo18 , lo cual permite que un cliente y un servidor de un mismo host se
comuniquen mediante protocolos TCP/IP. La comunicación a través de sockets
, aunque habitual y eciente es considerada de bajo nivel ya que los datos
enviando por la conexión carecen de estructura , la cual debe ser impuesta por
el cliente o el servidor.

3.6.2. Llamadas a procedimientos remotos (RPC)


Las RPC se diseñaron como un método para abstraer los mecanismos de
llamada a procedimientos , con el n de utilizarlos entre sistemas conectados
en red. Similares al mecanismo IPC , pero con la diferencia que estos están
bien estructurados. Cada mensaje se dirige a un demonio RPC que escucha en
un puerto del sistema remoto y cada uno contiene un identicador de función
a ejecutar y los parametros a pasarle. La función se ejecuta y se devuelve los
posibles datos de salida.
Un puerto es simplemente un número incluido al principio de un paquete
de mensajes , estos sirven para diferenciar los servicios de red que soporta.
La semántica de las llamadas RPC permite a un cliente invocar un proced-
imiento remoto al igual que uno local. El sistema RPC oculta los detalles que
permiten la comunicación , proporcionando un stub19 en el lado del cliente.
Existe un stub diferente para cada procedimiento remoto. Todos los paquetes
de información enviados , tanto del cliente para el servidor y viceversa , son
envueltos por el stub y enviando por el mediante paso de mensajes.
Un problema importante es la diferencias entre representaciones de datos
en el servidor y el cliente a causa de diferencias en las maquinas. Esto se solu-
ciona mediante una representación de datos independiente de la arquitectura ,
como por ejemplo la representación de datos externos (XDR) la cual se
encarga de convertir los datos a esta representación interna y recrear de esta
representación a la correspondiente para la maquina determinada.
Las RPC son mas caóticas , pueden fallar o duplicarse a causa de errores en
la red. Una forma de solucionarlo es asegurar que el sistema actúe exactamente
una vez en respuesta a los mensajes. Para esto necesitamos eliminar el riesgo
de que el servidor nunca reciba una solicitud. Para ello debemos implementar
también que sea como máximo una vez , pero también conrmar que a recibido
y ejecutado la llamada RPC.
Para el caso como máximo una vez el servidor debe mantener un historial
de todas las marcas temporales ( al menos lo sucientemente grande como para
detectar las repeticiones) de los mensajes que ya ha procesado. Los mensajes
16 Ver Fig 3.18 como ejemplo
17 Hay un ejemplo de programas clientes-servidor en Java en las Fig 3.19 y 3.20
18 Por eso es conocida como dirección de bucle
19 Un stub es un código que se encarga de simular la acción de otro recibiendo la infor-
mación de este y realizando una conversión que entrega un resultado mas apropiado para el
destinatario de este.
CAPÍTULO 3. PROCESOS 20

entrantes que tengan una marca temporal20 que ya este en el historial son
ignorados , permitiendo así que el cliente envíe su mensaje cuantas veces desee
y este seguro de que este solo se ejecuto 1 sola vez.
Estos mensajes de conrmación son comunes en redes. El cliente reenviara
la llamada hasta recibir uno de estos mensajes.
Para poder lograr la comunicación , el esquema RPC requiere conocer los
puertos del cliente y del servidor. Para lograr esto existen 2 metodos :

Que las direcciones de puerto de los servicios sean jas tanto para el
cliente como para el servidor , por lo tanto quedaran denidas desde
tiempo de compilación.

De forma dinámica mediante un mecanismo de negociación. Los SO tiene


un demonio de rendezvous ( o matchmaker ) en un puerto RPC jo.
El cliente envía un mensaje con el nombre de la llamada al matchmaker
, solicitando el puerto de la RPC a ejecutar. Matchmaker devuelve el
puerto de la RPC. Esto agrega la carga de la llamada al matchmaker
pero es mas exible21 .

20 En la Sección 18.1 del libro se estudia la generación de marcas temporales , tema que
no se presentada en esta sintesis.
21 Ver Fig 3.21 , tiene un ejemplo muy ilustrativo del proceso de llamada de la RPC
Capítulo 4

Hebras (Threads)

4.1. Introducción
Un Thread es una unidad básica de utilización de la CPU compuesta por un
thread-ID , program-counter , un conjunto de registros y una pila. Comparte
con otros threads del mismo proceso la sección de código , de datos y otros
recursos que tenga el proceso en posesión1 .

4.1.1. Ventajas
Podemos agruparlas en 4 categorías :

1. Capacidad de respuesta : Permite que un programa continúe eje-


cutándose incluso aunque parte de el este ocupado o bloqueado , lo que
incrementa capacidad de interacción con el usuario.

2. Compartición de recursos : Estas comparten la memoria y los recur-


sos del proceso al que pertenecen, esto permite varios threads distintos
activos dentro del mismo espacio de direcciones.

3. Economía : Dado que comparten recursos , es mas económico que crear


subprocesos con sus propios recursos2 . Es mucho mas liviano crear un
thread que un proceso.

4. Multiprocesadores : Las ventajas pueden verse incrementadas signica-


tivamente una arquitectura multiprocesador, donde los threads pueden
ejecutarse en paralelo.

Una de las desventajas es la falta de control de la asignación de memoria y


que los threads compartan el espacio. Esto peligra a que pueda pisarse toda la
memoria por culpa de un thread.
1 Ver Fig 4.1
2 En Linux los threads también son procesos , pero congurados de manera tal que com-
partan recursos con el proceso padre en vez de ser un proceso completamente nuevo e inde-
pendiente.

21
CAPÍTULO 4. HEBRAS (THREADS) 22

4.2. Modelos multihebras


El soporte puede proporcionarse a nivel usuario ( user-threads) o kernel
( kernel-threads). El primero es por encima del kernel y se gestionan sin
soporte del mismo , mientras que las de kernel si.
Existen tres formas de establecer esta relación entre los user-threads y los
kernel-threads.

4.2.1. Modelo muchos-a-uno ( n-1 )


Asigna múltiples user-threads a un solo kernel-thread. La gestión de los
threads se hace mediante la biblioteca de threads en user-space por lo que
resulta eciente , pero el proceso completo se bloquea si un thread realiza
una llamada bloqueante al SO3 . Esto no permite aprovechar el paralelismo en
sistemas multiprocesador.

4.2.2. Modelo uno-a-uno ( 1-1 )


Proporciona una mayor concurrencia que el modelo n-1 , permitiendo par-
alelismo , pero la creación de tantos kernel-threads afecta a rendimiento de la
aplicación4 .

4.2.3. Modelo muchos-a-muchos ( n-m )


Multiplexa muchas user-threads sobre un numero menor o igual de kernel-
threads. El modelo n-m no sufre los inconvenientes de los anteriores. Los desar-
rolladores pueden crear tantos user-threads como deseen y la correspondientes
kernel-threads pueden ejecutarse en paralelo en sistemas multiprocesador.
Además cuando una user-thread realiza una llamada bloqueante el kernel
puede planicar otra para su ejecución5 . Una variación muy popular de este
es agregarle una kernel-thread aparte a la que se le puede acoplar un solo
user-thread.

4.3. Bibliotecas de Threads


Estas proporcionan una API para su creación y gestión. Existen 2 formas
para implementarlas :

Implementarla toda en user-space , incluido estructuras de datos y código


de la biblioteca .

Implementarla toda en kernel-space, siendo soportada por el SO. Invocar


una función de la API produce una SYSCall.

Las tres librerías principales son :

1. POSIX Pthreads : Puede proporcionarse tanto en user-space como en


kernel-space.
3 Ver Fig 4.2
4 Ver Fig 4.3
5 Lo cual es mas eciente , pero de muy bajo nivel.
CAPÍTULO 4. HEBRAS (THREADS) 23

2. Win32 : Esta en kernel-space disponible para los SO windows.


3. Java : Permite crear y gestionar los threads en los programas Java. La
API de Java se implementa utilizando la ya existen en el sistema host

4.3.1. Pthreads
Se basa en el estándar POSIX (IEEE 1003.1c). Se trata de una especicación
para el comportamiento de los threads , no una implementación, lo que da
libertad a los diseñadores.
Veamos los elementos de pthreads presentes en el Listing 4.1 :

Listing 4.1: Programa C usando la API Pthreads


#include <p t h r e a d . h>
#include <s t d i o . h>

int sum ; / * l o s t h r e a d s cmparten e s t a variable global


*/
void * r u n n e r ( void *param ) ; / * f u n c i ó n para el thread */

int main ( int a r g c , char * argv [ ] )


{

pthread_t t i d ; / * i d e n t i f i c a d o r * /
pthread_attr_t a t t r ; / * a t r i b u t o s * /

i f ( a r g c != 2 )
{
f p r i n t f ( s t d e r r , " uso : a . out <v a l o r e n t e r o >\n" ) ;
return − 1;
}

i f ( a t o i ( argv [ 1 ] ) < 0 )
{
f p r i n t f ( s t d e r r , " %d debe s e r >= 0 " , a t o i ( argv [ 1 ] ) ) ;
return − 1;
}

/* c o n f i g u r a r a t r i b u t o s */
p t h r e a d _ a t t r _ i n i t ( &a t t r ) ;
/* c r e a r e l t h r e a d */
p t h r e a d _ c r e a t e ( &t i d , &a t t r , r u n n e r , argv [ 1 ] ) ;
/* e s p e r a r q u e e l t h r e a d t e r m i n a */
p t h r e a d _ j o i n ( t i d , NULL ) ;

p r i n t f ( "sum = %d\n" , sum ) ;

return 0 ;

}
CAPÍTULO 4. HEBRAS (THREADS) 24

/ * Función q u e ejecuta el thread */

void * r u n n e r ( void *param )


{

int i , upper ;
sum = 0 ;
upper = a t o i ( param ) ;

for ( i = 1 ; i <= upper ; i ++)


sum += i ;

pthread_exit (0) ;

pthread_t : Tipo para la declaración de identicadores de threads.


pthread_attr_t : Tipo para la declaración de los atributos de un
thread.

pthread_attr_init() : Función para la inicialización de los atributos ,


si no se les pasa ningún parametro mas que la variable de atributos , le
coloca atributos predenidos.

pthread_create() : Función crea un thread , a la cual se le debe pasar


el identicador del thread , sus atributos , la función a ejecutar y los
argumentos requeridos por esta función.

pthread_join() : Función que se utiliza para esperar a un determinado


thread que termine , por lo cual se requiere que se pase el identicador
del thread.

pthread_exit() : Le permite a un thread terminarse a si mismo , re-


quiriendo como único parametro el retorno de error correspondiente a
dicha ejecución.

4.4. Consideraciones sobre Threads


4.4.1. SYSCall's fork y exec
En algunos SO Unix, existen 2 versiones de fork ; una en la que se duplican
todos los threads del proceso y otra que solo duplica el thread que invoco el
fork.
La SYSCall exec , al ser llamada por un thread , reemplaza todo el proceso.

4.4.2. Cancelación
La cancelación termina un thread antes de que se complete. Es muy común
en cualquier programa que use threads. Existen 2 formas de cancelación :
CAPÍTULO 4. HEBRAS (THREADS) 25

1. Cancelación asíncrona : Una determinado thread hace que otro ter-


mine. Si son independientes pueden cancelarse asíncronamente.
2. Cancelación diferida : Ella misma comprueba si debe terminar , lo
cual le permite terminar de forma ordenada.
Los problemas se producen cuando un thread es cancelado en la mitad de una
actualización de datos compartidos6 . El SO reclamará los recursos asignados a
un thread cancelado , pero no todos7 . La cancelación asíncrona puede producir
que ciertos recursos no sean liberados.
Con la diferida , el thread se cancelara en alguno de los puntos de can-
celación para mayor seguridad.

4.4.3. Tratamiento de señales


Se presenta la situación de decidir a que thread se le debe suministrar la
señal enviada a dicho proceso. En general se encuentran estas opciones :
1. Al thread al que sea aplicable la señal.
2. A todos los threads.
3. A una serie de threads determinados.
4. Que haya un thread especico que las reciba.
El método depende del tipo de señal generada. En el caso de las señales sin-
crónicas deben suministrarse a proceso que causo la señal8 . Sin embargo la de
las asíncronas no es tan clara y depende mucho de la señal y/o el contexto en
que se produce.
En UNIX se permite que los threads decidan que señales aceptan y cuales
bloquean. Como las señales se tratan una vez , solo se suministran al primer
thread que la acepte.
Pthread de POSIX suministra la función pthread_kill ( pthread_t , int
signal ) para suministrarla a un thread especico.

4.4.4. Conjuntos compartidos de threads ( Thread-Pool )


Es un conjunto de threads creados con anterioridad a la espera de que se
les asigne un trabajo a realizar. Un vez terminada la tarea , no se destruye ,
vuelve a la thread-pool. De estar todos los thread ocupados , se esperara hasta
que alguno se libere.
Algunas de las ventajas de este método son :
1. Utilizar un thread existente es mas rápido que crear uno nuevo para
atender la solicitud.
2. Optimiza el uso de recursos en sistemas que están limitados para el mul-
tithread.
6 Nota del Autor : Lo cual es mas probable que suceda con cancelación asíncrona
7 Nota del Autor : Depende el intercambio entre threads. La liberación de un thread no
devuelve la memoria asignada
8 Recordemos que la señales síncronas son producto directo de fallos y se producen de
forma automática apenas sucede este , por lo tanto el causante debe recibirlas.
CAPÍTULO 4. HEBRAS (THREADS) 26

El número de threads disponible puede denirse teniendo en cuenta los recur-


sos del sistema 9 y el número de solicitudes esperadas. Los thread-pools mas
complejos son capaces de ajustar en número de acuerdo al uso.

4.4.5. Activaciones del planicador


Una ultima cuestión es el mecanismo de comunicación entre el kernel y la
biblioteca de threads , la cual es importante en los modelos n-m y 1-1. Para esto
existen los procesos ligeros ( LWP , lightweight process ) los que hacen
de estructuras intermedias. Para la biblioteca de threads , el LWP se comporta
como procesador virtual en el que puede ejecutarse un user-thread. Cada LWP
esta asociado a una kernel-thread , la cual se ejecutan en los procesadores
físicos10 .
En el caso de los procesos CPU-BOUND estos requieren un solo LWP ,
ya que todo el tiempo va a estar haciendo trabajo útil. Pero en el caso de los
I/O-BOUND se requieren tantos LWP como threads que ejecuten llamadas
bloqueantes se tengan , ya que los LWP se quedaran esperando hasta que se
termine la solicitud de E/S.
Un esquema posible de comunicación entre kernel y biblioteca de threads
es el de activación del planicador. Funciona de la siguiente manera : El
kernel proporciona a la aplicación los LWP y la aplicación puede planicar la
ejecución de los threads en los LWP disponibles. Las suprallamadas ( upcall
) son mensajes que envía el kernel a la biblioteca de threads cuando se produce
un suceso ( por ejemplo un thread esta por ser bloqueado). Estas son tratadas
por una rutina de tratamiento de upcalls , la cual se ejecuta también en un
nuevo LWP entregado para ese propósito. De esta manera la rutina se encarga
de guardar el estado del thread que iba a ser bloqueado para luego liberar ese
LWP ( y luego el de la rutina cuando esta termine ) para que sea usado por
otro thread que este en condiciones. Cuando se produce el suceso que el thread
bloqueado esperaba , se envía otra upcall . Se vuelve a pedir un LWP para
la rutina y otro para el thread bloqueado , el cual se desbloquea y continua
ejecutándose, liberando liego el LWP de la rutina.

4.5. Ejemplo de SO : Linux


Este proporciona la capacidad de crear threads usando la SYSCall clone.
Linux no diferencia procesos de threads , por lo que clone inicializa el nivel de
compartición entre procesos. Los indicadores que recibe como parametros son
estos :

indicador signicado
CLONE_FS Se comparte información de FileSystem.
CLONE_VM Se comparte información de memoria.
CLONE_SIGHAND Se comparte los descriptores de señales.
CLONE_FILES Se comparte el conjunto de archivos.

9 Principalmente la cantidad de procesadores y memoria principal


10 Ver Fig 4.9
CAPÍTULO 4. HEBRAS (THREADS) 27

En caso de no utilizar ninguno de estos indicadores , el thread sera inde-


pendiente , considerándose un proceso lo que seria lo mismo que haber hecho
fork.
La variabilidad de la compartición es posible gracias a que Linux imple-
menta la estructura de datos task_struct la cual contiene punteros a otras
estructuras que almacenan los datos del proceso. Cuando se realiza un fork se
copia toda la task_struct entera. Pero cuando se realiza un clone , dependiendo
de los indicadores , se apuntan a ciertos elementos de la task_struct del padre
con la task_struct del hijo y otros se copian11 .

11 Nota del Autor : De esta manera de querer agregarse nuevos elementos a la task_struct
, luego solo habría que agregar un nuevo indicador para ese elemento para así decidir si se
comparte o no .
Capítulo 5

Planicación de CPU

5.1. Conceptos básicos


Un proceso se ejecuta hasta que tenga que esperar , normalmente a causa
de una solicitud de E/S. Cada vez que un proceso tiene que esperar, otro
proceso puede hacer uso de la CPU. Este tipo de planicación es una función
indispensable del SO , casi todos los recursos se planican antes de usarlos.

5.1.1. Ciclos de ráfagas de CPU y de E/S


La ejecución de un proceso de un ciclo de ejecución en la CPU , seguido de
una espera de E/S y así sucesivamente , hasta que por ultimo viene una ráfaga
nal de CPU para terminar con el proceso1 .

5.1.2. Planicador de CPU


El planicador a corto plazo lleva a cabo la selección del proceso que va ser
tomado de la cola para ejecutarse en un procesador determinado. La cola de
procesos preparados no necesariamente es una FIFO , pero conceptualmente la
consideramos de esa manera. Luego veremos que tipos de criterios de plani-
cación hay.

5.1.3. Planicación Apropiativa


La necesidad de un planicador se presenta cuando :

1. Cuando un proceso cambia del estado de ejecución al estado de espera (


Cooperativo ).
2. Cuando un proceso cambia del estado de ejecución al estado de preparado
( Apropiativo ).

3. Cuando un proceso cambia del estado de espera al estado de preparado


( Apropiativo ).

4. Cuando un proceso termina ( Cooperativo ).


1 Ver Fig 5.1 y 5.2

28
CAPÍTULO 5. PLANIFICACIÓN DE CPU 29

En 1 y 4 se requiere seleccionar un nuevo proceso , mientras que en 2 y 3 se


puede decidir si hacerlo o no. Además la cooperativa no requiere el hardware
especial que la apropiativa si.
La apropiativa tiene un coste asociado con el acceso a los datos compar-
tidos. Pensemos en 2 procesos compartiendo datos y mientras uno los esta
actualizando este resulta desalojado. El segundo intentara leer los datos que se
encuentran incoherentes. Esto nos obliga a pensar nuevos metodos de acceso
compartido , los cuales veremos en el Capitulo 6 en la página 38.
El desalojo también puede afectar al kernel mientras atiende una llama-
da. En estos casos se considera que se debe esperar hasta que se complete
la SYSCall o se transera un bloque de E/S antes de realizar el cambio de
contexto. Esta solución permite una estructura de kernel simple , pero no re-
sulta adecuado para tiempo real y multiprogramación. Veremos este problema
desarrollado en la Sección 5.4 en la página 33.
Las interrupciones puede producirse en cualquier momento y el kernel no
siempre puede ignorarlas. La solución es desactivar todas las interrupciones al
principio de cada sección y volver a activarlas al nal. No son muy numerosas
las secciones de código que requieren este trato especial y además estas contiene
pocas instrucciones.

5.1.4. Despachador
Es un modulo que proporciona el control de la CPU a los procesos selec-
cionados. Esta función implica :
Cambio de contexto.
Cambio al modo usuario
Salto a la posición correcta dentro del programa de usuario para reiniciar
dicho programa
Este debe ser muy rápido , ya que se invoca en cada conmutación de proceso.
Este tiempo se conoce como latencia de despacho.

5.2. Criterios de Planicación


Los criterios para seleccionar un algoritmo de planicación son :
Utilización de la CPU : Deseamos mantener la CPU siempre ocupada
haciendo trabajo útil.
Tasa de procesamiento ( Throgh put ) : Es una medida que deter-
mina el número de procesos que se completan por unidad de tiempo.
Tiempo de ejecución ( Turn around ) : Es el intervalo que va desde
el instante en que se ordena la ejecución de un proceso hasta el instante
en que se completa. Es la suma de los periodos que el proceso invierte en
esperar para cargarse en memoria , en la cola de procesos preparados ,
ejecutarse en CPU y realizar E/S.
Tiempo de ejecución : El tiempo que invierte un proceso en la cola
de espera , el cual es la suma de todos estos periodos de espera.
CAPÍTULO 5. PLANIFICACIÓN DE CPU 30

Tiempo de respuesta : Un proceso puede generar parte de la salida


con relativa rapidez y luego seguir calculando lo que falta mientras envían
los previos. Entonces este es el tiempo que tarda en empezar a responder
, el cual esta limitado por la velocidad del dispositivo de salida.

El objetivo de los planicadores es maximizar la utilización de CPU , tasa de


procesamiento , y minimizar el tiempo de ejecución , de espera y de respuesta.
Para los sistemas interactivos es mas importante minimizar la varianza del
tiempo de respuesta.

5.3. Algoritmos de Planicación


5.3.1. FCFS ( First Come - First Serve )
En este se asigna primero la CPU al proceso que primero la solicito. Esta se
gestiona mediante una cola FIFO. El tiempo medio de espera de este algoritmo
es bastante largo. El problema es que si tenemos adelante un proceso I/O Bound
, este se apropiara la CPU , aun cuando solo este esperando la respuesta de la
E/S. Esta situación se denomina efecto convoy , ya que todos los procesos
de atrás tienen que esperar hasta que el primero termine , este o no usando la
CPU. Este algoritmo es Cooperativo.

5.3.2. SJF ( Shortest job rst )


Selecciona el trabajo mas corto para ser ejecutado. Este algoritmo asocia
cada proceso la duración de la siguiente ráfaga de CPU del proceso. Cuando
la CPU esta disponible , se le asigna el de la menor ráfaga. Si 2 rafagas son
iguales se usa el FCFS para desempatar. Este algoritmo se considera óptimo
ya que el tiempo promedio de espera se disminuye a causa que los procesos de
mayor ráfaga de CPU tienen que esperar poco en comparación al tiempo que
ellos mismos consumen.
La dicultad se encuentra en determinar la ráfaga de CPU del proceso.
No existe forma de conocer esta duración en forma exacta y por lo tanto este
algoritmo no puede ser utilizado para planicadores de corto plazo , pero si es
recomendable para los de largo plazo.
Una manera de predecir el tiempo de duración de la siguiente ráfaga de
CPU de un proceso es considerar que sera de similar duración a las anteriores.
Esta se calcula obteniendo la media exponencial de la duración de las rafagas
anteriores de la siguiente manera :

τn+1 = αtn + (1 − α)τn

Esta formula dene el promedio exponencial donde 0 ≤ α ≤ 1 y tn+1 es


el valor predicho para la ráfaga siguiente , y mientras tn contiene la información
mas reciente , τn contiene el historial previo. Vemos que entonces α dene que
tanto vamos a tener en cuenta el historial anterior y que tanto vamos a valorar
la nueva muestra para predecir el valor de la próxima ráfaga2 .
El algoritmo SJF puede ser cooperativo o apropiativo. En el caso de ser
apropiativo podrá detener un proceso al que le queda mas ráfaga de CPU que
2 Ver Fig 5.3 como ejemplo
CAPÍTULO 5. PLANIFICACIÓN DE CPU 31

el que lo va a reemplazar. El SJF detendrá su ejecución , pero un algoritmo


sin desalojo esperara que dicho proceso termine su ráfaga de CPU3 . También
debemos tener en cuenta los context-switch , ya que si el tiempo agregado que
lleva realizar el context switch de dicho proceso ( el de sacarlo y luego ponerlo
) es similar en comparación a lo que le resta para terminar su ejecución ,
deberiamos dejarlo proseguir.

5.3.3. Por Prioridades


A cada proceso se le asocia una prioridad y se le asigna la CPU al de mayor
prioridad4 . De esta manera podemos ver SJF como un algoritmo de prioridad
en el que la menor ráfaga determina la mayor prioridad.
Las prioridades pueden denirse de 2 formas :
Internamente : Se utilizan valores que calculables de forma interna
teniendo en cuenta el historial y la información propia del proceso5 .
Externamente : Se utilizan factores externos al SO en si , los cuales
son mayoritariamente de carácter político6 .
Esta puede ser apropiativa o cooperativa. Cuando llega un nuevo proceso , este
compara su prioridad con el que esta en ejecución. Aquí de ser apropiativo , si
el nuevo tiene mayor prioridad , quitara el que esta para poner el recién llegado
. Si es cooperativo solo podrá el nuevo proceso al principio de la cola si este
tiene prioridad mayor o igual a de actual ejecución.
Un problema importante de este algoritmo es la llamada muerte por
inanición , la cual se produce cuando un proceso queda esperando indenida-
mente a causa de su baja prioridad7 . Una solución es aplicar un mecanismo de
envejecimiento en el cual consiste en gradualmente aumentar la prioridad de
los procesos para asegurar que estos nalmente se ejecuten.

5.3.4. Por turnos ( RR , Round Robin )


Esta diseñado especialmente para los sistemas de tiempo compartido. Sim-
ilar a FCFS , pero añade la técnica de desalojo para conmutar entre procesos.
En este sistema se dene una pequeña unidad , el cuanto de tiempo ( quan-
tum ) el cual es generalmente no mayor a 100 milisegundos. La cola de procesos
preparados se trata como una cola circular. El planicador le asigna la CPU a
cada proceso durante un tiempo no mayor a un quantum. Los procesos nuevos
se añaden al nal. Si el proceso termina antes se toma el siguiente en la cola.
El tiempo medio de este algoritmo es generalmente largo. Pero como ben-
ecio obtenemos un tiempo máximo garantizado el cual sera (n − 1)q , donde
q es el quantum de tiempo y n es la cantidad de procesos.
Lo importante es el quantum que se elija , si es muy largo sera casi igual
a un FCFS y si es muy corto tendremos el problema de sobrecargarnos de
3 Nota del Autor : O dicho de otra forma , comience una ráfaga de E/S o termine su
quantum en caso de tenerlo
4 Para los empates seguimos usando FCFS
5 La cual puede ser números de archivos abiertos , requisitos de memoria , etc
6 Nota inútil : Si Adobe me nancia , Flash va primero! :P
7 Existe el rumor de que cuando apagaron la IBM 7094 en el MIT en 1973 , había un
proceso que llevaba muerto 5 años.
CAPÍTULO 5. PLANIFICACIÓN DE CPU 32

context-switch. Se considera que el tiempo de quantum sea tal que el 80 %de


los procesos no haga context-switch. Este algoritmo es apropiativo.

5.3.5. Por colas multinivel


En ciertos casos podemos clasicar los procesos en conjuntos bien denidos
, los cuales son :

Primer plano o interactivos.

Segundo plano o por lotes.

Estos tienen requisitos diferentes de tiempo de respuesta y pueden tener difer-


entes necesidades de planicación. Los de primer plano pueden tener prioridad
sobre los de segundo.
El algoritmo de planicación divide la cola de procesos preparados en en
varias colas distintas8 . Los procesos se asignan a una determinada cola dependi-
endo de sus propiedades. Cada cola tiene su propio algoritmo de planicación.
Además deben denirse una planicación entre las colas , la cual suele ser
apropiativa y de prioridades jas. Una posibilidad podría ser entregarle priori-
dad a las colas , entonces nunca puede ejecutarse una cola de menor prioridad
hasta que no haya terminado una de mayor prioridad ( este es cooperativo ).
Otra posibilidad es entregarle una fracción del tiempo de CPU a cada cola
dependiendo de su prioridad para que pueda planicar sus procesos dentro de
este ( este es apropiativo).
En el de multinivel los procesos se asignan a una determinada cola de forma
permanente. Esto reduce la carga de planicación , pero entrega poca exibil-
idad.

5.3.6. Colas multiniveles realimentadas


Este algoritmo permite mover un proceso de una cola a otra. La idea es
separar los procesos en función de las características de sus rafagas de CPU. Si
son CPU-BOUND , se pasa a una cola de prioridad mas baja. Este esquema deja
los procesos I/O-BOUND y los interactivos en las colas mas altas. Además un
proceso que este demasiado tiempo en una cola de baja prioridad puede pasarse
a una cola de prioridad mas alta. Este mecanismo de envejecimiento evita el
bloqueo indenido9 . En esta sección , en el libro ,existe un buen ejemplo de
este tipo de planicadores.
Un planicador de este tipo se dene mediante los siguientes parametros :

El numero de colas.

Los algoritmos de planicación de cada cola.

El método de desalojo de los procesos hacia colas de mayor prioridad.

El método de desalojo de los procesos hacia colas de menor prioridad.

El método para decidir donde introducir un proceso para darle servicio.


8 Ver Fig 5.6
9 Ver Fig 5.7 , muy útil
CAPÍTULO 5. PLANIFICACIÓN DE CPU 33

Este es el algoritmo de planicación mas general de todos. Puede congurarse


para adaptarlo a cualquier sistema que se desee diseñar. Por desgracia es el
algoritmo mas complejo , puesto que requiere disponer de algún mecanismo
para seleccionar los valores de todos los parametros.

5.4. Planicación de Sistemas Multiprocesador


Vamos a concentrarnos en los sistemas en los que los procesadores son idén-
ticos y homogéneos en cuanto a sus funcionalidades. Esta homogeneidad debe
ser no solo en el procesador en si , sino también en sus funcionalidades.

5.4.1. Métodos de planicación


Existen 2 metodos de planicación de multiprocesamiento :
Asimétrico : Resulta simple , ya que en este todas las decisiones so-
bre la planicación de recursos ( y otras actividades ) la realiza un solo
procesador , el procesador maestro , mientras los demás procesadores so-
lo ejecutan código de usuario. Esto nos permite reducir la necesidad de
compartir datos , ya que un solo procesador accede a las estructuras de
datos del sistema.
Simétrico ( SMP ) : En el que cada uno de los procesadores se auto-
planica. Todos los procesos pueden estar en una misma cola o cada
procesador puede tener su cola privada de procesos preparados. Indepen-
dientemente de esto , el planicador de cada procesador tomara procesos
de la cola de preparados y los ejecutara.

5.4.2. Anidad al procesador


Vemos que cuando un proceso se estuvo en un procesador especico los
datos mas recientes se almacenan en la cache del procesador , luego los accesos
a estos datos se harán consultadando a esta memoria . El problema es que si
migramos el proceso a otro procesador deberiamos rellenar la cache del nuevo
procesador con los datos residentes en el anterior y luego limpiar este ultimo.
Este proceso tiene un alto coste , por lo tanto la mayoría los SMP tratan de
evitarlo.
Existen 2 tipos de anidad a los procesadores :
Anidad na : En este caso se promueve la anidad pero a no estar
garantizada se permite la migración.
Anidad dura : Esta permite a un proceso especicar que no debe
migrar a otros procesadores.

5.4.3. Equilibrado de carga


Estos mecanismos intentan distribuir equitativamente la carga de trabajo
entre todos los procesadores del sistema SMP. El equilibrado de carga solo es
necesario cuando cada procesador tiene su propia cola de procesos preparados.
En los sistemas asimétricos no es necesario el equilibrado de carga.
Existen 2 metodos de migración para el equilibrado :
CAPÍTULO 5. PLANIFICACIÓN DE CPU 34

Migración comandada ( Push migration ) : Una tarea especica


comprueba periódicamente la carga en cada procesador y , en caso de
desequilibrio , la distribuye equitativamente.

Migración solicitada ( Pull migration ) : Sucede cuando un proce-


sador vacío le extrae un proceso en espera a otro procesador.

Estos metodos no son mutuamente excluyentes , por ejemplo el planicador de


Linux equilibra la carga cada 200 milisegundos ( comandada ) o cuando la cola
de ejecución de un procesador esta vacía ( solicitada ).
Debemos considerar que el uso del equilibrado de carga contrarresta los
benecios de la anidad al procesador. No hay una regla clara para elegir una
u otra.

5.4.4. Mecanismos multithread simétricos


El SMP permite que varios threads se ejecuten en forma concurrente , ya que
tenemos varios CPU físicos. Otra alternativa es entregar varios CPU lógicos ,
esta estrategia se conoce como multithread simétrico ( SMT ) o hyperthreading.
Cada CPU lógico tiene su propio estado de la arquitectura , el cual
incluye todos los registros y los propios tratamientos de interrupciones. Estos
recursos están compartidos con su CPU físico10 .
SMT es una funcionalidad proporcionada por hardware , este debe propor-
cionar la representación del estado de la arquitectura y el tratamiento de las
interrupciones para los CPU lógicos. Algo interesante a considerar en el diseño
del SO es que , de tener una SMT , debemos tener cuidado de no planicar los
threads en 2 procesadores lógicos pertenecientes al mismo físico , ya que podría
quedarnos uno físico vacío.

5.5. Planicación de threads


En los SO que permiten su uso , estos planican kernel-threads , no procesos.
El kernel no es consciente de las user-thread.

5.5.1. Ámbito de contienda


En los modelos n-1 y n-m , la biblioteca planica las user-threads para que
se ejecuten sobre un LWP disponible , lo cuales conocido como ámbito de
contienda del proceso ( PCS ; process-contention scope ) dado que la
competencia se produce entre los threads de un mismo proceso.
Para saber cual kernel-thread planicar en la CPU se usa el ámbito de
contienda del sistema ( SCS ). En el modelo 1-1 solo existe el SCS.
La planicación PCS se lleva de acuerdo con la prioridad, la cual es estable-
cida para cada thread por el programador. La biblioteca no reajustara estas
prioridades , pero el programador puede reajustarlas en tiempo de ejecución si
lo desea. El PCS desaloja el thread en ejecución , si el que a ingresado tiene
mayor prioridad , así que no se asegura ningún reparto de tiempo equitativo.
10 Ver Fig 5.8
CAPÍTULO 5. PLANIFICACIÓN DE CPU 35

5.5.2. Planicación de Pthreads


Podemos especicar como funcionaran los ámbitos de contienda , para elegir
cual se usan los siguientes parametros en la variable scope :

PTHREAD_SCOPE_PROCESS para el PCS.

PTHREAD_SCOPE_SYSTEM para el SCS.

La API proporciona 2 funciones para consultar y denir el ámbito de contien-


da11 :

pthread_attr_setscope(pthread_attr_t *attr, int scope)


pthread_attr_getscope(pthread_attr_t *attr, int *scope)

5.6. Ejemplo de SO : Planicación en Linux


Esta basado en prioridades y es apropiativo , con dos rangos distintos de
prioridades : tiempo real de 0 a 99 y el normal ( nice ) entre 100 y 140. Los
valores mas bajos indican prioridades mas altas. Los procesos mas largos tiene
prioridad mas alta que los de tiempo mas corto12 . Un proceso a ejecutar se
considera elegible cuando todavía le quede tiempo en su quantum de tiempo.
EL kernel mantiene una lista de todas las tareas ejecutables en una estructura
de datos denominada cola de ejecución ( runqueue ). Debido al soporte para
sistemas SMP , cada procesador mantiene su propia runqueue y la planica de
forma independiente. Estas contienen 2 matrices de prioridades13 :

Activa : Contiene los procesos que todavía disponen de quantum de


tiempo.

Caducada : Contiene los procesos que ya caducaron.

Cuando la activa esta vacía , vuelve a ser llenada con todos los procesos de la
caducada , la cual es vaciada.
Linux implementa la planicación en tiempo real como se dene en POSIX.1b.
A las de tiempo real se les asigna prioridades estáticas , las restantes son dinámi-
cas las cuales pueden variar ±5 . La interactividad se determina si estos 5 se
suman o restan. El recalculo de la prioridades se produce cuando se intercam-
bian las matrices.

5.7. Evaluación de Algoritmos


El primer problema consiste en denir los criterios que se van a emplear
para seleccionar un algoritmo , los cuales fueron expresados en la Sección 5.2.
Veamos los siguientes metodos de evaluación.
11 Ver Fig 5.9 donde hay un ejemplo de su funcionamiento.
12 Ver Fig 5.13
13 Ver Fig 5.14
CAPÍTULO 5. PLANIFICACIÓN DE CPU 36

5.7.1. Modelado determinista


En la evaluación analítica utiliza el algoritmo especicado y la carga de
trabajo de sistema para generar una formula o numero que evalúe el rendimien-
to de algoritmo para dicha carga.
Una de estas formas de evaluación es el modelado determinista la cual
toma una carga de trabajo predeterminada concreta y dene el rendimiento de
cada algoritmo para dicha carga de trabajo.
Este método es simple y rápido , proporciona números exactos , permitien-
do así comparar los algoritmos. Sin embargo requiere datos exactos y solo se
aplica al caso suministrado. Este metodos es útil para cuando repetidas veces
utilizamos el mismo programa o cuando queremos indicar ciertas tendencias en
los planicadores.

5.7.2. Modelos de colas


Podemos determinar en el tiempo la distribución de rafagas de CPU y
de E/S. Estas pueden medirse , aproximarse o estimarse. El resultado es una
formula matemática que describe la probabilidad de aparición de una ráfaga
de CPU concreta. Esta distribuciones suelen ser exponenciales y se describe
mediante su media. Similarmente podemos describir la distribución de los
tiempos de llega de los procesos. Con estas 2 distribuciones podemos calcular
, para la mayoría de los algoritmos , la tasa media de procesamiento , la
utilización media , etc. Esta área de investigación es denominada análisis
de redes de colas.
Sea n la longitud media de la cola , W el tiempo medio de espera en la cola
y λ la tasa media de llegada de un nuevo proceso. Si el sistema esta operando
en régimen permanente , el numero de procesos que abandonan la cola debe
ser igual al que llegan. Por lo tanto :

n = λW

Esta ecuación es la formula de Little , muy útil ya que es valida para


cualquier algoritmo de planicación y para cualquier distribución de las lle-
gadas.
El análisis puede resultar útil para comparar limitaciones. Los análisis
matematicos necesarios para las distribuciones y algoritmos complejos pueden
ser enormemente difíciles. Debido a esto , aveces solo se representa una aprox-
imación.

5.7.3. Simulaciones
Para una evaluación precisa podemos usar simulaciones. Estas se puede
realizar en maquinas virtuales preparadas para modicar su comportamiento en
cuanto a recursos y otros aspectos , los cuales pueden generarse aleatoriamente
, determinan el rango posible. Este rango son distribuciones matemáticas o
empíricas. Los resultados de las medidas denen la distribución y luego son
usados para controlar la simulación.
Sin embargo el uso de distribuciones puede ser impreciso ya que estas no
tienen en cuenta el orden de los elementos . Para esto podemos usar cintas de
traza. Para crear una cinta de traza se monitorea el sistema real y se registra una
CAPÍTULO 5. PLANIFICACIÓN DE CPU 37

secuencia de sucesos reales. Luego esta secuencia se emplea para controlar la


simulación. Las cintas de traza proporcionan una forma excelente de comparar
dos algoritmos cuando se emplea exactamente el mismo conjunto de entradas
reales.

5.7.4. Implementación
La única forma de evaluar un algoritmo es codicandolo , incluyendolo en
un SO y ver como funciona. La principal dicultad es su alto coste , no solo
en codicación y modicaciones , sino también en que los usuarios tengan que
soportar el cambio continuo del sistema.
Otro problema es que el entorno en donde se ejecuta esta sujeto a cam-
bios. Los algoritmos mas exibles son los que pueden ser modicados por los
administradores de sistemas o los usuarios, de modo que puedan ser ajustados
a sus necesidades. Unix permite que el administrador ajuste parametros de
planicación para cada conguración concreta del sistema. Solaris proporciona
el comando dispadmin para que el administrador modique los parametros de
las clases de planicación.
Otro método consiste en utilizar aplicaciones que permitan modicar la
prioridad de los procesos y threads. La desventaja es que no permite mejorar
el rendimiento en otras situaciones mas generales.
Capítulo 6

Sincronización de Procesos

6.1. Fundamentos
El acceso concurrente a datos compartidos puede producir incoherencias de
datos. Veamos el caso de productor-consumidor que presentamos en la Sección
3.4.1 utilizando el mismo Buer limitado , con al diferencia que en vez de tener
máximo BUFFER_SIZE −1 , tendremos una variable entera counter inicializada
con el valor 0. Veamos los códigos del productor y del consumidor :

Listing 6.1: Código de proceso Productor


While ( true )

/* produce un e l e m e n t o en nextProduced */
while ( counter == BUFFER_SIZE )

; /* s k i p */

buffer [ in ] = nextProduced ;

in = ( in + 1) % BUFFER_SIZE ;

c o u n t e r ++;

Listing 6.2: Código del Proceso Consumidor


While ( true )

/* consume un e l e m e n t o que hay en nextConsumed */


while ( counter == 0 )

; /* s k i p */

nextConsu me d = b u f f e r [ out ] ;

out = ( out + 1) % BUFFER_SIZE ;

counter −−;
}

Si bien estas rutinas son correctas por separado , en forma concurrente


no funcionan como se espera. Si las instrucciones counter++ y counter−−
se ejecutaran concurrentemente , estas no se cancelarían manteniendo counter

38
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 39

intacto , sino que podrían tener un valor entero en [counter − 1, counter + 1]1 .
Esto se ve claramente cuando chequeamos el lenguaje maquina2 de counter++
y counter−− :

Listing 6.3: Código maquina de counter++


registro1 = counter

registro1 = registro1 + 1

counter = registro1

Listing 6.4: Código maquina de counter - -


registro2 = counter

registro2 = registro2 − 1

counter = registro2

Vemos claramente que podría producirse una intercalación entre las lineas
de los 2 códigos , por ejemplo ejecutamos las 2 primeras de cada código y luego
ejecutamos la ultima de cada uno , generando que el valor de counter quede
incrementado o decrementado , pero nunca intacto.
Esta situación se denomina condición de carrera ( race condition ) ,
necesitamos asegurar que solo un proceso a la vez pueda acceder a los recursos
compartidos.

6.2. Sección crítica


Cada proceso tiene un segmento de código , llamado sección crítica ,
donde puede modicar variables compartidas. Para acceder a estas debemos
pasar por las siguientes secciones :

do {

/ * Sección de entrada */
. . . . Sección critica

/ * Sección de salida */
. . . . Sección restante

} while ( true ) ;

Cualquier solución deberá satisfacer estos requisitos :

1. Exclusión mutua : Si Pi esta ejecutando su sección critica , los demás


no pueden ejecutar las suyas3 .
1 Corrompiendo el
contador y comprometiendo la estructura de almacenamiento
2 Nota del Autor :
Dependiendo de los compiladores ( y múltiples variables relacionadas
con el contexto de la sentencia ) y los parametros que utilicemos , el código maquina puede
no parecerse a este , incluso puede llegar a ser atomico.
3 Nota del Autor : Esto es así para los casos en que en dichas secciones se acceda (
especialmente modique ) a variables compartidas.
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 40

2. Progreso : Si ninguno esta ejecutando su sección critica y alguno desea


hacerlo , solo los que no estén en sus secciones restantes pueden participar.
Esta elección no puede posponerse indenidamente.

3. Espera limitada : Existe un limite de veces que se permite que otros


procesos entren en sus secciones criticas despues de que un proceso haya
solicitado entrar y antes de que sea concedida.

Existen varias estructuras del kernel que están sujetas a posibles race-conditions.
Para esto existen 2 metodos generales para gestionar las secciones criticas en
los SO's4 :

Kernels apropiativos : Permite desalojo de procesos mientras se esta


en modo kernel. Puede haber varios procesos activos en el kernel, por lo
que sufre de posibles race-conditions. Son muy difíciles de implementar
en arquitecturas SMP , por que en estas puede que dos procesos que
compartan variables se estén ejecutando en procesadores distintos. La
razón de su existencia es que son mas adecuados para la programación
en tiempo real. Además tienen mejor capacidad de respuesta y menos
riesgo de apropiación excesiva del CPU por parte de un proceso. Esto
ultimo puede minimizarse diseñando código de kernel que no presente
este comportamiento.

Kernels no apropiativos : No permite desalojo de procesos mientras


se esta en modo kernel. El proceso se ejecutara hasta que salga de modo
kernel , se bloquee o ceda voluntariamente la CPU. Estos kernels están
libres de race-conditions , ya que solo hay un proceso activo en el kernel.

6.3. Hardware de sincronización


Cualquier solución al problema de la sección critica requiere un cerrojo.
Un proceso debe adquirir dicho cerrojo para poder entrar a una sección critica
y al salir debe liberarlo.
El soporte hardware puede facilitar la tarea de programación y mejorar
la eciencia del sistema. La sección critica es simple de resolver en entorno
monoprocesador , con solo desactivar las interrupciones mientras se modican
variables compartidas. Esto aseguraria el orden de la secuencia , ya que no hay
desalojos. Este método emplean los kernels no apropiativos.
En cambio , en entornos multiprocesador , desactivar interrupciones es muy
costoso, ya que hay que comunicarse con los otros procesadores para realizarlo.
Esto hace la entrada a la sección critica muy lenta. Además es peligroso si el
reloj se actualiza mediante interrupciones.
Para esto ultimo , muchos SO's actuales proporcionan instrucciones hard-
ware especiales que nos permiten consultar , modicar o intercambiar con-
tenidos entre variables de forma atomica , siendo una instrucción atomica un
trabajo irreductible e ininterrumpible.
Veamos algunos ejemplos de estas instrucciones que podrían ser soportadas
por hardware :
4 Nota informativa : Windows XP y 2000 utilizan no apropiativos , al igual que el kernel
tradicional de Unix. Desde la versión 2.6 , Linux usa apropiativo. También Solaris y IRIX.
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 41

Listing 6.5: Denición de la instrucción TestAndSet


boolean TestAndSet ( boolean * target )

boolean rv = * target ;

* target = TRUE ;

return rv ;

La instrucción del Listing 6.5 me permite implementar exclusión mutua


declarando una variable booleana lock la cual sera liberada 5 cuando hayamos
llegado a la sección de salida. Mientras tanto todos los procesos se clavaran en
un bucle en la sección de entrada. Podemos ver como podría implementarse en
el Listing 6.6.

Listing 6.6: Exclusión mutua con TestAndSet()


do
{

/* s e c c i ó n de e n t r a d a */
while ( TestAndSet (& l o c k ) )

; /* s k i p */

/* S e c c i ó n c r í t i c a */
. . . . . . . .

/* S e c c i ó n de s a l i d a */
lock = FALSE ;

// S e c c i ó n r e s t a n t e

} while (TRUE) ;

Otra función seria Swap, en el Listing 6.7 , la cual es para realizar el inter-
cambió entre 2 valores.

Listing 6.7: Denición de Swap()


void Swap ( boolean *a , boolean *b )

boolean temp = *a ;
*a = *b ;
*b = tempo ;

Estos algoritmos no satisfacen el requisito de espera limitada. Veamos el


código del Listing 6.8 , el cual utiliza TestAndSet() , que satisface todos los
requisitos. Las estructuras de datos comunes son :
boolean waiting [n ];

boolean lock ;

Esta estructura se inicializa con FALSE. Para probar que el requisito de


exclusión mutua se cumple , vemos que pi puede entrar en su sección critica
solo si waiting [ i ] == FALSE && key == FALSE . Solo sucede key == FALSE si
5 lock == FALSE
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 42

Listing 6.8: Exclusión mutua con espera limitada , usando SetAndTest()


do
{

waiting [ i ] = TRUE ;

key = TRUE ;

/* S e c c i ó n de Entrada */
while ( waiting [ i ] && key )

key = T e s t A n d S e t (& l o c k ) ;

waiting [ i ] = FALSE ;

/* S e c c i ó n c r í t i c a */
j = ( i +1) %n;

while ( ( j != i ) && ! waiting [ j ] )

j = ( j +1) %n;

/* S e c c i ó n de s a l i d a */
if ( j == i )

lock = FALSE ;

else
wainting [ j ] = FALSE ;

// S e c c i ó n r e s t a n t e

} while (TRUE) ;

se ejecuta TestAndSet() , siendo el primero que lo ejecute el que obtenga key ==


FALSE , mientras que todos los demás deberán esperar a que lock == FALSE .
6
También waiting [ i ] == FALSE solo sucederá cuando otro proceso salga de su
sección critica , asignándole el valor a una única i . Esto mantiene la exclusión
mutua.
Vemos que el requisito de progreso también se cumple ya que cuando uno
esta en la sección de salida lock == FALSE o waiting [ j ] == FALSE En ambos
casos se permite que un proceso en espera entre a su sección critica.
Por ultimo la espera limitada esta asegurada en la sección critica , la cual
se encarga de seleccionar el primer proceso que se encuentra en la sección
de entrada ( waiting [ j ] == TRUE ) como siguiente proceso que entrara en la
sección critica7 . Los proceso que quiera entrar a su sección critica tendrán que
esperar n − 1 turnos como máximo.

6.4. Semáforos
Un semaforo es una variable entera a la que , salvo la inicialización , solo
se acceder mediante las siguientes operaciones :
6 Esto se produce únicamente en la sección de salida , donde se libera el cerrojo.
7 Esto ultimo se concede cuando el proceso actual esta en la sección de salida.
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 43

wait() signal ()

wait (S)

{ signal (S)

while ( S <= 0) {

; S++;

S −−; }

6.4.1. Utilización
Los So's diferencian entre 2 tipos de semaforos :

Semaforos contadores : Se usan para controlar el acceso a un de-


terminado recurso compuesto por cantidades nitas. Se inicializa con el
número de recursos disponibles. Cuando llega a 0 todos los recursos están
en uso.

Semaforos binarios ( mutex ) : Se usan para abordar el problema


de la sección critica. Los procesos comparten el mutex , inicializado en 1.
Estos proveen exclusión mutua8 ya que un solo proceso puede adquirir el
mutex , y luego en su sección de salida debe liberarlo9 .

También estos pueden ser utilizados para resolver diversos problemas de sin-
cronización.

6.4.2. Implementación
La desventaja de la denición dada es que requiere espera activa la cual
es provocada por el ciclo while de wait() , desperdiciando ciclos de CPU. Es-
tos semaforos se denominan cerrojo mediante bucle sin n ( spinlock ).
La ventaja es que no requiere ningún context-switch cuando los procesos es-
peran para recibir el cerrojo , lo cual hace a este método muy recomendable
si las esperas por el cerrojo son cortas. Se emplean a menudo para entornos
multiprocesador. Para salvar la espera activa , redeniremos las operaciones :

wait() signal ()

wait ( semaphore * S ) s i g n a l ( semaphore * S )


{ {
S−>value −−; S−>v a l u e++;
i f ( s−>v a l u e < 0) i f ( s−>v a l u e < 0)
{ {
a ñ a d i r e l p r o c e s o a S−> l i s t ; e l i m i n a r p r o c e s o P de S−> l i s t ;
block ( ) ; wakeup (P ) ;
} }
} }

Esto permite que los procesos se bloqueen a si mismos usando wait() , para
pasar así a la cola de espera y dejando libre la CPU. Estos son luego reiniciados
8 Ver Fig6.9
9 Esto esalgo que el programador esta obligado a cumplir para asegurar el correcto
funcionamiento del mutex.
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 44

por un proceso que sale de su sección critica mediante signal () , poniendo así
en la cola de preparados10 .
La lista de procesos en espera puede implementarse mediante un campo
de enlace en cada PCB. Cada semaforo contiene un entero y un puntero a la
lista de PCB correspondiente. Para garantizar la espera limitada podríamos
utilizar una cola FIFO, pero podría usarse cualquier otro ya que el semaforo
no depende de que estrategia de gestión se elija.
Debemos desactivar las interrupciones de los procesadores para poder eje-
cutar wait() y signal () . Este método es perfectamente aplicable en entornos
monoprocesador , pero en los multiprocesador hemos visto que no. Para este
ultimo caso conviene usar los semaforos spinlock para asegurar la ejecución
atomica.
No hemos eliminado por completo la espera activa , sino que la hemos
trasladado a las secciones criticas de las operaciones wait() y signal () , las
cuales son cortas y rara vez se tiene que esperar. En los programas de aplicación
esto no es así ya que las esperas pueden ser largas , la espera activa resulta
extremadamente ineciente.

6.4.3. Interbloqueos e inanición


Un semaforo da lugar a que 2 o mas procesos estén esperando indenida-
mente a que se produzca un suceso que solo puede producirse por otro proceso
en espera, siendo signal () el suceso en cuestión. Esta situación se denomina
interbloqueo.
Un claro ejemplo de esta situación es el siguiente , en el cual vemos como
tanto P0 como P1 requieren que alguno de los 2 llegue a realizar su primera
signal , pero esto no sucede a causa de la interdependencia que hay :

P0 P1
wait (S) ; w a i t (Q) ;

w a i t (Q) ; wait (S) ;

. .

. .

. .

signal (S) ; s i g n a l (Q) ;

s i g n a l (Q) ; signal (S) ;

Otro problema relacionado con interbloqueos es el bloqueo indenido o


muerte por inanición. Este puede producirse cuando se usan esquemas LIFO
para los semaforos.

6.5. Problemas clásicos de sincronización


6.5.1. Buer limitado ( Productor - Consumidor )
Habitualmente se utiliza para ilustrar la potencia de las primitivas de sin-
cronización. Suponga que la cola consta de n buferes , siendo cada uno capaz
10 Nota del Autor : Dependiendo de si el algoritmo de planicación de CPU es apropiativo
o no , podría ser que se saque el proceso que se este ejecutando actualmente para permitirle
a proceso que se acaba de liberar que se ejecute.
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 45

de almacenar un elemento. El mutex proporciona el acceso a los buferes , ini-


cializandolo con 1. Los semaforos empty y full cuentan el numero de bueres
vacíos y llenos , inicializados con n y 0 respectivamente. Veamos los códigos de
cada uno :
Productor Consumidor
do do
{ {

. . . wait ( f u l l ) ;

// produce un e l e m e n t o n e x t p w a i t ( mutex ) ;

. . . . . .

w a i t ( empty ) ; // e l i m i n a b u f f e r a n e x t c
w a i t ( mutex ) ; . . .

. . . s i g n a l ( mutex ) ;

// a ñ a d i r n e x t p a l b u f e r s i g n a l ( empty ) ;

.. . . .

s i g n a l ( mutex ) ; // consume n e x t c
signal ( full ) ; ..

} while (TRUE) ; } while (TRUE) ;

6.5.2. El problema de los lectores-escritores


Si 2 lectores acceden simultaneamente a los datos compartidos esto no dará
lugar a ningún problema , pero si un lector y otro proceso ( lector o escritor
) acceden concurrentemente , sera un caos.Este problema es muy usado para
probar primitivas de sincronización. Presenta diversas variantes :
Primer Problema : Que ningún lector se mantenga esperando a menos
que un escritor este escribiendo.
Segundo Problema : Un escritor listo debe realizar su acción lo antes
posible.
Soluciones para estos dos problemas pueden llevar a problemas de bloqueos
indenidos.
Una solución para el primero seria la siguiente :

Escritor Lector
do
{

w a i t ( mutex ) ;

r e a d c o u n t ++;

do { if ( readcount == 1 )

w a i t ( wrt ) ;
w a i t ( wrt ) ;
signal ( mutex ) ;
. . .
. . .
// s e r e a l i z a l a e s c r i t u r a
// s e r e a l i z a l a l e c t u r a
..
. . .
s i g n a l ( wrt ) ;

} while (TRUE) ;
w a i t ( mutex ) ;

readcount −−;
if ( readcount == 0 )

s i g n a l ( wrt ) ;

s i g n a l ( mutex ) ;

} while (TRUE) ;
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 46

Los procesos lectores comparten la siguiente estructura :


semaphore mutex , wrt ;

int readcount ;

Los semaforos mutex y wrt se inicializan con 1 , mientras readcount con 0.


wrt es común a los lectores y escritores . mutex se utiliza para asegurar exclusión
mutua al actualizar readcount, la cual almacena el numero de procesos que están
leyendo el objeto actualmente. wrt funciona como semaforo de exclusión mutua
para los escritores , también usado por el primer o el ultimo lector que entra o
sale de su sección critica.
Si un escritor esta en la sección critica y n lectores están esperando , en-
tonces un proceso lector estará en la cola de wrt y n − 1 procesos estarán en
la cola del mutex. cuándo un escritor ejecuta signal (wrt) , podemos reanudar la
ejecución de todos los procesos lectores en espera o de un único proceso escritor
en espera11 .
En algunos sistemas la solución a este problema a sido generalizada para
proporcionar bloqueos lector-escritor , el cual entrega 2 modos de acceso ;
acceso de lectura o de escritura.
Estos cerrojos resulta especialmente útiles en la siguientes situaciones :
Son fáciles de identicar los procesos solo lectores y solo escritores.
La cantidad de procesos lectores es mayor a la de escritores. Esto se debe
a que establecer estos bloqueos requiere una carga importante de trabajo
, la cual es amortizada luego con la concurrencia entre lectores.

6.5.3. La cena de los lósofos


Son 5 lósofos en sus sillas , una fuente de arroz y 5 palitos en la mesa12 .
Cuando un losofo piensa , no requiere los palitos. Cuando tiene hambre trata
de tomar los 2 palitos mas próximos a el, los cuales debe tomar de a uno. Al
conseguirlos , come hasta estar satisfecho , para luego dejarlos en su lugar y
volver a pensar.
Este problema genera una amplia clase de problemas de control de concur-
rencia. Este representa claramente la necesidad de repartir recursos limitados
evitando interbloqueos.
Podemos representar cada palito mediante un semaforo; con wait ( palito [ i ])
nos hacemos de dicho palito , mientras que con signal ( palito [ i ]) lo dejamos. Por
lo tanto los lósofos comparten la siguiente estructura de datos13 : semaphore
palito [5]; , todos los palitos se inicializan con 1
14 .
Aunque esto garantice que 2 vecinos no coman simultaneamente , puede
crear interbloqueos tales como el que sucedería si todos los lósofos tomaran el
palito de la izquierda.
Hay varias soluciones posibles para este problema :
Permitir como máximo n-1 lósofos , siendo n la cantidad de palitos.
Permitir a cada losofo tomar los palitos solo si ambos están disponibles.
11 Esta decisión corresponde al planicador
12 Ver Fig 6.14
13 Que pena que no comparten sus pensamientos...
14 Ver Fig 6.15
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 47

Solución asimétrica : Los lósofos impares toman primero los palitos de


su izquierda , y los lósofos pares toman primero los su derecha.

Recordemos que una solución libre de interbloqueos no necesariamente esta


libre de muerte por inanición.

6.6. Monitores
El uso incorrecto de semaforos puede dar lugar a errores de temporización
difíciles de detectar , los cuales por ejemplo aparecen en el productor-consumidor
( Sección 6.1 ).
En la solución para la sección critica con semaforos todos los procesos com-
parten un mutex , inicializado en 1. Se debe respetar la secuencia wait(mutex
) ;...; signal (mutex); , sino 2 procesos o mas podrían estar en su sección critica

al mismo tiempo. Para evitar esto se desarrollo una estructura de alto nivel
llamada monitor.

6.6.1. Utilización
Un tipo monitor tiene un conjunto de operaciones con exclusión mutua
dentro de este. También contiene declaraciones de variables cuyos valores de-
nen el estado de una instancia de dicho tipo , junto con los cuerpos de los
procedimientos y funciones que operan sobre estas15 . El monitor es un TAD
Opaco.
La estructura del monitor asegura que solo un proceso este activo a la
vez dentro del mismo16 . Si bien esto nos evita tener que encargarnos de la
sincronización en ciertos casos , en otros requrimos de un mecanismo especial ,
el cual es proporcionado por las estructura condition17 . Las únicas operaciones
aplicables a condition son wait(condition) y signal (condition).
Suponga ahora que , cuando un proceso invoca la operación signal (condition
) , hay un proceso Q en estado suspendido asociado con condition. Si se permite

a Q continuar , P deberá esperar ; sino P y Q se activarían simultaneamente


dentro del monitor. Conceptualmente ambos procesos pueden continuar su eje-
cución. Existen 2 posibilidades :

1. Señalizar y esperar : P espera hasta que Q salga del monitor o espere


a que se produzca otra condición.

2. Señalizar y continuar : Q espera hasta que P salga del monitor o


espere a que se produzca otra condición.

Hay argumentos razonables en favor de adoptar cualquiera de estas opciones18 .


15 Ver Fig 6.16
16 Ver Fig 6.17
17 Ver Fig 18
18 En Pascal seadoptó un compromiso : cuando P ejecuta signal (condition) , sale in-
mediatamente del monitor reanudándose inmediatamente Q.
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 48

6.6.2. Solución al problema de los lósofos usando monitores.


Esta impone la restricción de que un losofo tomar sus palitos solo si ambos
están disponibles. Necesitamos diferenciar los siguientes estados que puede estar
un losofo : enum { pensar , hambre , comer } state [5];.
El losofo i puede congurar state [ i ] = comer sii( state [( i +4) %5]!= comer
!= state [(i+1) %5])== TRUE. Tambie debemos declarar : condition self [5];

donde el losofo i tiene que esperar cuando tiene hambre pero no puede con-
seguir los palitos.
La distribución de los palitos se controla mediante el monitor dp :

monitor dp

enum { PENSAR , HAMBRE , COMER } state [5];

condition self [5];

void pickup ( int i )

state [ i ] = HAMBRE;

test ( i ) ;

if ( state [ i ] != COMER )

s e l f [ i ] . wait ( ) ;

void putdown ( int i )

state [ i ] = PENSAR ;

test ( ( i +4) % 5) ;

test ( ( i +1) % 5) ;

void test ( int i )

if ( ( s t a t e [ ( ( i +4) % 5) ] != COMER) &&

( state [ i ] == HAMBRE ) &&

( s t a t e [ ( ( i +1) % 5) ] != COMER) )

state [ i ] = COMER;

s e l f [ i ] . signal () ;

initialization code ( )

for ( int i = o ; i < 5 ; i ++)

state [ i ] = PENSAR ;

Para comer se invoca pickup() el cual puede suspender el losofo si no en-


cuentra los palitos. Al terminar de comer se usa putdown() para dejar los pali-
tos. Por lo tanto el losofo debe respetar la secuencia pickup()-comer-putdown().
Aun en esta solución produce muerte por inanición.
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 49

6.6.3. Implementación de un monitor utilizando semaforos


Para cada monitor se proporciona semaphore mutex = 1;. La secuencia de
acceso es wait(mutex) ;...; signal (mutex);. Se introduce un semaforo adicional
semaphore next = 0; , en el cual los procesos que efectúan una señalización

pueden quedarse suspendidos. Se proporciona int next_count = 0; para contar


el numero de procesos suspendidos en next. para cada condición x , introduci-
mos un semaphore x_sem = 0 y una int x_count = 0 . Veamos los códigos 6.9 ,
6.10 y 6.11 :

Listing 6.9: Código del Proceso F


w a i t ( mutex ) ;
...
/* c u e r p o de F */
...
i f ( next_count > 0 )
{
s i g n a l ( next ) ;
}
else
{
s i g n a l ( mutex ) ;
}

Listing 6.10: Código de x.wait()


x _ co u n t++;

if ( next_count > 0 )

s i g n a l ( next ) ;

else i f
{

s i g n a l ( mutex ) ;

w a i t ( x_sem ) ;

x_count −−;

Listing 6.11: Código de x.signal()


if ( x _ c o un t > 0)

n e x t _ c o u n t ++;

s i g n a l ( x_sem ) ;

wait ( next ) ;

next_count −−;
}

Esta implementación es aplicable a las deniciones dadas por Hoare y por


Brinch-Hansen. En algunos casos la generalidad de la implementación es in-
necesaria y puede conseguirse merar signicativamente la eciencia.
CAPÍTULO 6. SINCRONIZACIÓN DE PROCESOS 50

6.6.4. Reanudación de procesos dentro de un monitor


Para determinar que proceso se reanudara una solución sencilla es usar
FCFS , de modo que el proceso que lleve mas tiempo sea el primero en re-
anudarse. En ciertos casos esto no resulta adecuado , por lo que podemos usar
una estructura de espera condicional x.wait(c) ; . int c ; se denomina numero
de prioridad , el cual se almacena con el numero del proceso suspendido.
Cuando se ejecuta x. signal () se reanuda el de menor prioridad.
El concepto de monitor no puede garantizar que la secuencia de adquisición-
liberación de recursos sea respetada por los desarrolladores que utilicen la her-
ramienta. Una solución ( la mía :P ) a esto podría ser entregarle al monitor el
deber de colocar esta secuencia , el cual ejecutaría la función que se entrego
junto con los parametros de la misma ( los cuales ninguno debe ser un recurso
compartido ) , esta función podría ser algo así :

void * R.use (void * func ( void * ) , void * arg_v , resource r )

Otra metodología seria comprobar 2 condiciones :

1. Los procesos de usuario siempre deben realizar sus llamadas en el monitor


en la secuencia correcta.

2. Tenemos que asegurarnos de que no haya ningún proceso no cooperativo


que ignore simplemente el mecanismo de exclusión mutua proporcionado
e intente acceder directamente a los recursos compartidos.

Si se cumplen estas condiciones podemos asegurar que no habrá errores de


temporización ni fallara el algoritmo de planicación.
Este método es solo aplicable en sistemas pequeños y estáticos. Este prob-
lema se resuelve correctamente con los mecanismos que se verán en el Capitulo
14.
Capítulo 7

Interbloqueos

7.1. Modelo de sistema


Un numero nto de recursos se distribuyen entre una serie de procesos en
competición. Se dividen en varios tipos , constando cada uno de un cierto
numero de instancias.
Si un proceso solicita una instancia del recurso , cualquier instancia debería
satisfacer la solicitud. Si no es así , los tipos de recursos no han sido bien
denidos1 .
La solicitud y liberación de recursos son realizadas por SYSCall's , las cuales
varían dependiendo del tipo de recurso en algunos sistemas. Para los recursos
no pertenecientes al sistema puede hacerse mediante la secuencia wait(mutex)
;...; signal (mutex); usando semaforos o cerrojos mutex. Cada vez que emplea un

recurso gestionado por el kernel el SO comprueba que el proceso ha solicitado


el recurso y que ha sido asignado a dicho proceso. Una tabla registra si cada
recurso esta libre o asignado y a quien. Si se solicita un recurso que ya fue
asignado , el proceso que solicito es puesto en la cola de espera de dicho recurso.
Un conjunto de procesos estarán interbloqueados cuando todos los procesos
del conjunto estén esperando que se produzca un suceso que solo puede producir
otro proceso del conjunto, siendo este suceso la adquisición y liberación de
recursos. Otro tipo de sucesos pueden dar lugar a interbloqueos ( las facilidades
IPC descritas en el Capitulo 3 ).
Los programas multihebras son buenos candidatos para los interbloqueos ,
porque las distintas hebras pueden competir por la utilización de los recursos
compartidos.

7.2. Caracterización de los interbloqueos


Los procesos nunca terminan de ejecutarse y los recursos están ocupados,
lo que impide que se inicien otros trabajos.
1 Nota del Autor : Esto nos obliga a tener en cuenta todas las diferencias de importancia
, las cuales dependen de ciertos factores que los desarrolladores o incluso los usuarios deben
clasicar en ciertos casos.

51
CAPÍTULO 7. INTERBLOQUEOS 52

7.2.1. Condiciones necesarias


Interbloqueo puede surgir si se dan simultaneamente las cuatro condiciones
siguientes :
1. Exclusión mutua : Al menos un recurso debe estar en modo no com-
partido , solo un proceso puede usarlo a la vez. Si otro lo solicita deberá
esperar hasta que el recurso sea liberado.
2. Retención y espera ; Un proceso debe estar reteniendo al menos un
recurso y esperando para adquirir otros.
3. Sin desalojo : Los recursos no pueden ser desalojados del proceso , el
cual los debe liberar voluntariamente.
4. Espera circular : < ∀ p[i] :: p[i] requiere recursos de p[(i + 1) %n] >
La condición de espera circular implica la de retención y espera. Sin embargo
veremos que resulta útil considerar cada condición por separado.

7.2.2. Grafo de asignación de recursos.


Lo interbloqueos pueden denirse mediante un grafo dirigido2 . El conjunto
de vértices V se divide en dos tipos diferentes de nodos :
P el conjunto de los procesos activos.
R el conjunto de tipos de recursos.
Existen 2 tipos de aristas :
Arista de solicitud ( Pi → Rj ) : Pi solicita una instancia de Rj .
Estas apuntan al tipo de recurso solicitado.
Arista de asignación ( Rj → Pi ) : Rj asigna una instancia a Pi .
Estas apuntan con una instancia al proceso que la solicito.
Dada la denición de este grafo podemos demostrar que la presencia de un
ciclo es condición necesaria y suciente para la existencia de interbloqueos3 .
Si cada tipo de recurso tiene varias instancias , entonces la existencia de un
ciclo es condición necesaria , pero no suciente ,para la existencia de interblo-
queos4 . Por lo tanto la ausencia de ciclos asegura la ausencia de interbloqueos
, pero su presencia no asegura lo opuesto.

7.3. Metodos para tratar los interbloqueos


Podemos abordar el problema de 3 formas5 :
Emplear un protocolo para impedir o evitar los interbloqueos , asegurando
que el sistema nunca entre en estado de interbloqueo.
2 Ver Fig 7.2
3 Esto permite generar un proceso de sistema que chequee dicho grafo ( en si la tabla de
recursos ) antes de cada nueva solicitud y/o asignación , evitando así cualquier ciclo.
4 Ver Fig 7.3 y 7.4
5 Desgraciadamente la mas usada es la ultima , dejando todo el trabajo al desarrollador.
CAPÍTULO 7. INTERBLOQUEOS 53

Permitir que entre en interbloqueo , detectarlo y realizar una recuperación.

Ignorar el problema en lo absoluto.

El sistema puede emplear 2 tipos de esquemas :

Prevención de interbloqueos : Proporciona un conjunto de metodos


para asegurar que al menos una de las condiciones necesarias ( Sección
7.2.1 ) no se pueda cumplir. Este método evita los bloqueos restringiendo
el modo en que se pueden realizar las solicitudes.

Evasión de interbloqueos : Requiere que se proporcione al SO in-


formación adicional sobre que recursos solicitara y utilizara un proceso
durante su vida. Con estos datos se puede decidir para cada solicitud si
el proceso tiene que esperar o no. Para decirlo , el sistema necesita con-
siderar los recursos disponibles en el momento , cuales están asignados a
cada proceso y las futuras solicitudes y liberaciones.

Si no se emplean estos esquemas , puede proporcionarse algoritmos que ex-


aminen el estado del mismo para detectar interbloqueos y luego eliminarlos o
recuperarse de ellos6 .
Si no nos encargamos de estos interbloqueos , seguirán creciendo deterio-
rando el rendimiento del sistema y obstruirá cada vez mas recursos.

7.4. Prevención de interbloqueos


7.4.1. Exclusión Mutua
Se aplica a los recursos que no pueden ser compartidos , ( por ejemplo
recursos tales como archivos de solo lectura pueden ser accedidos de forma
concurrente ). Negar esta condición no funciona , ya que los recursos son in-
trínsecamente no compartibles.

7.4.2. Retención y Espera


Debemos garantizar que cuando un proceso solicite un recurso no este rete-
niendo ningún otro. Existen 2 protocolos posibles :

1. Exigir que cada proceso solicite todos sus recursos ( y se le asignen) antes
de comenzar a ejecutarse.

2. Permitir a un proceso solicitar recursos cuando no tenga ninguno retenido


, liberando los que tenga antes de solicitar adicionales.

Ambos tienen 2 desventajas :

1. La taza de utilización de los recursos puede ser baja , dado que ciertos
recursos estarán asignados pero no en uso.

2. Un proceso que necesite varios recursos muy solicitado se quedara es-


perando indenidamente.
6 Nota del Autor : En general estos algoritmos utilizan buena parte del funcionamiento
de los esquemas anteriores como veremos posteriormente
CAPÍTULO 7. INTERBLOQUEOS 54

7.4.3. Sin Desalojo


Similar a retención y espera , siendo el primero protocolo casi idéntico al
segundo de retención y espera. Sin embargo presenta otro nuevo protocolo7 :
Si un proceso solicita recursos comprobaremos si están disponibles. Si es así
los asignamos , sino buscamos alguno proceso que los tenga y este esperando
por recursos adicionales. Ese sera nuestra víctima a la que le quitaremos sus
recursos para darselos a los mas necesitados 8 . Si no encontramos víctima ( lo
cual indica que los recursos o no están o están siendo utilizados ) entonces el
proceso tendrá que esperar9 . Un proceso solo puede reiniciarse cuando tiene
todos los recursos que tenia y los que solicito.
Este protocolo es aplicado a tipos de recursos cuyo estado puede fácilmente
guardarse y restaurarse , como registros de CPU y espacios de memoria.

7.4.4. Espera Circular


Para evitar esto debemos imponer una ordenación total de todos los tipos
de recursos y requerir que cada proceso solicite sus recursos en orden creciente
, el cual seria nuestro protocolo.Si se necesitan varias instancias del mismo
recurso , se ejecuta una única solicitud para todas ellas.
Formalmente , denimos una función 1-1 F : R → N con la cual diremos
Pk → Rj ⇐⇒ F (Rj ) > F (Ri ). Alternativamente podríamos requerir que Pk →
Rj debe liberar todos los Ri F (Ri ) ≥ F (Rj ).
Usando estos 2 como protocolos evitamos la espera circular. Esto puede
demostrarse por el absurdo :

Sea los procesos en espera circular{P }n0 :: Pi → Rj Rj → P(i+1) %n .Siendo


(Rj → P(i+1) %n P(i+1) %n → R(j+1) %m ) implica que < ∀jN : 0 ≤ j ≤ m :
F (Rj ) < F (R(j+1) %m ) >. Por transitividad tenemos que F (R0 ) < F (R0 )
, absurdo. Por lo tanto , no puede existir espera circular. Desgraci-
adamente es deber del desarrollador de aplicaciones respetar esto10 . Podemos
utilizar herramientas de debuging para vericar que los bloqueos se obtienen
en orden. Una de ellas es witness , para BSD , la cual utiliza bloqueos de ex-
clusión mutua para proteger las secciones criticas ; opera manteniendo de for-
ma dinámica el orden de los bloqueos en el sistema. Por ejemplo si hebra_uno
quiere adquirir los bloqueos y lo hacen en el orden primer_mutex;segundo_mutex
; , luego si viene hebra_dos y lo hace en un orden diferente consideraremos que

hebra_dos no respeta el orden.

7.5. Evasión de Interbloqueos


Conociendo exactamente la secuencia completa de solicitudes y liberaciones
de cada proceso , el sistema puede decidir para cada solicitud si el proceso debe
7 Nota del Autor: Este lo considero una buena opción por que se adapta a la situación y
no tiene actitudes tan despiadadas como los anteriores.
8 by R.H
9 Nota del Autor : Si no existen dichos recursos deberiamos enviar una interrupción fatal.
10 Nota del Autor : Seria interesante ver si se puede realizar o existe algo como las invari-
antes FUSE para este caso.
CAPÍTULO 7. INTERBLOQUEOS 55

esperar o no , con el n de evitar un posible interbloqueo futuro. Los diversos


algoritmos dieren en la cantidad y el tipo de información requerida .

7.5.1. Estado seguro


Un sistema esta en estado seguro solo si existen una secuencia segura.
< P 1, ..., P n >es una secuencia segura para
Pi−1el estado de asignación actual
si < ∀ i : 1 ≤ i ≤ n : N eeded(Pi ) ≤ j=1 Allocated(Pj ) > . Si esto se
cumple entoncesPi tendrá todos los recursos que necesita cuando los Pj hayan
terminado. Si tal secuencia no existe entonces el sistema esta en un estado
inseguro. Al estar en este estado el sistema no puede impedir que se produzca
un interbloqueo ( si todavía no lo hay ).
Con este esquema , si un proceso solicita un recurso que esta disponible ,
es posible que tenga que esperar. Por lo tanto la tasa de utilización de recursos
sera menor.

7.5.2. Grafo de asignación de recursos


Se agrega una nueva arista , la arista de declaración ( Pi → Rj ) , indica
que Pi puede a futuro solicitar Rj . Esta se convierte en arista de solicitud
cuando se pide Rj y vuelve a ser arista de declaración cuando es liberado Rj
removiendo la arista de asignación. Podríamos pedir que todas las aristas de
declaración sean añadidas antes de que comience Pi , pero podemos permitir
que cuando Allocated(Pi ) = 0 entonces pueden agregarse nuevas aristas de
declaración11 .
Solo permitiremos la conversión de una arista de solicitud en arista de asig-
nación no provoca un ciclo. Los algoritmos de detección de ciclo son del orden
de n2 , siendo n el numero de procesos en el sistema.

7.5.3. Algoritmo del Banquero


El algoritmo del grafo no es aplicable para múltiples instancias de cada
recurso , para eso usamos este algoritmo. Cada proceso nuevo debe declarar el
máximo de instancias de cada recurso que necesitara , no puede exceder el total
de recursos del sistema , y el sistema debe determinar si existe una secuencia
segura para realizar la asignación; sino esperara hasta que se liberen sucientes
recursos. Sea n = número de procesos y m = número de tipos de recursos.
Necesitamos las siguentes estructuras :

Available : Un vector de longitud m que indica el numero de recursos


disponibles de cada tipo.

Max : Una matriz n×m que indica la demanda máxima de cada proceso.
Allocation : Una matriz n × m que indica la cantidad de cada recurso
asignados a cada proceso.

Need : Una matriz n × m que indica la necesidad restante de cada


proceso12 .
11 Si no fuera así , podrían generarse un estado de interbloqueo. Ver Fig 7.6 y 7.8
12 N eed[i][j] = M ax[i][j] − Allocation[i][j]
CAPÍTULO 7. INTERBLOQUEOS 56

Estas estructuras varían con el tiempo , tanto en tamaño como sus valores.
Para simplicar la lectura diremos que X, Y  V Resources :: X ≤ Y ⇐⇒<
∀ i :: X[i] ≤ Y [i] >.

7.5.3.1. Algoritmo de seguridad


Sirve para averiguar si se encuentra en un estado seguro o no y funciona de
la siguiente manera :

1. Sean W ork, F inish  V Resources :: #W ork = m  #F inish = n , ini-


cializadolos con Work = Available; /*Para todo i */ Finish [ i ] = false .

2. Halla i tal que :

a) Finish [ i ] == false

b) N eedi ≤ W ork

Si no existe i que lo cumpla , pasar al paso 4.

3. Work += Allocatión[i]; Finish[ i ] = true; e ir al paso 2.

4. Si Finish [ i ] == true para todo i , entonces estamos en un estado seguro.

Este algoritmo es del orden del n2 × m.

7.5.3.2. Algoritmo de solicitud de recursos


Requesti es el vector de solicitud para el proceso Pi . Si Requesti [j] == k
, entonces Pi desea k instancias del tipo de recurso Rj . Cuando Pi solicita
recursos , se toman las siguientes acciones :

1. Si Requesti ≤ N eedi , ir al paso 2. Sino , generar un mensaje de error.

2. Si Requesti ≤ Available , ir al paso 3. Sino , tendrá que esperar.

3. Hacer como si el sistema hubiera asignado al proceso Pi los recursos


solicitados. Esto se realiza de la siguiente manera :
Available −= Request [ i ] ;

Allocation [ i ] += Request [ i ] ;

Need [ i ] −= Request [ i ] ;

Si el estad que resulta de la asignación es seguro , la transacción se completa


; sino es así Pi debe esperar a que se le asignen los recursos Requesti y se
restaura el antiguo estado de asignación de recursos.

7.6. Detección de Interbloqueos


En este caso , el sistema debe proporcionar :

Un algoritmo que examine el estado del sistema para determinar interblo-


queos.

Un algoritmo para recuperarse de ellos.


CAPÍTULO 7. INTERBLOQUEOS 57

Los esquemas de detección y recuperación tienen un coste signicativo asociado


, que incluye no solo el coste de tiempo asociado con el mantenimiento de
la información necesaria y la ejecución de los algoritmos de detección , sino
también las potencias perdidas inherentes al proceso de recuperación de un
interbloqueo.

7.6.1. Una sola instancia en cada tipo de recurso


Utilizando una variante del grafo de asignación de recursos , denominada
grafo de espera. Se obtiene a partir del de asignación , eliminando los nodos
de recursos y colapsando sus correspondientes aristas13 . Esto hace que Pi → Pj
signique que Pi esta esperando algunos recursos de Pj . Se necesita mantener
este grafo e invocar periódicamente el algoritmo de detección de ciclos , el cual
es del orden de n2 siendo n el numero de vértices.

7.6.2. Varias instancias de cada tipo de recurso


El esquema del grafo de espera no es aplicable a la asignación de recursos
con múltiples instancias. Para eso tenemos un algoritmo que emplea varias
estructuras de datos variables , similares a las del algoritmo del banquero (
Sección 7.5.3 ) :

Available : Un vector de longitud m que indica el numero de recursos


disponibles de cada tipo.

Allocation : Una matriz n × m que indica la cantidad de cada recurso


asignados a cada proceso.

Request : Una matriz n×m que indica la cantidad de recursos solicitados


por cada proceso.

La relación ≤ se mantiene igual que en la Sección 7.5.3. El algoritmo investiga


cada posible secuencia de asignación de los procesos que quedan por comple-
tarse.

1. Sean W ork, F inish  V Resources :: #W ork = m  #F inish = n , ini-


cializadolos con Work = Available; /*Para todo i */ if (Allocation [ i ] != 0)
Finish [ i ] = false ; .

2. Halla i tal que :

a) Finish [ i ] == false

b) Requesti ≤ W ork

Si no existe i que lo cumpla , pasar al paso 4.

3. Work += Allocatión[i]; Finish[ i ] = true; e ir al paso 2.

4. Si Finish [ i ] == false para algún i , entonces estamos en un estado de


interbloqueo , estando Pi bloqueado.
13 Ver Fig 7.8
CAPÍTULO 7. INTERBLOQUEOS 58

La razón para reclamar los recursos de Pi luego de determinar cierto 2(b) , es


por que sabemos que no esta implicado en el interbloqueo. Por esto suponemos
que Pi no requerirá mas recursos y terminar por devolver los que tenga asigna-
dos. Si esto no es cierto , mas adelante se producirá un interbloqueo que sera
detectado luego14 .

7.6.3. Utilización del algoritmo de detección


Podemos identicar el conjunto de procesos en interbloqueo , siendo com-
pletado cada ciclo por la solicitud mas reciente y estando causado por dicho
proceso perfectamente identicable15 .
Otra alternativa mas barata , es invocar el algoritmo a intervalos menos
frecuentes o cuando la utilización de la CPU caiga por debajo del 40 %. En
este caso puede haber muchos ciclos , por lo que el causante no es identicable.

7.7. Recuperación de un interbloqueo


Podemos informar al operador acerca del interbloqueo y dejar que lo trate
manualmente , o podemos delegar la responsabilidad de la recuperación al
sistema. Veamos las siguientes 2 opciones para esto ultimo.

7.7.1. Terminación de procesos


Existen 2 posibles metodos :

Interrumpir todos los procesos interbloqueados : Los procesos en


interbloqueo pueden haber consumido un largo periodo de tiempo y los
resultados de estos calculos parciales deben descartarse , para probable-
mente luego tener que repetirse mas tarde.

Interrumpir un proceso a la vez : Este método requiere una gran


cantidad de trabajo adicional a causa de las múltiples invocaciones de
algoritmo de detección luego de que se elimino un proceso que se encon-
traba dentro del ciclo.

Interrumpir un proceso no es fácil , puede que este estaba actualizando un


archivo y su terminación deje al archivo en un estado incorrecto. Si utilizamos
el método de terminación parcial tenemos que determinar que procesos serán
las sacricados. Deberemos elegir los procesos que su terminación tenga coste
mínimo ( siendo este coste denido por múltiples factores , entre ellos los antes
mencionados , la prioridad del proceso , subprocesos relacionados , tiempo que
estuvo trabajando y tiempo que le queda, etc ).

7.7.2. Apropiación de recursos


Desalojamos de forma sucesiva los recursos de los procesos y asignamos
dichos recursos a otros procesos hasta terminar con los interbloqueos.
Para esto debemos abordar 3 cuestiones :
14 Nota del Autor : Hay un ejemplo muy bueno en el libro , en esta misma sección.
15 Nota Inutil : Apaidemonos de el , quizás llego ultimo.
CAPÍTULO 7. INTERBLOQUEOS 59

1. Seleccionar una víctima : Es necesario determinar el orden de apropiación


de forma que se minimicen los costes. Los factores de coste pueden incluir
el numero de recursos que esta reteniendo un proceso y la cantidad de
tiempo que el proceso ha consumido para su ejecución.

2. Anulación : Si nos apropiamos de sus recursos no puede continuar su


ejecución normalmente. Debemos devolver el proceso a un estado seguro
y reiniciarlo desde ahí. Esto es difícil de determinar , así que esto requiere
que el sistema mantenga mas información sobre el estado de los procesos
en ejecución.

3. Inanición : Debemos evitar que siempre se elija al mismo proceso como


víctima , para esto deberemos tener en cuenta dentro del coste cuantas
veces se ha elegido a dicho proceso como víctima16 .

16 Nota del Autor : Esto sucede por que el que tiene menor coste generalmente sigue siendo
el mismo y una forma de evitarlo es agregando esta nueva variable.
Parte III

Gestión de Memoria

60
Capítulo 8

Memoria Principal

8.1. Fundamentos
La memoria esta compuesta por una matriz de bytes , cada uno con su
propia dirección. La CPU extrae instrucciones de la memoria marcadas por el
program-counter, las cuales pueden provocar operaciones adicionales de carga
en memoria. La forma de proceder es extraer la instrucción de memoria , de-
codicarla y extraer los operandos adicionales. Ya procesada , se guardan los
resultados en memoria.
La memoria solo ve un ujo de sus direcciones, no sabe ni como se generan
ni para que se usaran.

8.1.1. Hardware Básico


La memoria principal es la única área a la que puede acceder la CPU direc-
tamente , aparte de sus registros internos. No existen instrucciones que acepten
otro tipo de direcciones que no sean las de la memoria principal1 .
Debemos tener en cuenta que el acceso a memoria puede tardar varios ciclos
de reloj de CPU , por lo que este podría llegar a detenerse para esperar los
datos faltantes. Para resolver esto existen una memoria rápida entre la CPU y
la principal , denominada cache.
Adicionalmente debemos proporcionar protección por hardware para evitar
accesos ilegales a la memoria. Para esto debemos asegurar que cada proceso
disponga de un espacio de memoria separado, determinando su rango de direc-
ciones legales y garantizando que solo accederá a estas. Podemos implementarla
utilizando 2 registros llamados base y limite2 . El base guarda la dirección física
inicial de la memoria mientras el limite especica el tamaño del bloque.
La protección se logra mediante la comparación de las direcciones de memo-
ria generadas por el usuario con las de los registros. Cualquier intento de un
acceso ilegal a memoria , provocara una interrupción al SO el cual la tratara
como un error fatal3 .
Estos registros solo pueden cargarse por el SO mediante instrucciones priv-
ilegiadas las cuales se ejecutan solo en modo kernel. Este esquema permite al
1 Nota del Autor : Por lo que el traslado de cualquier dato necesario para un proceso debe
realizarse desde los dispositivos de almacenamiento a la memoria principal.
2 Ver Fig 8.1
3 Ver Fig 8.2.

61
CAPÍTULO 8. MEMORIA PRINCIPAL 62

SO manipular los programas de usuario en muchas situaciones distintas.

8.1.2. Reasignación de Direcciones


Clásicamente la reasignación de las instrucciones y los datos a direcciones
de memoria puede realizarse en cualquiera de los siguientes pasos4 :

Tiempo de compilación : Si sabemos en el momento de compilación


donde va a residir el proceso , podremos generar un código absolu-
to. Si dicha ubicación debería cambiarse luego , entonces es necesario
recompilarlo5 .

Tiempo de carga : De conocer en tiempo de compilación donde va a re-


sidir el proceso , el compilador debe generar código reubicable. En este se
retarda la reasignación nal hasta el momento de carga. De cambiarse la
dirección inicial solo es necesario volver a cargar el código para incorporar
el valor modicado.

Tiempo de ejecución : Si el proceso va a desplazarse de un segmento


a otro , entonces debemos retrasar la reasignación hasta el instante de
ejecución , lo cual requiere hardware especial6 .

8.1.3. Direcciones Lógicas y Físicas


Los metodos de reasignación en tiempo de compilación y de carga generan
direcciones lógicas y físicas idénticas , mientras el de ejecución dieren7 .
La correspondencia entre las lógicas y las físicas es establecida por el dis-
positivo MMU ( memory-management unit ). Este es una versión simple del
esquema MMU , el cual es una generalización de esquema de registros base
de la Sección 8.1.1. El registro base se denomina registro de reubicación.
El valor de este es sumado a todas las direcciones generadas por proceso de
usuario en el momento de enviarlas a memoria8 .
Los programas de usuarios nunca ven las direcciones físicas , solo las lógicas.
Este maneja sus direcciones lógicas y el hardware de conversión ( mapeo ) las
pasa a físicas. Este método de acoplamiento es el expuesto en la Sección 8.1.2
en tiempo de ejecución.

8.1.4. Carga Dinámica


Para obtener una mejor utilización de la memoria podemos utilizar un
mecanismo de carga dinámica. Una rutina no se carga hasta que se invoca;
estas se mantienen en disco en formato de carga reubicable.
Para esto primero se comprueba que la rutina que un programa este lla-
mando se encuentra ya en memoria , de no ser así se invoca el cargador de
montaje reubicable para que la cargue y actualice las tablas de direcciones de
programa. Luego se pasa el control a la rutina cargada.
4 Ver Fig 8.3.
5 Esta metodología es utilizada para los programas .COM de MS-DOS.
6 Este es el método mas usado por los SO de propósito general.
7 En este ultimo caso las direcciones lógicas son llamadas virtuales.
8 Ver Fig 8.4.
CAPÍTULO 8. MEMORIA PRINCIPAL 63

La ventaja es que este resulta útil para gestionar programas grandes con
muchas rutinas de uso muy infrecuente.
Este mecanismo de carga dinámica no requiere ningún soporte especial por
parte del So. Es responsabilidad de los usuarios diseñar sus programas para
poder aprovechar este método.

8.1.5. Montaje Dinámico y Bibliotecas Compartidas


Algunos SO solo permiten montaje estático, el cual trata a las bibliotecas
del sistema como cualquier otro modulo objeto y las integra dentro del binario
del programa. En la carga dinámica , el montaje de estas se realiza en tiempo
de ejecución en lugar de en tiempo de carga. Esto se hace generalmente con
bibliotecas de sistema.
Con el montaje dinámico , se incluye un stub dentro del binario para cada
referencia a una rutina de la biblioteca. Este stub es un pequeño código que
indica como localizar la rutina solicitada. Este comprueba si la rutina ya esta
cargada en memoria y sino , se carga. Luego se sustituye el mismo por la
dirección de la rutina.
Esta funcionalidad puede ampliarse para la actualización de bibliotecas ,
ya que todos los programas que hagan referencia a la biblioteca emplearan
automáticamente la nueva versión. Para evitar incompatibilidades con nuevas
versiones , la información de versión se encuentra tanto en las bibliotecas como
en los programas. Solo los programas que se compilen con una determinada
versión sera afectados por los cambios en ella. Este sistema se conoce como
bibliotecas compartidas.
El montaje dinámico requiere que el SO ese encargue de detectar si la rutina
a ejecutar se encuentra en el espacio de memoria de otro proceso y de permitir
que el proceso que la solicito pueda acceder a esta.

8.2. Intercambio
Los procesos pueden ser intercambiados temporalmente, sacandolos de la
memoria y almacenandolos en un almacén de respaldo9 ( backing-store )
para luego continuar su ejecución.
Los proceso descargados se vuelven a colocar en el mismo espacio de memo-
ria que ocupaban anteriormente. Esta restricción esta dictada por el método de
reasignación de direcciones. Solo en se puede realizar la reasignación en tiempo
de ejecución con facilidad , por que las direcciones físicas se calculan en ese
momento.
El almacén de respaldo debe también poder albergar todas las imágenes de
memoria para todos los usuarios y debe proporcionar un acceso directo a estas
imágenes de memoria. En la cola de procesos preparados estarán no solo los
que estén en memoria principal , sino también los que estén en el almacén de
respaldo.
En los sistemas con intercambio el tiempo de context-switch es muy alto
ya que se debe almacenar una imagen de respaldo10 . Para conseguir un uso
9 Este generalmente es un disco rígido o símil.
10 La cual debe ser actualizada aveces luego de cada ejecución.
CAPÍTULO 8. MEMORIA PRINCIPAL 64

eciente de la CPU la duración del tiempo de ejecución de un proceso debe ser


mucho mas larga que su context-switch.
Una forma de optimizar esto es teniendo conocimiento cuanto del espacio
de memoria asignado a un proceso esta siendo utilizado realmente , para así
solo intercambiar esa cantidad de datos. Para lograr esto en procesos dinámicos
, ellos deberán realizar SYSCall's para informar sus cambios de necesidad de
memoria.
También para poder intercambiar un proceso debemos estar seguros de que
este completamente inactivo , también contando las operaciones I/O pendi-
entes. Para esto ultimo se puede toman dos caminos :

No descargar nunca procesos con operaciones I/O pendientes.

Que dichas operaciones trabajen con bueres del SO , los cuales luego
transferirán los datos al proceso cuando este sea restablecido.

Estos mecanismos son muy poco utilizados actualmente ya que requieren un


tiempo de intercambio muy alto y proporcionan un tiempo de ejecución de-
masiado pequeño. Lo que si existen varias versiones modicadas mas usadas.

8.3. Asignación de memoria contigua


El situado de la partición de memoria del SO es importante por el vector
de interrupciones , ya que los programadores tienden a situar el SO cerca de
dicha zona.
En este esquema cada proceso tiene una única sección de memoria contigua.

8.3.1. Mapeo de memoria y protección


Para proporcionar el mapeo ( mapping ) y protección debemos utilizar 2
registros :

Registro de reubicación 11
Registro de limite 12
La MMU convertirá la dirección lógica dinámicamente sumándole el valor con-
tenido en el registro de reubicación. Esta dirección es la que se envía al memo-
ria13 . Como todas las direcciones se comparan con estos registros , este sistema
nos permite proteger todo lo que este en memoria de posibles accesos ilegales.
Este mecanismo es útil para efectuar carga dinámica de SO de ciertas ruti-
nas poco utilizadas , las cuales serán denominadas código transitorio.

8.3.2. Asignación de Memoria


Uno de los metodos mas simples de asignación es dividir la memoria en
partes de tamaño jo. Cada partición puede contener exactamente un proce-
so14 .
11 Ver Sección 8.1.3
12 Ver Sección 8.1.1
13 Ver Fig 8.6
14 Nota del Autor : Lo cual limita el grado de multiprogramación
CAPÍTULO 8. MEMORIA PRINCIPAL 65

En este esquema el SO mantiene una tabla que indica que particiones es-
tán disponibles. Cuando llega un proceso , buscamos una partición lo su-
cientemente grande para este. Solo le asignamos la cantidad necesaria de esta
partición , dejando el resto en una partición aparte.
Este método genera varios agujeros menores , dispersos por toda la memoria
, los cuales pueden llegar a ser tan pequeños que no sirvan para los procesos en la
cola de entrada. Generalmente las particiones adyacentes son combinadas para
generar particiones mas grandes. Existen 3 formas para asignar las particiones :
Primer Ajuste : Se asigna el primera partición que sea lo suciente-
mente grande.
Mejor Ajuste : Se asigna la partición mas pequeña. Al menos que este
ordenada , debemos recorrer toda la lista. Esto hace que la partición
sobrante sea lo mas pequeña posible
Peor Ajuste : Se asigna el de mayor tamaño. Se requiere exploración
total de la lista , salvo que este ordenada. La ventaja de este método es
que deja particiones muchos mas grandes con la memoria sobrante.

8.3.3. Fragmentación
Existen 2 problemas en la asignación de memoria contigua :
Fragmentación Externa : A medida que asignamos y liberamos par-
ticiones , estas se vuelven cada vez mas pequeñas , hasta llegar a ser
inútiles en el caso de que estos no sean contiguos15 . En varios casos esto
es una situación común y genera graves perdidas de memoria. Esta puede
solucionarse mediante compactación , el cual se encarga de ordenar las
particiones para luego fusionarlas. Esta solo es posible si la reubicación
es dinámica y en tiempo de ejecución.
Fragmentación Interna : Sucede cuando el espacio adicional de par-
tición de un proceso no es lo sucientemente grande para este. La técnica
general consiste en descomponer la memoria en bloques de tamaño jo
y asignar esta en unidades basándose en el tamaño del bloque. Con esto
la memoria asignada a un proceso puede ser un poco mas grande. Esta
cantidad extra es la fragmentación externa.

8.4. Paginación
Es un esquema de gestión de memoria que permite que el espacio de di-
recciones físicas de un proceso no sea contiguo. Este evita la fragmentación
externa , pero no la interna.

8.4.1. Método básico


Toda dirección generada por CPU esta dividida en 2 partes :
Numero de Pagina : Se utiliza como índice en la tabla de paginas , la
cual contiene la dirección base de cada pagina en memoria física.
15 Y por lo tanto no puedan unirse para crear una partición mas grande.
CAPÍTULO 8. MEMORIA PRINCIPAL 66

Desplazamiento de Pagina : Es el que se combina con la dirección


base para denir la dirección de memoria física que se envía a la unidad
de memoria.
Descomponer la memoria física en una serie de bloques de tamaño jo denom-
inados marcos y la lógica en paginas16 .
Los tamaños de las paginas generalmente son 2n , lo cual facilita la traduc-
ción de las direcciones. Podríamos elegir los m − n bits de mayor peso de cada
dirección lógica para designar el numero de pagina , mientras los n bits de
menor peso indicaran el desplazamiento de pagina17 .
Esta distribución de los tamaños de las paginas no determina que el máximo
de la fragmentación por pagina sera menor a la mitad del tamaño de la pagina
que se le asigne ( ya que siempre tendremos tamaños menores ). Debemos
recordar que si tenemos muchas paginas de tamaño muy pequeño requeriríamos
tablas muy grandes18 .
Vemos que el esquema de paginación es un tipo de reubicación dinámica.
La utilización de la paginación es similar al uso de tablas de registro base, uno
por marco.
El tamaño de los procesos es representado en paginas , las cuales requerirán
un marco para cada una19 .
La diferencia entre la vision del usuario y la del SO acerca de la memoria ,
se resuelve mediante el hardware de traducción de direcciones. Esta conversión
queda oculta al usuario , siendo solo controlada por el SO. Por esto , el SO
mantiene una tabla de marcos para controlar el manejo de la memoria física.
Adicionalmente , para poder realizar cosas tales como la traducción de
direcciones de memoria entregadas por el usuario , el SO mantiene una tabla de
paginas , mas una copia del program-counter y los contenidos de los registros ,
para cada proceso junto con su PCB. Esto se utiliza para traducir las direcciones
lógicas en físicas. También es utilizado por el despachador para denir la tabla
de paginas hardware al asignar la CPU a un proceso. Vemos claramente que la
paginación aumenta el tiempo necesario para un context-switch.

8.4.2. Soporte Hardware


La mayoría de los SO asignan una tabla de paginas por proceso , almace-
nando un puntero a la tabla de paginación , junto con otros registros , dentro
de su PCB. El despachador se encarga de cargar luego todos estos valores al
cargar de nuevo el PCB.
Una de las implementaciones mas simples consiste un conjunto de registros
dedicados. Estos deben construirse lo mas ecientemente posible. Este método
solo es razonable cuando las tablas son pequeñas20 .
Para el caso de que sean muy grandes , se utiliza un registro base de
tablas de paginas ( PTBR , page-table base register ) para apuntar a
la tabla de paginas. Esto reduce en gran medida el tiempo de context-switch
, pero aumenta el tiempo requerido para acceder a un espacio de memoria del
usuario.
16 Ver Fig 8.7 y 8.8
17 Ver Fig 8.9
18 Nota del Autor : Si es 1-1 no alcanzaría la memoria para solo la tabla de paginas.
19 Ver Fig 8.10.
20 Nota del Autor : No seria el caso en las computadoras actuales.
CAPÍTULO 8. MEMORIA PRINCIPAL 67

Ahora para acceder a la dirección física debemos buscar la tabla de paginas


, desplazarse según el numero de pagina para obtener el numero de marco. Por
lo tanto requerimos 2 accesos a memoria para acceder a un byte.
La solución estándar consiste en utilizar una cache hardware de pequeño
tamaño y con capacidad de acceso rápido. denominada buer de consulta
de traducción ( TLB , translation look-aside buer ). Cada entrada a la TLB
esta compuesta por una clave y un valor. cuando se presenta un elemento se
compara simultaneamente con todas las claves en paralelo.
Se realizan los accesos de la siguiente forma :

La TLB contiene solo unas cuantas entradas de la tabla de paginas ;


cuando la CPU genera una dirección lógica , se busca el numero de
pagina en el TLB. De encontrarse , su numero de marco correspondi-
ente estará inmediatamente disponible para ser usado para acceder
a memoria. De no encontrarse ( conocido como Fallo de TLB ) es
necesario una referencia a memoria para consultar la tabla de pagi-
nas21 . Luego podemos añadir el numero de pagina y de marco obtenidos a la
TLB. De estar esta llena , el SO debe elegir la entrada sustituir. Esto se realiza
aleatoriamente o puede llegar a usarse algoritmos tales como sustituir la menos
recientemente utilizada. Algunos TLB permiten cablear las entradas , lo cual
hace que no se puedan borrar22 .
Algunos TLB almacenan identicadores del espacio de direcciones
( ASID , address-space identier ) en cada entrada TLB. Cada ASID
identica univocamente cada proceso y se utiliza para proporcionar mecanismos
de protección de direcciones de ese proceso controlando su coincidencia , que
en caso de no coincidir se trata como un fallo de TLB. Hay casos en los que
la TLB no soporta entradas de procesos distintos , por lo que debe limpiarse
completamente al cargar el PCB de otro proceso , sino este podría ingresar en
direcciones ilegales. La tasa de acierto es la cantidad de veces que la TLB
encuentra un numero de pagina solicitado.

8.4.3. Protección
Se consigue mediante una serie de bits de protección asociados con cada
marco, los cuales se mantienen en la tabla de paginas. Uno dene una pagi-
na como read-write o read-only. Este bit puede ser comprobado al mismo
tiempo que se calcula la dirección física. De producirse un intento de escritura
en un read-only provocara una interrupción de hardware al SO. Este tipo de
protección puede ampliarse y anarse fácilmente.
Se utiliza también un bit valido-invalido el cual determina si la pagina
asociada es del proceso que la intenta acceder. Este bit es congurado por el
SO al cargar un nuevo proceso23 . De realizarse una acceso invalido se enviara
una interrupción al SO.
Este esquema genera el problema de que ciertas posiciones de memoria
que no pertenecen al proceso serán marcadas como validas , a causa de la
fragmentación interna por el tamaño jo de las paginas. Esto se soluciona con
21 VerFig 8.11
22 Porejemplo , las del kernel pueden estár cableadas
23 Por lo que solo las entradas de este que fueron agregadas a la TLB son conguradas
como validas , mientras las otras como invalidas.
CAPÍTULO 8. MEMORIA PRINCIPAL 68

un registro de longitud de la tabla de paginas ( PTLR , page-table


length register ) el cual sirve para vericar que una dirección se encuentra
dentro del rango de direcciones validas para ese proceso. De fallar se genera
una interrupción al SO.

8.4.4. Paginas compartidas


La paginación permite compartir código común , solo si se trata de código
reentrante ( o puro )24 . Este código es el que nunca se modica durante
su ejecución ( auto-modica ). Por lo tanto es read-only lo cual permite su
acceso simultaneo , siendo esta característica de read-only impuesta por el SO
a ese código. Algunos sistemas implementan la memoria compartida utilizando
paginas compartidas25 .

8.5. Estructura de la tabla de paginas


8.5.1. Paginación jerárquica
Una solución para los sistemas modernos consiste en dividir la tabla de
paginas en varios niveles , siendo paginadas las propias tablas de paginación26 .
Esto hará que el numero de bits reparta en cada nivel adecuadamente27 . Esta
solución es valida para sistemas con direcciones de memoria de 32 bits , ya
que superior a esto se requerirían demasiados niveles de paginación lo que
provocaría una gran aumento del tiempo de context-switch.

8.5.2. Tablas de paginación hash


Para direcciones superiores a 32 bits se utiliza este método donde el valor
hash es el numero de pagina virtual. Cada entrada de la tabla hash contiene
una lista enlazada de elementos que tienen como valor hash una misma ubi-
cación. Cada elemento esta compuesto por :

1. El numero de pagina virtual.

2. El valor del marco de pagina mapeado.

3. Un punto del siguiente elemento de la lista enlazada.

Este algoritmo funciona de la siguiente manera :

Aplicamos la función hash al numero de paginación virtual el cual


nos devuelve un valor que usamos como índice en la tabla hash.
Se compara este valor con el primer campo de cada elemento de
la lista enlazada hasta obtener correspondencia28 . Para 64 bits existe
una variante mas adecuada llamada tabla de paginas en cluster. Estas se
24 Ver Fig 8.13 la cual muestra un buen ejemplo de como se aprovecha este benecio.
25 Nota del Autor : Lo cual también sucede con los threads en cuanto al código del programa
y otros recursos compartidos
26 Dependiendo del procesador , este puede tener registros especiales para estas tablas
27 Ver ejemplo de esta Sección y ver Fig 8.14 y 8.15
28 Ver Fig 8.16
CAPÍTULO 8. MEMORIA PRINCIPAL 69

diferencias de las tablas hash en que cada entrada de la tabla hash apunta a
varias paginas en lugar de a una sola29 .

8.5.3. Tablas de paginas invertidas


Una desventaja del método de paginación usual es que cada tabla de pagina
pueden ocupar gran cantidad de memoria física solo para controlar otras partes
de la memoria30 .
Para resolver esto , las tablas de paginas invertidas tienen una entrada por
cada marco ( o pagina real ) de la memoria. Cada entrada comprueba esta com-
puesta por la dirección lógica de la pagina almacenada en dicha ubicación de
memoria real e incluye información acerca del proceso que posee dicha pagina.
Esto hace que solo haya una única tabla de paginas y esta tendrá una sola
entrada por cada pagina de memoria física31 . Se requiere que se almacene un
ASID32 , ya que contiene varios espacios de direcciones distintos que corre-
sponde con la memoria física.
Cada dirección lógica del sistema esta compuesta de la siguiente manera :

<id −proceso −pagina


, nro , desplazamiento >

Al encontrar una correspondencia con los primeros 2 parametros se genera


una dirección física < i , desplazamiento >. Si no se encuentra se considera un
acceso ilegal a memoria.
Este esquema incrementa el tiempo para explorar la tabla cuando se produce
una referencia pagina. Para aliviarlo , se utiliza una tabla hash con el n de
limitar la busqueda a una sola entrada de la tabla de paginas o a unas pocas en
el peor caso. Este acceso a la tabla hash añade al procedimiento una referencia
a memoria, lo cual hace que se requieran 2 lecturas de memoria para llegar a
la memoria deseada33 . Para mejorar esto primero consultamos la TLB antes
de la tabla hash.
Este sistema de paginación genera dicultades para implementar memoria
compartida. Generalmente esta se implementa con múltiples direcciones lógicas
( una por proceso ) las cuales corresponden a la misma dirección física. Esto no
se puede ya que tenemos una sola dirección de pagina virtual para cada pagina
física. Una solución simple consiste en permitir que la tabla de paginas solo
contenga una única correspondencia de una dirección virtual con al dirección
física compartida. Las referencias a direcciones virtuales no asociadas darán un
fallo de pagina.
29 Estas son especialmente útiles para espacios de direcciones dispersos en los que las
referencias a memoria no son contiguas.
30 Nota del Autor : Pensando en esto , si el tamaño de la tabla de pagina siempre es
menor a la fragmentación interna promedio , todos seriamos felices , ya que no estariamos
desperdiciando espacio de memoria.
31 Ver Fig 8.17 y 8.7 , comparelas
32 Ver Sección 8.4.2
33 Nota del Autor : Hasta ahora se presenta tan malo como la paginación estándar con
TLB
CAPÍTULO 8. MEMORIA PRINCIPAL 70

8.6. Segmentación
8.6.1. Método Básico
Ciertos inconvenientes por la incompatibilidad de la vista usuario de la
memoria y la paginación fueron evidentes a la hora de concebir la composición
en segmentos de un programa34 . Esto es claro cuando nosotros escribimos un
programa , el cual esta compuesto por un método principal y un conjunto de
metodos que son llamados por el anterior o por estos mismos de forma interna
, adicionalmente también hay estructuras de datos diversas.
La segmentación es un esquema de gestión de memoria que soporta esta
vision. El espacio lógico de direcciones es una colección de segmentos y en cada
segmento tiene un nombre y una longitud. Las direcciones especican tanto
el nombre del segmento como el desplazamiento dentro de ese segmento. Por
simplicidad los segmentos están numerados y se hace referencia a ellos mediante
ese numero35 .

8.6.2. Hardware
Deberemos denir una implementación para mapear las direcciones bidi-
mensionales denidas por el usuario sobre las físicas. Este mapeo se lleva a
cabo mediante una tabla de segmentos. Cada entrada de la tabla de segmentos
tiene 2 direcciones36 :
Dirección Base : La dirección física inicial del segmento.
Dirección Limite : La longitud de este.
La tabla de segmentos es una matriz de parejas de registros base-limite.

8.7. Resumen
Lo primero a tener en cuenta para determinar el algoritmo de gestión de
memoria es el hardware proporcionado , ya que ciertas comprobaciones de
seguridad y otras acciones no pueden implementarse ecientemente mediante
software.
Para seleccionar un algoritmo debemos tener en cuenta las siguentes con-
sideraciones :
Soporte Hardware : Un simple registro base o una pareja de registros
base-limite alcanza para la partición simple y múltiple. Mientras que para
la paginación y la segmentación se necesitan tablas de mapeo.
Rendimiento : La complejidad del algoritmo afecta el tiempo requerido
para acceder a la memoria física, lo cual aumenta el tiempo de context-
switch. Para los sistemas simples solo necesitamos realizar operaciones
de comparación o de sumas con la dirección lógica , mientras que en la
paginación y segmentación puede ser igual de rápida si utiliza registros de
34 Ver Fig 8.18
35 Nota del Autor : Véase que el compilador gcc con -S te devuelve código Assembler , en
el que vemos la segmentación de las funciones y otras partes de código.
36 Ver Fig 8.19 y 8.20
CAPÍTULO 8. MEMORIA PRINCIPAL 71

alta velocidad para las tablas de mapeo. Pero si las tablas se encuentran
en memoria , los accesos a memoria de usuario se vuelven mucho mas
lentos. Un TLB puede reducir esto.

Fragmentación : Altos niveles de multiprogramación requiere tener


muchos procesos en memoria. Para eso se necesita reducir la memoria
desperdiciada por fragmentación. Los esquemas de unidades jas presen-
tan fragmentación interna mientras lo de variables es externa.

Reubicación : Para solucionar la fragmentación externa utilizamos la


compactación. Este implica mover un programa a otro sector de la memo-
ria sin que este lo note. Esto requiere que las direcciones lógicas se
reubiquen dinámicamente únicamente en tiempo de ejecución.

Intercambio : Podemos añadir mecanismos de intercambio a cualquier


algoritmo. En estos se copia a almacenes de respaldo los procesos en
memoria , para luego restablecerlos a esta.

Compartición : Otro medio de incrementar el nivel de multiprogra-


mación consiste en compartir el código y los datos entre diferentes usuar-
ios. Esta requiere el uso de mecanismos como paginación o segmentación.
De esta manera los procesos que utilicen los mismos recursos read-only
no tendrán necesidad de tener una copia personal , sino que sera una sola
para todos a la cual accederán concurrentemente.

Protección : En paginación y segmentación , las secciones de un pro-


grama de usuario pueden declararse como read-only o read-write. Esta
restricción es necesaria para el código o los datos compartidos y resulta
útil en cualquier caso para proporcionar un mecanismo simple de compro-
bación en tiempo de ejecución , con el que evitar errores de programación
comunes.
Capítulo 9

Memoria Virtual

9.1. Fundamentos
Los algoritmos de gestión de memoria resulta necesarios debido a que las
instrucciones que se estén ejecutando debe estar en la memoria física. El primer
enfoque consiste en colocar el espacio completo de direcciones lógicas dentro
de la memoria física. Los mecanismos de carga dinámica pueden ayudar a
aliviar esta restricción, pero requieren que el programador tome precauciones
especiales y lleve a cabo trabajo adicional.
Este requisito parece razonable , pero también poco deseable ya que limita el
tamaño de los programas. En muchos casos no es necesario tener el programa
completo para poder ejecutarlo. Algunas partes que podrían despreciarse de
este serian :
El código para tratar condiciones de error poco usuales.
Matrices , listas y otras estructuras de datos no que han sido sobre di-
mensionadas, pueden encontrarse de forma parcial.
Incluso de necesitarse todo el programa , puede suceder que no todo el programa
sea necesario al mismo tiempo.
El ejecutar parcialmente un programa nos entrega las siguientes ventajas :
Los programas no estarían restringidos a la memoria física.
Al ocupar menos memoria , se puede ejecutar mas programas aumentando
el nivel de multiprogramación.
Se necesitarían menos operaciones I/O para cargar o intercambiar los
procesos. Esto disminuye el tiempo de context-switch.
La memoria virtual incluye la separación de la memoria lógica con respecto de
la física , lo cual evita la restricción de memoria1 .
El espacio de direcciones virtuales de un proceso hace referencia a la
forma lógica de almacenar un proceso en la memoria , la cual consiste en que
el proceso comienza en una cierta dirección lógica y esta almacenada de forma
contigua en la memoria2 .
1 Ver Fig 9.1.
2 Ver Fig 9.2.

72
CAPÍTULO 9. MEMORIA VIRTUAL 73

Los tipos de espacios de direcciones que contiene agujeros entre la pila y


el cumulo son conocidos como espacios de direcciones dispersos. Estos
segmentos pueden rellenarse con la pila y el cumulo o pueden ser usados para
montar dinámicamente bibliotecas.
Además la memoria virtual también permite compartir archivos y la memo-
ria mediante mecanismos de compartición de paginas3 . Esto proporciona las
siguientes ventajas :

Las bibliotecas del sistema pueden ser compartidas por numerosos pro-
cesos. Las bibliotecas se mapean en modo de read-only.

Permite a los procesos compartir memoria. Esos consideraran esa parte


compartida como parte de su región de memoria virtual , aunque las
paginas físicas no sean así4 .

Permite que se compartan paginas durante la creación de procesos medi-


ante la SYSCall fork , acelerando la tarea de creación de procesos.

9.2. Paginación bajo demanda


Esta consiste en cargar inicialmente las paginas únicamente cuando sean
necesarias. Solo se cargan cuando las solicita el programa. Ese es similar a
sistema de paginación con intercambio en el que los procesos residen en memoria
secundaria5 .
Cuando queremos ejecutar un proceso se realiza un intercambio para car-
garlo en memoria , siendo este intercambio un paso de solo las paginas que vaya
a ser necesarias. Este método se lo conoce como intercambio perezoso.

9.2.1. Conceptos Básicos


Necesitamos algún tipo de soporte hardware para distinguir entre las pag-
inas en memoria y las en disco. Podemos usar para este propósito el esquema
de bit valido-invalido6 . La única diferencia sera que cuando el bit esta con-
gurado como invalido querrá decir que o bien la pagina no es valida o bien
es valida pero esta en disco7 . El acceso a una pagina invalida provocara una
interrupción de fallo de pagina. El procedimiento para tratar el fallo de
pagina es8 :

1. Comprobamos una tabla interna correspondiente al proceso , para deter-


minar si la referencia era una acceso de memoria valido o invalido.

2. Si la referencia era invalida terminamos el proceso. Si era valida pero esa


pagina todavía no ha sido cargada , la cargamos en memoria.

3. Buscamos un marco libre.


3 Sección 8.4.4
4 Ver Fig 9.3
5 Ver Fig 9.4.
6 Sección 8.4.4.
7 Ver Fig 9.5
8 Ver Fig 9.6
CAPÍTULO 9. MEMORIA VIRTUAL 74

4. Ordenamos una operación de disco para leer la pagina deseada en el


marco recién asignado.

5. Una vez completada la lectura de disco , modicamos la tabla interna que


se mantiene con los datos de proceso y la tabla de paginas para indicar
que dicha pagina se encuentra ahora en memoria.

6. Reiniciamos la instrucción que fue interrumpida. El proceso podrá ahora


acceder a la pagina como si siempre hubiera estado en memoria.

En el caso extremo , podríamos empezar a ejecutar un proceso sin ninguna


pagina en memoria. Esto provocaría que el proceso generara fallos de pagina ,
hasta que todas las paginas requeridas se encontraran en memoria. Este esque-
ma seria una paginación bajo demanda pura : nunca cargar una pagina en
memoria hasta que sea requerida.
Algunos programas podrían acceder a varias nuevas paginas con cada eje-
cución de una instrucción lo que provocaría múltiples fallos de pagina , lo que
degradaría inaceptablemente el sistema. Afortunadamente este comportamien-
to es bastante improbable. Los programas tienden a tener una localidad de
referencia , que se describirá en la Sección 9.6.1.
El hardware necesario para soportar la paginación bajo demanda es el mis-
mo que para los mecanismos de paginación e intercambio9 , una tabla de pag-
inas y una memoria secundaria.
Un requisito fundamental es la necesidad de poder reiniciar cualquier in-
strucción despues de un fallo de pagina. Este requisito se satisface volviendo a
extraer la instrucción que provoco el fallo de pagina.
La principal dicultad surge cuando una instrucción puede modicar varias
ubicaciones diferentes. Ciertas operaciones de transferencia o movimiento de
datos10 provocan esta situación al ser interrumpidas en la mitad de la op-
eración.
Este problema puede resolverse de 2 formas :

Asegurarse que los fallos de paginas se produzcan en momentos seguros


donde la instrucción puede reiniciarse sin provocar luego otro fallo de
pagina11 .

Utilizar registros temporales para las transferencias. Si un fallo de pagina


se produce , se vuelven a escribir los antiguos valores antes de que se
produzca la interrupción. Esto permite poder repartir la instrucción.

El mecanismo de paginación se añade entre la CPU y la memoria , esto debe


ser completamente transparente para el usuario. Por ello todos tendemos a
pensar que se puede añadir a cualquier sistema un mecanismo de paginación.
En ciertos casos esto no es cierto para la paginación bajo demanda ya que los
fallos de pagina no representan necesariamente errores fatales.
9 Ver Sección 8.2 y 8.4
10 En el caso de que las direcciones de memoria de envió y destino estén solapadas en una
cierta intersección.
11 Nota del Autor : Por ejemplo , para copiar datos , las 2 series de direcciones deben
estar cargadas en memoria y marcadas para la transferencia, evitando así cualquier fallo de
pagina.
CAPÍTULO 9. MEMORIA VIRTUAL 75

9.2.2. Rendimiento de la Paginación Bajo Demanda


Para la mayoría de los sistemas , el tiempo de acceso a memoria ma , va
de 10 a 200 na. Mientras no tengamos fallos de pagina, el tiempo de acceso
efectivo sera igual sera igual a tiempo de acceso a memoria. Si se produce un
fallo de pagina , deberemos primero leer la pagina relevante desde disco y luego
acceder a la pagina deseada.
Sea p la probabilidad de que se produzca un fallo de pagina ( 0 ≤ p ≤ 1
). Cabe esperar que p este próxima a cero. El tiempo de acceso efectivo sera
entonces :

tiempo de acceso ef ectivo = (1 − p) × ma + p × tiempo de f allo de pagina


Para calcularlo debemos conocer cuanto tiempo se requiere para dar servicio a
un fallo de pagina. Cada fallo tiene estos componentes principales :

1. Servir la interrupción de fallo de pagina.

2. Leer la pagina.

3. Reiniciar el proceso.

La primera y la tercera pueden reducirse codicando cuidadosamente el soft-


ware , la segunda depende de los dispositivos de almacenamiento. Si hay una
cola de procesos esperando a que el dispositivo les de servicio tendremos que
añadir el tiempo de espera en cola.
Las operaciones I/O de disco dirigidas al espacio de intercambio son mas
rápidas que las del Filesystem, por que el de intercambio tiene bloques mas
grandes y no utiliza ni mecanismos de busqueda ni asignación indirecta , por lo
que generalmente los procesos se copian a estos espacios para mejorar su tasa
de transferencia.
Otra opción es demandarlas del Filesystem pero escribirlas en el espacio de
intercambio a medida que se las sustituye , leyendose solo del Filesystem la
primera vez , solo las que paginas que sean necesarias.
Algunos sistemas tratan de limitar la cantidad de espacio de intercambio
usado mediante mecanismos de paginación bajo demanda de archivos binarios.
Estas se cargan directamente desde el Filesystem , pero cuando hace falta
sustituir paginas , estos marcos puede simplemente sobreescribirse12 para luego
volverse a leer del Filesystem. De esta manera el FileSystem se utiliza como
almacén de respaldo.

9.3. Copia durante la Escritura


( Copy-on-Write , CoW )
La creación de un proceso mediante la SYScall fork puede inicialmente evi-
tar que se tenga que cargar ninguna pagina , utilizando una técnica similar a la
de compartición de paginas13 , cual permite crear rápido procesos y minimizar
el numero de nuevas paginas que se les debe asignar a estos.
12 Ya que nunca fueron modicados.
13 Ver Sección 8.4.4.
CAPÍTULO 9. MEMORIA VIRTUAL 76

La técnica CoW14 funciona permitiendo que los procesos padre e hijo com-
partan inicialmente las mismas paginas. Estas paginas compartidas se marcan
como paginas CoW , lo que signica que cualquier proceso escribe en una de
las paginas compartidas , se creara una copia de ella la cual sera mapeada en
el espacio de direcciones del proceso que intento modicarla , el cual modi-
cara esta copia y no la original15 . La técnica CoW es común en varios SO's ,
incluyendo Windows XP , Linux y Solaris.
Es importante jarse la ubicación desde la que se asignara la pagina libre
donde se realizara la copia. Muchos SO's proporcionan un conjunto com-
partido de paginas libres para estos casos. Esas se suelen asignar cuando la
pila o el cumulo del proceso deben expandirse o para realizar CoW. Los SO's
asignan esas paginas utilizando una técnica denominada relleno de ceros ba-
jo demanda. Esta técnica llena de ceros las paginas antes de asignarlas para
eliminar el contenido previo.

9.4. Sustitución de Páginas


Hemos realizado la mala suposición de que para cada pagina solo se produce
como mucho un fallo, el cual es el de la primera vez que se hace referencia. Si in-
crementamos nuestro grado de multiprogramación , estaremos sobreasignando
la memoria.
Es posible sin embargo , que cada uno de estos procesos , para un deter-
minado conjunto de datos , trate repentinamente de utilizar todas las paginas
sobreasignadas. Además los bueres de E/S también consumen una cantidad
signicativa de memoria. Algunos sistemas asignan porcentajes jos de memo-
ria para estos buferes , mientras otros permiten que los procesos de usuario y
el subsistema de E/S salida compitan por la memoria.
La reasignación de memoria se produce cuando , por ejemplo , se esta ejecu-
tando un proceso de usuario y se produce un fallo de pagina. El SO determina
donde reside la pagina deseada dentro del disco y entonces se encuentra con
que no hay ningún marco libre en la lista de marcos libres16 .

9.4.1. Sustitución Básica de Páginas


Veamos con se implementa en la rutina de servicio del fallo de pagina este
mecanismo de sustitución17 :
1. Hallar la ubicación de la pagina deseada dentro del disco.
2. Localizar un marco libre :

a) Si hay uno , usarlo.


b) Si no hay , utilizar un algoritmo de sustitución de paginas para
seleccionar la víctima.
c) Escribir el marco víctima en disco; cambiar las tablas de paginas y
de marcos correspondientes para reejar su ausencia.
14 Nota inutil : This CoW doesn't have Super CoW Powers :P.
15 Ver Fig 9.7 y 9.8.
16 Ver Fig 9.9.
17 Ver Fig 9.10.
CAPÍTULO 9. MEMORIA VIRTUAL 77

3. Leer la pagina deseada y cargar en el marco recién liberado ; cambiar las


tablas de paginas y de marcos.
4. Reiniciar el proceso de usuario.
Al requerir de dos transferencias de memoria , en caso de no haya marcos libres
, el tiempo de servicio del fallo de pagina se incrementa junto con el tiempo
efectivo de acceso.
Podemos reducir el trabajo adicional usando un bit de modicación llamado
bit sucio. Este bit es activado por hardware cada vez que se escribe un byte
de la pagina. Con este bit a seleccionar una pagina a sustituir sabremos si hace
falta escribirla en disco si es que su bit sucio esta activo. De esta manera nos
evitamos escribir todas las paginas de solo lectura y las que todavía no fueron
escritas18 .
Requerimos entonces de 2 algoritmos : el algoritmo de asignación de
marcos y el algoritmo de sustitución de paginas.
Podemos evaluar algoritmos ejecutando una cadena concreta de referencia
de memoria y calculando los fallos de pagina , la cual es conocida como cadena
de referencia. Podemos generarla articialmente o podemos obtener una traza
de un sistema determinado y registrar las direcciones de cada referencia de
memoria. Esta ultima opción produce gran cantidad de datos inútiles los cuales
se pueden reducir teniendo en cuenta que tan solo necesitamos considerar el
numero de pagina y que si tenemos una referencia a dicha pagina , todas las
referencias inmediatamente 19 siguientes a esta pagina nunca provocaran un
fallo de pagina.
Podemos considerar que a medida que se aumenta el numero de marcos
disponibles , el numero de fallos decae hasta un nivel mínimo20 .

9.4.2. Sustitución de Páginas FIFO


Este algoritmo asocia con cada pagina el instante en que dicha pagina fue
cargada en memoria. Cuando hace falta sustituir una , elige la mas antigua. Se
utiliza una cola fo para almacenar todas las paginas en memoria.
Este algoritmo sufre de la anomalía de Belady : la tasa de fallos aumenta
al incrementarse el numero de marcos asignados21 .

9.4.3. Sustitución Óptima de Paginas


Este fue uno de los resultados del descubrimiento de la anomalía de Belady
y consiste un algoritmo de sustitución tal que la tasa mas baja de fallos de
pagina entre todos los algoritmos y que nunca este sujeto a la anomalía de
Belady. Dicho algoritmo existe , denominado OPT o MIN , el cual consiste en
sustituir la pagina que no vaya a ser utilizada durante el periodo de tiempo mas
largo. Como vemos esto es imposible ya que tendríamos que predecir sucesos
futuros22 . Por esto este algoritmo solo se utiliza con propósitos comparativos.
18 Nota del Autor : Veremos que en ciertos algoritmos de sustitución esto es muy tenido
en cuenta a la hora de elegir un candidato.
19 Nota del Autor : Esto puede ser asegurado si son contiguas.
20 Ver Fig 9.11.
21 Ver Fig 9.13.
22 Nos hemos encontrado con una situación similar en el algoritmo SJF de planicación de
CPU en la Sección 5.3.2.
CAPÍTULO 9. MEMORIA VIRTUAL 78

9.4.4. Sustitución de Páginas LRU ( Least-Recently-Used )


Este algoritmo utiliza el pasado reciente determinar la pagina que menos a
sido usada durante un largo periodo y sustituirla.23 . Vemos que este es el pen-
samiento inverso al del algoritmo óptimo. Este algoritmo no sufre la anomalía
de Belady.
El principal problema es la implementación , el cual consiste en determinar
un orden para los marcos denidos por el instante correspondiente al ultimo
uso. Existen 2 posibles implementaciones :

Contadores : Cuando se realiza una referencia a una pagina , se copia el


contenido del registro de reloj en el campo de tiempo de uso de la entra-
da de la tabla de paginas correspondiente a dicha pagina. Este esquema
requiere realizar una busqueda en al tabla de paginas para localizar la
pagina menos recientemente utilizada y realizar una escritura en memo-
ria para cada acceso a memoria. Los tiempos deben también mantener
cuando se modiquen las tablas de paginas. Así mismo es necesario tener
en cuenta el desbordamiento del reloj.

Cola Doblemente Enlazada : Cada vez que se hace referencia a una


pagina , se extra esa de la pila y se la coloca en la parte superior24 . La
pagina mas reciente siempre se encuentra en la cabeza y la menos reciente
en la cola. Luego la eliminación de una pagina y su reubicación en la
parte superior requiere como máximo la modicación de 6 punteros. Cada
actualización es algo mas cara en los contadores , pero no hay necesidad
de realizar busquedas. Es ideal para implementar LRU mediante software
o microcódigo.

Ambos pertenecen a la clase algoritmo de pila , la cual determina que el


conjunto de paginas en memoria para n marcos es siempre el subconjunto del
conjunto de paginas que habría en memoria con n + 1 marcos. Esto determina
que para el LRU si un conjunto de paginas son las mas referenciadas para
n marcos , también lo serán para n + 1 , por que continuaran estando en la
memoria.
Ninguna de las 2 implementaciones es posible sin soporte hardware mas
complejo que los registros TLB estándar , ya que la actualización de los campos
de reloj o de la pila deben realizarse para toda referencia.

9.4.5. Sustitución de paginas mediante aproximación LRU


Algunos sistemas no proporcionan soporte hardware en absoluto , por lo
que deben utilizarse otros algoritmos de sustitución de paginas. Dichos sistemas
proporcionan un bit de referencia el cual es activado por hardware cada vez que
se hace referencia a esa pagina ( para leer o escribir ). Estos bits están asociados
a cada entrada de la tabla de paginas , el cual en un principio comienza en 0.
Examinando este bit determinamos si han sido usadas estas paginas , pero no
podemos determinar un orden.
23 Ver Fig 9.15.
24 Ver Fig 9.16.
CAPÍTULO 9. MEMORIA VIRTUAL 79

9.4.5.1. Algoritmo de los Bits de referencia adicionales


Para disponer información de ordenación adicional se registran los bits de
referencia a intervalos regulares. Mantenemos un byte para cada pagina , el
cual a intervalos regulares el So se encarga de desplazar los bits de referencia
de cada pagina. Si interpretamos ese byte como enteros sin signo , la pagina
con el numero mas bajo sera la menos recientemente usada , la cual elegiríamos
para sustituir. De haber varias con el mismo valor , FIFO realiza el desempate.
En el caso extremo , ese numero puede reducirse a cero , dejando solo el
propio bit de referencia. Este se denomina algoritmo de segunda oportu-
nidad.

9.4.5.2. Algoritmo de segunda oportunidad


Si el valor del bit de referencia es 0 , sustituimos dicha pagina , si es 1 le
damos otra oportunidad , cambiamos su bit a 0 y seleccionamos la siguiente
pagina de la FIFO para chequear hasta encontrar uno que valga 0. Esto hace
que una pagina a la que se le dio segunda oportunidad no vaya a ser sustituida
hasta que todas las otras hayan sido chequeadas. Si una pagina se utiliza lo su-
cientemente frecuente entonces nunca se sustituirá ya que su bit de referencia
permanecerá activado.
Se puede implementar con una cola circular. Este algoritmo degenera en
FIFO si todos los bits están activados.

9.4.5.3. Algoritmo de segunda oportunidad mejorado


( NRU , No-Recently-Used )
Agregaremos un segundo bit , el bit de modicación . Ahora existen 4
casos posibles :

1. (0,0) : No ha referenciado recientemente ni se modicada. El mejor can-


didato

2. (0,1) : No ha referenciado recientemente pero si se modico. No tan bueno


candidato , ya que hay que escribirla en disco antes de sustituirla.

3. (1,0) : Se ha referenciado pero no se modico. Consideramos que podría


volver a usarse.

4. (1,1) : Se ha referenciado y modicado. Por la combinación de los 2


anteriores , es la peor opción.

Cada pagina pertenece a una de esas cuatro clases , de las cuales siempre
elegimos la clase mas bajo no vacía. Tenemos que recorrer varias veces la cola
antes de encontrar dicha pagina25 .
25 Nota del Autor : Seria mejor usar 4 colas e ir degradando cada una cada cierto tiempo ,
para así solo nos quedaría realizar FIFO en la cola de menor categoría no vacía. El aumento
de estructura no parece ser muy grande e inclusive el tiempo usado para mantenimiento
se compensa con la tener sustitución en tiempo constante , además la cola inferior solo
tendrá que contener una cantidad suciente hasta que se llegue a próximo intervalo donde se
recargara.
CAPÍTULO 9. MEMORIA VIRTUAL 80

9.4.6. Sustitución de Paginas Basadas en Contador


Veamos estos 2 esquemas :
LFU ( Least-Frequently-Used ) : Sustituye la pagina con el valor
mas pequeño de contador , siendo estas las menos usadas. Puede surgir
problemas cuando una pagina se utiliza con gran frecuencia durante la
fase inicial de un proceso y luego no se usa mas, lo que hará que no
sea descartada mientras que otro que podrían llegar a usarse si. Para
evitar esto se puede desplazar el contenido de contador hacia la derecha
a intervalos regulares , obteniendo así un contador de uso medio con
decrecimiento exponencial.
MFU ( Most-Frequently-Used ) : Se basa en el argumento de que la
pagina que tenga el valor de contador mas alto ya que se considera que
esta ya fue usada lo suciente , mientras que las otras puede que hayan
sido recién cargadas y no haya sido todavía usadas.
La implementación de estos resulta bastante cara y no proporciona una buena
aproximación al algoritmo óptimo.

9.4.7. Algoritmo de Buer de Paginas.


Los sistemas suelen mantener un conjunto compartido de marcos libres. Por
cada fallo de pagina seleccionamos una víctima para sustituir. Sin embargo , la
que la sustituye se lee en un marco libre extraido de el conjunto compartido ,
antes de escribir en disco a la víctima. Por lo tanto no tenemos que esperar a
que esta se descargue para reiniciar el proceso. Cuando la víctima se descargo
, su marco se añade al conjunto.
Según esto podríamos es mantener una lista de paginas modicadas. Cada
vez que el dispositivo de paginación esta inactivo , se selecciona una pagina
modicada y se le escribe en el disco , desactivando a continuación su bit de
modicada. Este esquema incrementa la probabilidad de que una pagina este
limpia . con lo que no sera necesario descargarla.
Otra posible modicación consiste en mantener un conjunto compartido de
marcos libres , pero recordando que pagina estaba almacenada en cada marco.
Puesto que el contenido de un marco no se modica despues de escribir el marco
en disco la pagina antigua puede reutilizarse directamente.

9.4.8. Aplicaciones y Sustitución de Paginas


Las aplicaciones que que acceden a los datos a través de memoria virtual
de sistema operativo tiene un rendimiento peor que si este no proporcionara
ningún mecanismo de Buer , ya que estas conocen su utilización de memoria
y de disco mejor que el So que implementa algoritmos de propósito general.
Por lo que también pasariamos por el mecanismo de buer de la aplicación.
Para evitar esto algunos SO's dan cientos programas especiales la capacidad
de utilizar una partición de disco sin tipo de estructura de datos , lo que es
conocido como disco sin formato. En este las operaciones I/O puentean todos
los servicios del sistema de archivos lo cual acelera el proceso de lectura.
Esta situación solo aparece en algunas aplicaciones , generalmente los ser-
vicios normales entregan un mejor rendimiento.
CAPÍTULO 9. MEMORIA VIRTUAL 81

9.5. Asignación de Marcos


9.5.1. Número Mínimo de Marcos
Una razón para asignar al menos un mínimo se reere al rendimiento. Esto
es así por que requerimos tener sucientes marcos como para albergar todas
las paginas a las que una instrucción puede hacer referencia.
El numero mínimo de marcos esta denido por la arquitectura informática
, ya que depende de como sea implementada la instrucción la cantidad de
referencias que esta podría afectar.
El caso peor se produce en arquitecturas que permiten múltiples niveles de
indirección , ya que una simple instrucción podría generar un referenciamiento
recursivo a direcciones indirectas26 . Para evitar esta catástrofe podemos poner
un tope al numero másico de referencias a memoria por instrucción, lo cual nos
daría el numero máximo de marcos.

9.5.2. Algoritmo de Asignación


Existen varias formas de repartir m marcos entre n procesos :
Asignación Equitativa : m/n, sin importar cuantos necesite cada pro-
ceso.
Asignación Proporcional : × m siendo si el tamaño de la
ai = si /S P
memoria virtual para el proceso pi y S = si el tamaño total requerido.
De esta manera se reparten los marcos de acuerdo a las necesidades de
cada proceso.
Esta metodología no distingue según la prioridad de los procesos. Así que de-
bería agregarse dicho parametro a la ecuación.

9.5.3. Asignación Global y Local


Global : Permite a un proceso seleccionar un marco de otro proceso
para ser adquirido y sustituido. Esta provoca que cada proceso no pueda
controlar su propia tasa de fallos de pagina , ya que depende de las
acciones de otros procesos. Esto afecta directamente su rendimiento. Esta
es generalmente la mas utilizada.
Local : Los procesos solo pueden seleccionar marcos de su conjunto
de marcos. En este caso si podemos controlar su comportamiento de
paginación , pero puede resultar perjudicial esta limitación al no poder
aprovechar paginas que no estén siendo utilizadas.

9.6. Sobrepaginación
Si el proceso no tiene suciente marcos generara rápidamente fallos de pag-
ina , provocando una alta tasa de paginación ,conocida como sobrepaginación,
lo que determina que estuvo mas tiempo paginandose que haciendo trabajo
util.
26 Signica que la instrucción referencias a una dirección indirecta , la cual referencia a
otra dirección indirecta y así sucesivamente.
CAPÍTULO 9. MEMORIA VIRTUAL 82

9.6.1. Causa de la Sobrepaginación


Este comportamiento se presenta cuando intentamos aumentar el nivel de
multiprogramación , ya que lo consideramos bajo en ese momento. A lo que
introducimos nuevos procesos con la intención de aumentar la tasa de uso de
CPU , a lo que si todos los procesos anteriores estaban en su mínimo de paginas
y no quedan sucientes paginas libres para el nuevo proceso .
A esto existen 2 escenarios posibles según el tipo de asignación :
Global : El nuevo proceso comenzara a quitarles paginas a otros pro-
cesos lo que provocara que esto también intenten recuperar sus paginas
, desencadenando una caída del nivel de multiprogramación a causa de
la propagación bacteriológica de la sobrepaginación a todos los procesos.
Como respuesta se intentara subir el nivel de multiprogramación , lo cual
desencadenara una caída aun profunda27 .
Local : El nuevo proceso sustituirá una de sus paginas , pero al no tener
mínimo necesario para ejecutar ninguna instrucción, quedara paginandose
continuamente. Esto no afectara directamente a otros procesos , pero este
ocupara continuamente el dispositivo de paginación , incrementando el
tiempo de acceso efectivo además de bloquear las paginas que le fueron
asignadas. Sin embargo la caída de la tasa de multiprogramación no están
alta como en el caso anterior.
Para evitar esto podemos utilizar una estrategia basada en el conjunto de
trabajo , examinando cuantos marcos esta utilizando realmente un proceso.
Esta técnica se dene modelo de localidad de ejecución del proceso.
Este modelo determina que los procesos durante su ejecución se desplazan
de una localidad a otra, siendo estas localidades conjuntos de paginas que se
utilizan activamente de forma combinada28 . Los programas están compuesto
por localidades diferentes , pudiendo estar solapadas.
Cuando se invoca una función esta dene una nueva localidad , al salir de la
función dejamos dicha localidad , a la cual podemos volver luego29 . Por lo tanto
las localidades están denidas por la estructura del programa y sus estructuras
de datos.
Un proceso realizara paginación bajo demanda al momento de entrar a una
localidad hasta que haya cargado esta. Vemos que tener menos paginas que la
localidad actual provoca sobrepaginación.

9.6.2. Modelo de Conjunto de Trabajo


Esta basado en la localidad de ejecución de los programas. Denimos ven-
tana del conjunto de trabajo = ∆ . Queremos examinar las ∆ referencias mas
frecuentes el cual sera el conjunto de trabajo30 . Entonces el conjunto de trabajo
es una aproximación de la localidad del programa. La precisión del conjunto
de trabajo depende de la selección del ∆. Para calcular el tamaño del conjun-
to de trabajo W SSi para cada proceso del sistema , podemos considerar que
27 Nota Inútil( a esta altura.. ) : Parece sacado de una clase intensiva de economía moderna.
28 Ver Fig 9.19.
29 Para ver como esto sucede , ver el apartado en la pagina 310 : Conjuntos de trabajo
y tasa
30
de fallo de pagina.
Ver Fig 9.20.
CAPÍTULO 9. MEMORIA VIRTUAL 83

W SSi siendo D la demanda total de marcos en el sistema. Si D > m


P
D=
siendo m el numero de marcos total entonces se producirá sobrepaginación.
Mientras D < m podremos agregar un nuevo proceso mientras este no
provoque que D > m. Si m es excedido se seleccionara un proceso para sus-
pender hasta volverlo al estado seguro. De esta manera impedimos la sobrepag-
inación y mantenemos el grado de multiprogramación al máximo posible.
Para poder aproximar el modelo del conjunto de trabajo podemos los un
bit de referencia. Le daremos un valor a∆ y realizaremos interrupciones luego
de una cantidad tr de referencias. Luego de cada interrupción copiamos y
borramos los bits de referencia , así podremos chequear examinar el rango
[∆, ∆ + tl] de referencias. Las paginas que tengan al menos un bit activado se
consideran parte del conjunto de trabajo. Para reducir la incertidumbre solo
hay que aumentar el tamaño del historial y/o aumentar la frecuencia de las
interrupciones reduciendo tr , pero esto aumentara el coste.

9.6.3. Frecuencia de Fallos de Pagina


( PFF , page-fault-frequency )
El modelo de conjunto de trabajo es una estrategia indirecta para controlar
la sobrepaginación.
La estrategia directa consiste en controlar la tasa de fallos de pagina , lo cual
se realiza estableciendo un limite superior e inferior. Cuando la tasa de fallos
de proceso aumenta le agregamos un marco31 . Si es lo contrario , liberamos
un marco de dicho proceso. De esta manera mantenemos estable la tasa de
fallos y por lo tanto evitamos la sobrepaginación. Llegado a un cierto mínimo
de marcos libres de reserva deberiamos buscar un proceso a suspender para
liberar marcos , para tener reservas en caso del aumento de tasa de fallos de
pagina de cualquier proceso.

9.7. Archivos Mapeados en Memoria


Podemos utilizar las técnicas de memoria virtual explicadas para tratar la
E/S de archivos como si fueran acceso a memoria. Esta técnica es conocida
como mapeo de memoria de un archivo , permite asociar archivos a una
parte de la memoria virtual.

9.7.1. Mecanismo Básico


Se lleva a cabo mapeando cada bloque de disco sobre una pagina de al
memoria. EL acceso inicial al archivo se produce con los mecanismos de pag-
inación bajo demanda. Se leen los datos del archivos equivalentes al tamaño
de una pagina , extrayendo los datos del sistema de archivos y depositando-
los en una pagina física. Las subsiguientes lecturas y escrituras en el archivo
se gestionan como accesos normales a memoria , simplicando el acceso y su
utilización.
31 Nota del autor : Esto solo funciona si nuestro paginador no sufre de la anomalía de
Belady
CAPÍTULO 9. MEMORIA VIRTUAL 84

Las escrituras al archivo no se transeren a disco inmediatamente. Cuando


se cierra el archivo se copian todos los cambios a disco y se elimina de la
memoria virtual del proceso.
También se proporciona CoW32 el cual permite la lectura concurrente y en
caso de que algún proceso intente modicarlo se le entrega una copia. Para
coordinar las múltiples lecturas y escrituras debemos utilizar los metodos de
exclusión mutua vistos en el Capitulo 6.

9.8. Asignación de la Memoria del Kernel


Cuando un proceso en modo usuario solicita memoria adicional , las paginas
se asignaran apartar de la lista de marcos libres mantenida por el kernel. Esta
lista suele rellenarse utilizando un algoritmo de sustitución de paginas como
hemos visto.
La memoria para el kernel puede asignarse apartar de un conjunto compar-
tido de memoria compartida que es distinta de la lista de los procesos normales.
Hay 2 razones para esto :

1. El kernel solicita memoria para estructuras de datos de tamaño variable


, algunas de las cuales tiene un tamaño inferior a una pagina. Por lo que
muchos sistemas no aplican paginación al código ni a los datos del kernel.

2. Ciertos dispositivos de hardware interactuaran directamente con memoria


física , por lo que pueden requerir memoria contigua.

9.8.1. Sistema de Descomposición Binaria ( Buddy-System )


Este sistema asigna memoria apartar de un segmento de tamaño jo com-
puesto de paginas físicamente contiguas. La memoria se asigna a partir de ese
segmento mediante un asignador de potencias de 2 que entrega cantidades
de memoria en potencias de 2. Todos los tamaños de las solicitudes son re-
dondeados a una potencia de 2 mayor.
La ventaja es la rapidez con la que se convenían los subsegmentos adyacentes
para formar segmentos de mayor tamaño mediante consolidación33 .
La desventaja es que redondea a la siguiente potencia de 2 , provocando
fragmentación interna.

9.8.2. Asignación de Franjas ( Slabs )


Un slab ( franja ) esta formado por una o mas paginas físicamente contiguas.
Una caché esta compuesta de uno o mas slabs. Existe una única cache por cada
estructura de datos del kernel. Cada cache se rellena con objetos los cuales son
instancias de la estructura de datos del kernel que corresponde a esa cache34 .
El algoritmo de asignación de franjas para almacenar objetos del kernel.
Cuando se crea una cache , se asigna a la cache un cierto numero de objetos (
están inicialmente marcado como libres ). La cantidad de objetos en la cache
dependerá del tamaño de la franja asociada.
32 Ver Sección 9.3
33 Ver Fig 9.27.
34 Ver Fig 9.28.
CAPÍTULO 9. MEMORIA VIRTUAL 85

Inicialmente todos los objetos de la cache están libres. Cuando hace falta
uno , el asignador asigna cualquiera de estos de la cache correspondiente a la
estructura de datos pedida y luego pone a dicho objeto como usado.
En Linux , un descriptor de procesos es del tipo task_struct. Cuando el ker-
nel crea una nueva tarea , solicita un objeto task_struct. En Linux se chequean
los estados de una franja , llena , vacía y parcialmente llena. El asignador de
franjas trata satisfacer los pedidos con objetos de franjas parcialmente llenas,
si no hay ninguna se usa una franja vacía. En caso de que tampoco haya se
crea una nueva franja.
Este método tiene 2 benecios claves :
1. No se pierde memoria debido a la fragmentación.
2. Las solicitudes de memoria pueden satisfacerse rápidamente. Es particu-
larmente efectivo para gestionar memoria en aquellas situaciones en que
los objetos se asignan y desasignan frecuentemente , como suele ser el
caso con las solicitudes de kernel.
El asignador de slabs apareció por primera ves en el kernel de Solaris 2.4. Debido
a su naturaleza de propósito general se usa también para ciertas solicitudes de
memoria en modo usuario. A partir de la versión 2.2 Linux adopto el sistema
de slabs.

9.9. Otras consideraciones


9.9.1. Propugnación
Se trata de evitar alto nivel de paginación inicial , intentando cargar en
memoria todas paginas que un proceso vaya a necesitar. Algunos sistemas , en
especial Solaris , prepagina los marcos de paginas para los archivos de pequeño
tamaño.
La prepaginacion puede ser muy ventajosa el coste de esta sea menor que
el coste de dar servicio a los correspondientes fallos de pagina , cual depende
de si se usan todas las paginas prepaginadas. Esto puede ser representado en
las siguientes formulas :

P I = s × (1 − α) y P F = s × α

siendo s la cantidad de paginas , α el porcentaje que se usara , y P I , P F


cantidad de paginas innecesarias y fallos de pagina respectivamente. Mientras
α sea cercana a 1 la paginación sera aconsejable.

9.9.2. Tamaño de pagina


Los tamaños de pagina son siempre potencias de 2. Uno de los aspectos que
ha que tener en cuenta es el tamaño de la tabla de paginas. Al reducir el tamaño
de las paginas aumentara el tamaño de la tabla de paginas por el aumento de
paginas. La memoria se aprovecha mejor si las paginas son pequeñas ya que
minimizamos la fragmentación interna.
Pero el tiempo requerido para leer o escribir una pagina también cuenta y
si las paginas son muy pequeñas tardaremos mas tiempo en leer o escribir ,
siendo mas rápido con tamaños mas grandes.
CAPÍTULO 9. MEMORIA VIRTUAL 86

Por otro lado paginas mas pequeñas reducen la cantidad total de E/S , ya
que se dene mejor la localidad.
En sintesis , con un tamaño de pagina mas pequeño tenemos una mejor
resolución , permitiendo usar solo la memoria necesaria.
Actualmente se utilizan paginas de tamaño grande para acelerar la lectura
y escritura.

9.9.3. Alcance de la TLB


Relacionada con la tasa de aciertos , esta métrica hace referencia a la canti-
dad de memoria accesible a partir de la TLB , el cual es el numero de entradas
multiplicado por el tamaño de pagina. La TLB debería almacenar el conjunto
de trabajo completo del proceso , sino este invertirá una considerable cantidad
de tiempo resolviendo referencias de memoria mediante la tabla de paginas.
Para incrementar el alcance de la TLB podemos aumentar el numero de
entradas , lo que aumenta en razón de 1-1 , mientras que si incrementamos el
tamaño de paginas podemos aumentar exponencialmente el alcance de la TLB.
Sin embargo esto puede conducir a mayor fragmentación.
Proporciona soporte para tamaños múltiples de pagina requiere que sea el
So el que gestione el TLB. Esto tiene un impacto en el rendimiento , pero es
compensado por el incremento en la tasa de aciertos y en el alcance del TLB.
Actualmente se emplea cada vez mas la TLB gestionada por software35 .

9.9.4. Interbloqueos de E/S


En ocasiones es necesario permitir que ciertas paginas queden bloqueadas
en memoria. Puede suceder cuando se realizan operaciones de E/S hacia o
desde la memoria de usuario (virtual). La E/S suele implantarse mediante un
procesador de E/S independiente.
Debemos asegurarnos que no se produzca la siguiente secuencia de sucesos :

Un proceso realiza una solicitud de E/S y es puesto en una cola


para dicho dispositivo de E/S; mientras , la CPU se asigna otros
procesos que provocan fallos de pagina. Uno de ellos , mediante
asignación global36 , sustituye la pagina que contiene el buer de
memoria del proceso en espera. Luego cuando el dispositivo de E/S
que se le entrego esa pagina va escribir , pisara todo lo que otro
proceso que tomo este marco tenga37 .
Una solución a esto consiste en nunca ejecutar una operación de E/S sobre la
memoria de usuario. Los datos se copian siempre entre la memoria del sistema
y la del usuario. Esto puede provocar un trabajo adicional inaceptablemente
alto.
Otra solución consiste en asociar un bit de bloqueo con cada marco. Si el
marco esta bloqueado , no podrá ser seleccionado para sustitución. Frecuente-
35 Las arquitecturas UltraSPARC ,MIPS y Alpha la gestionan por software , mientras que
PowerPC y Pentium por hardware.
36 Nota del Autor : Debo decirlo , la asignación global es el ejemplo de todos los errores.
37 Nota inutil : TfH
CAPÍTULO 9. MEMORIA VIRTUAL 87

mente parte o todo el kernel esta bloqueado en memoria ya que muchos sistemas
no soportan fallos de pagina provocados por el kernel.
Otra utilidad del bloqueo esta relacionada con la sustitución normal de
paginas , al tener en cuenta la prioridad de los procesos. Permite a los procesos
de baja prioridad protejer las paginas adquiridas , activando su bit de bloqueo
, mientras esperan para poder usarlas al menos una vez. Luego se desbloquean
, permitiendo ser tomadas por otros procesos38 .
El peligro del uso del bit de bloqueo es que puede dejar bloqueadas paginas
de forma indenida a causa de algún error de sistema. Para esto Solaris permite
que se proporcionen consejos de bloqueo , pero el SO esta libre para descartar
estos consejos cuando sea conveniente.

38 Nota del Autor : Si es que tenemos asignación global.

Das könnte Ihnen auch gefallen