*HVWLyQGH0HPRULD Para ejecutar un proceso, éste debe encontrarse en memoria principal. Como hemos visto en el capítulo anterior, para mejorar el aprovechamiento de la CPU, ésta se reparte entre varios procesos, por lo tanto, también habrá que repartir la memoria principal. A la parte del sistema operativo que se ocupa de gestionar la memoria se le denomina Gestor de Memoria. Su cometido consiste en llevar la cuenta de las partes de memoria que se están utilizando y las que están libres, así como de gestionar el trasvase de información entre la memoria principal y la secundaria cuando la memoria RAM no sea suficientemente grande para acoger a todos los procesos. En este capítulo trataremos los distintos enfoques que hay en la gestión de memoria. Los algoritmos varían desde el correspondiente a una simple máquina desnuda, hasta las estrategias de paginación y segmentación.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
,QWURGXFFLyQ\2EMHWLYRV En la evolución de la arquitectura de ordenadores, la cantidad de memoria principal ha ido creciendo. Pero el tamaño de los programas crece aún más rápido que la memoria disponible. El primer intento para resolver el problema de la limitación de memoria fue la utilización de una técnica denominada RYHUOD\V (solapamientos). Un programa con solapamientos se dividía en varias partes. Una de estas partes siempre estaba presente en memoria principal y controlaba la carga sucesiva de las demás partes (RYHUOD\V) desde la memoria secundaria a la principal. Estas otras partes se cargaban en memoria de tal forma que ocupaban la misma región de memoria en la que había residido otra parte previamente utilizada, sobreescribiendo así (solapando) a la parte anterior. Los programadores pasaron mucho tiempo dividiendo un programa en trozos, en lugar de concentrarse simplemente en la escritura del programa. Posteriormente, la necesidad de repartir la memoria entre varios usuarios redujo la cantidad de memoria para cada uno, e hizo necesaria la introducción de algún mecanismo de protección para aislar entre sí las actividades de los programas. Así, tenemos que en la gestión de memoria se deben perseguir los siguientes objetivos: • 3URWHFFLyQ Si varios procesos comparten la memoria principal, se debe asegurar que ninguno de ellos pueda modificar posiciones de memoria de otro proceso. Aunque la escritura de memoria tiene efectos más desastrosos, la lectura de memoria ajena tampoco debe estar permitida, pues cada proceso debe mantener su privacidad. Ya que muchos lenguajes de programación disponen de punteros dinámicos e indexación de vectores o matrices, las comprobaciones en tiempo de compilación no son suficientes y se requiere que el sistema de gestión de memoria realice chequeos adicionales durante la ejecución. Debe disponerse de un sistema de permisos de acceso que especifique los derechos que tiene cada proceso en el acceso a zonas de memoria de otros procesos. • &RPSDUWLPLHQWR El compartimiento de la memoria parece estar en contradicción con la protección, pero es que a menudo también es necesario que varios procesos puedan compartir y actualizar estructuras de datos comunes, por ejemplo, en un sistema de bases de datos. En otras ocasiones, lo que se requiere es compartir zonas de código, por ejemplo, en rutinas de biblioteca, para no tener en memoria distintas copias de la misma rutina. En este caso, se hace necesaria alguna protección para que un proceso no modifique inadvertidamente el código de las rutinas. • 5HXELFDFLyQ La multiprogramación requiere que varios procesos residan simultáneamente en memoria. Lo que no se puede saber antes de llevarlo a memoria es la dirección absoluta en la que se va a cargar el proceso, por lo que no es práctico utilizar direcciones absolutas en el programa. En su lugar, es preferible realizar direccionamientos relativos para permitir que un programa pueda ser cargado y ejecutado en cualquier parte de la memoria.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
• 2UJDQL]DFLyQGHODPHPRULD La memoria se debe organizar tanto física como lógicamente. Debido al coste de la rápida memoria RAM, normalmente se necesita ampliarla con memoria secundaria más barata (y más lenta), utilizando para ello dispositivos tales como discos o cintas magnéticas. Por el contrario, también puede resultar conveniente añadir memoria de acceso más rápido que la RAM principal, como es el caso de la memoria caché, en la que se mantienen los datos de acceso más frecuente. Esta jerarquía física de memorias hace necesario un sistema que controle el flujo de información entre los distintos dispositivos de almacenamiento. Esta tarea la realizaba el programador utilizando RYHUOD\V, pero consume una gran cantidad de tiempo y, debido a la reubicación dinámica, el programador no siempre sabe la cantidad y lugar de la memoria que se va a necesitar. Por esto, parece conveniente que sea el gestor de memoria el que se ocupe de esta labor. Aunque la mayoría de las memorias están organizadas como un único espacio lineal de direcciones secuenciales, que van desde 0 hasta un máximo, esto no refleja la estructura lógica de los programas, que utilizan estructuras lógicas de instrucciones y datos, tales como módulos, rutinas o procedimientos, matrices, registros, etc. Si una gestión de memoria pudiera proporcionar varios espacios de direcciones, cada estructura lógica podría ser una entidad independiente: un VHJPHQWR. Esto sería ventajoso por varias razones, pues los segmentos pueden compilarse y cargarse de forma independiente, teniendo cada uno de ellos sus propios derechos de acceso (lectura, escritura, ejecución). Un sistema simple de segmentación puede constar de sólo dos segmentos: uno para código (con permiso de ejecución) y otro para datos (con permiso de lectura y escritura). Una segmentación más sofisticada podría utilizar un segmento para cada estructura lógica.
Los sistemas de gestión de memoria pueden dividirse en dos clases: los que mueven los procesos entre memoria principal y secundaria (intercambio y paginación), y los que no lo hacen. Tanto el intercambio como la paginación son mecanismos originados por la insuficiencia de memoria principal para contener todos los procesos en memoria simultáneamente. A medida que la memoria se vaya haciendo más barata, puede que los argumentos en favor de unos u otros tipos de gestión vayan cambiando, aunque teniendo en cuenta la ley de Parkinson: “Los programas tienden a expandirse hasta llenar la memoria que los contiene”, parece difícil que puedan cambiar radicalmente. Pasemos a ver, en los siguientes apartados, las distintas técnicas de gestión de memoria.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
,QWURGXFFLyQ\2EMHWLYRV +D\TXHWHQHUHQFXHQWDTXH
+/RVSURJUDPDVFUHFHQPXFKR 6HQHFHVLWD PXFKDPHPRULD
+/RVVLVWHPDVVXHOHQPXOWLXVXDULR
/D*HVWLyQGH0HPRULDGHEHSURSRUFLRQDU 33527(&&,Ð13HUPLVRVGH$FFHVR 3&203$57,0,(172GHODLQIRUPDFLyQ 35(8%,&$&,Ð1SDUDVLVWHPDVPXOWLSURJUDPDGRV 325*$1,=$&,Ð1'(/$0(025,$ Ã )tVLFD -HUDUTXtDV Ã /yJLFD 6HJPHQWDFLyQ 23&,21(6
6LQ,QWHUFDPELR Ï0RQRSURJUDPDFLyQ Ï0XOWLSURJUDPDFLyQ FRQ3DUWLFLRQHV)LMDV
&RQ,QWHUFDPELR Ï0XOWLSURJUFRQ 3DUWLFLRQHV9DULDEOHV Ï0HPRULD9LUWXDO 3DJLQDFLyQ6HJPHQWDFLyQ
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULDVLQ,QWHUFDPELR En los sistemas de gestión de memoria sin intercambio, la idea básica consiste en cargar el programa a ejecutar en algún lugar de la memoria principal, donde permanece hasta que finaliza su ejecución, momento en el que abandona el espacio de memoria utilizado. Veamos a continuación las técnicas más habituales para los diferentes modelos de programación sin intercambio de memoria.
0RQRSURJUDPDFLyQ El esquema de memoria más simple consiste en mantener la memoria ocupada con un único proceso. Cuando se carga un programa que se hace cargo de toda la memoria y del control completo de la máquina, se dice que el programa se carga sobre una PiTXLQDGHVQXGD, es decir, una máquina en la que solamente se ofrece el hardware puro, sin ninguna ayuda software que lo recubra. Las máquinas desnudas se utilizaron de forma general hasta principios de los años 60, y actualmente se suelen utilizar en sistemas empotrados de control. Más tarde, a las máquinas desnudas se les añadió una especie de sistema operativo muy rudimentario al que se le denominó PRQLWRU. Las funciones que ofrecía el monitor eran las estrictamente necesarias para cargar un programa en memoria y controlar su ejecución, todo de forma muy manual y con una completa supervisión del usuario. La técnica actual más utilizada en los pequeños ordenadores es la que se muestra en la parte inferior de la Figura 3. La memoria está ocupada por el sistema operativo, que suele estar en RAM, y por el cargador inicial del sistema operativo (IPL) y los GULYHUV de dispositivos, que suelen estar en memoria ROM. El resto de la memoria RAM queda disponible como área de usuario. Cuando el sistema está organizado de esta manera (por ejemplo MS-DOS), sólo se ejecuta un proceso a la vez. El usuario teclea un comando en el terminal, y el sistema operativo carga el programa correspondiente de disco a memoria principal y lo ejecuta. Cuando el programa termina, el sistema queda a la espera de que el usuario escriba otro comando, para cargar otro programa en la misma zona de memoria que el anterior.
0XOWLSURJUDPDFLyQFRQ3DUWLFLRQHV)LMDV Ya que, en general, es deseable que haya varios procesos de usuario residiendo en memoria al mismo tiempo, se hace necesario considerar el problema de cómo asignar memoria disponible a varios de los procesos que están en la cola de espera para ser traídos a memoria principal. Lo más inmediato y simple es dividir la
$SXQWHVGH62,
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULDVLQ,QWHUFDPELR 1RKD\WUDVLHJRGHSURFHVRV HQWUHPHPRULDSULQFLSDO\VHFXQGDULD 0212352*5$0$&,Ð1 8Q8VXDULR
8Q3URFHVR
3URJUDPDV 3HTXHxRV
0iTXLQD'HVQXGD (OXVXDULRHVUHVSRQVDEOHGHWRGRORTXHVHFDUJDHQPHPRULD 5$0
3URJUDPDGHXVXDULR 6LVWHPD2SHUDWLYR &RQWURODGRUHV
&RQ6LVWHPD2SHUDWLYR5HVLGHQWH PiVFRQWURODGRUHVGHGLVSRVLWLYRV
,3/'ULYHUV 3URJUDPD GHXVXDULR
3URJUDPD GHXVXDULR
0RQLWRU
62
6LVWHPDV2SHUDWLYRV,
520
5$0
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
memoria en Q particiones (posiblemente de distinto tamaño), de tal forma que en cada partición se mete un proceso, donde permanece hasta que finaliza su ejecución. Una vez terminado el proceso, la partición queda libre para acoger a un nuevo trabajo. Con la llegada de la multiprogramación y este esquema de memoria aparecen algunos problemas y cuestiones que deben solventarse, como la planificación de procesos a largo plazo, la determinación del número y tamaño de las particiones, la ubicación de los programas y la protección de las particiones de memoria. • 3ODQLILFDFLyQ Un esquema posible para la planificación de procesos a largo plazo, o sea, para seleccionar los procesos que van cargarse en memoria para ser ejecutados, puede consistir en que cada una de las particiones tenga una cola asociada, de tal manera que en cada una de ellas se van encolando los trabajos o procesos dependiendo del espacio de memoria requerido. Cuando hay que cargar un trabajo, se le pone en la cola de entrada de la partición más pequeña en la que quepa. Ya que en este esquema las particiones son de tamaño fijo preestablecido, cualquier espacio de una partición no utilizado por el proceso cargado, se desaprovecha. La desventaja de meter en colas los trabajos según su tamaño se hace patente cuando la cola de una partición grande está vacía, pero la cola de una partición pequeña tiene muchos trabajos. Una solución consiste en tener una única cola (Ver la Figura 4-inferior). Cuando una partición se queda libre, el primer trabajo de la cola que quepa en esa partición, se carga en ella y se ejecuta. Ya que no es deseable malgastar una partición grande con un trabajo pequeño, una alternativa puede ser el recorrer la cola entera y elegir el trabajo más grande que se pueda cargar en la partición que acaba de quedar libre. No obstante, esto supone una discriminación de los trabajos con pocos requisitos de memoria, que no son merecedores de tener una partición grande, mientras que normalmente a los trabajos pequeños se les suele dar el mejor servicio, no el peor. Para solucionarlo, se suele disponer siempre de alguna partición de poco tamaño, para permitir que los pequeños trabajos también se ejecuten sin necesidad de asignarles grandes particiones de memoria. No obstante, debe tenerse en cuenta que un proceso que requiera poca memoria y mucha CPU puede entonces formar una larga cola de espera por su partición. • 7DPDxRGHODVSDUWLFLRQHV El tamaño de cada una de las particiones lo puede establecer el operador en el momento de arranque del sistema o figurar en algún fichero de configuración del sistema. Como se puede ver, el grado de multiprogramación está directamente ligado al número de particiones del sistema. A más particiones, más procesos cargados y,
$SXQWHVGH62,
*HVWLyQGH0HPRULD
6LQ,QWHUFDPELR
0XOWLSURJFRQ3DUWLFLRQHV)LMDV
5HVXOWD~WLOWHQHUYDULRVSURFHVRV VLPXOWiQHDPHQWHHQPHPRULD
5$0 .E
6HGLYLGHODPHPRULD HQQSDUWLFLRQHV .E .E 62 3HURVHGHEHQWHQHUHQ FXHQWDDOJXQDVFXHVWLRQHV
33ODQLILFDFLyQGHSURFHVRV 37DPDxRGHODSDUWLFLyQ 35HXELFDFLyQ 33URWHFFLyQ
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
3ODQLILFDFLyQGH3URFHVRV
3DUWLFLRQHV)LMDV
+0~OWLSOHV&RODVGH(QWUDGD
4
.E
4
.E
4
.E 62
(O3UREOHPDGHODV&RODV9DFtDV£
+8QDÓQLFD&RODGH(QWUDGD
.E
4
.E .E 62
6LVWHPDV2SHUDWLYRV,
$SXQWHVGH62,
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULD
por lo tanto, mayor aprovechamiento de la CPU. Así, cuanta más memoria se desperdicie, menos procesos se podrán tener en memoria. Para lograr el mayor aprovechamiento de la memoria los procesos deberían cargarse en particiones cuyo tamaño se ajuste lo más posible (por exceso) al del proceso. Por una parte tenemos que si dividimos la memoria del sistema en muchas particiones pequeñas, puede dar lugar a que algunos programas grandes no puedan cargarse en memoria aunque haya suficiente memoria disponible, si ésta no se encuentra adyacente en una única partición, dando lugar, en este caso, a una IUDJPHQWDFLyQ H[WHUQD de la memoria. Si por el contrario, se dispone de unas pocas particiones grandes, los programas pequeños desaprovecharán la memoria sobrante de la partición que ocupen, lo que da lugar a una IUDJPHQWDFLyQLQWHUQD. Los tamaños de las particiones suelen establecerse después de un estudio sobre los tamaños habituales de los programas que se ejecutan en cada máquina.
7 D P D x R G H OD 3 D U W LF Ly Q
3 D U W LF LR Q H V ) LM D V
¢ & X i Q W D V 3 D U W LF LR Q H V " ¢ ' H T X p 7 D P D x R "
1 LY H OG H P X OW LS U R J U D P D F Ly Q
$ S U R Y H F K D P LH Q W R G H OD & 3 8
$ P D \ R U Q ~ P H U R G H
0 D \ R U Q LY H OG H
S U R F H V R V H Q P H P R U LD
P X OW LS U R J U D P D F Ly Q
$ P i V P H P R U LD
0 H Q R V S U R F H V R V
G H V S H U G LF LD G D
H Q P H P R U LD
3 D U W LF Ly Q 3 H T X H x D
) U D J P H Q W D F Ly Q ( [ W H U Q D
3 D U W LF Ly Q * U D Q G H
) U D J P H Q W D F Ly Q , Q W H U Q D
+
( OW D P D x R G H OD V S D U W LF LR Q H V G H S H Q G H G H OR V U H T X LV LW R V G H OR V S U R J U D P D V K D E LW X D OH V
6 LV W H P D V 2 S H U D W LY R V ,
1 R U P D OP H Q W H
* H V W Ly Q G H 0 H P R U L D
$SXQWHVGH62,
*HVWLyQGH0HPRULD
• 5HXELFDFLyQGHSURJUDPDV Cuando se monta o enlaza un programa compuesto por diferentes módulos, todos ellos se combinan en un único módulo cargable, en el que las referencias a sus objetos locales (rutinas o datos) son direcciones que van desde cero hasta la correspondiente al tamaño del módulo. Así, si el módulo se carga en la dirección cero de memoria principal, se ejecutará correctamente, pero no si se carga en cualquier otra dirección. Según esto, habría que decirle al montador en qué dirección se va a cargar ese programa. Pero observando cualquiera de los esquemas de la Figura 7, se puede ver que cada proceso se puede cargar en cualquier dirección de memoria, y no hay forma de conocerla hasta llegar el momento de su ejecución. Supongamos que la primera instrucción de un programa es una llamada a un procedimiento en la dirección 100 dentro del fichero binario producido por el montador. Si este programa se carga en la partición 1, esa instrucción saltará a la dirección absoluta 100, la cual, muy posiblemente, corresponderá a un área del sistema operativo. Lo que se necesita es que la instrucción salte a la dirección Base_Partición + 100. Así, siempre se realizará la llamada correcta, independientemente de la partición en la que se cargue el programa. Una posible solución consiste en modificar las direcciones del programa a medida que se carga en memoria. O sea, que a los programas que se carguen en la partición 1 se les añadirá Base_Partición_1 a todas las direcciones a las que haga referencia, Base_Partición_2 a los que se carguen en la partición 2, etc.; siendo la base de la partición, su dirección de comienzo. Para realizar esto, el montador debe incluir en el módulo cargable una lista con las direcciones relativas del programa en las que hay bytes o palabras cuyo contenido son referencias a memoria, de tal forma que puedan ser reubicadas en la carga. Con esta solución se consiguen programas HVWiWLFDPHQWH UHXELFDEOHV (reubicables en tiempo de carga), pero tienen una pega: “No permite que un programa cargado en una partición pueda moverse a otra distinta antes de finalizar su ejecución, lo cual resulta de gran utilidad, como veremos posteriormente.” Este problema se solventan con un poco de ayuda del hardware, tal como se describe en la parte superior de la Figura 7. Consiste en equipar al procesador con un nuevo registro: el 5HJLVWUR %DVH. Cuando un proceso es seleccionado para pasar a ejecución, el registro base se carga con la dirección de la partición que contiene al proceso en cuestión. Una vez que el proceso está en ejecución, a cada referencia a memoria se le añade automáticamente el contenido del registro base, generando así la dirección definitiva, sin necesidad de modificar el contenido del módulo ejecutable, ni en la carga ni durante la ejecución. A este sistema en el que ya sí se permite ejecutar programas en cualquier partición de memoria e incluso cambiarlos de partición durante su ejecución, se le conoce como sistema de UHXELFDFLyQGLQiPLFD.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
Solucionado el asunto de la reubicación, debemos afrontar un último problema, pues el sistema de gestión de memoria que hemos visto hasta ahora no asegura que un programa no pueda construir una instrucción en la que referencie cualquier dirección de memoria y acceda ilegalmente a datos de áreas no autorizadas. Esto se resuelve con un “sistema de protección”. • 3URWHFFLyQ El sistema de protección se encarga de evitar el acceso indiscriminado a cualquier área de memoria. A partir del sistema de reubicación dinámica resulta fácil limitar las referencias a memoria que puedan hacerse desde un programa. Para ello añadimos un registro más al procesador: el 5HJLVWUR/tPLWH. En la parte inferior de la Figura 7 vemos que para evitar un acceso indiscriminado o por error a cualquier dirección de memoria, lo que se hace es cargar el registro límite al mismo tiempo que el registro base. El registro límite expresa el tamaño del programa en ejecución. Cada dirección virtual (relativa) a la que se hace referencia en el proceso se compara con el valor del registro límite, de tal forma que si tal dirección es menor que el registro límite, quiere decir que hace referencia a una posición dentro del programa, por lo que simplemente hay que añadirle el contenido del registro base para obtener la dirección efectiva. (También puede realizarse la comprobación del registro límite paralelamente a la suma del registro base). Si por el contrario, la dirección virtual excede al valor del registro límite, indica que se intenta un acceso a una posición de memoria fuera del área ocupada por el programa, en cuyo caso habrá que generar la correspondiente excepción de (UURUGH'LUHFFLRQDPLHQWR para darle el tratamiento pertinente. El tamaño que ocupa un proceso suele venir indicado en el módulo cargable, y puede guardarse en el BCP de cada proceso. El uso de las particiones fijas es prácticamente nulo hoy día. Como ejemplo de un sistema operativo con esta gestión de memoria tenemos al antiguo OS/MFT, utilizado por los grandes ordenadores de IBM.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
5HXELFDFLyQ\3URWHFFLyQ
3DUWLFLRQHV)LMDV
+5(8%,&$&,Ð1 /DVGLUHFFLRQHVGHXQSURJUDPDVXHOHQVHU UHODWLYDVDVXGLUHFFLyQGHFRPLHQ]R 3(52DOPRQWDUXQSURJUDPDQRVHVDEH GyQGHVHYDDHMHFXWDU 62/8&,21(6
30RGLILFDFLyQGHGLUHFFLRQHVGXUDQWHODFDUJD 35HJLVWURGH5HXELFDFLyQRUHJLVWUREDVH SHUPLWHUHXELFDFLyQGLQiPLFD 6000
1345
A
'LU 9LUWXDO
4418
0
0
'LUHFFLyQ 5HDO
B
4200
747
5HJLVWURGH 5HXELFDFLyQ 5D 5E 5F
5166
B
A 2854
2341 0
C
512
C
0
(VSDFLR 9LUWXDO
(VSDFLR 5HDO
+3527(&&,Ð1
'LUHFFLyQ 9LUWXDO
£ 6LVWHPDV2SHUDWLYRV,
$SXQWHVGH62,
5HJLVWUR /tPLWH
5HJLVWUR %DVH
(5525GH GLUHFFLRQDPLHQWR
5$0
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULD
,QWHUFDPELRGH0HPRULD En un sistema batch, la organización de la memoria en particiones fijas resulta simple y efectiva. Siempre que se puedan mantener en memoria suficientes trabajos como para mantener la CPU ocupada, no hay razón para complicar la organización. En cambio, en un sistema de tiempo compartido la situación es diferente, pues normalmente hay más usuarios que memoria para contener sus procesos, por lo que se hace necesario llevar a disco los procesos en exceso. En cualquier caso, debe quedar claro que para que pase a ejecución alguno de estos procesos que se han llevado a disco, antes hay que traerlo a memoria. Cuando al proceso en ejecución se le acabe su porción de tiempo, es posible que se le vuelva a llevar a memoria secundaria para dejar espacio al proceso que haya seleccionado el planificador. Al trasiego de procesos que se llevan de memoria a disco y de disco a memoria se le denomina LQWHUFDPELR (VZDSSLQJ). El intercambio de memoria requiere un dispositivo de memoria secundaria. Normalmente, éste suele ser un disco rápido, debe tener suficiente capacidad para acoger las copias de todas las imágenes de memoria de todos los procesos y debe proporcionar un acceso rápido y directo a estas imágenes. El área del disco que contiene este espacio de memoria virtual con el que se realiza el intercambio desde memoria principal, recibe el nombre de iUHDGHLQWHUFDPELR o área de VZDSSLQJ. En la cola de Preparados se mantienen todos BCP’s de los procesos dispuestos a ejecutarse (cuyas imágenes de memoria se encuentran en memoria principal o secundaria). Cuando el planificador elige un proceso para pasar a ejecución, llama al dispatcher, el cual, a su vez, comprueba si el proceso seleccionado se encuentra en memoria principal o no. Si no lo está y no hay memoria libre, el GLVSDWFKHU saca a un proceso de memoria a disco y trae a memoria el proceso seleccionado. Seguidamente realiza el cambio de contexto. Está claro que el tiempo de cambio de contexto en un sistema con intercambio es bastante alto. Para obtener un buen aprovechamiento de la CPU, la porción de tiempo de los procesos deberá ser alto en comparación con el tiempo dedicado al cambio completo de proceso, es decir, incluyendo el tiempo de intercambio. Pero por otra parte, tenemos que en un sistema de tiempo compartido si la porción de tiempo es muy alta, el tiempo de espera de los procesos preparados sube mucho, lo cual no es en absoluto deseable. Por lo tanto, lo que se debe conseguir es minimizar lo más posible el tiempo de intercambio. Para ello hay dos estrategias claras: • Utilizar una memoria secundaria muy rápida. • Intercambiar solamente la memoria necesaria. La necesidad del primero de estos puntos es obvia. El segundo quiere decir que si en un sistema hay un área de usuario de 900 Kbytes, por ejemplo, y hay que sacar un proceso que ocupa 100 Kbytes, no hay por qué mover a disco un área de 900 Kbytes, sino solamente los 100 Kbytes ocupados, ya que el tiempo de intercambio es prácticamente proporcional al tiempo de transferencia, que a su vez es proporcional al tamaño del bloque que se lee o escribe en el disco. Esto obliga a
$SXQWHVGH62,
*HVWLyQGH0HPRULD
,QWHUFDPELRGH0HPRULD %DWFK
7LHPSR &RPSDUWLGR
6HDFHSWDQWDQWRVWUDEDMRVFRPRTXHSDQHQ PHPRULD 6XHOHKDEHUPiVSURFHVRVGHXVXDULRGHORV TXHFDEHQHQPHPRULD £\KD\TXHDWHQGHUORVDWRGRV
$OJXQRVKD\TXHOOHYDUORVDGLVFR \SDUDHMHFXWDUORVKD\TXHOOHYDUORVGHYXHOWDDODPHPRULD ,17(5&$0%,2'(0(025,$ &XDQGRVHGHYXHOYHXQSURFHVRD PHPRULDSULQFLSDO ¢9XHOYHDVXSDUWLFLyQRULJLQDO" 62/8&,Ð1
£2-2DOVDFDUGHPHPRULD SURFHVRVHQHVSHUDGH(6
1RVDFDUORVQXQFDGHPHPRULDSULQFLSDO 5HDOL]DU(6VREUHEXIIHUVGHO62
3DUDXWLOL]DFLyQHILFLHQWHGHOD&38 6LSRUFLyQGHWLHPSRJUDQGH 7LQWHUFDPELR3RUFLyQGH7LHPSR
£
+D\TXHPLQLPL]DUHO 7LQWHUFDPELR
6LVWHPDV2SHUDWLYRV,
$SXQWHVGH62,
7HVSHUDDOWRGHORVSURFHVRV SUHSDUDGRV
Ï8WLOL]DQGRPHPRULDVHFXQGDULD PX\UiSLGD Ï8WLOL]DQGRVRORODPHPRULD QHFHVDULD *HVWLyQGH0HPRULD
*HVWLyQGH0HPRULD
que el sistema operativo conozca la ocupación de memoria de cada proceso y a que lleve la cuenta de cada petición o devolución de memoria. Cuando un proceso es sacado a disco, y posteriormente se le vuelve a traer a memoria principal, es posible que no se le instale en la misma partición de memoria que ocupaba la última vez que estuvo en RAM. Esto puede traer complicaciones si el proceso está realizando operaciones de E/S. Supongamos que un proceso en ejecución realiza una operación de E/S, con lo que el sistema operativo lo pone en espera y lo expulsa a memoria secundaria para liberar su área de memoria. Sin embargo, es posible que el dispositivo de E/S acceda asíncronamente a los EXIIHUV que el proceso le indicó, para dejar/recoger los datos a leer/escribir. Estos EXIIHUV se encontraban en el área de memoria que ocupaba el proceso cuando se estaba ejecutando, pero cuando pasó a espera y se le expulsó al disco, este área de memoria pasó a estar ocupada por otro proceso. Así, es posible que el dispositivo de E/S ahora esté leyendo o escribiendo en un área de memoria que pertenece a otro proceso distinto del que solicito la operación de E/S. Este problema tiene dos soluciones: • No sacar nunca de memoria a un proceso que espera por una operación de E/S. • Que los dispositivos de E/S realicen las operaciones sobre EXIIHUV del sistema operativo. De esta manera, una vez finalizada la operación de E/S solicitada, cuando el planificador selecciona al proceso para continuar, se le trae a memoria principal y se transfiere la información solicitada (si es que era una lectura) del buffer del sistema al buffer del proceso. Por último se le cede el control del procesador. Conviene recordar que para que un proceso pueda ocupar distintas particiones de memoria durante una ejecución completa, se requiere que el mecanismo de reubicación sea dinámico.
0XOWLSURJUDPDFLyQFRQ3DUWLFLRQHV9DULDEOHV En principio, un sistema de intercambio podría estar basado en particiones de tamaño fijo, de tal forma que cuando un proceso queda bloqueado en espera, se le puede mover al disco y traer otro a la partición que deja libre. Pero este sistema no es aconsejable cuando se dispone de poca memoria principal, pues los programas pequeños desperdician mucha memoria cuando ocupan particiones grandes (fragmentación interna). Otro enfoque mejor es el basado en particiones de tamaño variable. Cuando se utilizan particiones variables, el número y tamaño de los procesos cargados en memoria varía con el tiempo, como se puede ver en la parte superior de la Figura 9. Al principio se carga el proceso $, seguidamente el % y el &. A continuación el proceso $ termina (o se le expulsa al disco) y se carga el proceso '; finaliza el % y se carga el (...
$SXQWHVGH62,
*HVWLyQGH0HPRULD
,QWHUFDPELR
0XOWLSURJFRQ3DUWLFLRQHV9DULDEOHV
(QODVSDUWLFLRQHVGHWDPDxRILMR VHGHVSHUGLFLDPXFKDPHPRULD )UDJPHQWDFLyQ,QWHUQD
6HSLHUGHWLHPSRHQUHDOL]DU HOLQWHUFDPELRGHPHPRUD
3$57,&,21(6'(7$0$f29$5,$%/( HOQWDPDxR\GLUHFFLyQYDUtDFRQHOWLHPSR
C
C
C
B
B
B
B
A
A
A
62
62
62
62 WLHPSR
£)UDJPHQWDFLyQ([WHUQD
C
C
E
D 62
D 62
D 62
&203$&7$&,Ð1
¢3XHGHFUHFHUGLQiPLFDPHQWHODPHPRULDGHXQSURFHVR" 3LODGH%
3DUD FUHFLPLHQWR
B
'DWRVGH% &yGLJRGH%
(QXVR
3LODGH%
3DUD FUHFLPLHQWR
A D
62
6LVWHPDV2SHUDWLYRV,
$SXQWHVGH62,
'DWRVGH% &yGLJRGH%
(QXVR
E
3DUD FUHFLPLHQWR
3DUD FUHFLPLHQWR
62 *HVWLyQGH0HPRULD
*HVWLyQGH0HPRULD
La principal diferencia entre las particiones fijas y las de tamaño variable es que en estas últimas el número, la dirección y el tamaño de las particiones varía constantemente a medida que los procesos van y vienen; mientras que en las de tamaño fijo no varían, son fijos. La flexibilidad de no estar sujeto a un número fijo de particiones que resulten demasiado grandes o demasiado pequeñas mejora la utilización de la memoria, pero también complica los algoritmos de asignación, liberación y contabilidad de la memoria disponible. Inicialmente toda la memoria de usuario está disponible, organizada como un único y gran bloque de memoria libre, un KXHFR. Cuando un proceso llega y necesita memoria, se busca un hueco suficientemente grande para el proceso. Si se encuentra uno, el hueco se parte en dos, asignándole únicamente la memoria requerida, dejando el resto del hueco como un nuevo hueco más pequeño y disponible para futuras peticiones. A medida que se crean procesos, se les pone en una cola de entrada, anotando sus requisitos de memoria. Cuando se le asigna memoria a uno de estos procesos, pasa a la cola de Preparados y puede competir por la CPU. Cuando un proceso termina libera la memoria utilizada, con lo que el sistema operativo puede utilizarla para asignársela a otro proceso de la cola de entrada. En un momento dado, se tiene una lista de bloques libres de memoria y sus tamaños. También se dispone de la cola de entrada de procesos. La memoria se va asignando a los procesos hasta que no haya un hueco suficientemente grande para el primer proceso de la cola de entrada, o bien se busca en la cola un proceso cuyos requisitos de memoria sean menores y puedan satisfacerse con alguno de los bloques disponibles. En general siempre hay un conjunto de huecos de distintos tamaños dispersos por toda la memoria. Cuando un proceso llega y necesita memoria, se busca en el conjunto de huecos uno que sea suficientemente grande. Si el hueco es demasiado grande, se parte en dos: uno se asigna al proceso, y el otro se devuelve al conjunto de huecos. Cuando un proceso finaliza, se libera su bloque de memoria, que se pone de vuelta en el conjunto de huecos. Si este hueco resulta estar adyacente a otros huecos (a otros bloques libres), se unen los bloques adyacentes en un único bloque del tamaño total. Ahora hay que comprobar si hay más procesos esperando memoria y si este nuevo bloque liberado y recombinado puede satisfacer las demandas de cualquiera de estos procesos que están esperando. En algún momento puede darse el caso de que haya mucha memoria libre, pero distribuida entre muchos huecos pequeños no adyacentes. Entonces tenemos IUDJPHQWDFLyQ H[WHUQD. La única solución a la fragmentación externa es la FRPSDFWDFLyQ, que consiste en mover los bloques asignados, de tal forma que queden adyacentes, dejando entonces grandes huecos libres que sí pueden asignarse a procesos. Obsérvese que la compactación es posible gracias a que se utiliza un sistema de reubicación dinámica, por lo que todo lo que hay que hacer es
$SXQWHVGH62,
*HVWLyQGH0HPRULD
mover el programa a su nueva posición de memoria y actualizar el registro base con la nueva dirección de comienzo. La compactación también tiene su coste. Téngase en cuenta que hay que mover todos los procesos hacia un extremo de la memoria, con lo que los huecos se van moviendo hacia el extremo opuesto, hasta formar un único gran bloque. Realizar este proceso puede resultar bastante costoso en tiempo. Consideremos ahora la cantidad de memoria que se le debe asignar a un proceso cuando se le crea o se le lleva a memoria principal. Si los procesos se crean con un tamaño fijo que nunca cambia, la gestión de memoria es simple: se asigna exactamente lo que necesita, ni más ni menos. Pero si los procesos pueden crecer, por ejemplo porque realizan peticiones dinámicas de memoria, se presenta un problema cuando los procesos intentan crecer (necesitan más memoria de la requerida inicialmente). Si hay un hueco libre adyacente al proceso, se le puede asignar el hueco al proceso y permitirle expandirse. Pero si el proceso está adyacente a otro proceso, el que pretende expandirse tendrá que moverse a otro hueco más grande, o bien habrá que sacar algún proceso a disco para dejar espacio libre. Si se espera que los procesos de un sistema puedan aumentar su tamaño a medida que se ejecutan, es conveniente asignarles inicialmente un poco de memoria extra, para evitar tener que moverles en caso de que necesite más memoria durante la ejecución (Figura 9, abajo-a). Si el proceso tiene dos segmentos que pueden crecer, por ejemplo el montículo (KHDS) y la pila, se suele utilizar la alternativa inferior-b de la Figura 9, en la que se puede ver que cada proceso tiene una pila en la parte superior de su memoria creciendo hacia direcciones bajas y, justo debajo, una zona de montículo creciendo hacia las direcciones altas. La memoria entre estas dos áreas puede asignarse a cualquiera de ellas. Si se acaba este espacio (porque intenten solaparse la pila y el heap), entonces ya no quedará más remedio que mover el proceso a otro hueco mayor. El área de KHDS es la zona de memoria de donde se sirven las peticiones dinámicas de memoria, mientras que la pila se utiliza para ubicar datos temporales, como por ejemplo: las variables locales, resultados intermedios en la evaluación de expresiones, parámetros y direcciones de retorno en las llamadas a procedimientos. OS/MVT es uno de los antiguos e importantes sistemas operativos de IBM que utilizaba particiones variables.
*HVWLyQGH%ORTXHVGH0HPRULD En general, hay tres métodos utilizados por los sistemas operativos para llevar la cuenta de la memoria utilizada y de los huecos libres: • Mapa de bits • Listas • Sistema EXGG\ $SXQWHVGH62,
*HVWLyQGH0HPRULD
Veámoslos en los siguientes apartados.
0DSDVGH%LWV Con este método de gestión de memoria, ésta se divide en XQLGDGHVGHDVLJQDFLyQ, cuyo tamaño puede ir desde unas cuantas palabras a varios Kbytes. Como se puede ver en la Figura 10, cada una de estas unidades de asignación se corresponde con un bit en el mapa de bits de memoria; la primera unidad con el primer bit, la segunda con el segundo, etc., de tal forma que si el bit está a 0 indica que la unidad de asignación correspondiente se encuentra libre, mientras que si está a 1 quiere decir que la unidad está ocupada, o sea, asignada a algún proceso. El tamaño de la unidad de asignación es una cuestión muy importante. Cuanto más pequeña sea la unidad, mayor será el mapa de bits. No obstante, obsérvese que incluso con una unidad de tan solo 4 bytes, tenemos que 32 bits de memoria requieren solamente 1 bit en el mapa. Si la unidad de asignación es de gran tamaño, habrá pocas unidades, luego el mapa de bits ocupará poco espacio; pero por contra, tenemos que puede empezar a ser significativo el espacio correspondiente al 50% de memoria que por término medio se desperdicia en la última de las unidades asignadas al proceso. Este método es simple y sencillo de implementar, pero tiene la desventaja de que cuando se buscan Q unidades consecutivas de memoria para asignar a algún proceso, se debe buscar en el mapa de bits hasta encontrar Q bits consecutivos a 0. Aunque esta operación es lenta, los procesadores actuales suelen incluir instrucciones de búsquedas de cadenas de bits, favoreciendo así la utilización de los mapas de bits.
/LVWDVGH%ORTXHV Otra forma de llevar la cuenta de la memoria utilizada es manteniendo listas enlazadas de bloques libres y bloques asignados. En este caso, la memoria se representa como una lista de bloques (Figura 10 inferior), en la que cada entrada o elemento de la lista indica un hueco (bloque libre), con su dirección, tamaño, y dirección del siguiente elemento. La lista de huecos suele mantenerse ordenada por la dirección de estos, pues así se facilita su actualización cuando se lleva un proceso a memoria o termina su ejecución. Un proceso que termina tiene dos bloques adyacentes (excepto si está al principio o al final de la memoria), cada uno de los cuales puede estar libre u ocupado por otro proceso. Si alguno de estos bloques está libre, debe compactarse con el bloque del proceso finalizado para formar un único bloque libre. Esta compactación requiere actualizar la lista de huecos, pues habrá que modificar, al menos, la nueva longitud del bloque y, dependiendo de la posición relativa de los bloques compactados, su dirección de comienzo.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
*HVWLyQGH%ORTXHV
0HPRULDFRQ,QWHUFDPELR
6HGHEHOOHYDUODFXHQWD GHODPHPRULDXWLOL]DGD\ GHORVKXHFRVOLEUHV
Ä0DSDGH%LWV Ä/LVWDGH%ORTXHV/LEUHV Ä6LVWHPD%XGG\
+0DSDGH%LWV $
%
0
&
8
11111000 11111111 11001111 11111000 00111111
'
16
( 24
32
¢7DPDxRGHOD8QLGDGGH$VLJQDFLyQ" 3HTXHxR
L0DSD*UDQGH
*UDQGH
J0DSD3HTXHxR L)UDJPHQWDFLyQ,QWHUQD
+/LVWDGH%ORTXHV/LEUHV &DEH]D
QLO
&DGDHOHPHQWRFRQWLHQH ODGLUHFFLyQ\ORQJLWXGGH XQEORTXHOLEUH
&XDQGRVHHOLJHXQEORTXHVLVREUDHVSDFLRVHSDUWHHQGRV IRUPDQGRXQEORTXHOLEUHFRQODSDUWHVREUDQWH 3(OSULPHURTXHVLUYD
¢
&yPRVHEXVFD XQEORTXHOLEUH
"
3(OVLJXLHQWHTXHVLUYD 3(OTXHPHMRUVHDGDSWH 3(OTXHSHRUVHDGDSWH 32UGHQDUSRUWDPDxR
6LVWHPDV2SHUDWLYRV,
$SXQWHVGH62,
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULD
Este sistema de gestión en el que los bloques libres se mantienen en una lista, se presta muy bien a la implementación de estos algoritmos: • • • •
El primero que sirva (ILUVWILW) El siguiente que sirva (QH[WILW) El que mejor se adapte (EHVWILW) El que peor se adapte (ZRUVWILW)
El algoritmo más simple es HO SULPHUR TXH VLUYD. El gestor de memoria recorre la lista hasta encontrar un hueco que sea suficientemente grande para satisfacer la petición. Si el hueco elegido no es exactamente del tamaño solicitado, se divide en dos partes: una (del tamaño solicitado) se le asigna al proceso que lo solicitó, y la otra (con la memoria sobrante) se deja como un hueco de menor tamaño. La lista de los huecos libres se mantiene como una cabecera con los nodos de la lista encolados a partir de ella. Cada vez que se realiza una búsqueda de un bloque, se comienza por la cabecera, hasta encontrar el primero que sirva. Cuando se encuentra el bloque, normalmente se parte en dos, uno para asignar al proceso, y otro que se convierte en un hueco más pequeño. Pues bien, lo que genera esto, a la larga, es que cerca de la cabecera de la cola se empiecen a formar una serie de bloques no contiguos y de pequeño tamaño, inservibles para ningún proceso. A partir de esta situación, cada vez que se realiza una búsqueda, hay que recorrer esta serie de bloques pequeños de manera infructuosa, hasta llegar a los bloques que todavía no han sido partidos, o no se han fraccionado demasiado. Esta fragmentación externa es inevitable, pero lo malo es la pérdida de tiempo que supone el atravesar reiteradamente la lista de los bloques pequeños agolpados al comienzo de la lista. Este problema se soluciona con HOVLJXLHQWHTXHVLUYD. Con este algoritmo, la lista es circular, es decir, que el último nodo apunta al primero y la cabecera no es siempre la misma, sino que va rotando por la lista, es decir, que el orden de la lista va rotando. Comienza por apuntar al primer bloque (en orden creciente de dirección), pero tras realizar la primera búsqueda de un bloque, la cabecera apunta ahora al bloque siguiente al asignado y así sucesivamente. Esto hace que los bloques fraccionados no se amontonen en una parte de la lista sino que se distribuyan de manera uniforme. Así, cada vez que se comienza una búsqueda, no habrá que atravesar todos los bloques pequeños, sino solamente alguno, con la consecuente ganancia de tiempo. Si bien en HOSULPHURTXHVLUYD se amontonaban los huecos pequeños al comienzo de la lista, dejando al final grandes particiones libres, ahora la fragmentación de la memoria es uniforme en el espacio de direcciones, encontrándonos con que, a la larga, ya no tenemos huecos grandes como en el algoritmo anterior, lo cual es un problema si llega un proceso con grandes necesidades de memoria. Otro algoritmo muy conocido es HO TXH PHMRU VH DGDSWH, que recorre la lista completa para seleccionar el hueco que mejor se adapte, o sea, el menor de los que sirvan, para no fraccionar un bloque grande que pudiera servir para alguna petición posterior.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
Este algoritmo es más lento que los anteriores, pues tiene que recorrer la lista entera por cada petición. Sorprendentemente también desperdicia más memoria, pues tiende a ir rellenando la memoria con huecos tan pequeños que resultan inservibles. En cambio, HO SULPHUR TXH VLUYD y HO VLJXLHQWH TXH VLUYD, al ir fraccionando bloques, por termino medio generan bloques más grandes que sí pueden ser aprovechables. Para solucionar el problema de HO TXH PHMRU VH DGDSWH, se pensó en utilizar la política opuesta, es decir, elegir HOTXHSHRUVHDGDSWH; o sea, que se selecciona el bloque más grande de la lista y se parte en dos, generando así un nuevo bloque libre que, aunque más pequeño, seguramente tendrá un tamaño significativo, evitando así la generación de huecos pequeños. Sin embargo, la simulación ha demostrado que tampoco ha resultado ser una buena idea. Para acelerar las búsquedas, puede ordenarse la lista por orden creciente de los tamaños de los bloques. En este caso, los algoritmos HOSULPHURTXHVLUYD y HOTXH PHMRU VH DGDSWH coinciden, y HO VLJXLHQWH TXH VLUYD no tiene sentido, pues la lista tiene un orden fijo no circular. El inconveniente que aparece con esta ordenación es la sobrecarga que se genera para mantener la lista ordenada cada vez que se pide o se libera un bloque. Para acelerar la búsqueda de los bloques se ha desarrollado otro algoritmo conocido como 4XLFN )LW, que mantiene listas separadas de bloques para los tamaños más usuales, por ejemplo, una lista para bloques de 4 Kbytes, otra para los de 8 Kbytes, 12 Kbytes, etc. Así, encontrar un bloque del tamaño requerido es extremadamente rápido. Su desventaja es la de todos los esquemas basados en el orden de bloques por su tamaño, o sea, la sobrecarga que se produce a la hora de liberar un bloque, pues resulta difícil encontrar a sus vecinos para comprobar si se pueden compactar con él. En cuanto a la implementación, conviene observar que los nodos de la lista no tienen por que ser estructuras de datos que contienen la dirección y el tamaño de los bloques correspondientes. Cada nodo puede ser el propio bloque de memoria, tal que los primeros y los últimos bytes del bloque están reservados (no son utilizables por los procesos) e indican el tamaño del bloque y la dirección del siguiente bloque libre. Todos los bloques, los libres y los asignados tienen en el área reservada un campo booleano indicador de si dicho bloque está libre u ocupado, facilitando así la tarea de la compactación.
6LVWHPD%XGG\ Como hemos visto, el problema de los algoritmos basados en el orden de los bloques por su tamaño es la lentitud en la fase de liberación de un bloque. El sistema %XGG\ (o de los compañeros) es un algoritmo que aprovecha el hecho de que los ordenadores utilizan números binarios en las direcciones, para así facilitar la tarea de la compactación de huecos adyacentes cuando se libera un bloque. Veamos en qué consiste.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
6LVWHPD%XGG\
*HVWLyQGH%ORTXHV
3 3
/RVEORTXHVVHSDUWHQHQIUDFFLRQHVFX\RWDPDxRHVSRWHQFLDGH 6HPDQWLHQHQWDQWDVOLVWDVFRPRWDPDxRVGLVWLQWRVKD\DHQFDGDPRPHQWR
,QLFLDO $SLGH
$ $ $
'HYXHOYH$
'SLGH
'HYXHOYH%
&SLGH
'HYXHOYH'
' '
& & & & &
'HYXHOYH&
J J L
.E
% % % %
%SLGH
%ORTXHV OLEUHV
.E
(VIiFLOHQFRQWUDUHOPHMRUEORTXHOLEUH (VIiFLOFRPELQDUEORTXHVOLEUHV
6RORKD\TXHEXVFDUHQODOLVWDFRUUHVSRQGLHQWH
2ULJLQDPXFKDIUDJPHQWDFLyQLQWHUQD
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
El gestor de memoria mantiene una serie de listas de bloques libres, una para los de tamaño 1 byte, otra para 2, otra para 4, 8, 16, etc. hasta llegar a Q, siendo Q el tamaño total del área de memoria libre inicial. Atendiendo al ejemplo de la Figura 11, vemos que si, por ejemplo, partimos de un área de memoria de usuario de 1Mbyte, inicialmente toda ella está libre, se tiene una lista con una única entrada correspondiente a un bloque de 1 Mbyte y el resto de las listas están vacías. Cuando se recibe una petición de un proceso $ de un bloque de 70 Kbytes, se busca un bloque cuyo tamaño sea la potencia de 2 que más se aproxime por exceso a los 70 K, esto es, 128 K. Pero no hay ningún bloque de 128 K, ni de 256 ni de 512. Entonces se parte el bloque de 1M en dos bloques de 512 K (a los que se les llama EXGLHV o compañeros). Uno de estos bloques se divide a su vez en dos nuevos bloques de 256 K y por último uno de estos se vuelve a partir en otros dos bloques de 128 K. De estos dos últimos bloques, uno de ellos se asigna al proceso que realizó la petición de los 70 K. A continuación, se recibe una petición % de 35 K, cuya potencia de 2 más cercana es la de 64 K, pero no hay ningún bloque de 64 K disponible, así que partimos uno de los bloques de 128 K en dos de 64, y uno de ellos se le asigna a %. Para servir la petición & de 80 K, es necesario partir uno de los bloques de 256 K en dos de 128, asignando uno de ellos. Obsérvese que cada vez que se ha dividido un bloque, se generan dos nuevos bloques DG\DFHQWHV de la mitad del tamaño.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
Ahora tenemos que el proceso $ libera el bloque de 128 K (de los que sólo utilizaba 70). Se acude a la lista de bloques de 128 K para ver si alguno de los bloques de 128 K es el compañero del liberado. En nuestro caso no lo es, por lo que simplemente se mete el bloque liberado en esta lista de bloques de 128 K. Seguidamente vuelve a haber una petición ' de 60 K, y se asigna un bloque de 64 K que había disponible. Ahora se libera el bloque del proceso %, de 64 K (ocupaba 35), pero como su compañero no está libre, no hay ninguna compactación. A continuación finaliza el proceso ', liberando un bloque de 64 K (ocupaba 60). En este caso, tenemos que el compañero de este bloque sí esta libre, por lo que se compactan formando un bloque de 128 K. Seguidamente se comprueba que este nuevo bloque de 128 K tiene su compañero libre, por lo que se vuelven a compactar para formar un único bloque de 256 K. Cuando se libera el bloque del proceso &, de 128 K (ocupadas 80) se pueden realizar 3 compactaciones consecutivas dando lugar al bloque original de 1 Mb de memoria libre. La ventaja de este algoritmo sobre otros que también ordenan conforme al tamaño, k es que cuando se libera un bloque de tamaño 2 bytes, el gestor de memoria k solamente tiene que buscar en la lista de huecos de longitud 2 para ver si la compactación es posible. La desventaja es su ineficiencia en el aprovechamiento de la memoria, debido a los redondeos hasta una potencia de 2 en el tamaño de los bloques asignados, dando lugar, en consecuencia, a una fragmentación interna. La gestión de memoria del sistema operativo Linux tiene lugar en dos niveles, disponiendo de gestión de memoria virtual (a alto nivel) y de gestión de memoria física a bajo nivel. Para la gestión de memoria física utiliza este algoritmo %XGG\ o de los Compañeros.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
0HPRULD9LUWXDO En las distintas estrategias de gestión de memoria vistas hasta ahora, todas tienen un objetivo común: mantener muchos procesos en memoria simultáneamente para permitir la multiprogramación. Y en cualquier caso, siempre hemos visto que se requiere que un proceso esté completamente en memoria principal antes de que se pueda ejecutar. Sabemos que para ejecutar las instrucciones de un programa, éstas, al igual que sus operandos, tienen que estar en memoria principal. Esta imposición parece necesaria y razonable, pero por desgracia limita el tamaño de un programa al espacio de memoria física (RAM). Sin embargo, el examen de los programas reales nos muestra que en muchos casos no es necesario que el programa entero permanezca siempre en memoria. Por ejemplo: • Los programas suelen tener código para manejar condiciones de error inusuales. Ya que estos errores raramente se producen en la práctica, este código casi nunca se ejecuta. • A veces, las tablas, listas o matrices se declaran con más tamaño del que luego realmente necesitan. Una matriz se puede declarar de 100 por 100 aunque raramente ocupe más de 10 por 10; o un ensamblador puede tener una tabla para albergar a 3000 símbolos, aunque la media de los programas no tengan más de 200. • Ciertos programas pueden ofrecer ciertas características que nunca o raramente se utilizan. Por ejemplo ciertas funcionalidades de programas como editores de texto, hojas de cálculo, bases de datos, etc. Incluso aunque en algunos casos sea necesario ejecutar o recurrir a todas las partes de un programa, no se necesita todo al mismo tiempo. Mediante el sistema de los RYHUOD\V ya se conseguía esto, pero se lograba gracias a la intervención directa del propio programador. En cambio, mediante la memoria virtual, esto se va a conseguir de forma transparente al programador. La memoria virtual es una técnica que permite la ejecución de procesos que pueden no estar completamente en memoria principal. La principal ventaja de este esquema es que los programas pueden ser mayores que la memoria principal. Esto se debe a que se crea una abstracción de la memoria principal, separando la memoria lógica, tal como la ve el usuario, de la memoria física de la que realmente se dispone. Esta técnica libera al programador de las preocupaciones concernientes a las limitaciones de la memoria. La habilidad de poder ejecutar un programa que sólo está parcialmente en memoria principal acarrea los siguientes beneficios:
$SXQWHVGH62,
*HVWLyQGH0HPRULD
• El tamaño de un programa no está limitado por la cantidad de memoria física disponible. Los usuarios escriben programas contando con un espacio de direcciones virtuales extremadamente grande. • Debido a que cada programa puede necesitar para ejecutarse menos memoria que la que ocupa su tamaño total, se pueden cargar más programas en memoria para ejecutarlos al mismo tiempo, con la consiguiente mejora en el aprovechamiento de la CPU. • Ya que de cada programa solamente hay que cargar la parte necesaria en un momento dado, a la hora de cargar un proceso o expulsarlo a disco, se necesitan menos operaciones de E/S debidas al intercambio, por lo que se consigue una ejecución global más rápida. Hasta ahora hemos dicho que con memoria virtual, el programador tiene a su disposición una cantidad de memoria extremadamente grande, y también hemos dicho que para que un programa se ejecute, no tiene por qué estar completamente en memoria en cada momento de su ejecución. Va siendo hora de empezar a aclarar cuál es el fundamento de esta técnica para ofrecer tanta memoria. Sabemos que para ejecutar una instrucción, ésta y sus operandos deben encontrarse en memoria principal. ¿Por qué no en memoria secundaria? Simplemente porque el acceso es mucho más rápido a memoria RAM que a los discos, y por lo tanto los mecanismos de direccionamiento que se han establecido en el procesador para hacer obtener los operandos de las instrucciones convencionales saben hacer referencia directa a memoria principal, no a los dispositivos periféricos. Pues bien, se trata de tener cargada en el disco la imagen del proceso en ejecución, y en memoria principal solamente la parte necesaria para la ejecución de la sección del programa que se está ejecutando en cada momento. El ancho del bus de direcciones establece el rango de direcciones que puede generar la CPU (GLUHFFLRQHVYLUWXDOHV). Por otra parte tenemos la memoria física, es decir, los transistores que forman bytes o palabras, tal que cada uno de los bytes tiene una dirección (GLUHFFLyQUHDO). Normalmente pensamos que a cada dirección que sale del procesador le corresponde una dirección física, pero claro, puede suceder que haya instalada menos memoria de la permitida por el bus de direcciones. Así, el procesador va alimentándose de instrucciones y datos que están en RAM, hasta que en un momento dado hace referencia a una dirección de memoria que realmente no existe, es decir, que no tiene su correspondiente memoria física. Sin embargo, la instrucción o dato que se está referenciando sí se encuentra en la imagen del proceso que reside en el disco duro, por lo tanto, lo que hay que hacer es traerlo a memoria principal para permitir que la CPU termine de obtener el dato solicitado a la memoria.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
0HPRULD9LUWXDO (QPXFKRVFDVRVQRVHHMHFXWDHOSURJUDPDFRPSOHWR 3&yGLJRSDUDJHVWLyQGHHUURUHVLQXVXDOHV 37DEODV\OLVWDVVREUHGLPHQVLRQDGDV 32SFLRQHV\FDUDFWHUtVWLFDVUDUDPHQWHXWLOL]DGDV + $XQTXHVHHMHFXWHHOSURJUDPDFRPSOHWR QRHVQHFHVDULRWHQHUORFRPSOHWDPHQWHFDUJDGRHQPHPRULD J6HSRGUtDQHMHFXWDUSURJUDPDVPD\RUHVTXHOD5$0 J6HSRGUtDQWHQHUPiVSURFHVRVHQPHPRULDSULQFLSDO J0HQRV(6SRULQWHUFDPELR 0iVYHORFLGDG /D0HPRULD9LUWXDOSHUPLWHODHMHFXFLyQGH SURFHVRVTXHSXHGHQQRHVWDUFDUJDGRV FRPSOHWDPHQWHHQPHPRULDSULQFLSDO 75$163$5(17($/352*5$0$'25
6HRFXSDGHWHQHUODPHPRULDGHXQSURFHVRSDULGDHQ WUR]RVHLUFDUJDQGRHQPHPRULDSULQFLSDOHOWUR]RTXH HVQHFHVDULRSDUDSRGHUFRQWLQXDUVXHMHFXFLyQ
7UR]RV,JXDOHV
7UR]RVGH WDPDxRYDULDEOH
3$*,1$&,Ð1
6(*0(17$&,Ð1
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
Esto es lo que realizaba el programador mediante la técnica de los RYHUOD\V o solapamientos, pero con la memoria virtual, ahora se realiza de forma transparente al programador, mediante software del sistema operativo, y soportado por la ayuda del hardware que proporciona la MMU (0HPRU\0DQDJHU8QLW). En la memoria virtual vamos a tener que separar muy bien el conjunto de las direcciones virtuales (las que salen de la CPU) de las direcciones físicas o reales (las que corresponden a memoria física instalada) y mediante los mecanismos software y hardware pertinentes habrá que traducir cada dirección virtual en su correspondiente dirección real. Lo que puede pasar es que la dirección real de un dato no corresponda a memoria principal, sino a memoria secundaria, por lo que entonces habrá que traer dicho dato a memoria principal para que la CPU pueda continuar su ejecución. Para implementar los sistemas de memoria virtual, normalmente se utiliza la SDJLQDFLyQ, que la veremos en el siguiente apartado. También se puede implementar mediante VHJPHQWDFLyQ, como lo hace el sistema OS/2 de IBM, pero los algoritmos son bastante más complejos que los de la paginación, pues a diferencia de las páginas, los segmentos son de tamaño variable.
3DJLQDFLyQ En la gestión de memoria con intercambio, cuando había que pasar un proceso a ejecución, era necesario traer el proceso entero de disco a memoria principal. Con memoria virtual hemos dicho que no se trae todo el proceso, sino que cuando se hace referencia a una dirección de memoria virtual cuya correspondiente memoria física reside en disco, se trae el contenido de disco a RAM. ¿Quiere esto decir que la unidad de intercambio de memoria es el byte? Si lo fuera, daría lugar a que continuamente se estuviesen generando operaciones de E/S, lo cual sería poco eficaz. Claramente, parece más conveniente utilizar bloques más grandes, para aprovechar de manera más eficiente cada operación de E/S. Si el tamaño de los bloques de intercambio es variable, por ejemplo de 1, 2, 3 y 4 Kb, podría fácilmente darse el caso de que sea necesario traer de disco a memoria un bloque de 3 Kb y en memoria tener dos huecos libres de 1 Kb y 2 Kb, pero no contiguos, con lo que a primera vista no podríamos traer el bloque de 3 Kb a pesar de disponer de 3 Kb libres en memoria principal, debido a la fragmentación externa existente. Para eliminar el problema de la fragmentación externa, lo que se hace es utilizar bloques de intercambio de tamaño único y dividir la memoria principal en trozos del mismo tamaño (de 512 bytes a 8 Kb), con lo cual a la hora de traer cualquier bloque de disco a memoria, si hay un hueco libre, seguro que cabe, y además no se desperdicia ningún espacio sobrante. A cada uno de los bloques de intercambio que están en el disco o en memoria principal le llamaremos SiJLQD y a cada uno de los huecos en que está dividida la memoria principal (esté libre u ocupado) le denominaremos PDUFR. O sea, que los marcos son espacios de memoria principal de tamaño fijo y de direcciones consecutivas, que pueden albergar bloques de información, exactamente del mismo tamaño, denominados páginas. $SXQWHVGH62,
*HVWLyQGH0HPRULD
3DJLQDFLyQ
0HPRULD9LUWXDO
3DJ
3DJ
3DJ 3DJ
3DJ 7DEODGH
0DUFR
3DJ
0DUFR
3DJ
0DUFR
0DUFRP
3iJLQDV
3DJ
&38
GLU
YLUWXDO
3DJQ
LU G D LF tV I
0HPRULD 3ULQFLSDO
3DJQ
(VSDFLRGH
0HPRULD
'LUHFFLRQHV9LUWXDOHV
6HFXQGDULD
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
Nos encontramos, entonces, con que el espacio de direccionamiento virtual está dividido en páginas y el espacio de direccionamiento físico está dividido en marcos. Así pues, cuando la CPU genera una dirección virtual, ésta estará contenida en una de las páginas. Si tal página se encuentra en alguno de los marcos de memoria física, se accede al dato; si no, habrá que cargar esa página, residente en el disco duro, en alguno de los marcos de memoria principal, y a continuación acceder al dato. En un sistema sin memoria virtual, la dirección virtual 1 se corresponde con la dirección física 1, la 2 con la 2, etc. ¿Sigue siendo esto válido con la memoria virtual? Pues no. Si fuese así, en un sistema con una espacio de direccionamiento virtual de 24 Mb y una memoria física de 4 Mb, por ejemplo, esto querría decir que una referencia a una dirección comprendida en el rango de los 4 primeros megabytes de memoria, siempre encontraría la página correspondiente en memoria principal, pero ¿qué pasaría al hacer referencia a una dirección superior? habría que traerla a memoria principal, pero ¿a dónde? si no existen las direcciones físicas correspondientes. Esto nos lleva a que hay que desligar la correspondencia directa y estática entre direcciones físicas y virtuales, o lo que es lo mismo, hay que desligar una relación estática entre páginas y marcos, y hacer que su relación se establezca dinámica e indirectamente mediante una tabla, a la que llamaremos WDEOD GH SiJLQDV. Esta tabla, además de guardar la correspondencia entre páginas y marcos, mantiene más información de control, como por ejemplo, el denominado ELW GH SUHVHQFLD, que indica si una página dada se encuentra en un marco de memoria o no. El resto de la información de control la iremos comentando a lo largo de este capítulo.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
Así pues, veamos a grandes rasgos y con ayuda de la Figura 13, qué pasos se suceden a partir de una referencia generada por la CPU: 1. La CPU hace referencia a una dirección virtual de memoria. 2. Se calcula a qué página corresponde tal dirección. 3. Mediante la tabla de páginas se comprueba si la página se encuentra en algún marco de memoria física o en disco. 4. Si se encuentra en un marco, se traduce la dirección virtual a la dirección física que ocupa la página. 5. Si no se encuentra en un marco, hay que traer la página desde disco a un marco libre de memoria. Posteriormente habrá que traducir la dirección virtual a la dirección física, dependiendo del marco asignado a la página. 6. Por último, se realiza la operación de L/E sobre memoria principal solicitada por la CPU.
Ahora empezaremos a ver con detalle cada uno de estos pasos. Comencemos con el FiOFXORGHODSiJLQDDSDUWLUGHXQDGLUHFFLyQYLUWXDO. Para saber a qué página corresponde una dirección, simplemente hay que dividir la dirección dada entre el tamaño de las páginas. El cociente es la página correspondiente; el resto es el desplazamiento dentro de la página. Este cálculo se puede facilitar mucho si el tamaño de la página es una potencia de dos. Sabemos que con números binarios, las multiplicaciones por 2, 4, 8, 16, etc., se consiguen con 1, 2, 3, 4, etc. desplazamientos a la izquierda, y una división se realiza con los correspondientes desplazamientos a la derecha. Esto quiere decir que si, por ejemplo, se tiene un espacio de direccionamiento virtual de 1 Mb (220) y 12 20 12 8 un tamaño de página de 4 Kb (2 ), habrá 2 /2 = 2 = 256 páginas de 4 Kb cada una. O sea, que de los 20 bits que ocupa una dirección virtual, el número de página se obtiene desplazando la dirección 12 bits a la derecha, lo cual es lo mismo que tomar directamente los 8 bits de mayor peso. Para cada una de las páginas, los 12 desplazamientos posibles van a ir variando de 0 a 2 . Ahora, seguramente, no resulta difícil ver que, en nuestro ejemplo, el número de página de una dirección virtual viene indicado por los 8 bits de mayor peso y el desplazamiento que ocupa dicha dirección dentro de la página viene indicada por los 12 bits de menor peso de la dirección. Así pues, habiendo elegido un tamaño de página que sea potencia de 2, con ayuda de un hardware muy elemental se puede conocer directa y rápidamente la página y el desplazamiento de cada dirección. Una vez descrito el algoritmo para obtener el número de la página referenciada, nos interesa saber cómo se realiza la WUDGXFFLyQ GH GLUHFFLRQHV YLUWXDOHV HQ GLUHFFLRQHVItVLFDV. El espacio de direcciones virtuales está dividido en bloques del tamaño de una página. Por lo tanto, una dirección virtual está formada por una pagina S y un desplazamiento G dentro de la página, o sea por el par (SG), mientras que el espacio de direcciones físicas o reales está dividido en P marcos del mismo tamaño de la página, por lo que las direcciones físicas estarán formadas por un marco m y un $SXQWHVGH62,
*HVWLyQGH0HPRULD
desplazamiento dentro del marco, es decir, por el par (PG). (Ver Figura 14). Por lo tanto, para realizar la traducción de direcciones virtuales a reales hay que construir la función de correspondencia (SG) → (PG). Recordamos que siendo MN el número de bits de una dirección virtual,S ocupa los M bits de mayor peso de la dirección, y G viene dado por los N bits de menor peso de la dirección, siendo 2j el número de páginas del sistema y 2k el tamaño de la página. Sabemos que una página que se encuentre en memoria principal se encuentra en uno de los marcos. Puesto que el tamaño de las páginas y de los marcos es el mismo, parece obvio que para una dirección virtual (SG) a la que le corresponde la dirección física (PG), el desplazamiento G será el mismo, tanto dentro de la página como dentro del marco asignado. Así, únicamente será necesario realizar la traducción de página a marco (S → P), por lo que la tabla de traducción (tabla de páginas) solamente tendrá que tener tantas entradas como páginas, no tantas como direcciones virtuales. Pues bien, a partir de una dirección virtual (SG), se comprueba en la tabla de páginas si la página S se encuentra en alguno de los marcos de memoria. Si es así, se obtiene de la tabla el marco P que contiene la página. La dirección física es entonces (PG).
7UDGXFFLyQGH'LUHFFLRQHV
3DJLQDFLyQ
0HPRULD 3ULQFLSDO
&38
'LU 9LUWXDO
SG
PG
'LU )tVLFD
P
7DEODGH 3iJLQDV
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
Obsérvese que, dado que el número de páginas puede ser mayor que el número de marcos de memoria física, el número de bits de la dirección virtual puede ser mayor que el de la dirección física. Conviene saber que en el bus de direcciones, que se dirige desde la CPU a la memoria principal, se encuentra interceptado por el hardware que nos ayuda a realizar el proceso de traducción, o sea, la Unidad de Gestión de Memoria (0HPRU\ 0DQDJHPHQW 8QLW o MMU). Ésta recibe las direcciones virtuales y con ayuda de la tabla de páginas genera las direcciones físicas correspondientes, sin necesidad de software adicional. (No obstante, hoy día la MMU suele estar integrada dentro de la propia CPU). Cuando en la referencia a una dirección virtual, la página correspondiente no se encuentra en memoria principal, entonces decimos que se produce una IDOWD GH SiJLQD. En este caso, las acciones a realizar son, como se indica en la Figura 15, las siguientes: 1. La CPU genera una dirección virtual. 2. La MMU consulta el bit de presencia de la tabla de páginas y se comprueba que la página de la dirección referenciada no se encuentra en memoria principal. 3. La MMU pone la CPU en espera y genera una interrupción (que suele ser (UURU GH%XV) que es capturada por el sistema operativo. 4. Las rutinas de carga de páginas del sistema operativo se encargan de localizar la página referenciada en el disco y cargarla en un marco libre de la memoria principal. 5. Se actualiza la tabla de páginas indicando que está presente y el marco en el que ha sido cargada. 6. Termina el tratamiento de la excepción. 7. La MMU indica a la CPU que debe rearrancar la instrucción que provocó la falta de página. 8. Se continua la ejecución de la instrucción a partir del direccionamiento que produjo la falta de página. Esta vez ya no se producirá la falta de página. Obsérvese que en el intercambio de memoria se traían y llevaban procesos enteros si había un área de memoria contigua suficientemente grande; con páginación basta con que se disponga de la memoria necesaria, aunque no sea contigua, pues el proceso puede estar repartido entre varias páginas. En principio, parece que cuando se produce una falta de página, son necesarias dos transferencias entre memoria y disco: una para llevar la víctima al área de intercambio y otra para traer la página referenciada al marco que se acaba de liberar. Esto duplica el tiempo de servicio de una falta de página, incrementando, por lo tanto, el tiempo medio de acceso a memoria. Esta sobrecarga en tiempo puede aliviarse utilizando el ELWGHHQVXFLDGR o bit de página modificada. En cada entrada de la tabla de páginas hay un bit de ensuciado que se pone a “falso” cuando se carga la página en memoria. Este bit permanece así hasta que se produzca una $SXQWHVGH62,
*HVWLyQGH0HPRULD
)DOWDGH3iJLQD
3DJLQDFLyQ
62
)DOWDGHSiJLQD £([FHSFLyQ
3URJUDPD
3
... ... MOVE A,B ... ...
3DJQ
5HIHUHQFLD
0HPRULD
5HDUUDQFDU
3ULQFLSDO
LQVWUXFFLyQ
7DEODGH 3iJLQDV
$FWXDOL]DU73
0DUFR /LEUH
/OHYDUSiJLQDD
PHPRULDSULQFLSDO
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
escritura en la página, en cuyo momento el hardware lo pone a “cierto”, indicando que la página ha sido modificada desde que se trajo de disco. De este modo, si en algún momento esta página es elegida como víctima en una sustitución, se consultará el bit de ensuciado; si la página no ha sido modificada quiere decir que la copia en memoria es idéntica a la copia en disco, luego no hay motivo para escribirla de nuevo en el disco. Si por el contrario ha sido modificada, habrá que actualizarla en el área de intercambio. Con esta técnica, el tiempo de sustitución de páginas se reduce a la mitad en el caso de que la víctima no haya sido modificada. Una pregunta que nos podemos hacer es: Cuando en un sistema paginado, un proceso va a pasar a ejecución ¿cuántas páginas se cargan? Si se carga un cierto número de las páginas del proceso que va a pasar a ejecución, decimos que es un sistema de SDJLQDFLyQFRQSUHDOLPHQWDFLyQ. Esto tiene la ventaja de que evita los retardos debidos a las faltas de página, pues ya no se producen; pero por contra, cada cambio de contexto implica la carga de muchas páginas, de las que la mayoría quizás no se van a referenciar en su porción de tiempo, y sin embargo va a conllevar un gran tiempo en operaciones de E/S para cargar tales páginas. Por esto, la mayoría de los sistemas van cargando cada página a medida que se referencia, bien para alimentar una instrucción o para obtener un operando, es decir, realizan una SDJLQDFLyQSRUGHPDQGD.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
(VWUXFWXUDGHOD7DEODGH3iJLQDV Ya dijimos que la función de correspondencia entre las direcciones virtuales y las físicas o reales se realiza mediante una tabla de páginas. Esta tabla tiene tantas entradas como páginas existentes y cada entrada contiene información que, aunque varía de unos sistemas a otros, suele estar compuesta de los siguientes campos: • 3URWHFFLyQ. Expresa los permisos de acceso del proceso. En caso de tener permiso para la operación de lectura/escritura que se va a realizar, se consulta el resto de los campos. • %LWGHSUHVHQFLD. Indica si la página está presente en memoria principal o no. Si se encuentra en RAM, también tienen sentido los siguientes campos. • 0DUFR RFXSDGR. Si la página se encuentra en memoria principal, este campo expresa el marco que la contiene. • 0RGLILFDGDELWGHHQVXFLDGR . Este campo indica si el contenido de la página ha sido modificado desde que se trajo de la memoria secundaria. • 5HIHUHQFLDGD. Este campo booleano se pone a cierto cada vez que se hace referencia a cualquier dirección de la página. Lo utiliza el sistema operativo para ayudar a los algoritmos de sustitución de páginas. En caso de que la página referenciada no esté cargada en memoria, sabemos que se encuentra en el área de intercambio del disco, pero ¿en qué lugar concreto? La dirección concreta de cada página virtual en el disco (cilindro, pista, sector), no se indica explícitamente en ningún campo de la tabla de páginas. En su lugar, lo que se hace es calcular la dirección. Esto se puede realizar fácilmente ya que el área de intercambio ocupa una porción contigua del disco, o sea, que no está fragmentada, bien sea por estar en una partición reservada para ello (caso de Unix y Linux), o por estar en un fichero creado en por el sistema operativo en la generación del sistema (caso de Windows: Pagefile.sys) y que tampoco está fragmentado. Conociendo la dirección de comienzo del área de intercambio y el tamaño de la página, el cálculo de la dirección de cada página es inmediato. De esta forma se ahorra mucho espacio en la tabla de páginas. Suele haber una tabla de páginas para cada proceso, en la que se expresan los permisos de acceso que tiene a cada página. Así, se consigue que un proceso no pueda acceder o realizar operaciones indebidas con información que no le pertenece o que no comparte con otros procesos. Ya que puede haber varias tablas de páginas, los procesadores suelen disponer de un registro denominado RBTP (Registro Base de la Tabla de Páginas) que apunta en todo momento a la dirección de comienzo de la tabla activa. De esta manera, la dirección de la entrada correspondiente a una página dada se obtiene así: Dir. Entrada = [RBTP] + (Nº_página x Tamaño_de_entrada) En los sistemas de hoy día, dado su amplio espectro de direccionamiento, las tablas de páginas pueden tener miles o millones de entradas, por lo que normalmente se
$SXQWHVGH62,
*HVWLyQGH0HPRULD
tendrá cargada en memoria principal solamente una parte de una tabla de páginas (la correspondiente al proceso en ejecución), el resto se mantendrá en disco. El cambio de contexto tendrá que incluir por tanto la carga y actualización de la tabla de páginas del proceso que pasa a ejecución, lo que viene a incrementar el tiempo necesitado para realizar dicho cambio. Dado el enorme rango de direccionamiento del que se dispone en los ordenadores actuales, y teniendo en cuenta que los procesos suelen referenciar sólo unas pocas páginas durante su ejecución, normalmente se utilizan unas pocas de los miles o millones de entradas que contiene la tabla de páginas, con el consiguiente desperdicio del espacio de memoria utilizado por las entradas de las páginas no correspondientes al proceso. Para evitar esto, otra posible organización de la tabla es la llamada WDEODGHSiJLQDVLQYHUWLGD. En esta tabla, hay una entrada por cada marco de memoria, conteniendo la dirección virtual de la página cargada en cada marco. Ahora solamente hay una tabla de páginas en el sistema, y cuando se genera una dirección virtual (SG), hay que buscar en la tabla para ver si alguna de las entradas contiene el número S de la página referenciada. Si la entrada i-ésima contiene el número de página referenciado, se forma la dirección real como (LG), siendo L el número del marco, y G el desplazamiento. Ya que solamente hay una tabla de páginas, antes de realizar el acceso, se debe comprobar en el campo de protección si el proceso en curso tiene permiso para realizar la operación de lectura/escritura de esa página. Ahora bien, ya que la tabla de páginas invertida solamente contiene información de las páginas en memoria, ¿que pasa cuando hay una referencia a una página que no está cargada en memoria principal? Para esto se debe mantener una tabla de páginas convencional completa. Ya que la tabla completa solamente es necesaria cuando se produce una falta de página, no necesita estar disponible con la máxima celeridad y, como puede ser muy voluminosa, suele estar ubicada en el disco duro, paginada como una parte más de la memoria. Aunque las tablas invertidas reducen radicalmente la cantidad de memoria utilizada para almacenar la tabla de páginas, también aumentan el tiempo de búsqueda de una página en la tabla, ya que la tabla está ordenada por direcciones físicas (marcos) y hay que buscar en el contenido de cada entrada para encontrar la página o dirección virtual buscada, por lo que puede llegar a ser necesario recorrer incluso la tabla entera, lo cual puede suponer demasiado tiempo. Para mejorar esta situación, se pueden utilizar algoritmos KDVK o aleatorios para limitar esta búsqueda a unas pocas entradas. También se suele recurrir a la ayuda del hardware, utilizando memoria asociativa para mantener la tabla invertida, o al menos, para contener las entradas más recientemente referenciadas. Otra forma de organizar la información de control de las páginas consiste en estructurarla en varios niveles de jerarquía, en lugar de tener una única tabla muy grande, dando lugar a las llamadas WDEODV PXOWLQLYHO, cuya organización la trataremos en la descripción del procesador Pentium de Intel.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
6RSRUWH+DUGZDUH Como acabamos de ver, uno de los soportes para la tabla de páginas puede ser un conjunto de registros dedicados a los que la CPU puede acceder con la misma rapidez que a los registros generales. En los cambios de contexto, el GLVSDWFKHU tendría que actualizar estos registros como el resto de los registros generales. Así era en el PDP-11, con direcciones de 16 bits y páginas de 8 Kb, por lo que la tabla de páginas constaba de 8 entradas mantenidas en registros de acceso rápido. Para otros pequeños ordenadores, la tabla de páginas puede llegar a tener hasta 256 entradas, por ejemplo; pero en la actualidad, la mayoría de los ordenadores permiten tablas que pueden tener millones de entradas, con lo que el uso de registros rápidos no es factible. En su lugar, la tabla de páginas se mantiene simplemente en memoria principal, y su dirección, cargada en un registro denominado 5%73 (Registro Base de la Tabla de Páginas). Como ya vimos anteriormente, para realizar una referencia a la página Q hay que acceder a la enésima entrada de la tabla, obtener el marco P y formar la dirección como ya sabemos. Pero según esto, para cada referencia de la CPU a una dirección de RAM se requieren dos accesos a memoria, uno para calcular la dirección real y otro para acceder a la dirección calculada y realizar la operación de lectura/escritura. Claramente, esto ralentiza los accesos a memoria en un factor de 2, lo cual puede resultar intolerable en ciertas circunstancias. La solución habitual a este problema consiste en utilizar una pequeña memoria asociativa de acceso muy rápido conocida como TLB (7UDQVODWLRQ/RRNDVLGH%XIIHU) que contiene las entradas de los últimos accesos a la tabla (de 8 a 2048). De este modo se consigue un tiempo medio de acceso sólo ligeramente mayor que en los accesos directos a memoria. El procesador Pentium de Intel cuenta con una memoria asociativa de 32 registros y consigue una tasa de aciertos del 98%. Para finalizar con la ayuda que se recibe del hardware en toda la operación de traducción, solamente recordar que todo el proceso de conversión de direcciones virtuales en reales se realiza bajo la dirección no de la CPU, sino de un procesador especializado en este trabajo denominado MMU (0HPRU\ 0DQDJHU 8QLW), que se sitúa entre la CPU y la memoria principal, recibiendo como entrada la dirección virtual que viene por el bus de direcciones y generando a su vez, como salida, una dirección real que va dirigida a la memoria. La utilización de memoria virtual es tan común hoy día, que los procesadores actuales suelen incluir la MMU en la misma pastilla de la CPU.
6XVWLWXFLyQGH3iJLQDV Según hemos visto hasta ahora, el tratamiento de una falta de página es como el descrito en la Figura 15, esto es, simplemente hay que traer la página referenciada desde disco a un marco libre de la memoria principal y rearrancar la instrucción que originó la falta de página. Pero ¿qué pasa si no hay ningún marco libre?
$SXQWHVGH62,
*HVWLyQGH0HPRULD
6XVWLWXFLyQGH3iJLQDV
3DJLQDFLyQ
&XDQGRQRKD\PHPRULDSULQFLSDOSDUDWRGRVORVSURFHVRV QRKD\QLQJ~QPDUFROLEUH 6DFDUXQSURFHVR GHPHPRULD
03( 3iJ HOHJLGD 1XHYD SDJ
6867,78,5 81$3É*,1$
(OHJLUYtFWLPD
6DFDUYtFWLPD DGLVFR
3iJLQD´QR SUHVHQWHµ $FWXDOL]DUQXHYD SDJ(Q73
¢&RPRHOHJLUODSiJLQDDVXVWLWXLU"
YtFWLPD
7UDHUSiJLQD UHIHUHQFLDGD
$/*25,702'( 6867,78&,Ð1
+6HGHVHDHOTXHJHQHUHODPHQRUWDVDGHIDOWDVGHSiJLQD
33DUDHYDOXDUORVVHFRQVLGHUD 6HULHVGH5HIHUHQFLD 1GHPDUFRVGLVSRQLEOHV
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
Esta situación no es extraña; pensemos que la memoria puede estar completamente ocupada por dos motivos: • Se utilizan muchas páginas del proceso en ejecución. • Se ha ido incrementando el grado de multiprogramación hasta ocupar toda la memoria principal, por lo que ésta se encuentra ocupada por páginas de diversos procesos. En este último caso, se podría optar por sacar alguno de los procesos completamente a disco, es decir, todas sus páginas; pero puede que no sea una buena idea, porque a lo mejor en la siguiente porción de tiempo hay que volver a cargar todas esas páginas. Puesto que lo único que se necesita en este momento es espacio para una página, la mejor salida para cuando se produce una falta de página suele ser la sustitución de alguna de las páginas que actualmente residen en memoria principal por la que acaba de ser referenciada. El proceso de sustitución tiene los siguientes pasos: 1. Se elige la “víctima”, es decir, una página cargada en memoria principal, para llevarla a disco y dejar un marco libre. La elección se realizará dependiendo de la política de sustitución de páginas. 2. Se lleva la página elegida al área de intercambio del disco. 3. En la entrada de la tabla de páginas correspondiente a la víctima, se indica que “no está presente”. 4. Se trae la página referenciada al marco que se acaba de dejar libre. 5. Se actualiza la entrada de la tabla de páginas correspondiente a la página que se acaba de traer a memoria principal, indicando que está presente y actualizando tanto el número de marco que ocupa como los campos de control que sean necesarios en cada caso. Nos queda un problema por resolver: ¿cómo elegir la página a sustituir cuando no hay ningún marco libre? Veamos ahora las distintas políticas de sustitución de páginas.
$OJRULWPRVGH6XVWLWXFLyQGH3iJLQDV Hay muchos algoritmos de sustitución; posiblemente cada sistema operativo tiene el suyo particular; pero en cualquier caso todos persiguen lo mismo: seleccionar páginas que causen la menor tasa posible de faltas de página. La eficacia de los algoritmos de sustitución se evalúa ejecutándolos sobre una serie concreta de referencias a memoria y contabilizando el número de faltas de página que se producen. A la serie de referencias a memoria se la denomina VHULH GH UHIHUHQFLD o cadena de referencia. Las series de referencia se pueden generar artificialmente (mediante un generador de números aleatorios) o capturando todas las referencias a memoria que se realizan en la ejecución de un programa dado. $SXQWHVGH62,
*HVWLyQGH0HPRULD
Esta última alternativa produce una gran cantidad de datos, pues cada dirección completa consta de muchos dígitos, y a nosotros solamente nos interesa el número de página. Además, cuando se hace una referencia a una página S, lo que sabemos con certeza es que las sucesivas referencias a esa misma página no causarán una falta de página si se realizan de forma inmediata, pues la página S seguro que está en memoria; por lo tanto no merece la pena considerar referencias sucesivas a la misma página. Por ejemplo, si se comprueba que las direcciones generadas por un programa son las siguientes: 0100, 0432, 0101, 0612, 0102, 0103, 0104, 0101, 0611, 0102, 0103 0104, 0101, 0610, 0102, 0103, 0104, 0101, 0609, 0102, 015 Si las páginas fueran de 100 bytes, la serie de referencia será la siguiente: 1, 4, 1, 6, 1, 6, 1, 6, 1, 6, 1, 0 Para determinar el número de faltas de página que genera un algoritmo de sustitución sobre una serie de referencia determinada, también se necesita conocer el número de marcos de memoria disponibles. Obviamente, cuanto mayor sea el número de marcos, menor será el número de faltas de página. Para la serie de referencia de nuestro ejemplo, con tres marcos se tendrían solamente tres faltas de página, las correspondientes a la primera referencia a cada página. Si por el contrario, solamente hubiera un marco, daría lugar a 11 faltas de página. Para la evaluación de los algoritmos que veremos a continuación, utilizaremos la siguiente serie de referencia: 7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1 sobre una memoria con tres marcos. 3ULPHUDHQ(QWUDU3ULPHUDHQ6DOLU),)2 Según este algoritmo, cuando hay que dejar libre un marco de memoria, se debe elegir la página que lleva más tiempo en memoria. En la Figura 17 se puede observar el comportamiento de este algoritmo para nuestra serie de referencia. La política FIFO es fácil de entender y de implementar, sin embargo su comportamiento no siempre es bueno. Resulta buena elección cuando se descargan páginas correspondientes a módulos que no se van a volver a utilizar, por ejemplo, la parte de inicialización de un programa; pero resulta pésimo cuando se elige una
$SXQWHVGH62,
*HVWLyQGH0HPRULD
página que contiene ciertas constantes que se cargaron al principio y que se utilizan a lo largo de todo el programa, por lo que enseguida va a ser referenciada. Si aplicamos este algoritmo a las estanterías de un supermercado, el resultado es igualmente desastroso. Supongamos que en un supermercado se recibe una nueva marca de jabón, y como no hay espacio en las estanterías se decide retirar el producto más antiguo del supermercado: ¡se elimina el pan del las estanterías! Hemos comentado anteriormente que, como parece obvio, a mayor número de marcos de memoria, menos faltas de página. Sin embargo hay situaciones (como la descrita en la Figura 17-inferior) en las que utilizando el algoritmo FIFO, al incrementar el número de marcos, aumentan las faltas de página. Cuando se produce esto, tenemos lo que se conoce como $QRPDOtDGH%HODG\. Para implementar este algoritmo puede bastar con mantener una cola FIFO de las páginas que entran en memoria principal. Las páginas que entran se van metiendo al final de la cola, mientras que cuando hay que elegir una página para ser sustituida, simplemente hay que tomar la que está en la cabeza de la cola.
$OJRULWPRVGH6XVWLWXFLyQ
3ULPHUDHQ(QWUDU3ULPHUDHQ6DOLU),)2
6HVXVWLWX\HODSiJLQDTXHOOHYDPiVWLHPSRHQPHPRULD 6HLPSOHPHQWDPHGLDQWHXQDFRODGHSiJLQDV
J L
%LHQ&XDQGRVHGHVFDUJDXQPyGXORGHLQLFLDOL]DFLyQ 0DO&XDQGRVHGHVFDUJDXQDSiJLQDFRQFRQVWDQWHV
PX\XWLOL]DGDV
6HULHGH5HIHUHQFLD
)DOWDVGH3iJLQD
3DUDODVHULH
6LVWHPDV2SHUDWLYRV,
$SXQWHVGH62,
&RQPDUFRV
IDOWDVGHSiJLQD
&RQPDUFRV
£IDOWDV
$QRPDOtD GH%HODG\ *HVWLyQGH3URFHVRV
*HVWLyQGH0HPRULD
$OJRULWPR¾SWLPR237 El descubrimiento de la Anomalía de Belady fue un incentivo para buscar el algoritmo óptimo, es decir, el que genera la menor tasa de faltas de página. El algoritmo óptimo dice: Reemplazar la página que más tiempo vaya a tardar en ser referenciada. En la Figura 18 vemos que para nuestra serie de referencia y con tres marcos de memoria, solamente se producen 9 faltas de página, 6 menos que con el algoritmo FIFO. Si ignoramos las seis faltas de página correspondientes a la carga inicial de cada página (pues estas faltas son inevitables en cualquier algoritmo), este algoritmo triplica en eficacia al FIFO. Por desgracia, este algoritmo tan bueno resulta difícil de implementar, pues requiere un conocimiento anticipado de la serie de referencia. Por lo tanto, suele utilizarse principalmente para realizar estudios comparativos de otros algoritmos cuya implementación sí es posible.
$OJRULWPRÐSWLPR
$OJRULWPRVGH6XVWLWXFLyQ
5HHPSOD]DUODSiJLQD TXHPiVWLHPSRYDDWDUGDUHQXWLOL]DUVH
6HULHGH5HIHUHQFLD
+
£)DOWDVGH3iJLQD
J
*DUDQWL]DODPHQRUWDVDGHIDOWDVGHSiJLQDSHUR
£ 6LVWHPDV2SHUDWLYRV,
1R 5HDOL]DEOH
6HXWLOL]DSDUD HVWXGLRVFRPSDUDWLYRV
*HVWLyQGH3URFHVRV
$SXQWHVGH62,
*HVWLyQGH0HPRULD
/D0HQRV5HFLHQWHPHQWH8WLOL]DGD/58 Ya que la implementación del algoritmo óptimo no es factible, se puede intentar realizar alguna aproximación que sí sea posible. La diferencia básica entre el algoritmo OPT y el FIFO es que este último utiliza el momento en el que la página se cargó en memoria, mientras que el OPT se sirve del momento en el que la página va a ser utilizada. Puesto que no es posible saber cuándo va a ser referenciada una página, quizás se podría utilizar el pasado más reciente como una aproximación al futuro más cercano, es decir, elegir como víctima a ODSiJLQDTXHOOHYDPiVWLHPSRVLQVHUUHIHUHQFLDGD. La sustitución mediante LRU asocia a cada página la hora a la que fue referenciada por última vez. Cuando hay que reemplazar una página, simplemente se elige la que tiene la hora de referencia más antigua. Para la serie de referencia de nuestro ejemplo, el LRU genera 12 faltas de página, que sin llegar a ser el algoritmo óptimo, sí mejora bastante al FIFO. Ya que en general es una buena política de sustitución de páginas, es el algoritmo más comúnmente utilizado, pues solamente se aprecia un comportamiento erróneo cuando se descarga una página que forma parte de un bucle que ocupa varias páginas. Posiblemente al llegar al final de bucle hay que descargar la página que se cargó al comienzo del bucle, por lo que se está eligiendo una página que se va a requerir inmediatamente. La mayor pega que presenta este algoritmo es su implementación. Hay dos enfoques posibles: • Implementarlo con ayuda del hardware • Mediante un algoritmo puramente software Una de las opciones más sencillas consiste en utilizar un campo de las entradas de la tabla de páginas, en el que se indica la hora cada vez que una página es referenciada. Para elegir una víctima, hay que recorrer la tabla de páginas buscando la entrada con la hora más antigua. Además de la sobrecarga que supone el tiempo de búsqueda en la tabla, hay que añadir la escritura que hay que realizar en la tabla de páginas cada vez que se referencia una página.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
0HQRV5HFLHQWHPHQWH8WLOL]DGD/58
$OJRULWPRVGH6XVWLWXFLyQ
6HVXVWLWX\HODSiJLQDTXHOOHYDPiVWLHPSR VLQVHUUHIHUHFQLDGD (VXQDDSUR[LPDFLyQDO$OJRULWPRÐSWLPR
6HULHGH5HIHUHQFLD
)DOWDVGH3iJLQD
J L
(QJHQHUDOHVXQEXHQVLVWHPD0X\XWLOL]DGR )DOODFRQDOJXQRVEXFOHVTXHRFXSDQYDULDVSiJLQDV
,PSOHPHQWDFLyQ
&RQWDGRUHV 3LOD
5HTXLHUHD\XGDGHOKZ&267262
$SUR[LPDFLyQ0HGLDQWH6RIWZDUH 6LVWHPDV2SHUDWLYRV,
*HVWLyQGH3URFHVRV
Una implementación alternativa consiste en mantener una pila de las páginas en memoria. Cuando se referencia una página, se elimina de la pila y se coloca en la cima de ésta. Así, la página que está en la cima de la pila es la página más recientemente referenciada, mientras que la página que se encuentra en el fondo de la pila es la que lleva más tiempo sin ser utilizada. El mantenimiento de esta pila es costoso, pues se requiere una lista de doble enlace, aunque al menos no hay que recorrer la tabla de páginas completa como en el caso anterior. No obstante, obsérvese que si cada referencia a memoria supone actualizar esta pila, para no generar una sobrecarga inaceptable, debería ser la propia MMU la que se encargue del mantenimiento de dicha pila. Como vemos, para que cualquiera de estos mecanismos sea efectivo, debe proporcionarse algún soporte hardware, cosa que muy pocas máquinas ofrecen; por eso se recurre en muchos casos a algoritmos en los que se requiere muy poca o ninguna ayuda del hardware, en los que se realizan, mediante algoritmos software, aproximaciones al LRU puro. Veamos a continuación el algoritmo más utilizado en estas aproximaciones. $OJRULWPRGHOD6HJXQGD2SRUWXQLGDGGHOUHORM Para la implementación de este algoritmo, la única asistencia que necesitamos del hardware es la del ELW GH UHIHUHQFLD. Este bit es un campo más de la tabla de páginas, y se pone a 0 al cargar la página. Cuando una página es referenciada por la CPU (lectura o escritura), el hardware pone el bit de referencia a 1. Así, pasado
$SXQWHVGH62,
*HVWLyQGH0HPRULD
6XVWLWXFLyQ
$OJRULWPRGHOD2SRUWXQLGDGGHOUHORM ),)2%LWGH´5HIHUHQFLDGDµ 7UDWDGHHYLWDUHOSUREOHPDGHODOJRULWPR),)2
+6HWLHQHXQDFROD),)2FLUFXODUGHODVSiJLQDVFDUJDGDV 6HDYDQ]DHOSXQWHURKDVWDHQFRQWUDUXQDSiJLQDFRQ5 SRQLHQGR5 DPHGLGDTXHDYDQ]D &XDQGRVHHQFXHQWUDODYtFWLPDVHVXVWLWX\HODSiJLQD LQVHUWDQGRODQXHYDHQHVDSRVLFLyQGHODFROD
6LVWHPDV2SHUDWLYRV,
$SXQWHVGH62,
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULD
un tiempo, examinando el bit de referencia se puede determinar qué páginas han sido accedidas y cuáles no. De esta forma, ya tenemos una primera aproximación al LRU: si una página no ha sido referenciada hasta ahora, se puede suponer que también va a tardar en ser accedida; por lo tanto es una aspirante a la sustitución. Pero en un corto periodo de tiempo, seguramente todas las páginas en memoria acabarán siendo referenciadas, y no sabemos en qué orden se ha ido accediendo a ellas. Por eso se requiere algún refinamiento. Para ello vamos a mantener una cola FIFO circular de las páginas cargadas (el último elemento apunta al primero de la cola), como se muestra en la Figura 20. Este algoritmo sigue los siguientes pasos: 1. La lista FIFO circular se empieza a formar a medida que se empiezan a cargar las primeras páginas en memoria. Nos serviremos de un puntero que en principio apunta a la primera página. 2. En un momento dado, al ir a cargar una nueva página, se detecta que ya no hay ningún marco libre. Hay que elegir una página a sustituir entre las de la lista. 3. Se analiza la página indicada por el puntero. 4. Si su bit de referencia está a 0, es la elegida. Se saca de la lista y en su lugar se inserta la nueva página que se trae de disco. Se avanza el puntero al siguiente elemento de la lista. Finaliza la selección. 5. Si está a 1, se pone a 0 y se avanza el puntero al siguiente elemento de la lista. Vuelta al paso 3. Aunque pueda ser un poco redundante, vamos a tratar de explicar el algoritmo con un poco más de prosa. Cuando haya que sustituir una página, comenzamos una exploración de la cola con la ayuda de un puntero. Se consulta el bit de referencia de la página apuntada por el puntero; si el bit está a 0, la página es elegida como víctima; si está a 1, suponemos que fue referenciada recientemente, por lo que se le da otra oportunidad, o sea, se pone el bit de referencia a 0, y se mueve el puntero a la siguiente página de la lista hasta encontrar una cuyo bit de referencia esté a 0, y poniendo también a 0 el bit de referencia de las páginas que se van analizando y que aparecen como accedidas. De esta manera, una página que acaba de ser analizada para ver si es sustituida, que tenía el bit de referencia a 1 y se le acaba de poner a 0, tiene la oportunidad de volver a ser referenciada antes de que el gestor de memoria de una vuelta completa recorriendo la cola en busca de páginas a sustituir. Téngase en cuenta que cuando se empieza a buscar una página en la lista circular, no se empieza siempre desde el mismo punto, sino desde la posición en la que terminó la búsqueda anterior. A medida que van siendo necesarias sustituciones de páginas, se va avanzando por la cola circular hasta encontrar una página con el bit de referencia a 0. Una vez que se elige una página, se extrae de la lista y en su lugar se inserta la correspondiente a la nueva página que se trae de disco. Si en dar una vuelta completa a la cola se
$SXQWHVGH62,
*HVWLyQGH0HPRULD
tarda un tiempo medio 7, esto significa que en un momento dado, si una página tiene su bit de referencia a 1, quiere decir que ha sido accedida por última vez en un espacio de tiempo menor que 7, por lo que no sería una buena elección como página a sustituir. En resumen, este algoritmo indica las páginas que han sido accedidas muy recientemente, por lo que simplemente hay que elegir cualquiera de las restantes. Nótese que en el peor de los casos, cuando los bits de referencia de todas las páginas están a 1, el gestor de memoria recorre toda la cola, y al empezar la segunda vuelta ya habrá una página con el bit a 0. Esto quiere decir que este sistema degenera en un algoritmo FIFO puro cuando todos los bits de referencia están a 1. Este algoritmo se puede mejorar si entre las páginas que tiene el bit de referencia a 0 se busca alguna que también tenga a 0 el bit de ensuciado, pues así no habría necesidad de escribirla en el disco antes de cargar la nueva página.
&XHVWLRQHVGH'LVHxR En los apartados anteriores hemos explicado cómo funciona un sistema de memoria virtual paginada, así como algoritmos de sustitución de páginas cuando la memoria física no puede contener todas las páginas necesarias. Pero esto no es suficiente para hacer que un sistema se comporte de una manera correcta y eficiente. En esta sección comentaremos algunas cuestiones que se deben tener en cuenta si se quiere obtener un buen rendimiento de un sistema de memoria paginada.
7UDVLHJR Supongamos que en un momento determinado, sobre un sistema con pocos marcos de página, un proceso que ocupa muchas páginas está ejecutando de forma cíclica una porción de código que implica a unas cuantas páginas. Vamos a llamar a éstas “páginas activas”. Si cuando va a cargar una de estas páginas se encuentra que no hay ningún marco libre, tendrá que elegir una víctima entre las páginas activas cargadas (u otra de otro proceso) para sustituirla por la recién referenciada. Por desgracia, está eligiendo (irremediablemente) una página que va a ser referenciada muy pronto, por lo que se volverá a producir otra falta de página, y así sucesivamente. Cuando se produce esta situación, se empieza a dedicar más tiempo a operaciones de E/S por la paginación que a ejecutar las instrucciones propias del programa. Entonces se dice que se produce WUDVLHJR. Veamos como se llega a producir el trasiego:
$SXQWHVGH62,
*HVWLyQGH0HPRULD
1. Se advierte una baja utilización de la CPU. 2. Se aumenta el grado de multiprogramación, es decir, se cargan más procesos. 3. Se hace necesario utilizar marcos de página de otros procesos cargados. 4. Se producen faltas de página. 5. Al paginar, los procesos causantes pasan a estado de espera. 6. La cola Preparados se vacía, pues empieza a haber muchos procesos en espera. 7. Disminuye el aprovechamiento de la CPU. Vuelta al paso 1. Este ciclo se repite hasta que se produce un colapso, o sea, que los procesos pierden todo el tiempo en paginar y no ejecutan instrucciones propias. En general, a medida que el grado de multiprogramación crece, también lo hace el aprovechamiento de la CPU, aunque no de forma lineal, sino cada vez más lentamente, hasta que se alcanza el máximo aprovechamiento posible de la CPU. Si llegado este punto se incrementa el grado de multiprogramación, aparece el trasiego y el rendimiento de la CPU cae drásticamente. Para evitar el trasiego, a cada proceso se le deben proporcionar los marcos de memoria que necesita para mantener sus páginas activas, para que cuando necesite cargar una página, sustituya uno de los marcos utilizados por él, no por otro proceso. Pero ¿cómo saber cuántos marcos ocupan las páginas activas? Esto lo va a indicar el “conjunto de trabajo”, que a su vez se basa en el llamado “principio de localidad” de los procesos en ejecución. Una localidad es el conjunto de páginas que realmente se están utilizando de forma activa en un momento determinado. Normalmente un programa está compuesto por varias localidades diferentes, que además pueden solaparse. El SULQFLSLR GH ORFDOLGDG dice que a medida que un proceso se ejecuta, se mueve de localidad en localidad. Por ejemplo, cuando se llama a un subprograma, éste define una nueva localidad. En esta localidad las referencias a memoria se realizan a instrucciones y datos locales del subprograma, y posiblemente a algunas de las variables globales. Cuando se sale del subprograma, el proceso abandona esta localidad, pues las variables locales y las instrucciones que lo componen ya no van a estar en uso activo, aunque quizás posteriormente se vuelva a esta localidad. Así vemos que las localidades vienen determinadas por la estructura del programa y por sus estructuras de datos. Supongamos que le asignamos a un proceso suficientes marcos para acomodar su localidad actual. Se producirán faltas de página de esta localidad hasta que se carguen todas ellas en memoria; después, no se volverán a producir faltas de página hasta que se cambie de localidad. Si no se le asignan suficientes marcos, se producirá trasiego, pues no podrá mantener en memoria todas las páginas que está utilizando de forma activa.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
7UDVLHJR
&XHVWLRQHVGH'LVHxR
6LXQSURFHVRQRWLHQHPDUFRVSDUDODVSiJLQDVDFWLYDV )DOWDGH3iJLQD
75$6,(*2
6XVWLWXFLyQSRURWUDTXH QHFHVLWDUiHQVHJXLGD
6HGHGLFDPiVWLHPSR DSDJLQDUTXHD HMHFXWDUHOSURJUDPD
25,*(1
%DMDXWLOL]DFLyQGHOD&38 6HDXPHQWDHOJUDGRGHPXOWLSURJUDPDFLyQ 6HXWLOL]DQPDUFRVGHRWURVSURFHVRV 6HSURGXFHQIDOWDVGHSiJLQD $OSDJLQDUORVSURFHVRVSDVDQDHVSHUD /DFRODGH´3UHSDUDGRVµVHYDFtD 'LVPLQX\HODXWLOL]DFLyQGHOD&38
£&RODSVR
/RVSURFHVRVSLHUGHQWRGRHOWLHPSRHQSDJLQDU 3DUD(YLWDUHO7UDVLHJR 36XVWLWXFLyQGHSiJLQDVORFDOHV 3$VLJQDUHO´&RQMXQWRGH7UDEDMRµ 3)UHFXHQFLDGHIDOWDGHSiJLQD 36XVSHQGHUHOSURFHVR
6LVWHPDV2SHUDWLYRV,
$SXQWHVGH62,
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULD
Por tanto, tenemos que las referencias a memoria que realiza un proceso, no están uniformemente distribuidas por el espacio de memoria, sino concentradas sobre la localidad en ejecución. El PRGHORGHOFRQMXQWRGHWUDEDMR está basado en el principio de localidad, y utiliza un parámetro, ∆, para definir la YHQWDQD GHO FRQMXQWR GH WUDEDMR. La idea es examinar las ∆ referencias a páginas más recientes. El conjunto de páginas accedidas en estas ∆ últimas referencias, forman el conjunto de trabajo. Así, si una página está en uso activo, estará en el conjunto de trabajo, si deja de utilizarse, posiblemente dejará de formar parte de él ∆ unidades de tiempo después de su última referencia. Así es cómo el conjunto de trabajo es una aproximación de la localidad del programa. Por ejemplo, dada la siguiente secuencia de referencias a páginas de memoria, . . .2 6 1 5 7 7 7 7 5 1 6 2 3 4 1 2 3 4 4 4 3 4 3 4 4 4 1 3 2 3 4 4 4 1 3 . . . ∆
∆
t2 t1 para un tamaño de ventana ∆=10, es decir que tiene en cuenta las últimas 10 referencias a memoria, tenemos que el conjunto de trabajo en el momento t1 es CT (t1, 10) = {1, 2, 5, 6, 7} mientras que para el momento t2, habrá cambiado a CT (t2, 10) = {3, 4} El tamaño del conjunto de trabajo en un momento dado para un proceso L lo podemos llamar TCTi, pues bien, la demanda total de marcos (D) de todos los procesos del sistema en un momento determinado será:
D = Σ TCTi Si el número de marcos necesarios es mayor que el número de marcos existentes, se producirá trasiego, pues algún proceso no tendrá suficientes marcos para su conjunto de trabajo. Si el tamaño de la ventana ∆ es igual a todas las referencias a memoria realizadas por el programa, el conjunto de trabajo a su terminación tendrá el límite máximo, es decir, todas las páginas utilizadas por el programa; sin embargo, puesto que se ha observado que el conjunto de trabajo varía muy lentamente, se puede elegir un tamaño mucho más pequeño, para el que la tasa de faltas de página sea razonable en relación a la memoria disponible. Lo que al sistema operativo le resulta difícil en la práctica, es llevar la cuenta del conjunto de trabajo de cada proceso para asignarle ese número de marcos. Si
$SXQWHVGH62,
*HVWLyQGH0HPRULD
mediante algún mecanismo se pudiera conocer, a la estrategia planificación de procesos y de sustitución de páginas se le debería añadir la siguiente condición: 1RUHHPSOD]DUQXQFDXQDSiJLQDTXHIRUPHSDUWHGHOFRQMXQWRGHWUDEDMRGH DOJ~QSURFHVRQLSDVDUDOHVWDGRGHHMHFXFLyQDXQSURFHVRFX\RFRQMXQWR GHWUDEDMRQRSXHGDFDUJDUVHFRPSOHWDPHQWHHQPHPRULD
7DPDxRGH3iJLQD Al diseñar un sistema operativo se debe tener en cuenta el tamaño de las páginas de memoria, y no existe un tamaño perfecto, sino que éste depende de una serie de factores que se deben tener en cuenta. Lo que sí es invariable es que el tamaño es una potencia de 2, y suele estar entre 512 bytes y 4 u 8 Kb. A los programas se les asigna un número entero de páginas, por ello, normalmente, en la última de ellas queda espacio sin utilizar; es decir, que se produce fragmentación interna. Por término medio, se suele desaprovechar media página por cada programa cargado, por lo que parece que cuanto más pequeña fuese la página, menor sería el espacio desperdiciado. Sin embargo, parece obvio que para un espacio virtual dado, cuanto más pequeñas sean las páginas, mayor será el número de ellas. Por ejemplo, para una memoria de 4 Mb (222) se pueden tener 4096 páginas de 1024 bytes, o 512 páginas de 8192 bytes. Entonces, a más páginas, mayor tamaño de la tabla de páginas. Teniendo en cuenta que en algunos sistemas hay una tabla de páginas por cada proceso, vemos que una página pequeña puede dar lugar a un espacio muy voluminoso dedicado a tablas de páginas. Otro problema que radica en el tamaño de la página es el tiempo dedicado a las operaciones de E/S para la carga y descarga de páginas. Para cualquier operación con el disco, sabemos que el tiempo de posicionamiento del brazo y el tiempo de latencia (o retardo rotacional) son fijos e independientes del tiempo de transferencia. Pues bien, puesto que estos dos tiempos son constantes, son preferibles las páginas grandes, para realizar pocas operaciones de E/S con gran transferencia de datos, frente a muchas operaciones de poca cantidad, puesto que crece el tiempo debido a estos dos factores constantes. Sin embargo hay programas cuyo conjunto de trabajo está formado por un gran número de regiones pequeñas y separadas en el espacio de direcciones, por lo que nos encontramos que para la lectura de un cierto dato se hace necesario leer una página completa, y si ésta es muy grande, se pierde mucho tiempo de transferencia en cargar y descargar información que no se va a utilizar. Como vemos, para determinar el tamaño óptimo de la página es necesario conocer el tipo de programas que se suelen ejecutar en un sistema. No obstante, la tendencia histórica es hacia páginas cada vez más grandes, probablemente debido
$SXQWHVGH62,
*HVWLyQGH0HPRULD
a que crece más deprisa la velocidad y la capacidad en los procesadores y en las memorias RAM que en los discos magnéticos.
7DPDxRGH3iJLQD
&XHVWLRQHVGH'LVHxR
6LHOWDPDxRGHXQSURJUDPDQR OOHQDXQQ~PHURH[DFWRGHSiJLQDV 3URJUDPD E\WHV 7DPDxRGHSiJLQD E\WHV
£ 4XHGDHVSDFLRVLQXWLOL]DU HQOD~OWLPDGHHOODV 1HFHVLWDSiJLQDV
2FXSDSiJLQDVFRPSOHWDV [ 'HOD~OWLPDSiJLQDRFXSD
'HVDSURYHFKD
)UDJPHQWDFLyQ ,QWHUQD
3DUDXQWDPDxRGHSiJLQDW SRUWpUPLQRPHGLRVHGHVDSURYHFKDWSRUSURJUDPD ,QWHUHVDQSiJLQDVSHTXHxDV 3(52 3iJLQDVSHTXHxDV 0XFKDVSiJLQDV 7DEODGH3iJLQDVJUDQGH
3+ZPiVFDUR 0iTXLQDPiVFDUD 30iVWLHPSRGH WUDQVIHUHQFLDGHVGHPHPRULD VHFXQGDULD
/DSiJLQDSHTXHxDLQWHUHVDFXDQGRHOFRQMXQWRGHWUDEDMRHVWi IRUPDGRSRUXQJUDQQ~PHURGHUHJLRQHVSHTXHxDV\VHSDUDGDV HQHOHVSDFLRGHGLUHFFLRQHVYLUWXDOHV
6LVWHPDV2SHUDWLY RV,
*HVWLyQGH0 HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
6HJPHQWDFLyQ Un aspecto importante de la gestión de memoria es la separación que hay entre la visión que el usuario tiene de la memoria y la memoria física que realmente existe. La visión que el usuario tiene de la memoria no es la misma que la memoria física real, por lo que se requieren las conocidas conversiones entre la memoria lógica y la memoria física. Sabemos que la memoria física se representa mediante un espacio de direcciones consecutivas, sin otra relación que la de orden, tal que cada dirección se corresponde con un byte de memoria física, sin mantener ninguna agrupación lógica entre ellos, y sin tener propiedades que no sean las características físicas de la memoria (RAM, ROM, tiempo de acceso, etc.). Para el programador, un programa consta de varios elementos lógicos que son los que él maneja, tales como el programa principal; un conjunto de procedimientos y funciones; estructuras de datos, como tablas, registros, pilas, variables elementales, constantes, etc. Y se refiere a ellos en el programa sin preocuparse de la posición real de memoria física que van a ocupar. Algunas partes del programa pueden ser de tamaño fijo, como las que contienen código o instrucciones, pero otras no tienen por qué serlo, sino que estructuras de datos como las tablas varían de tamaño a medida que discurre la ejecución del programa. También pueden tener ciertas propiedades dependientes de la estructura del programa, no de la memoria física que ocupan, como por ejemplo: la porción de código de un programa puede tener solamente permiso de lectura, al igual que los datos declarados como constantes, mientras que las estructuras de datos variables pueden tener permiso de lectura y escritura. Puede haber estructuras de datos públicas con permiso de lectura para todos los procesos que deseen acceder a ellas, mientras que los datos confidenciales pueden tener restringido su acceso a ciertos procesos. Para ir introduciendo el concepto de Segmentación nos serviremos del siguiente cuadro comparativo, en el que se comentan algunas diferencias básicas entre la paginación y la segmentación. 3DJLQDFLyQ Con la paginación se realiza una organización lógica de la memoria, dividiéndola en páginas, y haciendo referencia a los objetos mediante la tupla (página, desplazamiento), pero la división que hace no tiene en cuenta para nada la organización de los programas que se ejecutan, sino que considera solamente características del hardware subyacente, como la cantidad de memoria principal, y peculiaridades del dispositivo de almacenamiento secundario, como tiempo de acceso, tamaño del sector, etc. es decir, que es transparente al proceso que se está paginando.
$SXQWHVGH62,
6HJPHQWDFLyQ Con la segmentación, la memoria no se divide en páginas de igual longitud, sino en segmentos de distinto tamaño; y las referencias a los objetos no se realizan mediante el par (página, desplazamiento), sino con (segmento, desplazamiento), donde el segmento se corresponde directamente con un objeto del programador (con sus propiedades correspondientes), no simplemente con una mera porción de memoria física. Ahora los mecanismos de gestión de memoria se van a referir a los objetos de una forma parecida al programador, indicando un número de segmento, y el desplazamiento dentro de él.
*HVWLyQGH0HPRULD
6HJPHQWDFLyQ
0HPRULD9LUWXDO
¢&yPRYHHOSURJUDPDGRUODPHPRULDSULQFLSDO"
(VSDFLR)tVLFRGH'LUHFFLRQHV 3(52SDUDHOSURJUDPDGRUXQSURJUDPDFRQVWDGH XQSURJUDPDSULQFLSDOFRQXQFRQMXQWRGHSURFHGLPLHQWRV IXQFLRQHVWDEODVSLODVYDULDEOHVHWF 6,1(0%$5*2HOSURJUDPDGRUVHUHILHUHDHVWRVREMHWRV SRUVXQRPEUHVLQSUHRFXSDUVHGHODSRVLFLyQUHDOTXH RFXSDQHQPDPHPRULD
&DOFXORB[
3 L O D
5DL] FXDGUDGD 7DEODGH 6tPERORV
9,67$/Ð*,&$ '(/$ 0(025,$
3URJUDPD 3ULQFLSDO
6HUtD~WLOSHQVDUTXHVHGLVSRQHGHXQDPHPRULD GLIHUHQWHSDUDFDGDXQRGHORVREMHWRVRJUXSRVGH REMHWRVHQORVTXHSLHQVDHOSURJUDPDGRUROYLGiQGRVH GHVXWDPDxR\GHVXGLUHFFLyQ
6(*0(1726 6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
6HJPHQWDFLyQ
0HPRULD9LUWXDO
(63$&,2/Ð*,&2 '(',5(&&,21(6 5DL] FXDGUDGD
&DOFXORB[ 6HJ /RQJ
3 L O D 6 /
6HJ /RQJ
7DEODGH 6tPERORV
6HJ /RQJ
3URJUDPD 3ULQFLSDO 6HJ /RQJ
',5(&&,Ð16(*0(17$'$ 16HJPHQWR \ 'HVSOD]DPLHQWR
/DVHJPHQWDFLyQHVXQVLVWHPDGHJHVWLyQGHPHPRULD FRQVLGHUiQGRODFRPRODYHHOXVXDULR (O352*5$0$'25
QRVHSUHRFXSDGHUHIHUHQFLDUVHJPHQWRV VLQRVXVREMHWRVPyGXORVUXWLQDV YDULDEOHVHWF
(O&203,/$'25 \HO0217$'25
VHRFXSDQGHDJUXSDUFRQYHQLHQWHPHQWHORV REMHWRVGHOSURJUDPDGRUHQVHJPHQWRV\ GHUHIHUHQFLDUORVFRPR VHJPHQWRGHVSOD]DPLHQWR!
(O&$5*$'25
VHHQFDUJDGHDVLJQDUXQQ~PHURDFDGDXQR GHORVVHJPHQWRVGHTXHFRQVWDHOSURJUDPD DHMHFXWDU
6LVWHPDV2SHUDWLYRV,
$SXQWHVGH62,
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULD
De todas formas, el programador no tiene que encargarse de definir los segmentos y las referencias a los objetos que contienen. Los programas se siguen escribiendo sin preocuparse del mecanismo subyacente de gestión de memoria. Así, al compilar un programa, el compilador construye automáticamente los segmentos necesarios para reflejar la estructura lógica del programa, creando, por ejemplo, un segmento para las variables globales, otro para la pila y uno para la porción de código de cada subprograma; cada tabla de datos podría estar también en un segmento distinto. La estructura en segmentos queda reflejada en el módulo objeto generado por el compilador. El cargador es el responsable de tomar el módulo objeto, identificar cada segmento con un número y situar a cada uno de ellos en una dirección de memoria libre (Figura 24).
,PSOHPHQWDFLyQ Aunque en un sistema con memoria segmentada se dispone de múltiples espacios virtuales, y las referencias a los objetos se realizan mediante la tupla (segmento, desplazamiento), la memoria física o real sigue siendo una secuencia unidimensional de bytes. Por lo tanto se debe implementar un mecanismo que realice la representación o proyección del espacio de dos dimensiones del usuario sobre la memoria física unidimensional. En la Figura 25 podemos ver un programa en el espacio de memoria virtual formado por cinco segmentos (0 a 4), así como su ubicación en memoria física. La representación de la memoria virtual sobre la física se realiza mediante la WDEOD GH VHJPHQWRV, en la que cada entrada contiene una EDVH y un OtPLWH de segmento. La base contiene la dirección de la memoria real en la que reside el segmento, mientras que el límite indica su longitud. La utilización de la tabla de segmentos se muestra en la Figura 26. La dirección lógica tiene dos partes: un número de segmento y un desplazamiento dentro de dicho segmento (VG). El número de segmento V se utiliza como un índice para acceder a la entrada de orden V de la tabla de segmentos; el desplazamiento G debe ser un valor entre cero y el correspondiente límite de segmento, si está fuera de ese rango, se debe producir una excepción de HUURUGHGLUHFFLRQDPLHQWR, pues significa que se intenta acceder a un área de memoria fuera de los límites del segmento. Si el desplazamiento es válido, hay que añadírselo a la base del segmento para obtener la dirección física de memoria donde se encuentra el objeto referenciado. Así, una referencia al byte 48 del segmento 2 del programa de la Figura 25, se traducirá a la dirección física 4300+48 = 4348. Un intento de acceso al byte 1227 del segmento 3 produciría un error de direccionamiento, pues ese segmento tiene una longitud de 1100. De forma similar a la paginación, la tabla de segmentos se puede tener en registros de acceso rápido, de tal forma que, además, la suma de la base y el desplazamiento, y la comparación con el límite se puede realizar simultáneamente por hardware para ahorrar tiempo. Pero en el caso en que un programa esté compuesto por un gran número de segmentos, puede que no resulte factible mantener la tabla en registros, por lo que habría que recurrir a tenerla en memoria principal. En tal caso, el Registro Base de la Tabla de Segmentos (RBTS) nos daría
$SXQWHVGH62,
*HVWLyQGH0HPRULD
,PSOHPHQWDFLyQ
6HJPHQWDFLyQ
$XQTXHODVUHIHUHQFLDVDORVREMHWRVGHXQSURJUDPDVHUHDOLFHQ PHGLDQWHGLUHFLRQHVGHGRVGLPHQVLRQHVODPHPRULDItVLFDVLJXH VLHQGRXQDVHFXHQFLDXQLGLPHQVLRQDOGHE\WHV $/*25,702 75$'8&725
'LUHFFLyQ %LGLPHQVLRQDO VHJPHQWDGD
7DEODGH 6HJPHQWRV
'LUHFFLyQ 8QLPHQVLRQDO UHDO
5DL] FXDGUDGD
&DOFXORB[ 6HJ
3 L O D
6HJ
6HJ
7DEODGH 6tPERORV 6HJ
6HJ
3URJUDPD 3ULQFLSDO
6HJ
%DVH /RQJ
6HJ 6HJ
6HJ 6HJ 0HPRULD )tVLFD
$SXQWHVGH62,
(VSDFLR/yJLFR GH'LUHFFLRQHV
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULD
,PSOHPHQWDFLyQ
6HJPHQWDFLyQ
¢&yPRVHUHDOL]DQODVUHIHUHQFLDVDXQDGLUHFFLyQItVLFD" 7DEODGH 6HJPHQWRV /tPLWH%DVH
&38
VG
0HPRULD )tVLFD
Vt
≤
QR
£
(UURUGH 'LUHFFLRQDPLHQWR
¢'yQGH3RQHUOD7DEODGH6HJPHQWRV" (Q0HPRULD
(Q5HJLVWURV
L/HQWR
L/D7DEODSXHGH VHUPX\JUDQGH (Q0HPRULD3ULQFLSDO5%76
(OQ~PHURGHVHJPHQWRVSXHGHVHUYDULDEOH
£
&DGDUHIHUHQFLDDXQD GLUHFFLyQOyJLFDUHTXLHUHGRV DFFHVRVDPHPRULDSULQFLSDO
6LVWHPDV2SHUDWLYRV,
5/76
0HPRULD$VRFLDWLYD SDUDODV~OWLPDV UHIHUHQFLDV *HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
el comienzo de su posición en todo momento. Puesto que el número de segmentos es variable (depende de cada programa), se necesita saber también el tamaño o número de entradas de la tabla de segmentos, lo cual viene expresado por el contenido del Registro de Longitud de la Tabla de Segmentos (RLTS). Así, cuando hay que calcular una dirección física, primero hay que comprobar si el segmento es válido (se encuentra en la tabla de segmentos y la longitud de ésta es menor que el contenido del 5/76). Si lo es, se accede a la siguiente dirección: 5%76 + (V x tamaño_de_la_entrada) donde se encuentra la entrada correspondiente al segmento V, y a continuación se termina de formar la dirección física como ya hemos visto anteriormente. Al igual que sucedía con la paginación, este mecanismo de traducción requiere dos accesos a memoria principal por cada referencia a memoria lógica, generando una ralentización del sistema en un factor de 2. Para aliviar este retardo se suelen utilizar memorias asociativas intermedias, normalmente embebidas en las MMU’s, que contienen los descriptores de los segmentos referenciados más recientemente. Así, un pequeño número de registros asociativos puede hacer que la ralentización del sistema no suponga más de un 10% sobre un sistema no segmentado.
9HQWDMDVH,QFRQYHQLHQWHVGHOD6HJPHQWDFLyQ La segmentación de memoria presenta dos ventajas principales: protección y compartimiento. • 3URWHFFLyQ. Ya que los segmentos representan porciones del programa con coherencia semántica (por ejemplo, puede haber segmentos de código y segmentos de datos), es normal que todo el contenido de cada segmento se utilice de la misma manera; por ejemplo, en las arquitecturas actuales los programas no son automodificables, por lo que los segmentos de código pueden definirse como de sólo-lectura o sólo-ejecución. El hardware de soporte de la gestión de memoria se encarga de comprobar los bits de protección que figuran en la tabla de segmentos para prevenir accesos ilegales a memoria, tales como el intento de escritura en un área de sólo-lectura o la utilización de un segmento de sólo-ejecución como área de datos. También, poniendo una tabla en un segmento dedicado, se comprobará automáticamente que todos los accesos a la tabla son válidos (no exceden el tamaño de la tabla). De esta manera, muchos errores comunes de programación se pueden detectar por el hardware antes de que lleguen a causar un daño de consideración. • &RPSDUWLPLHQWR. La otra ventaja de la segmentación es la posibilidad de compartir código o datos. Cada proceso tiene su propia tabla de segmentos, cuya dirección se mantiene en el BCP de cada proceso, de tal forma que en cada cambio de contexto, se establece el RBTS correspondiente al proceso que pasa a ejecución. En la Figura 27 puede verse cómo dos procesos pueden
$SXQWHVGH62,
*HVWLyQGH0HPRULD
9HQWDMDV
6HJPHQWDFLyQ
+)DFLOLWDHO6LVWHPDGH3URWHFFLyQ
0 1
/tPLWH
%DVH
3HUPLVRV
25286 8550
43062 90003
ej ej ej le - -
+)DFLOLGDGSDUD&RPSDUWLU'DWRVR&yGLJR
Editor Seg. 0 'DWRV3 Seg. 1
/tPLWH %DVH 0 25286 43062 1 4425 63384 7DEODGH VHJPHQWRVGH3
0HPRULD/yJLFD GH3
Editor Seg. 0
(GLWRU
'DWRV /tPLWH %DVH 0 25286 43062 1 8550 90003 7DEODGH VHJPHQWRVGH3
'DWRV
'DWRV3 Seg. 1
0HPRULD/yJLFD GH3
6LVWHPDV2SHUDWLYRV,
£2-2 (OVHJPHQWRGHFyGLJRGHEHWHQHUHO PLVPRQ~PHURHQWRGRVORVSURFHVRV TXHORFRPSDUWHQ *HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
compartir segmentos. Esto es así cuando las entradas de las tablas de segmentos de los dos procesos apuntan a las mismas direcciones físicas. Ya que el mecanismo de compartimiento reside en el nivel de segmentación, cualquier información puede compartirse si se define en un segmento. Puesto que los segmentos pueden compartirse, un programa compuesto por varios segmentos también puede compartirse entre varios procesos. Un ejemplo puede ser el compartimiento de un editor de texto en un sistema multiusuario, donde en lugar de tener Q copias completas del programa, solamente se requiere una copia de los segmentos de código. Lo que sí se requiere para cada usuario son segmentos separados (no compartidos) de datos para almacenar la información o texto particular de cada uno. A la hora de compartir un segmento de código, se debe tener en cuenta la siguiente sutileza. Los segmentos de código contienen instrucciones que suelen hacer referencia a otras instrucciones del mismo segmento, por ejemplo, una instrucción de salto. La dirección de destino del salto estará formada por un número de segmento y un desplazamiento, siendo este número de segmento el mismo que el que contiene la instrucción de salto. Si se quiere compartir este segmento, todos los procesos que lo compartan deben tener definido ese segmento con el mismo número. Esto no es necesario si las referencias a las direcciones no son absolutas, es decir si se realiza mediante indirecciones o direccionamientos relativos (respecto al contador de programa o al registro que contiene el número de segmento actual). El planificador junto con el gestor de memoria deben ocuparse de encontrar y asignar memoria para todos los segmentos de un programa. Esta situación es similar a la paginación, excepto que los segmentos son de tamaño variable, mientras que las páginas son todas de la misma longitud. Así, aquí nos encontramos con las mismas características de asignación de memoria que se tenían en el modelo de particiones de memoria de tamaño variable visto anteriormente; donde la principal diferencia está en que aquí, los programas pueden ocupar varios segmentos o particiones. Uno de los problemas que se tenía con las particiones variables (y que aquí se mantiene) era el de la fragmentación externa. En este caso, puede hacerse necesaria una recompactación de la memoria para aprovechar los pequeños huecos no adyacentes. La facilidad para poder ubicar mejor o peor los segmentos depende de su tamaño. Si un programa ocupa un solo segmento, puede hacerse difícil su ubicación en memoria; por el contrario, si cada byte del programa se mete en un segmento separado, serán fáciles de ubicar, pero se necesitaría un registro base para cada byte, lo cual significa que habría que duplicar la memoria para poder contener los registros base de todos los segmentos. Por último, veamos otra pega que se nos puede presentar con la segmentación. Puesto que un programa está formado por segmentos, el gestor de memoria puede ocuparse de cargar en memoria, en cada momento, exclusivamente los segmentos necesarios para la ejecución, por lo que no es preciso tener presentes en memoria todos los segmentos del programa. No obstante, ¿qué pasa si el tamaño de un
$SXQWHVGH62,
*HVWLyQGH0HPRULD
,QFRQYHQLHQWHV
6HJPHQWDFLyQ
3DUDFDUJDU\HMHFXWDUXQSURJUDPDVHOHGHEHDVLJQDUPHPRULD OLEUHSDUDVXVVHJPHQWRVGHPHPRULD /RVVHJPHQWRVGHPHPRULDVRQGHWDPDxRYDULDEOH
£
7LHQHQODVPLVPDVFDUDFWHUtVWLFDVTXHODVSDUWLFLRQHVGH PHPRULDGHWDPDxRYDULDEOH
/RVPLVPRVPpWRGRV GHJHVWLyQ
/RVPLVPRVSUREOHPDV
£
)5$*0(17$&,Ð1 (;7(51$
6ROXFLRQHVDOD)UDJPHQWDFLyQ([WHUQD 3 (VSHUDUDTXHHVWpGLVSRQLEOHXQEORTXHGHPHPRULD VXILFLHQWHPHQWHJUDQGH£LQYLDEOHHQPXFKRVFDVRV 3 &RPSDFWDFLyQSHULyGLFDGHODPHPRULD £PX\FRVWRVR
3XHGHUHVXOWDUPX\GLItFLOPDQWHQHUWRGRVORV VHJPHQWRVFRPSOHWRVGHXQSURJUDPDHQ PHPRULDSULQFLSDO
6HJPHQWDFLyQ3DJLQDGD 6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
segmento sobrepasa el tamaño de la memoria física disponible? ¡Pues que no se podría ejecutar! Este es otro problema de los sistemas segmentados. En los sistemas paginados, la unidad de carga en memoria es la página, y ésta siempre es de un tamaño bastante menor que la memoria real disponible, mientras que en los sistemas segmentados la unidad de carga en memoria es el segmento, que por definición es de tamaño variable y no tiene límites preestablecidos.
6HJPHQWDFLyQ3DJLQDGD La solución al problema de la fragmentación en el modelo de particiones de tamaño variable fue la paginación. Por otra parte tenemos que con la segmentación se facilita el sistema de protección y compartimiento de la información, así como una mayor flexibilidad para variar dinámicamente el tamaño de las particiones que ocupan los procesos. Entonces, ¡por qué no combinar los dos sistemas, la paginación y la segmentación? En efecto, la solución ha venido con sistemas híbridos, dando lugar a sistemas de memoria con paginación segmentada y a los más comunes, los de segmentación paginada, es decir, un esquema en el que en primer lugar se establece un sistema de segmentación de cara al usuario y, a continuación, acercándonos al hardware, lo que se hace es paginar los segmentos. Lo que tenemos, por tanto, son programas formados por segmentos, y estos a su vez, formados por la siguiente unidad de memoria, es decir, por páginas. Con esta combinación se consiguen los beneficios de la segmentación sin las conocidas desventajas de la fragmentación externa y con el consiguiente beneficio de poder no solamente ejecutar programas con segmentos mayores que la memoria física, sino de obtener también un mayor grado de multiprogramación al no ser obligatorio cargar segmentos completos de procesos, sino solamente las porciones (páginas) imprescindibles. Veamos como ejemplo, en el siguiente apartado, el sistema de segmentación paginada utilizada por Intel en sus procesadores Pentium.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULDHQHO3HQWLXPGH,QWHO &DUDFWHUtVWLFDV*HQHUDOHVGHVX$UTXLWHFWXUD El Pentium de Intel es un procesador de propósito general con el soporte para gestión de memoria integrado. Ya que una descripción muy detallada requeriría un seminario en exclusiva, aquí simplemente vamos a intentar dar una visión lo suficientemente clara como para entender los mecanismos de soporte para la gestión de memoria que ofrecen estos procesadores. Puesto que los conceptos de alto nivel de la segmentación y paginación ya se han visto, vamos realizar una descripción de abajo hacia arriba, es decir, comenzaremos comentando conceptos básicos, para irlos agrupando poco a poco hasta llegar al concepto general. Para empezar, algunos datos básicos sobre su arquitectura. Este procesador utiliza ELWV para el EXVGHGLUHFFLRQHV y ELWV para el de GDWRV, lo que significa que puede direccionar 4 Gigabytes de memoria física y puede leer o escribir operandos de hasta 8 bytes en una sola operación de lectura/escritura. Tiene dos modos de funcionamiento: Real y Protegido. El modo Real es el modo compatible con la arquitectura del 8086. Aquí nosotros vamos a referirnos exclusivamente al PRGR 3URWHJLGR, que es el modo particular de trabajo del Pentium y que ofrece la gestión de memoria que nos ocupa.
En la Figura 29 pueden verse los registros accesibles de este procesador. EAX, EBX, ECX y EDX son registros de uso general; ESI y EDI se utilizan para direccionamientos indexados; mientras que EBP y ESP están destinados a la gestión de la pila y llamadas a subprogramas. El contador de programa es el EIP y el registro de estado es EFLAGS. De especial interés para nosotros son los registros de segmento; el CS hace referencia al segmento actual de código y el SS al segmento de la pila en uso, mientras que DS, ES, FS y GS se utilizan como segmentos de datos. Por último tenemos 4 registros de control: CR0, CR1, CR2 y CR3, cuya utilidad iremos viendo más adelante. Ahora entraremos ya en conceptos directamente relacionados con el direccionamiento y la gestión de memoria. Empecemos por los WUHV HVSDFLRV GH GLUHFFLRQDPLHQWR del Pentium: Virtual, Lineal y Físico. • La direcciónYLUWXDO o lógica esta formada por una GLUHFFLyQGHVHJPHQWR y un desplazamiento o GLUHFFLyQHIHFWLYD. La dirección efectiva se forma a partir de la información sobre el operando que figura en la instrucción máquina. Puede estar formada por un registro base, uno de índice, más un desplazamiento. En caso de que los operandos a los que haga referencia sean bytes, palabras, o dobles o cuádruples palabras, puede aplicarse un factor de escala al registro de índice, por lo que habría que multiplicarlo por 1, 2, 4 u 8. En cuanto a la
$SXQWHVGH62,
*HVWLyQGH0HPRULD
,QWHO3HQWLXP
&DUDFWHUtVWLFDV*HQHUDOHV
'LUHFFLRQHV
*E\WHV GH0HPRULD)tVLFD
'DWRV
'DWRVGH \ELWV
0RGRVGH )XQFLRQDPLHQWR 5($/ 3527(*,'2
5HJLVWURV *HQHUDOHV
($;(%;(&;('; (6,(',
&RQWDGRUGH 5HJLVWURV 5HJLVWURV 3URJUDPD GH6HJPHQWR GH&RQWURO \5HJ(VWDGR (,3()/$*6
&666'6
&5&5
(6)6*6
&5&5
(%3(63
6LVWHPDV2SHUDWLYRV,
$SXQWHVGH62,
*HVWLyQGH0HPRULD
*HVWLyQGH0HPRULD
dirección de segmento, como ya veremos, se obtiene a partir de un VHOHFWRU. El selector es el contenido de uno de los registros de segmento, y se describe en detalle un poco más adelante. • La unidad de segmentación transforma la dirección virtual de 46 bits en una GLUHFFLyQOLQHDO de 32 bits. Si no está activa la paginación, la dirección lineal es directamente la dirección física. • Si se trabaja con paginación (está activo el bit PG del CR0), la unidad de paginación convierte la dirección lineal en una GLUHFFLyQItVLFD de 32 bits, con la que ya se accede a una posición de memoria física.
(VSDFLRVGH'LUHFFLRQDPLHQWR
,QWHO3HQWLXP
&iOFXORGHOD',5(&&,Ð1()(&7,9$
%$6(
Ì1',&(
'(63/$=$0,(172
; &5 3
(6&$/$
*
5$0
'LU(IHFWLYD
'LU9LUWXDO 6(/(&725
8QLGDGGH 6(*0(17$&,Ð1
'LU
8QLGDGGH 3$*,1$&,Ð1
/LQHDO
6LVWHPDV2SHUDWLYRV,
'LU )tVLFD
*HVWLyQGH0HPRULD
En primer lugar estudiaremos el mecanismo de segmentación y a continuación pasaremos a ver el de paginación.
0HFDQLVPRGH6HJPHQWDFLyQ Hasta ahora hemos visto, en los sistemas generales de segmentación, cómo el procesador al hacer referencia a una posición de memoria, en la dirección que saca por el bus de direcciones indica explícitamente el segmento correspondiente a esa posición de memoria. Esto lo hacía incluyendo el número de segmento en los bits más significativos de la dirección. Como vemos, HQFDGDUHIHUHQFLDDPHPRULDVH LQGLFDH[SOtFLWDPHQWHHOQ~PHURGHVHJPHQWRHQODGLUHFFLyQ.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
Pues bien hay otro método alternativo para indicar el segmento en una referencia a memoria. En este otro métodoHOQ~PHURGHVHJPHQWRVHREWLHQHLPSOtFLWDPHQWH dependiendo del tipo de acceso a memoria que se esté realizando. Como ejemplo de valores implícitos en los direccionamientos tenemos las instrucciones de pila, tales como push y pop. En estas instrucciones no se indica explícitamente en la propia instrucción la dirección del operando (la cima de la pila), sino que tal dirección se asume implícitamente, y realmente se obtiene a partir del registro Puntero de Pila. Los procesadores de Intel con arquitectura de 32 bits (80386, 80486 y Pentium) utilizan este sistema en el que el número de segmento no forma parte de la dirección que figura en el operando de una instrucción, sino que dicho número de segmento se mantiene en unos UHJLVWURVGHVHJPHQWRdentro del procesador, de tal forma que el mecanismo de segmentación se encarga de formar una dirección completa a partir de la dirección que figura en el operando de una instrucción (o en el registro Puntero de Instrucción) más el segmento que se obtiene a partir de un registro de segmento. Hecha esta aclaración sobre el método utilizado por los procesadores de esta familia para referenciar un segmento, pasemos a ver su mecanismo completo de segmentación. Empezaremos diciendo que el tamaño máximo de un segmento es 4 Gigabytes, y que un proceso puede acceder hasta un máximo de 8 Ksegmentos locales (particulares del proceso) y a 8 Ksegmentos globales (compartidos con los demás procesos). Si cada tarea puede tener hasta 16 Ksegmentos (214) y el tamaño máximo de cada segmento es 4 Gb (232 bytes), esto significa un total de 64 46 terabytes (2 bytes) de memoria virtual. Toda la información relativa a un segmento (dirección base, tamaño o límite, permisos de acceso, atributos, etc.) está contenida en el GHVFULSWRUGHVHJPHQWR. Un descriptor de segmento es una estructura de 8 bytes que contiene los siguientes campos de información: %DVH
Dirección de comienzo del segmento.
/tPLWH
Longitud del segmento
3
Bit de presencia. 1= Presente; 0= No presente
'3/
Nivel de privilegio del descriptor
6
0= Descriptor del sistema; 1= Descriptor de código o datos
7LSR
Tipo de segmento. Permisos de acceso
$
Bit de “accedido”. Lo pone el procesador cuando accede al segmento.
*
Granularidad.
'
Longitud por defecto de los operandos. 0=16 bits; 1=32 bits
0= Longitud del segmento indicada en bytes 1= Longitud expresada en páginas (de 4 Kb)
'HVFULSWRUGH6HJPHQWR
$SXQWHVGH62,
*HVWLyQGH0HPRULD
/RV6HJPHQWRV
,QWHO3HQWLXP
+7DPDxR0i[LPRGHO6HJPHQWR*E\WHV ++D\KDVWDN6HJPHQWRV*OREDOHVDWRGRVORVSURFHVRV +&DGDSURFHVRSXHGHWHQHUKDVWDN6HJPHQWRV/RFDOHV
%DVHGHO6HJPHQWR %DVH *'
/tPLWH
/tPLWHGHO6HJPHQWR 3'3/67LSR$
%DVH
'HVFULSWRUGH6HJPHQWR
*'75
GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU
7DEODGH 'HVFULSWRUHV*OREDOHV
6LVWHPDV2SHUDWLYRV,
/'75
GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU
7DEODVGH 'HVFULSWRUHV/RFDOHV
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
Hay una 7DEOD GH 'HVFULSWRUHV *OREDOHV (GDT) que contiene todos los descriptores de los segmentos globales. Por otra parte, cada proceso tiene una 7DEOD GH 'HVFULSWRUHV /RFDOHV (LDT) que contiene los descriptores de sus segmentos particulares. La dirección de la GDT está contenida en un registro del procesador, el GDTR, mientras que la dirección de la LDT que se está utilizando en un momento dado, se mantiene en otro registro, el LDTR. Obsérvese que en un sistema hay una única tabla de descriptores globales y muchas tablas de descriptores locales, una por cada proceso. Y cuando se realiza el direccionamiento a un dato en memoria ¿cómo se sabe qué descriptor de segmento hay que utilizar? La respuesta está en el VHOHFWRU, cuyo formato se muestra en la Figura 32. Como vemos, consta de tres campos: ºQGLFH
Indica una de las 8 K entradas de la tabla de descriptores globales o de la tabla de descriptores locales, dependiendo del valor del campo TI.
,QGLFDGRUGH7DEOD (TI)
TI=0 → Se hace referencia a la tabla de descriptores globales (GDT) TI=1 → Se hace referencia a la tabla de descriptores locales (LDT)
1LYHOGH3ULYLOHJLR GHO3HWLFLRQDULR (RPL)
Contiene el nivel de privilegio del proceso que efectúa el acceso al segmento &DPSRVGHXQ6HOHFWRU
Para hacer referencia a un segmento se utiliza un selector, cuyo campo TI indica si se trata de un segmento global o local del proceso. Una vez identificada la tabla de descriptores (GDT o LDT), se toma el descriptor señalado por el Índice y a continuación se comprueba si el nivel de privilegio del proceso (contenido en el RPL) es igual o mayor que el nivel de privilegio del segmento (expresado en el campo DPL del descriptor). Bien, sabemos que los descriptores de segmentos están en las tablas de descriptores, pero ¿dónde están los selectores? Los selectores están en los registros de segmento (CS, DS, SS, ES, FS, GS). Así tenemos que en cada momento dado hay 6 descriptores que están siendo apuntados por los selectores contenidos en los 6 registros de segmento del procesador. Pero si hay 6 registros ¿cuál se utiliza? 6LVHHVWiDOLPHQWDQGRXQDLQVWUXFFLyQ 6LVHHVWiDFFHGLHQGRDXQRSHUDQGR 6LVHHVWiUHDOL]DQGRXQDRSHUDFLyQFRQODSLOD
$SXQWHVGH62,
→&6 →'6 →66
*HVWLyQGH0HPRULD
/RV6HOHFWRUHV
,QWHO3HQWLXP
6(/(&725
ÍNDICE
TI
RPL 7,
7,
7DEODGH 'HVFULSWRUHV /RFDOHV
GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU
7DEODGH 'HVFULSWRUHV *OREDOHV
GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU
'RV$FFHVRVD0HPRULDSDUD2EWHQHUXQ'DWR£
£12 5HJLVWURVGH'HVFULSWRUHV 'LU%DVH &6
/tPLWH $WULEXWRV
6(/(&725
'6 6(/(&725 66 6(/(&725 (6
6(/(&725
)6
6(/(&725
*6 6(/(&725 6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
La selección del registro a utilizar se realiza automáticamente en función del tipo de direccionamiento que se está realizando (a código, a datos o a la pila). Para acceder a un dato situado en uno de los segmentos auxiliares de datos (ES, FS o GS) hay que indicarlo explícitamente mediante un prefijo de instrucción. ¡Pero según este esquema, cada referencia a memoria generada por la CPU originaría realmente dos accesos! uno para obtener el descriptor del segmento indicado por el selector y otro para acceder propiamente al dato, lo que significaría una duplicación en el tiempo de acceso. Para evitar esto, se dispone de una memoria caché, también conocida como 5HJLVWURV GH 'HVFULSWRUHV, que contiene los 6 descriptores apuntados por los selectores cargados en los registros de segmento (Ver Figura 33). El contenido de estos registros de descriptores solamente se modifica cuando se realiza una operación de “carga de registro de segmento”, momento en el que habría que acceder a memoria para cargar el descriptor correspondiente al nuevo selector cargado en el registro de segmento. De esta manera, si está alimentando una instrucción (interviene el registro IP) se utiliza directamente el registro de descriptor de código, cuyo contenido es el descriptor del segmento de código; si se está accediendo a un dato, se utiliza el registro de descriptor de datos, etc. Para ayudar a ver de forma general el sistema de segmentación, presentaremos el escenario completo de un programa desde que se escribe hasta que se ejecuta. 1. El usuario escribe, en un lenguaje de alto nivel, un programa compuesto de distintos módulos, procedimientos, funciones, variables globales y locales a los subprogramas, tablas, etc. sin preocuparse de si la máquina en la que se va a ejecutar tiene o no una memoria segmentada. 2. Al traducir cada módulo, el compilador, además de producir el código correspondiente, estructura el código y datos generados en varios segmentos de código, de datos y de pila. 3. En la fase de montaje, al unir todos los módulos componentes del programa, se resuelven todas las referencias externas y se forma un único módulo cargable, estructurado en múltiples segmentos correspondientes a los subprogramas y estructuras de datos del programa completo. El montador también se encarga de numerar cada uno de los segmentos utilizados. El código generado incluye las instrucciones necesarias para que cada vez que se haga referencia a una nueva sección de datos, se cargue el registro de segmento correspondiente (DS, SS, ...) con el selector apropiado. 4. Una vez cargado el módulo en memoria principal, las primeras instrucciones que se ejecutarán serán las encargadas de cargar los registros de segmento CS, DS y SS con los selectores correspondientes a los segmentos que contienen las primeras instrucciones y datos del usuario, así como de la pila de trabajo.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
5. Al cargar los registros de segmento, se accede a las tablas de descriptores para obtener los descriptores correspondientes, y automáticamente se almacenan los descriptores obtenidos en los respectivos registros de descriptores. 6. Ahora, al empezar a ejecutar ya las instrucciones propias del usuario, se utilizan directamente los registros de descriptores de los segmentos que contienen las instrucciones, datos o pila actualmente en uso. 7. Durante la ejecución, si hay una instrucción que hace referencia a un dato de un segmento distinto de los que se están utilizando, previamente a está instrucción, debe haber otra instrucción que se encargue de cargar uno de los registros de segmento de datos (DS, ES, FS o GS) con el selector correspondiente al segmento que contiene el dato que se va a referenciar. Las instrucciones de carga de registros de segmento las inserta automáticamente el compilador, o sea, que el programador no tiene que preocuparse de ninguna cuestión relativa al mecanismo utilizado en la gestión de memoria.
9LVLyQ*OREDOGHOD(WDSDGH6HJPHQWDFLyQ
,QWHO3HQWLXP
5HJLVWURV GH6HJPHQWR *6 )6
'LUHFFLyQ (IHFWLYD
(6 '6 66 &6
6(/(&725
!
VL
£(5525
QR
',5(&&,Ð1 /,1($/
6HJPHQWR 6HOHFFLRQDGR 3HUPLVRV /tPLWH 'LU%DVH
'LU%DVH GHO6HJPHQWR
5HJLVWURV GH'HVFULSWRUHV
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
0HFDQLVPRGH3DJLQDFLyQ Una vez tratada la etapa de segmentación, en la que se produce como resultado una dirección lineal de 32 bits, pasemos a ver la fase de paginación, cuya utilización es optativa. Para tener activada la paginación se debe tener a 1 el bit PG (3DJLQJ (QDEOH) del registro de control CR0. 12 El tamaño de las páginas del Pentium se puede establecer en 4 Kb (2 ) o en 4 Mb 22 (2 ). Aquí consideraremos un tamaño de 4 Kb, por lo que para un rango de direcciones de 32 bits, la memoria virtual está dividida en 220 páginas, o sea, 1 Mpágina. Si adelantamos que un descriptor de página ocupa 4 bytes, tenemos que la tabla de páginas de cada proceso ocuparía 4 Mb, lo cual, desde luego, resulta ser una ocupación de memoria bastante considerable. Por otra parte, por el principio de localidad, no es normal que un proceso haga una referencia uniforme a todo el espacio de direccionamiento de memoria, por lo que seguramente la ejecución de un proceso podría realizarse con bastante menos de 1 Mpágina.
Por este motivo, los descriptores de páginas están estructurados en dos niveles: • El Directorio de Páginas • La Tabla de Páginas El contenido, tanto del directorio como de la tabla de páginas, son descriptores de páginas, cuyo formato es el siguiente: 0DUFR
Indica el número de marco ocupado por la página
(
Bit de ensuciado.
$
Bit de accedida o referenciada
86
Página de Usuario o Supervisor
/(
Permiso de lectura/escritura
3
Bit de presencia
Echémosle un vistazo a la Figura 34. Puesto que el tamaño de la página es de 4 Kb, los 12 bits de menor peso de la dirección lineal corresponden al desplazamiento. Por lo tanto los 20 bits de mayor peso indican la página. Pues bien, tomando los 10 bits de mayor peso, y teniendo en el registro de control CR3 la dirección de comienzo del directorio, se hace referencia a una de las entradas del directorio de páginas. Esta entrada hará referencia a su vez a una de las 210 tablas de página del sistema. Ahora, tomando como referencia la tabla indicada en el directorio de páginas y tomando la entrada indicada por los siguientes 10 bits (12 a 21) de la dirección lineal, se obtiene ya el descriptor de la página correspondiente a la dirección referenciada por la CPU.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
3DJLQDFLyQ
,QWHO3HQWLXP
',5(&&,Ð19,578$/ 6(/(&725 'LUHFFLyQ(IHFWLYD
GHVFULSWRUGHVHJPHQWR
7DEODGH'HVFULSWRUHV
3iJLQD
'HVSOD]DPLHQWR
'HVFUGHSiJ
'HVFUGHSiJ
'LUHFWRULR GH3iJLQDV
7DEODGH 3iJLQDV
'LU)tVLFD
0DUFR
'LUHFFLyQ9LUWXDO
7/%
'LUHFWRULR
5$0
&5
'LU0DUFRGH3iJ UHVHUYDGR( $
8/ 3 6 (
'(6&5,3725'(3É*,1$ 6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
De esta forma, no es necesario tener cargada en memoria una tabla de 1 Megaentrada, sino que simplemente hay que tener en memoria el directorio y las tablas de páginas que utilice el proceso, no todas. Una vez obtenido el descriptor de la página referenciada por la CPU, se forma la dirección física a partir del marco (si la página está en memoria) y del desplazamiento. El mecanismo de paginación de estos procesadores está diseñado para soportar sistemas de memoria virtual mediante paginación por demanda; sin embargo, sus prestaciones se verían muy afectadas si el procesador tuviera que acceder a los dos niveles de tablas en cada referencia a memoria. Para resolver este problema, el Pentium mantiene una memoria caché con los descriptores de las páginas más recientemente referenciadas. Esta caché (denominada TLB o %XIIHUGH7UDGXFFLyQ) consiste en una memoria asociativa de 32 entradas, es decir, que contiene los últimos 32 descriptores de páginas referenciadas. Ya que las páginas son de 4 Kbytes, en la TLB se cubre un área de memoria de 128 Kbytes. Para sistemas multitarea convencionales, el TLB obtiene una tasa de aciertos del 98%, lo que significa que solamente el 2% de las referencias a memoria requerirán el acceso a la estructura de tablas de dos niveles.
(MHPSORGH'LUHFFLRQDPLHQWR Vamos a ver un ejemplo de direccionamiento a memoria virtual con paginación segmentada en el microprocesador de Intel que hemos tratado. Vamos a suponer que la CPU, durante la ejecución de una instrucción realiza una referencia a la dirección de memoria 4C703H para leer un operando. Sabemos que para calcular la dirección física de la memoria, necesitamos tener el descriptor del segmento correspondiente, y para conseguir el descriptor de segmento previamente se requiere disponer del selector. Como ya sabemos, los selectores se encuentran en los registros de segmento. Ya que se trata del acceso a un dato (no alimentar una instrucción), el selector que nos interesa se encuentra en el registro de segmento de datos DS. Como podemos ver en la Figura 35, el selector del DS contiene un índice con valor 27 (1B H), el indicador de tabla (TI) referencia a la Tabla de Descriptores Locales, y el nivel de privilegio de acceso (RPL) es 2. Con estos datos, se puede acceder a la entrada 27 de la Tabla de Descriptores Locales y obtener el descriptor del segmento de datos. Obtenido el descriptor, se almacena en su registro de descriptor correspondiente. Analizando el contenido del descriptor de segmento obtenemos los siguientes datos relevantes:
$SXQWHVGH62,
*HVWLyQGH0HPRULD
(MHPSORGH'LUHFFLRQDPLHQWR
,QWHO3HQWLXP
/HFWXUDGHXQRSHUDQGRHQODGLUHFFLyQ&+GLUGLUHFWR 'LUHFFLyQ(IHFWLYD&+ TI
RPL
1
1 0
'6 00 0000 0001 1011
6HOHFWRU
7, GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU GHVFU%
7DEODGH 'HVFULSWRUHV /RFDOHV
5HJLVWURVGH'HVFULSWRUHV 5'&6 5''6 5'66 5'(6 5')6 5'*6
'HVFULSWRUGHO 6HJPHQWR'6 Base del Segmento 15..0
Límite del Segmento 15..0
0011 0000 0000 0000
0010 0000 0000 0000
Base 31..24
G
D
0000 0000 0
0
0
0
Lím. 19..16
P
DPL
S
Tipo
A
Base 23..16
0101
1
0 1 0
0 0 0
0
0000 0000
%DVHGHO6HJPHQWR+ /tPLWH + !& *UDQXODULGDG
'3/ 6LVWHPD XVXDULR
'LU2SHUDQGR%DVH6HJ'LU(IHFWLYD & )+
',5(&&,Ð1/,1($/)+ 6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,
*HVWLyQGH0HPRULD
Base del segmento: Límite del segmento: DPL: Sistema: Granularidad:
3000H 52000H 1 0 (de usuario) 0 (byte)
Ya que la dirección efectiva no excede el límite del segmento y que el DPL no es mayor que el RPL (se tiene privilegio de acceso), se puede continuar calculando la dirección lineal, así, ésta se obtiene sumando simplemente la base del segmento más la dirección efectiva, esto es, 3000H + 4C703H, obteniendo que OD GLUHFFLyQ OLQHDOHV)+. En la Figura 36 podemos ver que el bit de paginación del registro de control CR0 está activado, luego hay que continuar con el proceso de paginación para obtener la dirección física. Descomponiendo la dirección lineal 4F703 obtenemos un directorio de páginas (0), una página (4FH) y un desplazamiento (703H). El registro de control CR3 contiene F12000H como dirección de comienzo del directorio de páginas. Tomando la entrada 0 de este directorio, obtenemos el descriptor de la página que contiene la tabla de páginas que nos corresponde, el cual apunta a la dirección 42B74H, dirección en la que se encuentra por tanto nuestra tabla de páginas. Obtenida la tabla de páginas, accedemos ahora al descriptor de la página donde se encuentra la dirección referenciada. La página en cuestión la obtuvimos de la descomposición de la dirección lineal, resultando ser la página 4FH, por lo que accedemos a la entrada 4FH de la tabla de páginas (que estará en la dirección 42B74H + (4FH x 4), ya que cada entrada ocupa 4 bytes) y obtenemos el descriptor de la página. Como se puede ver en la parte inferior de la Figura 36, el marco donde se encuentra la página es el 125A3H y está presente en memoria. Ya que el tamaño de cada página ( o marco) es de 4 Kb (1000H), la dirección del marco indicado se hallará 12 multiplicando el número de marco por 2 , es decir, 125A3000H. Ya solamente resta por sumar el desplazamiento a la dirección base de la página para obtener la GLUHFFLyQItVLFD, esto es, 125A3000H + 703H = $+. Debemos comentar ahora, que al entrar en la etapa de paginación se consulta la memoria asociativa de direcciones (TLB), y si la dirección lineal se encuentra en esta memoria, no es necesario realizar todo el proceso de paginación pues directamente se obtendría la dirección física correspondiente.
$SXQWHVGH62,
*HVWLyQGH0HPRULD
(MHPSORGH'LUHFFLRQDPLHQWR
,QWHO3HQWLXP
)+'LUHFFLyQ/LQHDO
3*
&5
'LUHFWRULR
3iJLQD
'HVSOD]DPLHQWR
+
'HVFUGHSiJ
'LUHFWRULR GH3iJLQDV )
)[
$ 'LU)tVLFD
'HVFUGHSiJ
0DUFR
$
% 7DEODGH
3iJLQDV
5$0
&5
'LUPDUFRGHSiJ 5HVHUYDGR($XVOH3
%+
'HVFULSWRUGHO'LUHFWRULRGH3iJLQDV 'LUPDUFRGHSiJ 5HVHUYDGR($XVOH3
$+
'HVFULSWRUGHOD7DEODGH3iJLQDV
6LVWHPDV2SHUDWLYRV,
*HVWLyQGH0HPRULD
$SXQWHVGH62,