316 Sistemas operativos. Una visión aplicada Tabla 5.1 Comparativa de los esquemas de gestión de la memoria del sistema. Esquema de gestión Registros límite Registro base y límite Segmentación Paginación Seg. paginada global Seg. paginada local SASOS Resolución/R.módulos Compilador/montador Compilador/montador Compilador/montador Compilador/montador Compilador/montador Compilador/montador Compilador/montador R. regiones R. procesos Montador Cargador Montador MMU MMU (seg) Montador MMU (pág) MMU (seg) MMU (seg) MMU (pág) Montador Cargador R. global ʊ ʊ ʊ ʊ MMU (pág) ʊ MMU (pág) Aclaración 5.7. En algunas arquitecturas, las direcciones que se usan para acceder a memoria (llamadas capabilities), además de la referencia a la posición de memoria propiamente dicha, incluyen uno o más bits de información de protección, que, entre otras cosas, identifican que se trata de una dirección de acceso a memoria. El aspecto interesante de las capabilities es que sólo se pueden modificar estando en modo privilegiado. Por tanto, sólo en dicho modo se pueden generar direcciones válidas de acceso a memoria. Un proceso podrá usar las posiciones de memoria que le ha asignado el sistema operativo, puesto que éste le ha proporcionado las capabilities requeridas para acceder a las mismas. Sin embargo, no podrá acceder a otras direcciones, puesto que la unidad de control del procesador asegura que una capability sólo se puede modificar en modo privilegiado. El mecanismo de capabilities asegura la protección requerida, permitiendo usar direcciones físicas directamente, lo que elimina el problema de las autorreferencias. La reubicación necesaria se puede realizar por software. El único problema de este mecanismo, que ha limitado considerablemente su impacto, es que requiere un procesador con una arquitectura muy específica. Para terminar esta sección, se plantea la tabla 5.1, que sirve de recapitulación de la misma y muestra una comparativa de los distintos esquemas de gestión de memoria estudiados, mostrando cómo se llevan a cabo las distintas etapas identificadas en la sección 5.2.5. 5.5. MEMORIA VIRTUAL En prácticamente todos los sistemas operativos de propósito general actuales se usa la técnica de memoria virtual. Hasta el momento, por motivos pedagógicos, se han presentado los diversos esquemas de memoria sin apenas hacer referencia a esta técnica. En esta sección se estudiará esta técnica mostrando cómo se integra con estos esquemas de gestión de memoria. En el primer capítulo ya se presentaron los fundamentos de la memoria virtual, explicando aspectos tales como la jerarquía de memoria o la proximidad de referencias, por lo que no se volverá a incidir en los mismos. Como se apreciará a lo largo de esta sección, la memoria virtual resuelve dos de los objetivos del sistema de memoria identificados al principio del capítulo: ejecutar más procesos de los que caben en la memoria principal, y ejecutar un proceso cuyo mapa no cabe en la memoria. Antes de la aparición de la memoria virtual, esas dos necesidades ya existían y se intentaban resolver de la mejor manera posible, teniendo en cuenta la tecnología presente por entonces. Esta sección, por tanto, comenzará presentando las técnicas de intercambio y de overlays como precedentes de la memoria virtual. A continuación, se analizarán las diversas políticas de administración de la memoria virtual, especialmente, las estrategias de reemplazo y las políticas de reparto de la memoria entre los procesos. 5.5.1. Intercambio Como se comentó previamente, la técnica del intercambio (swapping) significó en su momento una manera de permitir que en los sistemas del tiempo compartido existieran más procesos de los que caben en memoria. Gestión de memoria 317 Se puede considerar que se trata de un mecanismo antecesor de la memoria virtual. En este apartado se presentarán de forma breve los fundamentos de esta técnica. El intercambio se basa en utilizar un disco o parte de un disco como respaldo de la memoria principal. Esta zona de almacenamiento se denomina dispositivo de swap. Cuando no caben en memoria todos los procesos activos (por ejemplo, debido a que se ha creado uno nuevo), se elige un proceso residente y se copia en swap su imagen en memoria. El criterio de selección puede tener en cuenta aspectos tales como la prioridad del proceso, el tamaño de su mapa de memoria, el tiempo que lleva ejecutando y, principalmente, su estado. Es preferible expulsar (swap out) procesos que estén bloqueados. Cuando se expulsa un proceso, no es necesario copiar toda su imagen al dispositivo de swap. Los huecos en el mapa no es preciso copiarlos, ya que su contenido es intrascendente. Tampoco se tiene que copiar el código, puesto que se puede volver a recuperar directamente del ejecutable. Evidentemente, un proceso expulsado tarde o temprano debe volver a activarse y cargarse en memoria principal (swap in). Sólo se deberían volver a cargar aquellos procesos que estén listos para ejecutar. Esta readmisión en memoria se activará cuando haya espacio de memoria disponible (por ejemplo, debido a que se ha terminado un proceso) o cuando el proceso lleve un cierto tiempo expulsado. Téngase en cuenta que al tratarse de un sistema de tiempo compartido, se debe repartir el procesador entre todos los procesos. Por ello, en numerosas ocasiones hay que expulsar un proceso para poder traer de nuevo a memoria a otro proceso que lleva expulsado un tiempo suficiente. La estrategia que decide cuándo expulsar un proceso a swap y cuándo reincorporarlo a memoria se corresponde con la planificación a medio plazo presentada en el capítulo 4. En cuanto al dispositivo de swap, hay dos alternativas en la asignación de espacio: • Con preasignación: Al crear el proceso ya se reserva espacio de swap suficiente para albergarlo. Si el proceso nunca se expulsa, se desperdicia el espacio asignado. • Sin preasignación. Sólo se reserva espacio de swap cuando se expulsa el proceso. Puede haber problemas si se intenta expulsar un proceso a swap para traer a otro proceso y no hay espacio en el dispositivo. Un último aspecto a tener en cuenta es que no debería expulsarse un proceso mientras se estén realizando operaciones de entrada/salida por DMA vinculadas a su imagen de memoria, ya que provocaría que el dispositivo accediera al mapa de otro proceso. 5.5.2. Overlays En los tiempos en los que todavía no se había propuesto la técnica de memoria virtual y las memorias tenían una capacidad limitada, se presentaba con cierta frecuencia el problema de que un determinado programa no cupiera en memoria. Para resolver dentro de lo posible este problema, se ideó la técnica de los overlays. Se trataba de un esquema que no era transparente al programador, puesto que éste tenía que determinar si ciertos módulos de su programa no requerían estar simultáneamente en memoria en tiempo de ejecución y que, por tanto, podrían usar la misma zona de memoria en distintas fases de ejecución del programa. El programador usaba un lenguaje de definición de overlays para notificar al montador esta información. El montador generaba un fichero ejecutable en el que incluía automáticamente código para cargar y descargar los módulos del programa. En la llamada desde un módulo a una función definida en otro módulo, el montador incluía código que comprobaba si el módulo destino estaba ya cargado en memoria. En caso de no estarlo, el código incluido por el montador cargaba ese módulo, usando para ello, si es necesario, el espacio ocupado por otro módulo cuya presencia no se requiere según indica la información suministrada al montador. 5.5.3. Fundamento de la memoria virtual Como se estudió en el primer capítulo, la memoria en un sistema está organizada como una jerarquía de niveles de almacenamiento entre los que se mueve la información dependiendo de las necesidades de los procesos 318 Sistemas operativos. Una visión aplicada en un determinado instante. La técnica de memoria virtual se ocupa de la transferencia de información entre la memoria principal y la secundaria. La memoria secundaria está normalmente soportada en un disco (o partición). Dado que, como se verá más adelante, la memoria virtual se implementa sobre un esquema de paginación, este dispositivo se denomina dispositivo de paginación. También se usa el término dispositivo de swap. Aunque este término proviene de la técnica del intercambio, por tradición se usa frecuentemente y se utilizará indistintamente en esta exposición. En cualquier caso, hay que resaltar que la memoria secundaria no sólo está formada por el dispositivo de paginación, sino que también forma parte de la misma el sistema de ficheros. Téngase en cuenta que, como se analizará en esta sección, al ser expulsadas, algunas páginas se transferirán al dispositivo de paginación, mientras que otras lo harán al sistema de ficheros. Es importante recordar en este punto que, como se explicó en el primer capítulo, el buen rendimiento del sistema de memoria virtual está basado en que los procesos presentan la propiedad de proximidad de referencias. Esta propiedad permite que un proceso genere muy pocos fallos aunque tenga en memoria principal sólo una parte de su imagen de memoria (conjunto residente). El objetivo del sistema de memoria virtual es intentar que la información que está usando un proceso en un determinado momento (conjunto de trabajo) esté residente en memoria principal, es decir, que el conjunto residente del proceso contenga su conjunto de trabajo. Algunos beneficios del uso de memoria virtual son los siguientes: • Se produce un aumento del grado de multiprogramación al no ser necesario que todo el mapa de memoria de un proceso esté en memoria principal para poder ejecutarlo. Este aumento implica una mejora en el rendimiento del sistema. Sin embargo, como se analizó en el segundo capítulo, si el grado de multiprogramación se hace demasiado alto, el número de fallos de página se dispara y el rendimiento del sistema baja drásticamente. Esta situación se denomina hiperpaginación y se estudiará más adelante. • Se pueden ejecutar programas más grandes que la memoria principal disponible. Hay que resaltar que el objetivo de la memoria virtual no es acelerar la ejecución de un programa. En algunos casos, puede hacerlo, especialmente, en situaciones donde el proceso no accede a todo su código o a todos sus datos durante su ejecución, no siendo necesario, por tanto, leerlos del ejecutable. Sin embargo, en otras ocasiones, puede incluso ralentizar la ejecución, debido a la sobrecarga asociada a las transferencias entre la memoria principal y la secundaria. Esto hace que esta técnica no sea apropiada para sistemas de tiempo real, además de por hacer que sea poco predecible el comportamiento de los procesos. Como se ha analizado en los apartados anteriores, la memoria virtual se construye generalmente sobre un esquema de paginación, ya sea paginación simple o segmentación paginada. Por tanto, las unidades de información que se transfieren entre la memoria principal y la secundaria son páginas. Las transferencias desde la memoria secundaria hacia la principal se realizan normalmente bajo demanda (paginación por demanda). Cuando un proceso necesita acceder a una página que no está en memoria principal (a lo que se denomina fallo de página), el sistema operativo se encarga de transferirla desde la memoria secundaria. Si al intentar traer la página desde memoria secundaria, se detecta que no hay espacio en la memoria principal (no hay marcos libres), será necesario expulsar una página de la memoria principal y transferirla a la secundaria. Por tanto, las transferencias desde la memoria principal hacia la secundaria se realizan normalmente por expulsión. El algoritmo para elegir qué página debe ser expulsada se denomina algoritmo de reemplazo y se analizará más adelante. Dado que se está usando la paginación para construir un esquema de memoria virtual, se puede usar indistintamente el término de dirección lógica y el de dirección virtual para referirse a las direcciones que genera un programa. Para construir un esquema de memoria virtual sobre un procesador que ofrezca paginación, se utiliza el bit de la entrada de la tabla de páginas que indica si la página es válida. Estarán marcadas como inválidas todas las entradas correspondientes a las páginas que no están residentes en memoria principal en ese instante. Dado que se utiliza el bit validez para marcar la ausencia de una página y este mismo bit también se usa para indicar que una página es realmente inválida (una página que corresponde a un hueco en el mapa), es necesario que el sistema operativo almacene información asociada a la página para distinguir entre esos dos casos. En caso de que la página sea válida pero no residente, el sistema operativo también deberá guardar información de en qué bloque de la memoria secundaria está almacenada la página. De esta forma, cuando se Gestión de memoria 319 Fichero Fallo Memoria Expulsión y modificada Figura 5.67 Ciclo de vida de una página de una región compartida con soporte. produzca un acceso a una de estas páginas, se producirá una excepción (fallo de página) que activará al sistema operativo, que será el encargado de traerla desde la memoria secundaria. 5.5.4. Ciclo de vida de una página Antes de analizar los distintos aspectos vinculados con la memoria virtual, es conveniente analizar cómo evoluciona una página en un sistema con memoria virtual, dependiendo de a qué tipo de región pertenece, fijándose en dos características de la misma: si es privada o compartida y si tiene soporte o es anónima. • Página de una región compartida con soporte en un fichero. Cada vez que se produzca un fallo al acceder a esta página por no estar presente en memoria principal, habrá que leerla del fichero que la contiene. Por otra parte, al ser expulsada estando modificada, habrá que volverla a escribir en el fichero, puesto que los cambios sobre una región compartida deben revertir al fichero. La figura 5.67 muestra cómo es la evolución de este tipo de páginas. • Página de una región privada con soporte en un fichero. Mientras no se modifique la página, estará vinculada al soporte y todos los fallos se servirán leyendo del fichero. En cuanto se modifique una vez, queda desvinculada del soporte original, y al ser expulsada, se escribirá en swap. Los fallos posteriores se servirán de swap, y las expulsiones que encuentren la página modificada la escribirán en swap. Esta es la esencia de una región privada: los cambios sobre la misma no afectan al soporte original. Para entender la utilidad de este modo de operación, recuerde que se utiliza con la región de datos con valor inicial de un programa o de una biblioteca dinámica. Después de acceder a una variable global con valor inicial para modificarla, cuando sea expulsada la página que la contiene, no podemos volver a escribirla en el fichero ejecutable, puesto que estaríamos cambiando el propio programa. La figura 5.68 muestra la evolución de una página de estas características. • Página de una región anónima, ya sea privada o compartida. Por motivos de seguridad, cuando se accede por primera vez a una página de este tipo, se rellena con ceros el marco de página usado para la misma, no requiriendo un acceso a disco. Si la página se expulsa sin ser modificada (lo cual es bastante improbable puesto que, al no tener un valor inicial, lo más habitual es que el primer acceso sea de escritura), no es necesario escribirla en el disco. El siguiente fallo volverá a rellenar el marco elegido con ceros. En cuanto se modifique una vez la página, al ser expulsada, se escribirá en swap. Los fallos posteriores se servirán de swap y las expulsiones que encuentren la página modificada la escribirán en swap. Recuerde que la región de datos sin valor inicial de un programa o de una biblioteca dinámica son de este tipo, teniendo, además, carácter privado. También entra en esta categoría una zona de memoria compartida que no esté basada en un fichero. La figura 5.69 muestra la evolución de una página de este tipo. 5.5.5. Políticas de administración de la memoria virtual Como se analizó en el primer capítulo, el modo de interacción entre dos niveles de la jerarquía queda definido por un conjunto de políticas que establecen de qué forma se realizan las transferencias entre ambos. En el 320 Sistemas operativos. Una visión aplicada Fichero Fallo (mientras no modificada una vez) Memoria Fallos y expulsiones una vez modificada Expulsión y modificada Swap Figura 5.68 Ciclo de vida de una página de una región privada con soporte. caso de la memoria virtual, los niveles implicados son la memoria principal y la secundaria, y las políticas que definen el comportamiento de un sistema de memoria virtual son las siguientes: • Política de localización. Permite localizar una determinada página dentro de la memoria secundaria. • Política de extracción. Define cuándo se transfiere una página desde la memoria secundaria a la principal. • Política de ubicación. Si hay varios marcos libres, establece cuál de ellos se utiliza para almacenar la página que se trae a memoria principal. • Política de reemplazo. En caso de que no haya marcos libres, determina qué página debe ser desplazada de la memoria principal para dejar sitio a la página entrante. • Política de actualización. Rige cómo se propagan las modificaciones de las páginas en memoria principal a la memoria secundaria. • Política de reparto de espacio entre los procesos. Decide cómo se reparte la memoria física entre los procesos existentes en un determinado instante. Sobre las políticas de ubicación y de actualización hay poco que comentar. Con respecto a la primera, en principio, se puede usar cualquier marco libre para albergar una página leída de la memoria secundaria. Sin embargo, algunos sistemas operativos intentan seleccionar el marco de manera que se mejore el rendimiento de la memoria caché. Esta técnica, denominada coloración de páginas, intenta que las páginas residentes en memoria en un momento dado estén ubicadas en marcos cuya distribución en las líneas de la caché sea lo más uniforme posible. En cuanto a la política de actualización, dada la enorme diferencia entre la velocidad de transferencia de la memoria principal y la de la secundaria, no es factible usar una política de actualización inmediata, utilizándose, por tanto, una política de escritura diferida: sólo se escribirá una página a memoria secundaria cuando sea expulsada de la memoria principal estando, además, modificada. En las próximas secciones se analizarán las cuatro políticas restantes. Fallo (mientras no modificada una vez) Rellenar con ceros Memoria Expulsión y modificada Fallos y expulsiones una vez modificada Swap Figura 5.69 Ciclo de vida de una página de una región anónima. Gestión de memoria 321 Fichero F Swap (SW1) 0 8192 R-X Comp. F Bl. inicial 0 8192 8192 RW- Privada F Bl. inicial 2 Mapa de memoria de P1 0 8192 16384 20480 BCP de P1 Región 1 (R-X) Región 2 (RW-) t. de regiones 16384 4096 RW- Privada Anónima Fichero DVI 1 Bloq. 0 Bloq. 1 Bloq. 2 Fichero3 Bloq. ......... ......... Cód. 0 Cód. 1 DVI 0 Memoria Marco 0 Marco 1 t. páginas Región 3 (RW-) ......... ......... 00004 V R-X 00000 I 000 00001 I SW1 00000 I 000 00000 I 000 00000 I 000 ......... Página 0 de P1 ......................... Marco 2 Marco 3 Marco 4 ........ Figura 5.70 Posibles ubicaciones de una página. 5.5.6. Política de localización Como se comentó al principio de esta sección, la paginación por demanda se basa en hacer creer a la MMU que la página no residente es inválida para que produzca una excepción de fallo de página cuando se acceda a la misma. De esta manera, el sistema operativo puede activarse para tratar la excepción y traer la página de memoria secundaria. Sin embargo, es necesario que el sistema operativo pueda saber si la página realmente es válida, y, en caso de serlo, cómo localizarla. Con respecto a la determinación de la validez, la mayoría de los sistemas operativos consultan la tabla de regiones buscando en qué región está englobada la dirección de fallo. Dado que un proceso puede tener un número elevado de regiones, para agilizar la operación de comprobar si la dirección es válida, la tabla de regiones se organiza como una estructura de datos que permita una búsqueda eficiente y elimine la necesidad de un recorrido lineal de la tabla. Habitualmente, se utiliza un árbol binario ordenado por la dirección de comienzo de cada región. De esta forma, el tiempo de búsqueda es logarítmico con respecto al número de regiones del proceso en vez de lineal. Además, esta estructura permite realizar eficientemente operaciones de inserción y borrado de regiones. En caso de que la dirección que provoca el fallo no encaje en ninguna región, se trata de una página inválida, que corresponde a un hueco en el mapa de memoria del proceso, por lo que se abortará el proceso o se le mandará una señal. En el ejemplo mostrado en la figura 5.70, la sexta entrada de la tabla de páginas se corresponde con un hueco en el mapa del proceso. Si el proceso intenta acceder a una dirección incluida en ese hueco, como la 20500, se producirá una excepción de fallo de página, dentro de cuyo tratamiento el sistema operativo determinará que el acceso es inválido, al no estar incluida en ninguna región la dirección de fallo. Para entender el proceso de localización, hay que analizar qué cuatro casos se pueden presentar para una página válida, como se muestra en la figura 5.70. • Página residente en memoria. Se trata de una página a la que se ha accedido y en este momento está residente en memoria. La entrada de la tabla de páginas identifica al marco que contiene la página. La primera entrada de la tabla de páginas de la figura se corresponde con este caso, estando almacenada en el marco 4. 322 Sistemas operativos. Una visión aplicada • Página no residente almacenada en un fichero. Corresponde a una página no residente cuyo contenido está almacenado en un bloque de un fichero. Cuando se produzca un fallo, el sistema operativo localizará la región a la que pertenece la página y podrá comprobar en el descriptor de la región que se trata de una región con soporte en un fichero, obteniendo información acerca de qué fichero se trata y a qué parte del mismo corresponde la región. Calculando el desplazamiento de la página con respecto al inicio de la región y sabiendo en qué posición del fichero comienza la región, se puede determinar en qué bloque del fichero se ubica la página. La segunda y cuarta entradas de la tabla de páginas de la figura son ejemplos de esta situación. La segunda entrada está vinculada a la región 1. El descriptor de esa región indica que se trata de una región con soporte en el fichero F, estando ubicada al principio del mismo. Dado que se trata de la segunda página de esa región, habrá que leer el bloque 1 del disco para obtener su contenido. En cuanto a la cuarta entrada, está incluida en la región 2, cuyo descriptor indica que también tiene soporte en el fichero F, pero, comenzando en el bloque 2 del mismo. Como se trata de la segunda página de esta región, habrá que leer el bloque 3 del disco para obtener su contenido. • Página no residente de tipo anónima. Se trata de una página no residente perteneciente a una región sin soporte. Cuando se produzca un fallo al acceder a esta página y se detecta que la región es de este tipo, basta con buscar un marco libre y, por motivos de seguridad, rellenarlo con ceros. La quinta entrada de la figura 5.70 corresponde a una página de este tipo. Cuando se produzca un fallo al intentar acceder a la misma, se detectará que pertenece a una región anónima y se procederá a rellenarla con ceros. Es interesante resaltar que en el sistema operativo Linux, en el inicio del sistema y durante todo el tiempo que está arrancado, se reserva un marco de página de sólo lectura relleno con ceros. De esta forma, en vez de rellenar la página con ceros explícitamente, se comparte, usando COW, que será explicado más adelante, hasta que el programa haga alguna modificación sobre la misma. • Página en swap. Como se explicó anteriormente, las páginas de las regiones sin soporte y de las regiones privadas, una vez modificadas, si se expulsan, son escritas en el dispositivo de paginación. En este caso, la información de ubicación de la página en el disco no puede almacenarse en el descriptor de la región, a no ser que habilitemos espacio para cada una de las páginas de la misma. En su lugar, la mayoría de los sistemas operativos optan por almacenar la información de ubicación en el swap dentro de la propia entrada de la tabla de páginas. Esta información suele consistir en un identificador del dispositivo de swap, ya que, normalmente, se permiten múltiples dispositivos de swap en el sistema, junto con un número de bloque dentro del dispositivo. Nótese que, dado que cuando una entrada de la tabla de páginas es inválida el valor de los campos de la misma es intrascendente, se aprovecha para almacenar la información de swap en esos campos. La tercera entrada de la tabla de páginas de la figura 5.70 corresponde a este caso. Se trata de una página de una región privada con soporte en fichero que ya ha sido modificada alguna vez y que, por tanto, está vinculada al swap. Cuando se produzca un fallo sobre esta página, después de realizar la validación de la dirección, se detectará que la entrada de la tabla de páginas tiene un valor distinto de cero (en los demás casos de páginas válidas pero no residentes, se habrán rellenado con ceros todos los campos de la entrada de la tabla de páginas). Se usará el valor almacenando en la entrada (en este caso, dispositivo SW1 y bloque 1 del mismo) para saber qué bloque leer. 5.5.7. Política de extracción La memoria virtual, en su forma ortodoxa, opera bajo demanda: sólo se trae a memoria principal una página cuando el proceso accede a la misma. Sin embargo, casi todos los sistemas operativos implementan algún tipo de agrupamiento de páginas o de lectura anticipada de páginas, de manera que cuando se produce un fallo de página, no sólo se trae a memoria la página involucrada, sino también algunas páginas próximas a la misma, puesto que, basándose en la propiedad de proximidad de referencias que caracteriza a los programas, es posible que el proceso las necesite en un corto plazo de tiempo. Estas técnicas suelen englobarse bajo el término de prepaginación. La efectividad de las mismas va a depender de si hay acierto en esta predicción, Gestión de memoria 323 es decir, si finalmente las páginas traídas van a ser usadas, puesto que, en caso contrario, se ha perdido tiempo en traerlas, expulsando, además, de la memoria principal a otras páginas que podrían haber sido más útiles. Hay que resaltar que en el sistema de ficheros también se utilizan técnicas de lectura anticipada, lo cual no puede sorprender por la fuerte interacción, y progresiva fusión, entre estos dos componentes. Dado que en la fase de arranque de un programa es donde más se hace patente la sobrecarga de la memoria virtual, al producirse numerosos fallos mientras el conjunto de trabajo del programa va cargándose en memoria, en algunos sistemas operativos se realiza un seguimiento de los primeros segundos de la ejecución del programa, guardando información sobre los fallos de página que provoca. De esta manera, cuando arranca nuevamente un programa, se puede realizar una lectura anticipada dirigida de las páginas que probablemente va a usar el mismo durante su fase inicial. Volviendo a la paginación por demanda, que es la médula espinal de la memoria virtual, la mayor parte de la lógica que controla esta técnica está articulada alrededor de la rutina del tratamiento del fallo de página. A continuación, se analizan con más detalle las operaciones vinculadas con un fallo de página. Tratamiento del fallo de página La paginación por demanda está dirigida por la aparición de excepciones de fallo de página que indican al sistema operativo que debe traer una página de memoria secundaria a primaria, puesto que un proceso la requiere. A continuación, se especifican los pasos típicos en el tratamiento de un fallo de página: • La MMU del procesador produce una excepción, dejando, habitualmente, en un registro especial la dirección que provocó el fallo. • Se activa el sistema operativo que comprueba si se trata de una dirección correspondiente a una página realmente inválida o se corresponde con una página ausente de memoria. Para ello, como se comentó previamente, accederá a la tabla de regiones para encontrar a qué región pertenece la dirección que produjo el fallo. Si no pertenece a ninguna, la página es inválida, por lo que se aborta el proceso o se le manda una señal. En caso contrario, se realizan los pasos que se describen a continuación. Evidentemente, si la dirección es del sistema y el proceso estaba en modo usuario cuando se produjo el fallo, será también inválida. • Se consulta la tabla de marcos para buscar uno libre. • Si no hay un marco libre, se aplica el algoritmo de reemplazo para seleccionar la página que se expulsará. El marco seleccionado se desconectará de la página a la que esté asociado poniendo como inválida la entrada correspondiente. Si la página está modificada, previamente hay que escribir su contenido a la memoria secundaria: ⎯ Si la página pertenece a una región de tipo compartida y con soporte en fichero, hay que escribirla en el bloque correspondiente del fichero. ⎯ En el resto de los casos, hay que escribirla en swap, almacenándose en la entrada de la tabla de páginas la identificación del bloque de swap que contiene la página. • Una vez que se obtiene el marco libre, ya sea directamente o después de una expulsión, se inicia la carga de la nueva página sobre el marco y, al terminar la operación, se rellena la entrada correspondiente a la página para que esté marcada como válida y apunte al marco utilizado. La localización de la página se realizará tal como se explicó en el apartado anterior. Téngase en cuenta que, en el peor de los casos, un fallo de página puede causar dos operaciones de entrada/salida al disco. En contraste, puede no haber ninguna operación sobre el disco si hay marcos libres o la página expulsada no está modificada y, además, la página que causó el fallo se corresponde con una región anónima que todavía no se ha modificado ninguna vez. Para terminar, es conveniente realizar algunas consideraciones sobre el caso de que se produzca un fallo de página cuando el proceso estaba en modo sistema: • Si la dirección de fallo corresponde a una dirección lógica de usuario y se estaba ejecutando una llamada al sistema, el fallo se produce debido a que se ha accedido al mapa de usuario del proceso para leer o escribir algún parámetro de la llamada. El tratamiento del fallo es el habitual (comprobar si está incluido en alguna región, buscar un marco libre, etc.) 324 Sistemas operativos. Una visión aplicada 0 8192 R-X Comp. F Bl. inicial 0 Memoria 8192 8192 RW- Privada F Bl. inicial 2 BCP de P1 ............. Tablas de páginas de segundo nivel V V t. de regiones I t. páginas I V I Tabla de páginas de primer nivel I I I I I I Figura 5.71 Creación de tablas de páginas por demanda. • En caso de que la dirección de fallo corresponda a una dirección lógica de usuario y se estuviera ejecutando una interrupción, se trataría de un error en el código del sistema operativo, puesto que, dado que una interrupción tiene un carácter asíncrono y no está directamente vinculada con el proceso en ejecución, no debe acceder al mapa de usuario del proceso en ninguna circunstancia. El tratamiento sería el habitual ante un error en el sistema operativo (podría ser, por ejemplo, sacar un mensaje por la consola y realizar una parada ordenada del sistema operativo). • Si la dirección de fallo es una dirección lógica de sistema, a su vez, podrían darse varios casos: ⎯ Si se trata de un sistema operativo que permite que páginas del sistema se expulsen a disco, se comprobaría si la dirección corresponde a una de esas páginas y, si es así, se leería de disco. ⎯ Si se usa un procesador que tiene una tabla única para direcciones de usuario y de sistema, y en el que, por tanto, se duplican las entradas correspondientes al sistema operativo en las tablas de páginas de todos los procesos, el fallo puede deberse a que se ha añadido una nueva entrada del sistema en la tabla de un proceso, pero no se ha propagado a los restantes. De esta forma, la propagación de este cambio se hace también por demanda, no tratándose de un error. ⎯ En todos los demás casos, se correspondería con un error en el código del sistema operativo. Creación de tablas de páginas por demanda Si se analiza el proceso de localización de las páginas explicado previamente, se puede apreciar en el mismo que las entradas de la tabla de páginas sólo se usan cuando las páginas están residentes, o cuando están almacenadas en swap, siendo la tabla de regiones el elemento clave de la gestión. Basándose en este hecho, se puede extender la política de operación bajo demanda a la propia creación de las tablas de páginas intermedias requeridas en los sistemas de paginación multinivel. De la misma manera que cuando se crea una región en un sistema con memoria virtual no es necesario asignarle marcos hasta que no se acceda a las páginas de la región, como se analizará en la sección 5.5.14, se puede diferir la creación de las tablas de páginas intermedias requeridas por la región hasta que se produzca ese acceso. De esta forma, el proceso inicialmente sólo dispone de la tabla de páginas de nivel superior con todas las entradas marcadas como inválidas. Cuando se produce un fallo y se detecta que las tablas de páginas intermedias requeridas por esa página no se han creado todavía, se reserva memoria para las mismas en ese momento, iniciándolas como inválidas. En la figura 5.71 se muestra un ejemplo de uso de este mecanismo. En el mismo, el proceso sólo ha accedido a dos páginas. El primer acceso provocó un fallo que se produ- Gestión de memoria 325 jo en la tabla de primer nivel. Una vez que se comprueba que la dirección es válida usando la tabla de regiones, se reserva la tabla de segundo nivel, además del propio marco que alojará la página. El segundo acceso ha provocado un fallo en la tabla de segundo nivel, por tanto, sólo requiere reservar el marco para la página. Con este mecanismo, la tabla de regiones es el elemento conductor del proceso y sólo es necesario tener desplegadas las tablas de páginas para las páginas residentes, lo cual resulta especialmente beneficioso para procesos con un espacio de direcciones disperso. Realmente, con el esquema que hemos planteado, también haría falta tener desplegadas las tablas si contienen entradas que se refieren a bloques de swap, aunque este último requisito podría eliminarse si se almacena esta información en otra estructura de datos, como, por ejemplo, la tabla de regiones. 5.5.8. Política de reemplazo Una de las políticas principales de un sistema de memoria virtual, y una de las que puede afectar más a su rendimiento, es la que determina qué página expulsar cuando hay que traer otra de memoria secundaria y no hay espacio libre. Este mismo problema se presenta en todos los niveles de la jerarquía y, aunque, en cierta medida, hay similitudes, en cada nivel se presentan aspectos específicos debido a las diferencias intrínsecas entre los distintos componentes de memoria usados en la jerarquía, en cuanto a su velocidad y capacidad. Por otra parte, hay que resaltar que el problema de gestionar la transferencia de información entre la memoria principal y el disco no se restringe al sistema de memoria virtual, sino que aparece en la gestión de la caché del sistema de ficheros, así como en los sistemas gestores de bases de datos. Aunque muchos algoritmos usados en estos ámbitos son similares a los utilizados en la memoria virtual, hay que tener en cuenta que existe una importante diferencia entre el modo de operación del sistema de ficheros, o de un gestor de base de datos, y el sistema de memoria. En los primeros, existe una petición explícita por parte de la aplicación para acceder a la información, pudiendo, por tanto, el sistema operativo (o el gestor de base de datos) tener control sobre qué accesos se van realizando. En cambio, en la memoria, el proceso accede directamente a la misma sin que el sistema operativo sea consciente de ello. El sistema operativo sólo toma control cuando se produce un fallo de página. Existen numerosos trabajos, tanto teóricos como experimentales, sobre algoritmos de reemplazo de páginas. En esta sección se describirán los algoritmos de reemplazo más habituales, intentando mostrar las últimas tendencias en este campo que, después de un cierto periodo con pocas innovaciones, ha presentando importantes contribuciones en los últimos tiempos. Las estrategias de reemplazo se pueden clasificar en dos categorías: reemplazo global y reemplazo local. Con una estrategia de reemplazo global se puede seleccionar para satisfacer el fallo de página de un proceso un marco que actualmente tenga asociada una página de otro proceso. Esto es, un proceso puede quitarle un marco de página a otro. La estrategia de reemplazo local requiere que para servir el fallo de página de un proceso sólo puedan usarse marcos de páginas libres o marcos ya asociados al proceso. Todos los algoritmos que se describirán a continuación pueden utilizarse tanto para estrategias globales como locales. Cuando se aplica un algoritmo determinado utilizando una estrategia global, el criterio de evaluación del algoritmo se aplicará a todas las páginas en memoria principal. En el caso de una estrategia local, el criterio de evaluación del algoritmo se aplica sólo a las páginas en memoria principal que pertenecen al proceso que causó el fallo de página. La descripción de los algoritmos, por tanto, se realizará sin distinguir entre los dos tipos de estrategias. El objetivo básico de cualquier algoritmo de reemplazo es minimizar la tasa de fallos de página, intentando además que la sobrecarga asociada a la ejecución del algoritmo sea tolerable y que no se requiera una MMU con características específicas. En principio, de manera intuitiva, parece razonable pensar en tres factores a la hora de diseñar un algoritmo de estas características: • Tiempo de residencia. Tener en cuenta el tiempo que lleva la página presente en memoria a la hora de expulsar una página. Como se analizará a continuación, no es un buen criterio. • Frecuencia de uso. Basarse en cuántas veces se ha accedido a una página para decidir cuál reemplazar. Aunque el criterio es razonable, tiene ciertas limitaciones, que se analizarán enseguida. 326 Sistemas operativos. Una visión aplicada • Frescura de la página. Utilizar como criterio si la página se ha usado recientemente. Como se explicará a continuación, es el criterio básico de este tipo de algoritmos. Algoritmo de reemplazo óptimo El algoritmo óptimo, denominado también MIN, debe generar el mínimo número de fallos de página. Por ello, la página que se debe reemplazar es aquélla que tardará más tiempo en volverse a usar. Evidentemente, este algoritmo es irrealizable, ya que no se puede predecir cuáles serán las siguientes páginas a las que se va a acceder. Este algoritmo sirve para comparar el rendimiento de los algoritmos que sí son factibles en la práctica. Algoritmo FIFO (First Input/First Output, primera en entrar/primera en salir) Una estrategia sencilla e intuitivamente razonable es seleccionar para la sustitución la página que lleva más tiempo en memoria, es decir, basarse en el tiempo de residencia. La implementación de este algoritmo es simple. Además, no necesita ningún apoyo hardware especial. El sistema operativo debe mantener una lista de las páginas que están en memoria, ordenada por el tiempo que llevan residentes. En el caso de una estrategia local, se utiliza una lista por cada proceso. Cada vez que se trae una nueva página a memoria, se pone al final de la lista. Cuando se necesita reemplazar, se usa la página que está al principio de la lista. Sin embargo, el rendimiento del algoritmo no es bueno en muchas ocasiones. La página que lleva más tiempo en memoria puede contener instrucciones o datos a los que se accede con frecuencia. Poniendo un ejemplo extremo, piense en un programa pequeño, cuyo código cabe en una sola página. Lo razonable sería que esa página nunca se expulsase, pero, con el algoritmo FIFO se expulsaría con bastante frecuencia. En determinadas situaciones este algoritmo presenta un comportamiento sorprendente, conocido como la anomalía de Belady. Intuitivamente, parece que cuantos más marcos de página haya en el sistema, menos fallos de página se producirán. Sin embargo, ciertos patrones de referencias causan que este algoritmo tenga un comportamiento opuesto. El descubrimiento de esta anomalía resultó al principio sorprendente y llevó al desarrollo de modelos teóricos para analizar los sistemas de memoria virtual. En la práctica, esta anomalía es sólo una curiosidad, que demuestra que los sistemas pueden tener a veces comportamientos inesperados. Algoritmo LRU (Least Recently Used, menos recientemente usada) El algoritmo LRU está basado en el principio de proximidad temporal de referencias: si es probable que se vuelvan a referenciar las páginas a las que se ha accedido recientemente, la página que se debe reemplazar es aquélla a la que no se ha hecho referencia desde hace más tiempo. Dicho de otra forma, se intenta predecir el futuro próximo usando el pasado reciente. Se basa, por tanto, en un criterio de frescura de la página. El algoritmo LRU no sufre la anomalía de Belady. Pertenece a una clase de algoritmos denominados algoritmos de pila. La propiedad de estos algoritmos es que las páginas residentes en memoria para un sistema con n marcos de página son siempre un subconjunto de las que habría en un sistema con n+1 marcos. Esta propiedad asegura que un algoritmo de este tipo nunca sufrirá la anomalía de Belady. Hay un aspecto sutil en este algoritmo cuando se considera su versión global. A la hora de seleccionar una página no habría que tener en cuenta el tiempo de acceso real, sino el tiempo lógico de cada proceso. O sea, habría que seleccionar la página que haya sido menos recientemente usada teniendo en cuenta el tiempo lógico de cada proceso. El algoritmo LRU se utiliza muy frecuentemente para la gestión de la caché del sistema de ficheros. Sin embargo, una implementación exacta en un sistema de memoria virtual es difícil, ya que requiere un considerable apoyo hardware. Una implementación del algoritmo podría basarse en lo siguiente: • El procesador gestiona un contador que se incrementa en cada referencia a memoria. Cada posición de la tabla de páginas ha de tener un campo de tamaño suficiente para que quepa el contador. • Cuando se hace referencia a una página, la MMU copia el valor actual del contador en la posición de la tabla correspondiente a esa página (realmente, debería ser en la TLB, para evitar un acceso a la tabla por cada referencia). • Cuando se produce un fallo de página, el sistema operativo examina los contadores de todas las páginas residentes en memoria y selecciona como víctima aquélla que tiene el valor menor. Gestión de memoria 327 Inicio Ref = 1 Ref = 0 Ref = 1 Ref = 1 Ref = 0 Expulsada Ref = 0 Ref = 0 Ref = 1 Ref = 0 Ref = 1 Figura 5.72 Ejemplo del algoritmo del reloj. Esta implementación es factible, aunque requiere un hardware complejo y muy específico, y eso no es una buena idea. Como ya hemos comentado, y reiteraremos, varias veces a lo largo de los diversos capítulos del libro, un sistema operativo, con el fin de facilitar su adaptación a distintos procesadores, debería seguir una política de mínimos requisitos con respecto al hardware. Lo que se precisa es un algoritmo con unas prestaciones similares, pero que no requiera ningún hardware específico. Algoritmo del reloj El algoritmo de reemplazo del reloj (o de la segunda oportunidad) es una modificación sencilla del FIFO, que evita el problema de que una página muy utilizada sea eliminada por llevar mucho tiempo residente, proporcionando unas prestaciones similares a las del algoritmo LRU, sin requerir un hardware específico. En este algoritmo, cuando se necesita reemplazar una página, se examina el bit de referencia de la página más antigua (la primera de la lista en orden FIFO). Si no está activo, se usa esta página para el reemplazo. En caso contrario, se le da una segunda oportunidad a la página, poniéndola al final de la lista y desactivando su bit de referencia. Por tanto, se la considera como si acabara de llegar a memoria. La búsqueda continuará hasta que se encuentre una página con su bit de referencia desactivado. Si todas las páginas tienen activado su bit de referencia, el algoritmo se convierte en FIFO. Para implementar este algoritmo se puede usar una lista circular de las páginas residentes en memoria, en vez de una lineal (en el caso de una estrategia local, se utiliza una lista circular por cada proceso). Existe un puntero que señala en cada instante al principio de la lista. Cuando llega a memoria una página, se coloca en el lugar donde señala el puntero y, a continuación, se avanza el puntero al siguiente elemento de la lista. Cuando se busca una página para reemplazar, se examina el bit de referencia de la página a la que señala el puntero. Si está activo, se desactiva y se avanza el puntero al siguiente elemento. El puntero avanzará hasta que se encuentre una página con el bit de referencia desactivado. Esta forma de trabajo imita al comportamiento de un reloj donde el puntero que recorre la lista se comporta como la aguja del reloj. Debido a ello, esta estrategia se denomina algoritmo del reloj. La figura 5.72 muestra un ejemplo de este algoritmo, donde se puede apreciar que a las dos primeras páginas revisadas se les da una segunda oportunidad por tener el bit de referencia a uno, siendo seleccionada la tercera para la expulsión. 328 Sistemas operativos. Una visión aplicada Como se comentó previamente, se trata de un algoritmo sencillo, que sólo requiere que el procesador gestione un bit de referencia, que suele ser lo habitual (incluso en procesadores que no gestionan este bit es relativamente sencillo implementarlo, simulándolo por software). Como el algoritmo LRU, también está basado en la frescura de la página, proporcionando un rendimiento similar. Esto ha hecho que, con pequeñas variaciones específicas, sea el algoritmo utilizado en la mayoría de los sistemas operativos actuales. Algoritmo LFU (Least Frequently Used, menos frecuentemente usada) Existe un número incontable de algoritmos de reemplazo en la literatura sobre gestión de memoria, por ello no tiene sentido hacer un repaso exhaustivo de los mismos en esta sección. Sin embargo, se ha considerado conveniente hacer una breve reseña de este algoritmo, puesto que, aunque no proporciona una buena solución al problema, tiene en cuenta un factor que no era tomado en consideración en los algoritmos presentados: la frecuencia de uso. La idea de este algoritmo es expulsar la página residente que menos accesos ha recibido. El problema de esta técnica es que una página que se utilice con mucha frecuencia en un intervalo de ejecución del programa obtendrá un valor del contador muy alto, no pudiendo ser expulsada en el resto de la ejecución del programa. Dada la escasa utilidad del algoritmo, no se entrará más en detalle sobre su implementación, aunque hay que resaltar que, como ocurría con el algoritmo LRU, necesitaría una MMU específica que gestionase un contador de referencias, o bien usar una versión aproximada del mismo, que gestionara el contador mediante muestreos periódicos del bit de referencia. Hay que resaltar que usar únicamente la frecuencia no es una buena estrategia, pero que, como se verá a continuación, olvidarse totalmente de la frecuencia, como sucede con el algoritmo LRU, tampoco es una buena idea. Más allá del LRU Durante mucho tiempo se ha considerado que el algoritmo LRU, y su aproximación práctica, el algoritmo del reloj, era la mejor solución posible al problema del reemplazo. Sin embargo, el estudio de su comportamiento ha detectado algunos problemas de rendimiento ante situaciones relativamente frecuentes. Una de estas situaciones aparece cuando se accede una sola vez a un conjunto de páginas. A estas páginas no se volverá a acceder, pero, sin embargo, se quedarán en memoria hasta que sean expulsadas siguiendo el orden LRU. Este tipo de situaciones aparece con cierta frecuencia, especialmente, cuando se accede a ficheros proyectándolos en memoria, como se analizará en la sección 5.5.15. Imagine, por ejemplo, que en el sistema se ejecuta un programa que realiza una copia de seguridad de un conjunto de ficheros usando la técnica de la proyección en memoria para acceder a los mismos. Aunque sólo se acceda una vez a cada página de cada uno de estos ficheros, va a permanecer en memoria conforme a la estrategia LRU, impidiendo que otras páginas realmente útiles puedan estar en memoria. Se podría decir que ese acceso único a los ficheros planteado en el ejemplo ha contaminado la memoria. El problema surge debido a que el algoritmo LRU se basa sólo en el criterio de frescura de la página, olvidando totalmente la frecuencia de accesos a la misma, que también es un indicador adecuado del interés de la página, siempre que no se use como criterio único. Para intentar adaptarse a esta circunstancia, surgió el algoritmo LRU/2 (realmente, la familia de algoritmos LRU/k), que usa el criterio LRU convencional, pero aplicado al penúltimo acceso (de ahí viene el 2 del nombre), es decir, se expulsa a aquella página cuyo penúltimo acceso ha sido menos reciente. Esta estrategia haría que en el ejemplo planteado las páginas de los ficheros desaparecieran rápidamente de la memoria al no producirse un segundo acceso a las mismas. En los últimos tiempos ha habido una proliferación de algoritmos, tanto en el ámbito de la gestión de memoria como en los de los sistemas de ficheros y las bases de datos, que han intentado conciliar estos criterios de frescura y frecuencia. La mayoría de estos algoritmos son complejos e incluyen algunos parámetros de configuración (tales como plazos de tiempo o tamaños) que son difíciles de ajustar satisfactoriamente en un sistema de propósito general. En cualquier caso, parece que hay un acuerdo general sobre que uno de los mejores algoritmos es el ARC (Adaptive Replacement Cache), que tiene en cuenta la frescura y la frecuencia, no requiriendo fijar Gestión de memoria 329 parámetros de configuración. Aunque queda fuera del alcance de esta exposición el estudio detallado del mismo, a continuación se plantea un bosquejo de su modo de operación: • Usa dos listas LRU: una de frescura, L1, que incluye las páginas a las que se ha accedido sólo una vez recientemente, y otra de frecuencia, L2, que incorpora aquellas páginas a las que se ha accedido al menos dos veces recientemente. • Estas listas almacenan información histórica. No sólo guardan el orden LRU de las N páginas residentes en memoria, sino también de otras N páginas adicionales, que corresponden a las últimas N páginas expulsadas. • Existe un parámetro de configuración p, que se ajusta automáticamente, y que determina qué tamaño debe tener la lista L1. • El algoritmo de reemplazo selecciona una página de la lista L1 sólo si su tamaño es mayor que p. En caso contrario, se elige una página de la lista L2. En ambos casos, la selección se realiza mediante LRU y la página expulsada pasa a formar parte del histórico de esa lista. • El parámetro p se ajusta automáticamente mediante el uso de la información histórica. Si se produce un fallo de página que corresponde a una página del histórico de la lista L1, se incrementa p, puesto que esto indica que el programa está entrando en una fase donde es más importante la frescura (nótese que si p=N, se convierte directamente en el algoritmo LRU). En caso de que corresponda al histórico de L2, se disminuye el valor de p, dado que esto denota que el programa ha entrado en una fase donde la frecuencia toma mayor importancia. De forma análoga a lo que ocurre con el algoritmo LRU, la implementación exacta del ARC en un sistema de memoria virtual requeriría un hardware específico muy complejo y, posiblemente, ineficiente. Por ello, se han planteado versiones aproximadas del mismo, que pueden implementarse sobre un hardware de gestión de memoria estándar. Entre ellas, se encuentra el algoritmo CAR (Clock with Adaptive Replacement), que, como su nombre indica, se basa en una extensión del algoritmo del reloj para que se ajuste a un comportamiento similar al algoritmo ARC. Buffering de páginas Se puede considerar que el algoritmo de reemplazo también se activa bajo demanda: en el momento que no queda más memoria libre, hay que expulsar alguna página. Sin embargo, esperar a que se llegue a una situación tan crítica de falta total de memoria no parece una buena estrategia, más aún, teniendo en cuenta que si se necesita reservar memoria dentro del contexto de una rutina de interrupción, no se puede bloquear la ejecución de la misma hasta que no se habilite memoria libre. Para incidir más en la problemática de llegar a ese momento extremo, imagine qué ocurriría si se necesitase reservar memoria durante la propia operación de reemplazo para, por ejemplo, llevar a cabo la escritura en disco. Al no haber memoria libre, no podría completarse la misma, quedando el sistema en un interbloqueo. Teniendo en cuenta estas consideraciones, la mayoría de los sistemas operativos mantienen una reserva mínima de marcos libres y realizan las operaciones de reemplazo de forma anticipada, fuera del contexto del tratamiento del fallo de página, sin esperar a que se llegue a la situación crítica de agotamiento de la memoria. Esta estrategia suele denominarse buffering de páginas. Cuando se produce un fallo de página, se usa un marco de página libre, pero no se aplica el algoritmo de reemplazo. Esto es, se consume un marco de página, pero no se libera otro. Cuando el sistema operativo detecta que el número de marcos de página disminuye por debajo de un cierto umbral, aplica repetidamente el algoritmo de reemplazo hasta que el número de marcos libres llegue a otro umbral que corresponda a una situación de estabilidad. Para realizar esta operación, dado que se requiere un flujo de ejecución independiente dentro del sistema operativo, habitualmente, se usa un proceso/thread de núcleo, que se suele denominar demonio de paginación. Hay que resaltar que debido a la complejidad de la gestión de memoria, ésta suele requerir la presencia de diversos procesos de núcleo (por ejemplo, procesos que escriben páginas modificadas a disco o procesos que rellenan de ceros marcos libres para tenerlos preparados para volver a utilizarlos). Según el demonio de paginación va aplicando repetidamente el algoritmo, las páginas liberadas que no están modificadas pasan a una lista de marcos libres, estando listas para poder utilizarse de nuevo, mientras 330 Sistemas operativos. Una visión aplicada que las páginas que han sido modificadas pasan a una lista de marcos modificados y deberán actualizarse en memoria secundaria antes de poder volver a utilizarse. Las páginas que están en cualquiera de las dos listas pueden recuperarse si se vuelve a acceder a las mismas antes de reutilizarse. En este caso, la rutina de fallo de página recupera la página directamente de la lista y actualiza la entrada correspondiente de la tabla de páginas para conectarla. Este fallo de página no implicaría operaciones de entrada/salida. Las páginas en la lista de modificadas se pueden escribir en tandas al dispositivo para obtener un mejor rendimiento. Cuando la página modificada se ha escrito al dispositivo, se la incluye en la lista de marcos libres. Esta estrategia puede mejorar el rendimiento de algoritmos de reemplazo que no sean muy efectivos. Así, si el algoritmo de reemplazo decide revocar una página que en realidad está siendo usada por un proceso, se producirá inmediatamente un fallo de página que la recuperará de las listas. Este proceso de recuperación de la página plantea un reto: ¿cómo detectar de forma eficiente si la página requerida por un fallo de página está en una de estas listas? La solución es la caché de páginas. Caché de páginas Con el uso de la técnica de memoria virtual, la memoria principal se convierte, a todos los efectos, en una caché de la memoria secundaria. Por otra parte, en diversas circunstancias, el sistema operativo debe buscar si una determinada página está residente en memoria (esa necesidad se acaba de identificar dentro de la técnica del buffering de páginas y volverá a aparecer cuando se estudie el compartimiento de páginas). Por tanto, parece lógico que ese comportamiento de caché se implemente como tal, es decir, que se habilite una estructura de información, la caché de páginas, que permita gestionar las páginas de los procesos que están residentes en memoria y pueda proporcionar una manera eficiente de buscar una determinada página. La organización de la caché se realiza de manera que se pueda buscar eficientemente una página dado un identificador único de la misma. Generalmente, este identificador corresponde al número de bloque dentro del fichero (o dispositivo de swap) que contiene la página. Obsérvese que las páginas anónimas que todavía no están vinculadas con el swap no están incluidas en la caché de páginas. Cada vez que dentro de la rutina del tratamiento del fallo de página se copia una página de un bloque de un fichero o de un dispositivo de swap a un marco, se incluirá en la caché asociándola con dicho bloque. Hay que resaltar que las páginas de la caché están incluidas, además, en otras listas, tales como las gestionadas por el algoritmo de reemplazo o la de marcos libres y modificados. En el sistema de ficheros, como se analizará en el capítulo dedicado al mismo, existe también una caché de similares características, que se suele denominar caché de bloques. Aunque el estudio de la misma se realiza en dicho capítulo, se puede anticipar que, en los sistemas operativos actuales, la tendencia es fusionar ambas cachés para evitar los problemas de coherencia y de mal aprovechamiento de la memoria, debido a la duplicidad de la información en las cachés. Retención de páginas en memoria Para acabar esta sección en la que se han presentado diversos algoritmos de reemplazo, hay que resaltar que no todas las páginas residentes en memoria son candidatas al reemplazo. Se puede considerar que algunas páginas están atornilladas a la memoria principal. En primer lugar, están las páginas del propio sistema operativo. La mayoría de los sistemas operativos tienen su mapa de memoria fijo en memoria principal. El diseño de un sistema operativo en el que las páginas de su propio mapa pudieran expulsarse a memoria secundaria resultaría complejo y, posiblemente, ineficiente. Tenga en cuenta, además, que el código de la rutina de tratamiento del fallo de página, así como los datos y otras partes de código usados desde la misma, deben siempre estar residentes para evitar el interbloqueo. Lo que sí proporcionan algunos sistemas operativos es la posibilidad de que un componente del propio sistema operativo reserve una zona de memoria que pueda ser expulsada, lo que le permite usar grandes cantidades de datos sin afectar directamente a la cantidad de memoria disponible en el sistema. Además, si se permite que los dispositivos de entrada/salida que usan DMA realicen transferencias directas a la memoria de un proceso, será necesario marcar las páginas implicadas como no reemplazables hasta que termine la operación. Gestión de memoria 331 Por último, algunos sistemas operativos ofrecen servicios a las aplicaciones que les permiten solicitar que una o más páginas de su mapa queden retenidas en memoria (en UNIX existe el servicio mlock para este fin). Este servicio puede ser útil para procesos de tiempo real que necesitan evitar que se produzcan fallos de página imprevistos. Sin embargo, el uso indiscriminado de este servicio puede afectar gravemente al rendimiento del sistema. 5.5.9. Política de reparto de espacio entre los procesos En un sistema con multiprogramación existen varios procesos activos simultáneamente que comparten la memoria del sistema. Es necesario, por tanto, determinar cuántos marcos de página se asignan a cada proceso. Existen dos tipos de estrategias de asignación: asignación fija o asignación dinámica. Asignación fija Con esta estrategia, se asigna a cada proceso un número fijo de marcos de página. Normalmente, este tipo de asignación lleva asociada una estrategia de reemplazo local. El número de marcos asignados no varía, ya que un proceso sólo usa para reemplazo los marcos que tiene asignados. La principal desventaja de esta alternativa es que no se adapta a las diferentes necesidades de memoria de un proceso a lo largo de su ejecución. Habrá fases en la que el espacio asignado se le quedará pequeño, no permitiendo almacenar simultáneamente todas las páginas que está utilizando el proceso en ese intervalo de tiempo. En contraste, existirán fases en las que el proceso no usará realmente los marcos que tiene asignados. Una propiedad positiva de esta estrategia es que el comportamiento del proceso es relativamente predecible, puesto que siempre que se ejecute con los mismos parámetros va a provocar los mismos fallos de página. Existen diferentes criterios para repartir los marcos de las páginas entre los procesos existentes. Puede depender de múltiples factores tales como el tamaño del proceso o su prioridad. Por otra parte, cuando se usa una estrategia de asignación fija, el sistema operativo decide cuál es el número máximo de marcos asignados al proceso. Sin embargo, la arquitectura de la máquina establece el número mínimo de marcos que deben asignarse a un proceso. Por ejemplo, si la ejecución de una única instrucción puede generar cuatro fallos de página y el sistema operativo asigna tres marcos de página a un proceso que incluya esta instrucción, el proceso podría no terminar de ejecutarla. Por tanto, el número mínimo de marcos de página para una arquitectura quedará fijado por la instrucción que pueda generar el máximo número de fallos de página. Asignación dinámica Usando esta estrategia, el número de marcos asignados a un proceso varía según las necesidades que tenga el mismo (y posiblemente el resto de procesos del sistema) en diferentes instantes de tiempo. Con este tipo de asignación se pueden usar estrategias de reemplazo locales y globales. • Con reemplazo local, el proceso va aumentando o disminuyendo su conjunto residente dependiendo de sus necesidades en las distintas fases de ejecución del programa. • Con reemplazo global, los procesos compiten en el uso de memoria quitándose entre sí las páginas. La estrategia de reemplazo global hace que el comportamiento del proceso en tiempo de ejecución no sea predecible. El principal problema de este tipo asignación es que la tasa de fallos de página de un programa puede depender de las características de los otros procesos que estén activos en el sistema. Hiperpaginación Si el número de marcos de página asignados a un proceso no es suficiente para almacenar las páginas a las que hace referencia frecuentemente, se producirá un número elevado de fallos de página. Esta situación se denomina hiperpaginación (thrashing). Cuando se produce la hiperpaginación, el proceso pasa más tiempo en la cola de servicio del dispositivo de swap que en ejecución. Dependiendo del tipo de asignación usado, este problema puede afectar a procesos individuales o a todo el sistema. Sistemas operativos. Una visión aplicada Utilización de la UCP 332 Grado de multiprogramación Figura 5.73 Hiperpaginación. En un sistema operativo que utiliza una estrategia de asignación fija, si el número de marcos asignados al proceso no es suficiente para albergar su conjunto de trabajo en una determinada fase de su ejecución, se producirá hiperpaginación en ese proceso, lo que causará un aumento considerable de su tiempo de ejecución. Sin embargo, el resto de los procesos del sistema no se verán afectados directamente. Con una estrategia de asignación dinámica, el número de marcos asignados a un proceso se va adaptando a sus necesidades, por lo que, en principio, no debería presentarse este problema. No obstante, si el número de marcos de página en el sistema no es suficiente para almacenar los conjuntos de trabajo de todos los procesos, se producirán fallos de página frecuentes y, por tanto, el sistema sufrirá hiperpaginación. La utilización del procesador disminuirá, puesto que el tiempo que dedica al tratamiento de los fallos de página aumenta. Como se puede observar en la figura 5.73, no se trata de una disminución progresiva, sino drástica, que se debe a que al aumentar el número de procesos, por un lado, crece la tasa de fallos de página de cada proceso (hay menos marcos de página por proceso) y, por otro lado, aumenta el tiempo de servicio del dispositivo de paginación (crece la longitud de la cola de servicio del dispositivo). Cuando se produce esta situación se deben suspender uno o varios procesos liberando sus páginas. Es necesario establecer una estrategia de control de carga que ajuste el grado de multiprogramación en el sistema para evitar que se produzca hiperpaginación. Este mecanismo de suspensión tiene similitudes con la técnica del intercambio y, como en dicha técnica, habrá que establecer algún tipo de criterio para decidir qué procesos se deberían suspender (criterios tales como si el proceso está bloqueado, su prioridad, el número de páginas residentes, el tamaño de su mapa de memoria o el tiempo que lleva ejecutando). La reactivación de los procesos seleccionados sólo se realizará cuando haya suficientes marcos de página libres. La estrategia que decide cuándo suspender un proceso y cuándo reactivarlo se corresponde con la planificación a medio plazo presentada en el capítulo 4. A continuación, se plantean algunas políticas de control de carga. Estrategia del conjunto de trabajo Como se comentó previamente, cuando un proceso tiene residente en memoria su conjunto de trabajo, se produce una baja tasa de fallos de página. Una posible estrategia consiste en determinar los conjuntos de trabajo de todos los procesos activos para intentar mantenerlos residentes en memoria principal. Para poder determinar el conjunto de trabajo de un proceso es necesario dar una definición más formal de este término. El conjunto de trabajo de un proceso es el conjunto de páginas a las que ha accedido un proceso en las últimas n referencias. El número n se denomina la ventana del conjunto de trabajo. El valor de n es un factor crítico para el funcionamiento efectivo de esta estrategia. Si es demasiado grande, la ventana podría englobar varias fases de ejecución del proceso, llevando a una estimación excesiva de las necesidades del proceso. Si es demasiado pequeño, la ventana podría no englobar la situación actual del proceso, con lo que se generarían demasiados fallos de página. Suponiendo que el sistema operativo es capaz de detectar cuál es el conjunto de trabajo de cada proceso, se puede especificar una estrategia de asignación dinámica con reemplazo local y control de carga. tasa de fallos de página Gestión de memoria 333 límite superior límite inferior número de marcos Figura 5.74 Estrategia de administración basada en la frecuencia de fallos de página. • Si el conjunto de trabajo de un proceso decrece, se liberan los marcos asociados a las páginas que ya no están en el conjunto de trabajo. • Si el conjunto de trabajo de un proceso crece, se asignan marcos para que puedan contener las nuevas páginas que han entrado a formar parte del conjunto de trabajo. Si no hay marcos libres, hay que realizar un control de carga, suspendiendo uno o más procesos y liberando sus páginas. El problema de esta estrategia es cómo poder detectar cuál es el conjunto de trabajo de cada proceso. Al igual que ocurre con el algoritmo LRU, se necesitaría una MMU específica que fuera controlando las páginas a las que ha ido accediendo cada proceso durante las últimas n referencias. Estrategia de administración basada en la frecuencia de fallos de página Esta estrategia busca una solución más directa al problema de la hiperpaginación. Se basa en controlar la frecuencia de fallos de página de cada proceso. Como se ve en la figura 5.74, se establecen una cuota superior y otra inferior de la frecuencia de fallos de página de un proceso. Basándose en esa idea, a continuación se describe una estrategia de asignación dinámica con reemplazo local y control de carga. • Si la frecuencia de fallos de un proceso supera el límite superior, se asignan marcos de página adicionales al proceso. Si la tasa de fallos crece por encima del límite y no hay marcos libres, se suspende algún proceso liberando sus páginas. • Cuando el valor de la tasa de fallos es menor que el límite inferior, se liberan marcos asignados al proceso seleccionándolos mediante un algoritmo de reemplazo. Estrategia de control de carga para algoritmos de reemplazo globales Los algoritmos de reemplazo globales no controlan la hiperpaginación. Incluso aunque se pudiera utilizar el algoritmo óptimo, el problema persistiría, puesto que dicho algoritmo seleccionaría la página menos útil, pero, en estas circunstancias, esa página también es útil. Necesitan trabajar conjuntamente con un algoritmo de control de carga. Normalmente, se usan soluciones de carácter empírico, que detectan síntomas de que el sistema está evolucionando hacia la hiperpaginación. Así, si la tasa de paginación en el sistema es demasiado alta y el número de marcos libres está frecuentemente por debajo del umbral mínimo, se considera que el sistema está en estado de hiperpaginación y se suspende uno o más procesos. 5.5.10. Gestión del espacio de swap Un dispositivo de swap se implementa sobre una unidad de disco o una partición de la misma. Normalmente, los sistemas operativos ofrecen la posibilidad de utilizar múltiples dispositivos de swap, permitiendo, incluso, añadir dispositivos de swap dinámicamente, e incluso usar ficheros como soporte del swap. Sin embargo, hay 334 Sistemas operativos. Una visión aplicada que tener en cuenta que el acceso a los ficheros es más lento que el acceso directo a los dispositivos. En cualquier caso, esta posibilidad es interesante, ya que alivia al administrador de la responsabilidad de configurar correctamente a priori el dispositivo de swap, puesto que si hay necesidad, se puede añadir más espacio de swap en tiempo de ejecución. Habitualmente, también es posible que el administrador defina el modo de uso de los dispositivos de swap, pudiendo establecer políticas tales como no usar un dispositivo hasta que los otros estén llenos, o repartir cíclicamente las páginas expulsadas entre los dispositivos de swap existentes. La estructura interna de un dispositivo de swap es muy sencilla: una cabecera y un conjunto de bloques. La cabecera incluye algún tipo de información de control, como, por ejemplo, si hay sectores de disco erróneos dentro de la misma. No es necesario que incluya información del estado de los bloques, puesto que el dispositivo de swap sólo se usa mientras el sistema está arrancado. Por tanto, no hay que mantener ninguna información cuando el sistema se apaga. El sistema operativo usa un mapa de bits en memoria para conocer si está libre u ocupado cada bloque del swap. El sistema operativo debe gestionar el espacio de swap reservando y liberando zonas del mismo según evolucione el sistema. Existen básicamente dos alternativas a la hora de asignar espacio de swap durante la creación de una nueva región: • Con preasignación de swap. Cuando se crea una región privada o sin soporte, se reserva espacio de swap para la misma. Con esta estrategia, cuando se expulsa una página ya tiene reservado espacio en swap para almacenar su contenido. En algunos sistemas, más que realizar una reserva explícita de bloques de swap, se lleva una cuenta de cuántos hay disponibles, de manera que al crear una región que requiera el uso del swap, se descuenta la cantidad correspondiente al tamaño de la misma del total de espacio de swap disponible. • Sin preasignación de swap. Cuando se crea una región, no se hace ninguna reserva en el swap. Sólo se reserva espacio en el swap para una página cuando es expulsada por primera vez. La primera estrategia conlleva un peor aprovechamiento de la memoria secundaria, puesto que toda página debe tener reservado espacio en ella. Sin embargo, la preasignación presenta la ventaja de que con ella se detecta anticipadamente si no queda espacio en swap. Si al crear un proceso no hay espacio en swap, éste no se crea. Con un esquema sin preasignación, esta situación se detecta cuando se va a expulsar una página y no hay sitio para ella. En ese momento habría que abortar el proceso aunque ya hubiera realizado parte de su labor. Como se explicó en la sección 5.5.4, sólo las páginas de las regiones privadas o sin soporte usan el swap para almacenarse cuando son expulsadas estando modificadas. En el caso de una página de una región compartida con soporte en un fichero, no se usa espacio de swap para almacenarla, sino que se utiliza directamente el fichero que la contiene como almacenamiento secundario. Dado que puede haber múltiples entradas de tablas de páginas que hacen referencia al mismo bloque de swap, el sistema operativo gestionará un contador de referencias por cada bloque de swap, de manera que cuando el mismo valga cero, el bloque estará libre. 5.5.11. Compartimiento de páginas Como se ha comentado a lo largo del capítulo, los procesos comparten regiones en diversas circunstancias: • Cuando usan explícitamente una zona de memoria compartida. • Cuando utilizan el mismo programa o biblioteca dinámica, comparten implícitamente su código, optimizándose de esta forma el uso de recursos en el sistema. • Cuando proyectan un mismo fichero. • En UNIX, cuando un proceso crea un hijo mediante el servicio fork, el proceso hijo comparte con el padre aquellas regiones de tipo compartido que tenga éste. El compartimiento de las regiones implica, evidentemente, compartir las páginas de la región. Como se analizó en la sección dedicada a la paginación, para ello, basta con que las entradas correspondientes de las tablas de páginas de los procesos involucrados hagan referencia al mismo marco. En la tabla de marcos, se Gestión de memoria 335 almacenará un contador de referencias que indicará cuántas entradas de tablas de páginas hacen referencia a ese marco, de manera que cuando el mismo valga cero, el marco estará libre. En principio, dado que la memoria virtual se basa en la paginación, la técnica de compartimiento será la misma. Sin embargo, aparecen dos dificultades añadidas: • Cuando la página compartida no está residente en memoria y uno de los procesos accede a la misma provocando un fallo que la trae a memoria, si otro proceso quiere acceder a la página, deberá usar la copia presente en memoria. La pregunta es cómo descubre ese proceso que la página está residente en memoria. La respuesta es consultando la caché de páginas dentro la rutina de tratamiento del fallo de página. Al encontrar la página en la caché, no es preciso leerla de memoria secundaria y sólo hay que hacer que la entrada de la tabla de páginas haga referencia al marco que la contiene. • Si el algoritmo de reemplazo decide expulsar una página compartida, habrá que invalidar todas las referencias a ese marco en todas las entradas de las tablas de páginas que la comparten. Esto requiere guardar información que permita realizar la traducción inversa a la que se usa normalmente: dado un marco, se requiere conocer qué entradas hacen referencia al mismo. La gestión de esta información puede causar una sobrecarga apreciable en tiempo y espacio, por lo que algunos sistemas operativos no la proporcionan y sólo pueden expulsar páginas no compartidas. En Linux se utiliza una técnica denominada rmap para gestionar la información necesaria para la traducción inversa, pero intentando minimizar esta sobrecarga. Además del compartimiento propiamente dicho, existe otro tipo de situaciones en las que se comparte información, aunque sea inicialmente. Se trata de situaciones en las que dos o más procesos usan una región cuyo contenido inicial es el mismo, pero tal que cada uno debe trabajar de forma independiente, de manera que las modificaciones que realice un proceso no sean visibles por el resto. Este modo de trabajo corresponde con la operación duplicar_región, y puede requerirse en distintas circunstancias: • Cuando utilizan el mismo programa o biblioteca dinámica, inicialmente, los procesos comparten de forma implícita su región de datos con valor inicial. • En UNIX, cuando un proceso crea un hijo mediante el servicio fork, inicialmente, el proceso hijo comparte con el padre aquellas regiones de tipo privado que tenga éste. • Cuando dos o más procesos proyectan un mismo fichero, pero en modo privado. De la misma manera que la paginación por demanda retarda la carga de una página desde la memoria secundaria hasta que realmente se accede a la misma, se puede plantear realizar un duplicado por demanda, es decir, diferir la copia de cada página de la región hasta que el proceso la intente modificar. Esta técnica se denomina copy-on-write (COW). Con esta técnica, en vez de copiar la región original, se comparte, pero marcándola de tipo COW. Mientras los procesos que usan esta región sólo la lean pueden seguir compartiéndola. Pero cuando un proceso intenta modificar una página de esta región, el sistema operativo se encarga de crear una copia privada de la página para ese proceso. Habitualmente, para forzar la activación del sistema operativo, se modifica la protección de la página en la entrada correspondiente para que sea sólo de lectura, produciéndose una excepción de fallo de protección, en cuyo tratamiento se realiza la copia. No obstante, en la tabla de regiones se sigue manteniendo la protección real de la región. Tratamiento de la excepción de COW Las dos excepciones que regulan el comportamiento del sistema de memoria virtual son la del fallo de página, que permite implementar la paginación por demanda y que ya se ha estudiado anteriormente, y la del fallo de protección, que permite implementar la técnica del COW. A continuación, se especifican los pasos habituales en el tratamiento de un fallo de protección: • La MMU produce una excepción, dejando, habitualmente, en un registro especial la dirección que provocó el fallo. 336 Sistemas operativos. Una visión aplicada • Se activa el sistema operativo que busca en la tabla de regiones a qué región pertenece la dirección que produjo el fallo, verificando si realmente se trata de un fallo de tipo COW. Para ello, comprueba la protección de la región. Si no tiene permiso de escritura, se trata de un acceso inválido, por lo que se aborta el proceso o se le envía una señal. En caso contrario, se realizan los pasos que se describen a continuación. • Si el contador de referencias del marco es mayor que 1 (varios procesos todavía comparten la página), se realizan las siguientes acciones: ⎯ Se reserva un marco libre (gracias al buffering de páginas siempre lo habrá). ⎯ Se copia el contenido de la página. ⎯ Se asocia la entrada de la tabla de páginas con este nuevo marco y se le devuelve el permiso de escritura. ⎯ Se resta una unidad al contador de referencias del marco. La nueva página no se incluye en la caché de páginas, puesto que, por el momento, no está vinculada con ningún soporte. • Si el contador de referencias del marco ya es igual a 1 (es el único proceso asociado a ese marco), sólo hay que devolver los permisos a la página, que quedará asignada únicamente a este proceso. Asimismo, hay que eliminar la página de la caché de páginas, rompiendo con ello su vinculación con el soporte actual de la misma (no queremos que un fallo de página de otro proceso que compartía esta página la encuentre en la caché de páginas y la utilice, puesto que se trata de una nueva copia). • Si la página ya tenía un bloque de swap asignado, hay que romper también esta asociación, restando una unidad al contador de referencias del bloque de swap, puesto que ya no está asociado a la página asignada al proceso como resultado del COW. Cuando se expulse la página, se le asignará un nuevo bloque de swap. 5.5.12. Gestión de la memoria del sistema operativo En esta sección se analiza cómo se lleva a cabo la gestión de memoria desde el punto de vista del sistema operativo, mostrando cómo gestiona la memoria física, así como su mapa de memoria lógico. Asimismo, se revisarán los distintos esquemas de asignación de espacio proporcionados por el sistema operativo. Gestión de la memoria física En su fase de iniciación, el sistema operativo recibe información (normalmente, invocando alguna rutina del firmware) sobre las características de la memoria física disponible. A priori, puede parecer que es suficiente con que sepa cuánta memoria existe. Sin embargo, las cosas son bastante más complejas y necesita conocer mucha más información, puesto que la memoria física no es algo uniforme o contiguo. Puede haber huecos en el espacio de direcciones de la memoria y zonas de memoria ROM. Además, puede haber restricciones en el uso de ciertas partes de la memoria. Por ejemplo, en algunos sistemas, por limitaciones del hardware, sólo se puede hacer DMA sobre determinadas zonas de la memoria, de lo cual debe ser consciente el sistema operativo a la hora de asignar espacio (en Linux, se utiliza el concepto de zona para gestionar este tipo de restricciones). Asimismo, si se trata de un sistema multiprocesador NUMA, será necesario que el sistema operativo conozca la topología del sistema, de manera que cuando un proceso necesite memoria, se le pueda asignar de la parte asociada al procesador donde está ejecutando (en Linux, se usa el concepto de nodo para implementar esta característica). Una vez que el sistema operativo averigua las características de la memoria disponible en el sistema, crea una tabla de marcos del tamaño adecuado e inicia las entradas de la misma, de manera que los marcos donde está ubicado el sistema operativo se ponen como ocupados y los restantes como libres. Gestión del mapa de memoria del sistema operativo El sistema operativo posee su propio mapa de memoria definido por la tabla de páginas del sistema. En el arranque del sistema, el hardware de gestión de memoria estará desactivado. En su fase de iniciación, el sis- Gestión de memoria 337 tema operativo construirá su tabla de páginas y activará el hardware de gestión de memoria. Normalmente, en el proceso de arranque del computador, se carga el sistema operativo en una zona contigua de la memoria física. Por tanto, se iniciarán las primeras entradas de la tabla de páginas del sistema de manera que hagan referencia a esos marcos contiguos que ocupa inicialmente el sistema operativo, creando un espacio lógico para el mismo. Gracias a esa contigüidad, se pueden usar superpáginas para dar cobertura al espacio ocupado por el sistema operativo, lo que minimiza el número de entradas requeridas. Las entradas restantes de la tabla de páginas del sistema se quedarán sin usar (marcadas como inválidas). Una vez terminada la fase de iniciación, todos los marcos libres disponibles quedan a disposición de los procesos y de las necesidades internas del propio sistema operativo (cachés, memoria dinámica, etc.). Cuando el sistema operativo quiera usar un marco de página libre, deberá crear una asociación al mismo, iniciando una entrada de la tabla de páginas del sistema que esté sin usar, de forma que haga referencia a ese marco. Si se trata de una página para un proceso, se producirá una asociación temporal mientras el sistema operativo carga el contenido de la página, que desaparecerá cuando la página se asigne al proceso incluyéndola en su tabla de páginas. En caso de que la página sea para el propio sistema operativo (por ejemplo, para reservar dinámicamente espacio para un nuevo BCP), se establecerá una asociación que se mantendrá hasta que se libere explícitamente ese espacio reservado. Hay que resaltar que cualquier modificación en una entrada de la tabla de páginas del sistema conlleva una cierta sobrecarga, puesto que, como se verá en la próxima sección, requiere operaciones sobre la TLB (además, en el caso de que el procesador no use tablas de usuario y sistema independientes, habría una sobrecarga debida a la propagación del cambio a todas las copias de la tabla de páginas del sistema). Algunos sistemas operativos usan una solución alternativa: incluyen toda la memoria física en el espacio del sistema. En vez de crear un mapa inicial del sistema que incluya sólo las regiones del sistema operativo y usar asociaciones para acceder al resto de la memoria, como se acaba de explicar, establecen un mapa del sistema que cubre toda la memoria física: la primera página del mapa del sistema operativo se hace corresponder con el primer marco, la segunda con el segundo, y así sucesivamente. Esta solución, usada en Linux, permite que el sistema operativo manipule directamente toda la memoria física, sin necesidad de realizar asociaciones. Esta solución elimina la sobrecarga asociada a los cambios en la tabla de páginas del sistema y permite usar superpáginas para representar toda la memoria física. En este tipo de sistemas es inmediato el cálculo de a qué dirección física corresponde una dirección lógica del sistema: basta con restar el valor de la primera dirección lógica del sistema (en una configuración con 3GB de usuario y 1GB de sistema, la primera dirección lógica de sistema es C0000000, que es el valor que hay que restar a una dirección lógica de sistema para obtener la dirección física que representa). Las entradas restantes de la tabla de páginas del sistema se usan para poder asignar espacio lógico contiguo usando espacio físico que no lo es. Esta solución tiene la limitación de que sólo se puede manejar tanta memoria física como tamaño tenga el espacio lógico del sistema (si hay un espacio lógico de 1GB, sólo se puede gestionar directamente esa cantidad de memoria física, que es claramente insuficiente actualmente). Por ello, en este tipo de sistemas suele ser habitual una mezcla de las dos técnicas, es decir, establecer un mapa de sistema que cubra toda la memoria física posible, dejando una cantidad suficiente de entradas de la tabla de páginas de sistema sin usar para manejar el resto de la memoria (denominada en Linux high memory) mediante asociaciones. Esquemas de asignación de espacio Antes de presentar los diversos tipos esquemas de asignación de memoria que proporciona habitualmente el sistema operativo para su uso interno, es conveniente comparar dos soluciones a la hora de satisfacer una solicitud de memoria de algún componente del sistema operativo que necesita una zona contigua que ocupe varias páginas: • Una alternativa es reservar un conjunto de marcos cualesquiera, aunque no sean contiguos, y crear entradas en la tabla de páginas del sistema para que aparezcan como contiguos en el espacio lógico. • La otra opción es reservar un conjunto contiguo de marcos. Esta puede ser la única opción posible si el espacio se necesita para realizar una operación de DMA sobre el mismo. Además, en caso de usar un sistema que incluya la memoria física dentro del mapa del sistema, como Linux, la operación es más eficiente, ya que no requiere manipular la tabla de páginas para establecer asociaciones. 338 Sistemas operativos. Una visión aplicada Habitualmente, el sistema operativo ofrece distintos tipos de gestores de memoria internos, que intentan adaptarse a las diversas necesidades del propio sistema operativo y de los procesos. • En ocasiones, el sistema operativo necesita reservar una o más páginas contiguas en memoria en el espacio físico. Un manejador de un dispositivo puede necesitar reservar un buffer de varias páginas como almacenamiento intermedio de datos del dispositivo. Otro ejemplo aparece en el tratamiento de un fallo de página, donde el sistema operativo necesita reservar una página para el proceso. Linux utiliza un sistema buddy binario para gestionar los marcos libres, con tamaños que van desde 1 página hasta 1024 páginas, y ofrece un conjunto de operaciones para reservar marcos de página, con funciones tales como get_free_pages. • El código del sistema operativo puede necesitar reservar memoria dinámica igual que cualquier aplicación. De hecho, para que no quepa duda de la similitud, en Linux la función interna se llama kmalloc. En Linux, este componente de reserva dinámica se construye directamente sobre el sistema de asignación de páginas explicado en el punto anterior, y usa un esquema de múltiples listas con particiones estáticas cuyos tamaños son potencias de 2, desde 32 bytes hasta 128KB. Este subsistema funciona como una caché. Cuando se realiza una petición y no hay espacio libre asociado a este subsistema, se solicitan páginas al sistema de asignación de páginas. Sin embargo, cuando se libera, no se devuelve al sistema de asignación de páginas. • El código del sistema operativo requiere frecuentemente reservar y liberar objetos del mismo tipo (por ejemplo, espacio para un BCP). Para agilizar esta labor, el sistema operativo permite crear cachés específicas para cada tipo de objeto. De esta forma, para crear un nuevo BCP se puede usar directamente uno disponible en la caché, eliminando la necesidad de la reserva. En Linux se usa el gestor de slabs para este fin, construyéndose también sobre el sistema de asignación de páginas. • Por último, como ya se comentó antes, se ofrecen funciones para reservar memoria contigua en el espacio lógico, aunque no contigua en el físico. En Linux, se usa la función vmalloc para tal fin. En algunos sistemas operativos toda la memoria del sistema está residente en memoria física. Sin embargo, otros sistemas operativos, como Windows, permiten reservar memoria que puede ser expulsada (memoria que puede ser paginada al disco). Esta opción puede ser interesante para un módulo del sistema operativo que requiera usar una gran cantidad de memoria, pero no de forma simultánea. Téngase en cuenta que, a excepción de esta modalidad de reserva de memoria que puede ser paginada, en todos los demás casos cada byte de memoria que utiliza cualquier componente del sistema operativo es un byte que deja de estar disponible para los procesos Como se ha podido apreciar en este apartado, el sistema operativo utiliza numerosas cachés (por ejemplo, en Linux las correspondientes a los slabs y a las listas de particiones estáticas del kmalloc). Estas cachés van creciendo y, en principio, nunca reducen su tamaño por iniciativa propia. Por tanto, cuando el demonio de paginación se activa para liberar marcos de página, deberá considerar por un lado las páginas de usuario, aplicando el algoritmo de reemplazo correspondiente, y, por otro lado, el espacio libre de las diversas cachés. Buscar un equilibrio en el uso de la memoria por parte de estas dos entidades es un asunto complejo, que requiere ajustes de carácter empírico. Un último aspecto a tener en cuenta es que en sistemas multiprocesadores hay que intentar minimizar la congestión en la reserva de memoria reduciendo dentro de lo posible el uso de los cerrojos que se requieren para la manipulación de las estructuras de datos que reflejan el estado de la memoria. Por ello, suele ser habitual que las diversas cachés presentes en el sistema tengan una prerreserva de elementos para cada procesador. Mientras haya elementos en la prerreserva de un procesador, se pueden usar en ese procesador sin necesidad de utilizar el cerrojo. En el caso de Linux, hay una prerreserva de marcos de página libre por procesador y una para los slabs. 5.5.13. Coherencia de la jerarquía de memoria La jerarquía de memoria es un sistema complejo formado por múltiples niveles y componentes, donde se presentan numerosos problemas de coherencia, más aún en el caso de un sistema multiprocesador. Depen- Gestión de memoria 339 diendo del tipo de problema y de las características específicas del hardware del sistema, el componente encargado de mantener la coherencia puede ser distinto. En algunos casos, será el propio hardware el que resuelva el problema, pero, en otros, tendrá que ser el sistema operativo. El objetivo de esta sección es tratar todos estos problemas de una manera integrada, a pesar de su enorme variedad. Para ello, se presenta un modelo general original que permite identificar aquellos aspectos de la jerarquía de memoria que influyen en el mantenimiento de la coherencia en un sistema de estas características. De acuerdo con este modelo, cada nivel en una jerarquía se caracteriza por los siguientes aspectos: • La función de traducción requerida para, dado un identificador único del objeto que se pretende acceder, encontrar en qué bloque de ese nivel está almacenada, en caso de que esté presente en el mismo. • La necesidad, por motivos de eficiencia, de, a su vez, una jerarquía de niveles en el propio esquema de traducción de un nivel. Este es el caso del mecanismo de traducción del nivel de memoria principal, que requiere el uso de dos niveles de traducción: la TLB y la tabla de páginas. • La existencia de múltiples componentes de memoria en ese nivel. En un sistema multiprocesador, en el nivel de caché hay múltiples componentes: una caché por cada procesador. También en un sistema uniprocesador pueden existir múltiples componentes en un nivel. Así, en el nivel de caché puede haber una caché de instrucciones y una de datos, cuyos contenidos no son totalmente disjuntos, ya que a las instrucciones también se accede como datos cuando se carga un programa en memoria. • Las entidades que pueden acceder a ese nivel. A algunos niveles de la jerarquía, además de los procesos, también pueden acceder dispositivos de entrada/salida mediante DMA, tal como se estudiará en el capítulo 8. • El tipo de multiplexación que se realiza para que un componente pueda almacenar información de varios procesos. Como se vio al principio del capítulo, puede usarse una multiplexación temporal o espacial. En cuanto a la función de traducción de un nivel, debería recibir un identificador único del objeto que se pretende acceder. Retomando la notación de la sección 5.2.5 y el proceso de transformación de direcciones, en un sistema de memoria virtual, no se puede usar la dirección física como identificador único de un objeto, puesto que puede variar a lo largo de la ejecución del programa. Básicamente, se presentan dos opciones dependiendo de si se trata de un sistema con un espacio lógico por proceso o global. En el primer caso, el identificador estaría formado por una dirección lógica en el contexto de un proceso ([P, Dp]), mientras que en el segundo correspondería con una dirección lógica en un espacio global ([Dg]). A continuación, se aplica este modelo a los niveles habituales de la jerarquía, de manera ascendente, obviando el nivel de registros para no alargar innecesariamente la exposición. Ese nivel de registros lo gestiona directamente el sistema de compilación y no incumbe al sistema operativo, pero, en cualquier caso, se le puede aplicar perfectamente el modelo. Nivel de memoria secundaria Tanto si es un sistema con un espacio lógico por proceso como si es global, la traducción de la dirección lógica al bloque de memoria secundaria que la contiene se llevaría a cabo usando las tablas del esquema de memoria virtual (tablas de páginas y de regiones). Dado que sólo se requiere localizar el bloque en memoria secundaria cuando no está en la principal, no es necesario que este proceso de localización sea especialmente eficiente, no requiriendo, por tanto, una jerarquía de traducción. Se trata de un nivel formado por un único componente, que realiza una multiplexación espacial para repartir el espacio entre los procesos. Nivel de memoria principal Se corresponde también con un nivel formado por un único componente, que lleva a cabo una multiplexación espacial para repartir el espacio entre los procesos (aunque también temporal puesto que a un marco se le asignan distintas páginas a lo largo del tiempo). La traducción en este nivel la realiza la tabla de páginas, que permite encontrar el marco de página que contiene el dato requerido. Dado que esta traducción debe ser extremadamente eficiente, como se explicó previamente, se usa la TLB. 340 Sistemas operativos. Una visión aplicada Existe, por tanto, a su vez, una jerarquía de traducción, a cuyos niveles se accede con el mismo identificador, pero para buscar la localización del dato en vez del dato propiamente dicho. Existen, habitualmente, dos niveles: las tablas de páginas y la TLB. Como se comentó previamente, la TLB puede incluir información de proceso, en cuyo caso realiza una multiplexación espacial del espacio dedicado a almacenar información de traducciones, o no incluirla, realizando una multiplexación temporal (es decir, no podrá existir información de varios procesos simultáneamente). En el caso de un multiprocesador, el nivel de TLB presenta múltiples componentes: una TLB por procesador. Además del procesador, a este nivel también acceden los dispositivos de E/S basados en DMA. En la mayoría de los sistemas, los dispositivos realizan operaciones de DMA usando direcciones físicas. Nivel de caché Para simplificar, sólo se considera un nivel de caché, pero se puede extrapolar a cualquier número de niveles. Se trata de un nivel que está formado por múltiples componentes. En el caso de un sistema multiprocesador, hay una caché por procesador. Incluso en un sistema monoprocesador puede haber múltiples componentes en el mismo nivel si se usa un procesador con caché de datos e instrucciones separadas (tenga en cuenta que no se trata de cachés disjuntas, puesto que una instrucción también es un dato cuando se está cargando en memoria). No requiere una jerarquía de traducción puesto que la información requerida para la traducción se maneja eficientemente al estar incluida en la propia lógica de control de la caché. En cuanto a la función de traducción planteada por el modelo, hay dos alternativas que establecen dos arquitecturas diferentes en el diseño de la caché: • Caché virtual. A este tipo de caché se accede con una dirección lógica, ya sea en el contexto de un proceso ([P, Dp]) o en un espacio global ([Dg]). El acceso a esta caché se realiza en paralelo con el de la memoria principal (con el acceso a la TLB, que comprueba aspectos como los permisos de acceso). La caché puede incluir información de proceso o no (multiplexación espacial o temporal, respectivamente). • Caché física. A este tipo de caché se accede con una dirección física ([Df]). Dado que en un sistema de memoria virtual la dirección física asociada a un objeto puede cambiar, esta caché requiere que se realice primero la etapa de traducción en el nivel de memoria principal (en el mejor de los casos, un acceso a la TLB) antes de poder realizar el acceso a la caché, para poder determinar cuál es la dirección física actual del objeto de memoria requerido. Esto provoca una falta de paralelismo en los accesos a memoria, puesto que no se puede consultar simultáneamente la caché y la TLB. Para paliar este problema, se puede usar una caché tal que el índice de selección que se utiliza para acceder a la misma corresponda con la parte de la dirección lógica que representa el desplazamiento dentro de la página, puesto que este valor sigue siendo el mismo en la dirección física. Con esta estrategia, se puede realizar la etapa de selección del conjunto de la caché que contiene el dato deseado mientras está trabajando la TLB, de manera que cuando termine la TLB, quede sólo por realizar la comparación de etiquetas. Esta solución, sin embargo, limita el tamaño máximo de la caché (una caché con correspondencia directa estaría limitada al tamaño de la página; una asociativa por conjuntos con dos líneas de caché por conjunto tendría un tamaño máximo de dos veces el tamaño de la página y, así sucesivamente). cia. Una vez aplicado el modelo, en los siguientes apartados se analizan los posibles problemas de coheren- Problemas de coherencia entre niveles adyacentes Este problema surge cuando se produce una actualización de un objeto de memoria en un nivel y hay una lectura de dicho objeto en otro nivel adyacente, no habiéndose propagado la actualización. A continuación, se plantean varias situaciones en las que se presenta este problema. En primer lugar, consideremos el problema de coherencia entre la caché y la memoria principal si un dispositivo realiza DMA directamente sobre la memoria principal. El problema puede presentarse en ambas direcciones: Gestión de memoria 341 • Actualización en el nivel más rápido. Un proceso escribe datos en un buffer y se arranca una operación de DMA. Si la caché no usa write-through, algunos datos pueden no haberse actualizado en memoria principal y, por tanto, la operación de DMA estará leyendo datos incorrectos. En algunos sistemas, este problema lo resuelve directamente el hardware, pero, en otros, debe hacerlo el sistema operativo, forzando que se actualicen en memoria principal todos los datos del buffer. • Actualización en el nivel más lento. Una vez que se completa una operación de DMA sobre un buffer, un proceso puede seguir leyendo datos obsoletos presentes en la caché, puesto que no se han actualizado. Si el hardware no soluciona este problema, cuando concluya la operación de DMA, habrá que invalidar de la caché las direcciones correspondientes a ese buffer. Además de en la jerarquía de datos, pueden producirse problemas de este tipo en una jerarquía de traducción. Concretamente, existirán problemas de coherencia si se actualiza la información de la tabla de páginas (nivel más lento) y se usa información de la TLB ya obsoleta. Este problema atañe exclusivamente al sistema operativo y es labor del mismo resolverlo. Cada vez que el sistema operativo modifique algún campo de una entrada de la tabla de páginas que potencialmente pueda estar incluida en la TLB, debe solicitar la invalidación de dicha entrada en la TLB. Eso incluye cambios en la protección de una página, como sucede con la técnica del COW, cambios en los bits de referencia o de modificado, la invalidación de una página en la tabla de páginas, ya sea debido a que se expulsa o a que deja de ser válida al eliminarse la región que la contiene, etcétera. Problemas de coherencia entre componentes del mismo nivel En un multiprocesador existen niveles en los que hay múltiples componentes, uno por procesador. Es preciso asegurar que no se producen problemas de coherencia cuando se realiza una actualización de un dato en un componente y una consulta del mismo en otro componente de ese mismo nivel. Se presentan dos situaciones de este tipo: • Coherencia entre múltiples cachés. Normalmente, el hardware del multiprocesador resuelve este problema. • Coherencia entre múltiples TLB. De esta labor se tiene que encargar el sistema operativo. Dado que la TLB es básicamente un componente de consulta, los problemas de coherencia se restringen a cuando se invalida una entrada de la TLB. En un multiprocesador, es necesario invalidar todas las entradas que puedan existir en el sistema correspondientes a esa página. Habitualmente, esta invalidación se implementa usando IPI (interrupciones entre procesadores): desde el procesador donde se invalida la TLB se envía una IPI a todos los procesadores cuyas TLB puedan contener esa entrada. Dada la sobrecarga de esta operación, algunos sistemas operativos utilizan algoritmos de reemplazo que no actualizan el bit de referencia en su versión para multiprocesadores (poner a 0 el bit de referencia implicar invalidar entradas de las TLB). Como se comentó anteriormente, en un sistema monoprocesador también pueden aparecer este tipo de problemas si existen cachés de instrucciones y de datos separadas. Cuando se está cargando en memoria el código de un programa, éste pasa a través de la caché de datos. Sin embargo, a la hora de comenzar la ejecución, las instrucciones se leen de las cachés de instrucciones. Algunos procesadores mantienen de forma automática la coherencia de estas cachés. Sin embargo, en otros, debe ser el sistema operativo el encargado de llevarla a cabo. Para ello, después de completarse la carga del programa en memoria, el sistema operativo debe volcar a memoria las entradas de la caché de datos vinculadas con el rango de direcciones donde se ha cargado el código, así como invalidar las entradas de la caché de instrucciones correspondientes a dicho rango. Problemas de coherencia debidos al uso de homónimos Un homónimo se produce cuando un mismo nombre hace referencia a dos objetos diferentes. En el caso de la jerarquía de memoria, esta situación sólo se puede producir en sistemas que usan un espacio lógico por cada proceso, puesto que en ellos una misma dirección lógica se refiere a distintos objetos dependiendo de qué proceso la use. En cualquier caso, los homónimos sólo generan problemas de coherencia en sistemas donde 342 Sistemas operativos. Una visión aplicada se realice una multiplexación temporal del componente de memoria, es decir, en aquéllos en los que no se gestiona información de proceso. Eso ocurre en la caché virtual y en la TLB cuando no incluyen información de proceso. El sistema operativo debe encargarse de resolver este problema, invalidando el componente de memoria (la TLB o la caché virtual) en cada cambio de contexto. Dentro de este apartado también se podría considerar el problema que surge en los accesos por DMA a memoria si el sistema operativo asigna otra página a un marco involucrado en la operación de DMA, que en la mayoría de los procesadores, usa directamente direcciones físicas. Se trata de un problema de homónimos que el sistema operativo resuelve reteniendo en memoria las páginas involucradas, tal como se explicó en la sección 5.5.8. Problemas de coherencia debidos al uso de sinónimos Un sinónimo se produce cuando dos o más nombres diferentes se refieren al mismo objeto. Esta situación es habitual en un sistema de memoria, puesto que va unida intrínsicamente al compartimiento de información. El problema surge cuando en un determinado nivel no se detecta que dos direcciones lógicas corresponden a un mismo objeto, gestionando dos copias independientes del mismo dato, con los problemas de coherencia asociados. En el nivel de memoria principal este problema queda resuelto mediante la caché de páginas, que asegura que las referencias a una página compartida utilizan el mismo marco. Tampoco se produce en la memoria secundaria, donde se usa un único bloque swap para la página compartida. En caso de utilizar una caché virtual con información de proceso, sí puede presentarse este problema de duplicidad de memoria en ese nivel, existiendo distintas alternativas para resolverlo: • El hardware de la caché lo detecta y resuelve automáticamente. • El sistema operativo establece restricciones en el alineamiento de las regiones compartidas (por ejemplo, que empiecen en una dirección que sea múltiplo de 64KB), asegurando que dos sinónimos ocupan la misma línea de la caché y, por tanto, nunca pueden almacenarse simultáneamente en la memoria caché. • El sistema operativo se asegura de que cuando ejecuta un proceso, no haya ninguna entrada en la caché asociada a otro proceso que sea un sinónimo de las usadas por el proceso actual, invalidando previamente las entradas conflictivas. • Se usa un esquema que utiliza un único espacio global, eliminado directamente el problema, puesto que no existen los sinónimos. 5.5.14. Operaciones del nivel de procesos y de regiones Añadir la técnica de memoria virtual a un esquema de paginación básicamente sólo cambia un aspecto en cuanto a la implementación de las operaciones de estos niveles: toda reserva de memoria del sistema se retrasa hasta justo el momento en el que se accede a la misma, realizándose en el contexto de la rutina de tratamiento del fallo de página o del fallo de protección. Las operaciones en el nivel de procesos son idénticas a las usadas en un esquema de paginación sin memoria virtual, que se estudiaron en la sección dedicada a este esquema. Las operaciones de regiones, vistas también en esa sección, sólo cambian en el aspecto reseñado en el párrafo anterior. A continuación, se analiza cómo se realizan las diversas operaciones sobre las regiones en un sistema con memoria virtual. Creación de una región Toda la gestión correspondiente al nivel de regiones es idéntica a la usada en un sistema de paginación sin memoria virtual. La variación se corresponde con la labor en el nivel de procesos, puesto que, en este caso, cuando se crea una región, no se le asigna memoria principal, dado que se cargará por demanda. Si se usa la técnica de la creación de páginas por demanda, ni siquiera hay que modificar la tabla de páginas. Si en el sistema hay preasignación de swap y se trata de una región privada o sin soporte, hay que reservar en el momento de la creación la zona correspondiente del swap. En un sistema sin preasignación, se reservará espacio en swap para una página cuando se expulse por primera vez estando modificada. Gestión de memoria 343 En la creación de la imagen inicial del proceso durante la activación de un programa, se crean todas las regiones iniciales siguiendo el procedimiento que se acaba de describir. Por tanto, en un sistema que utilice tablas de páginas multinivel, basta con asignar una tabla del nivel superior con todas las entradas inválidas. Dado que la memoria virtual gestiona la información usando páginas completas y el sistema de ficheros utiliza bloques completos, la forma natural de asignar los bloques de un fichero ejecutable a las regiones del proceso sería la siguiente: • El primer bloque (bloque 0) del ejecutable corresponde con la cabecera del ejecutable, que no se carga en memoria durante la ejecución del programa. • A la región de código se le asignará un número entero de bloques en el fichero y un número entero de páginas en el mapa. Si la región ocupa C bloques, se almacenará en los bloques del ejecutable desde el 1 hasta el C. • La región de datos con valor inicial comienza justo después, ocupando también un número entero de bloques y de páginas. El problema de esta solución es que dedica un bloque completo a la cabecera, aunque normalmente sólo ocupa un centenar de bytes. Además, asigna más espacio del requerido a la región de código y de datos con valor inicial, al tener que redondear su tamaño a un número entero de bloques. Con esa estrategia, incluso un ejecutable muy pequeño ocuparía tres bloques. Ante esta deficiencia, algunos sistemas operativos usan este esquema alternativo, que optimiza el uso del disco haciendo que toda la información se almacene de forma contigua en el fichero: • La cabecera y el principio del código comparten el primer bloque del fichero. Por tanto, cuando se carga la primera página del mapa, se trae a memoria también la cabecera, aunque ésta no se necesite. • La región de código y de datos están dispuestas en el fichero de forma contigua. Por tanto, puede haber un bloque que contenga una primera parte de código y una segunda de datos con valor inicial. Dado que ese bloque debe ser visto a la vez como una página de código, con permiso de lectura y ejecución, y de datos con valor inicial, con permiso de lectura y escritura, habrá dos páginas diferentes, contiguas en el mapa, que se corresponderán con ese bloque. Se trata, por tanto, de un objeto compartido por el mismo proceso, pero con distinta protección. Dicho de otra forma, en la tabla de regiones, el último bloque del fichero ejecutable asociado a la región de código es el mismo que el primero de la región de datos con valor inicial. Obsérvese que el proceso sólo accederá a la primera parte de la última página de código y a la segunda parte de la primera página de la región de datos con valor inicial, quedando una zona sin usar en el mapa del proceso del tamaño de una página (aunque no alineada en memoria). Esta solución ahorra espacio en disco, presentando únicamente dos aspectos negativos, perfectamente tolerables: la sobrecarga de leer la cabecera del disco y el espacio en memoria que ocupa, a pesar de que su presencia en memoria no sea necesaria, y la pérdida de protección de la última página de código, que puede modificarse por error a través de la primera página de la región de datos sin que se detecte. El único detalle que falta por aclarar es dónde se crea el contenido inicial de la pila, formado por los argumentos y las variables de entorno que recibe el programa. Las dos opciones habituales son almacenarlo en marcos de página y hacer que la tabla de páginas inicial haga referencia a esos marcos, o bien guardarlo en bloques de swap, indicándolo en las entradas de la tabla de páginas involucradas. En la figura 5.75 se muestra cómo es la imagen de memoria inicial de un proceso en un sistema donde se usa la segunda opción de implementación de pila planteada. Para simplificar, se ha supuesto que se utiliza una tabla de páginas de un solo nivel. En esta figura se muestra, con zonas sombreadas, cómo la cabecera ocupa una primera parte de la región de código, y cómo la última parte de la segunda página de código y la primera de la de datos con valor inicial no son utilizadas. La tabla de regiones muestra el compartimiento del bloque 1 del fichero por parte de la región de código y la de datos con valor inicial. Eliminación de una región Nuevamente, las acciones son iguales que en un sistema de paginación sin memoria virtual. Además, si se trata de una región privada, hay que liberar los bloques de swap asociados a la región. 344 Sistemas operativos. Una visión aplicada 0 8192 R-X Comp. F Bl. inicial 0 8192 8192 RW- Privada F Bl. inicial 1 Fichero F Swap (SW1) Bloq. 0 Bloq. 1 Bloq. 2 Bloq. 0 Bloq. 1 Bloq. 2 Fichero Fichero ......... Mapa de memoria inicial 0 8192 16384 20480 BFFFF000 C0000000 BCP Código t. de regiones Datos con valor inicial Datos sin valor inicial t. páginas ......... 16384 4096 RW- Privada Anónima BFFFF000 4096 RW- Privada Anónima Memoria Marco 0 Marco 1 ......... Pila 00000 I 000 00000 I 000 00000 I 000 00000 I 000 00000 I 000 ......... 00000 I 000 ................ ......................... Marco 2 Marco 3 Marco 4 ........ 00002 I SW1 Figura 5.75 Estado inicial de ejecución de un programa. Cambio del tamaño de una región Vuelve a ser muy similar a cómo se realiza en un esquema de paginación sin memoria virtual. Con respecto a una disminución de tamaño en una región, si se trata de una región privada, habrá que liberar los bloques de swap asociados a la misma. Por lo que se refiere a un aumento de tamaño, la única diferencia con el sistema sin memoria virtual es que, como en la creación de una región, no hay que asignar marcos ni actualizar la tabla de páginas. Sólo se necesita ajustar el tamaño en la tabla de regiones. Si en el sistema hay preasignación de swap y se trata de una región privada o sin soporte, hay que reservar el espacio requerido en el swap. El tratamiento de la expansión de la pila es algo más complejo, ya que no proviene de una solicitud del proceso, sino de la propia evolución de la pila. Cuando se produce una expansión de pila, se genera un fallo de página asociado a una dirección que se corresponde con un hueco. El sistema operativo podría pensar, en principio, que se trata de un acceso erróneo, puesto que no corresponde a ninguna región. Para diferenciarlo, debe comparar la dirección que causó el fallo con el puntero de pila. Si la dirección es mayor, está dentro de la pila. Se trata de una expansión de la pila que implica simplemente actualizar la tabla de regiones, reservar espacio de swap si hay preasignación, y servir el fallo de página de manera convencional. En caso contrario, se trata de un acceso erróneo. Tenga en cuenta que si hay un hueco de una sola página entre la pila y la región anterior, no se permitirá la expansión, ya que si se llevara a cabo, la próxima expansión usaría directamente una página de la otra región sin avisar al sistema operativo (este hueco actúa como una página de seguridad). Duplicado de una región Esta operación sí cambia de manera considerable al aplicar en este caso la técnica de copia diferida basada en el COW, tal como se explicó en la sección 5.5.11. Con esta técnica, la operación de duplicado no requiere reservar marcos de página ni realizar una copia del contenido de las páginas de la región. Basta con duplicar las entradas de la tabla de páginas que corresponden con la región, marcándolas como de sólo lectura tanto en la tabla de páginas origen como en la destino. Asimismo, habrá que incrementar los contadores de referencias de los bloques de swap y de los marcos de página asociados a la región. Con esta técnica, se optimiza considerablemente la ejecución de un servicio fork, ya que sólo es necesario duplicar su tabla de páginas, en vez de su mapa. En la figura 5.76 se muestra cómo se llevaría a cabo una operación fork. En ella se puede apreciar como esta operación implica duplicar tanto la tabla de regiones como la de páginas. Obsérvese como se ha eliminado el permiso de escritura en la quinta entrada de ambas tablas de páginas. Para simplificar, se ha supuesto que se utiliza una tabla de páginas de un solo nivel. Gestión de memoria 345 Mapa de memoria de P1 0 8192 16384 20480 BFFFF000 C0000000 Código Datos con valor inicial Datos sin valor inicial ......... Pila BCP de P1 t. de regiones Mapa de memoria de P2 0 8192 16384 20480 BFFFF000 C0000000 t. páginas Código Datos con valor inicial Datos sin valor inicial 0 8192 R-X Comp. F Bl. inicial 0 0 8192 R-X Comp. F Bl. inicial 0 8192 8192 RW- Privada F Bl. inicial 1 8192 8192 RW- Privada F Bl. inicial 1 16384 4096 RW- Privada Anónima BCP de P2 t. de regiones t. páginas Bloq. 0 Bloq. 1 Bloq. 2 Bloq. 0 Bloq. 1 Bloq. 2 Fichero Fichero ......... ......... 16384 4096 RW- Privada Anónima BFFFF000 4096 RW- Privada Anónima BFFFF000 4096 RW- Privada Anónima 00004 V R-X 00000 I 000 00001 I SW1 00000 I 000 00004 V R-......... 00000 I 000 ................ 00004 V R-X 00000 I 000 00001 I SW1 00000 I 000 00004 V R-......... 00000 I 000 ................ 00002 I SW1 00002 I SW1 ......... Pila Fichero F Swap (SW1) Memoria Marco 0 Página 4 de P1 P2 Marco 1 Marco 2 Marco 3 Página 0 de P1 P2 Marco 4 ........ ......................... Figura 5.76 Resultado de la llamada fork. 5.5.15. Ficheros proyectados en memoria La generalización de la técnica de memoria virtual permite ofrecer a los usuarios una forma alternativa de acceder a los ficheros. Como se ha visto en la sección anterior, en un sistema de memoria virtual, se hacen corresponder entradas de la tabla de páginas con bloques del fichero ejecutable. La técnica de proyección de ficheros en memoria plantea usar esa misma idea, pero aplicada a cualquier fichero. El sistema operativo va a permitir que un programa solicite que se haga corresponder una zona de su mapa de memoria con los bloques de un fichero cualquiera, ya sea completo o parte del mismo. En la solicitud, el programa especifica el tipo de protección asociada a la región y si la región será compartida o privada. El sistema operativo simplemente realiza una operación de crear región, tal como se ha explicado en la sección previa. Una vez que el fichero está proyectado, si el programa accede a una dirección de memoria perteneciente a la región asociada al fichero, estará accediendo al fichero. El programa ya no tiene que usar los servicios del sistema operativo para leer (read) y escribir (write) en el fichero. La figura 5.77 muestra un ejemplo de una proyección de un fichero de 40.000 bytes. Como se puede apreciar en la misma, existe una entrada en la tabla de regiones que representa la proyección y que almacena las características de la misma; en este caso, es una proyección compartida de lectura y escritura. En el ejemplo de la figura, si el programa lee un byte de la dirección de memoria 20480, estará leyendo el primer byte del fichero, mientras que si escribe un byte en la dirección 20481, estará escribiendo en el segundo byte del fichero. En la figura, se ha supuesto que ya se ha producido este acceso y, por ello, el primer bloque del fichero está residente en el marco 4 de la memoria. La proyección de un fichero no implica que se le asigne memoria principal. El propio mecanismo de memoria virtual será el que se encargue de ir trayendo a memoria principal los bloques del fichero cuando se produzca un fallo de página al intentar acceder a la región asociada al mismo, y de escribirlos cuando la página sea expulsada estando modificada. El acceso a un fichero mediante su proyección en memoria presenta numerosas ventajas sobre el acceso convencional basado en los servicios de lectura y escritura. A continuación, se detallan algunas de estas ventajas: 346 Sistemas operativos. Una visión aplicada ............ ............ ............ ............ ............ Mapa de memoria 20480 40000 RW- Comp F Bl. inicial 0 Código Datos con valor inicial Datos sin valor inicial Fichero F Bloq. 0 Bloq. 1 Bloq. 2 Fichero ......... ............ BCP 20480 Fichero proyectado F t. de regiones t. páginas ............ ............ ............ ............ Memoria Marco 0 Marco 1 ......... Pila ......... 00000 II 000 ................ 00000 000 00004 V RW00000 I 000 00000 I 000 ......... 00000 I 000 ................ Bloque 0 de F ......................... Marco 2 Marco 3 Marco 4 ........ Figura 5.77 Proyección de un fichero. • Se disminuye considerablemente el número de llamadas al sistema necesarias para acceder a un fichero. Con esta nueva técnica, una vez que el fichero está proyectado, no hay que realizar ninguna llamada adicional. Esta reducción implica una mejora considerable en los tiempos de acceso, puesto que, como ya es conocido, la activación de una llamada al sistema tiene asociada una considerable sobrecarga. • Se evitan copias intermedias de la información. Esto repercute también en un acceso más eficiente. Con esta técnica, el sistema operativo transfiere directamente la información entre la memoria y el fichero. Con la forma de acceso convencional, todas las transferencias se realizan pasando por la caché de bloques del sistema de ficheros. • Se facilita la forma de programar los accesos a los ficheros. Una vez proyectado el fichero, se accede al mismo igual que a cualquier estructura de datos en memoria que haya declarado el programa. No es necesario utilizar ningún servicio especial del sistema operativo para acceder al mismo. Así, por ejemplo, dado un programa que realiza un cierto tratamiento sobre una matriz de datos almacenada en memoria, su modificación para que leyera la matriz de un fichero sólo implicaría añadir al principio del programa la proyección del fichero. No sería necesario modificar el código restante. 5.6. SERVICIOS DE GESTIÓN DE MEMORIA Como se ha podido apreciar a lo largo del capítulo, las labores que lleva a cabo el sistema de gestión de memoria son más bien de carácter interno. Más que proporcionar una colección de servicios explícitos a las aplicaciones, el objetivo del sistema de memoria tiene un carácter más implícito: dotar a las aplicaciones de un espacio de almacenamiento para su ejecución que satisfaga las necesidades de las mismas. Además, algunos servicios de gestión de memoria usados por los programas, como, por ejemplo, los vinculados con el uso de memoria dinámica, no son labor directa del sistema operativo, sino que los suministra el propio lenguaje de programación. Debido a todo ello, este componente del sistema operativo apenas ofrece servicios directos a las aplicaciones. Entre ese número relativamente reducido de servicios, en esta sección se ha considerado que los de mayor interés se pueden agrupar en dos categorías: Gestión de memoria 347 • Servicios de proyección de ficheros, que permiten incluir en el mapa de memoria de un proceso un fichero o parte del mismo. Bajo esta categoría existirán, básicamente, dos servicios: ⎯ Proyección de un fichero. Con esta operación se crea una región asociada al objeto de memoria almacenado en el fichero. Normalmente, se pueden especificar algunas propiedades de esta nueva región. Por ejemplo, el tipo de protección o si la región es privada o compartida. ⎯ Desproyección de un fichero. Este servicio elimina una proyección previa o parte de la misma. • Servicios de montaje explícito de bibliotecas, que permiten que un programa cargue en tiempo de ejecución una biblioteca dinámica y use la funcionalidad proporcionada por la misma, tal como se analizó en la sección 5.3.2. En esta categoría se englobarían, básicamente, tres servicios: ⎯ Carga de la biblioteca. Este servicio realiza la carga de la biblioteca, llevando a cabo todas las operaciones de montaje requeridas. ⎯ Acceso a un símbolo de la biblioteca. Con esta operación, el programa puede tener acceso a uno de los símbolos exportados por la biblioteca, ya sea éste una función o una variable. ⎯ Descarga de la biblioteca. Este servicio elimina la biblioteca del mapa del proceso. En las siguientes secciones, se muestran los servicios proporcionados por UNIX y Windows dentro de estas dos categorías. 5.6.1. Servicios UNIX de proyección de ficheros Los servicios de gestión de memoria más frecuentemente utilizados son los que permiten la proyección y desproyección de ficheros (mmap, munmap). ž caddr_t mmap (caddr_t direccion, size_t longitud, int protec, int indicadores, int descriptor, off_t despl); Este servicio proyecta el fichero especificado creando una región con las características indicadas en la llamada. El primer parámetro indica la dirección del mapa donde se quiere que se proyecte el fichero. Generalmente, se especifica un valor nulo para indicar que se prefiere que sea el sistema el que decida donde proyectar el fichero. En cualquier caso, la función devolverá la dirección de proyección utilizada. El parámetro descriptor corresponde con el descriptor del fichero que se pretende proyectar (que debe estar previamente abierto), y los parámetros despl y longitud establecen qué zona del fichero se proyecta: desde la posición despl hasta desp + longitud. El argumento protec establece la protección de la región, que puede ser de lectura (PROT_READ), de escritura (PROT_WRITE), de ejecución (PROT_EXEC), o cualquier combinación de ellas. Esta protección debe ser compatible con el modo de apertura del fichero. Por último, el parámetro indicadores permite establecer ciertas propiedades de la región: • MAP_SHARED. La región es compartida. Las modificaciones sobre la región afectarán al fichero. Un proceso hijo compartirá esta región con el padre. • MAP_PRIVATE. La región es privada. Las modificaciones sobre la región no afectarán al fichero. Un proceso hijo no compartirá esta región con el padre, sino que obtendrá un duplicado de la misma. • MAP_FIXED. El fichero debe proyectarse justo en la dirección especificada en el primer parámetro. Esta opción se utiliza, por ejemplo, para cargar el código de una biblioteca dinámica, si en el sistema se utiliza un esquema de gestión de bibliotecas dinámicas, tal que cada biblioteca tiene asignado un rango de direcciones fijo. En el caso de que se quiera proyectar una región sin soporte (región anónima), en algunos sistemas se puede especificar el valor MAP_ANON en el parámetro indicadores. Otros sistemas UNIX no ofrecen 348 Sistemas operativos. Una visión aplicada esta opción, pero permiten proyectar el dispositivo /dev/zero para lograr el mismo objetivo. Esta opción se puede usar para cargar la región de datos sin valor inicial de una biblioteca dinámica. ž int munmap(caddr_t direccion, size_t longitud); Este servicio elimina una proyección previa o parte de la misma. Los parámetros direccion y longitud definen la región (o la parte de la región) que se quiere eliminar del mapa del proceso. Antes de presentar ejemplos del uso de estos servicios, hay que aclarar que se utilizan conjuntamente con los servicios de manejo de ficheros que se presentarán en el capítulo que trata este tema. Por ello, para una buena comprensión de los ejemplos, se deben estudiar también los servicios explicados en ese capítulo. A continuación, se muestran dos ejemplos del uso de estas funciones. El primero es el programa 5.5 que cuenta cuántas veces aparece un determinado carácter en un fichero utilizando la técnica de proyección en memoria. Programa 5.5 Programa que cuenta el número de apariciones de un carácter en un fichero. #include <sys/types.h> #include <sys/stat.h> #include <sys/mman.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> int main(int argc, char *argv[]) { int i, fd, contador=0; char caracter; char *org, *p; struct stat bstat; if (argc!=3) { fprintf (stderr, ”Uso: %s caracter fichero\n”, argv[0]); return 1; } /* Para simplificar, se supone que el carácter a contar corresponde con el primero del primer argumento */ caracter=argv[1][0]; /* Abre el fichero para lectura */ if ((fd=open(argv[2], O_RDONLY))<0) { perror(”No puede abrirse el fichero”); return 1; } /* Averigua la longitud del fichero */ if (fstat(fd, &bstat)<0) { perror(”Error en fstat del fichero”); close(fd); return 1; } /* Se proyecta el fichero */ if ((org=mmap((caddr_t) 0, bstat.st_size, PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) { perror(”Error en la proyeccion del fichero”); close(fd); return 1; } Gestión de memoria 349 /* Se cierra el fichero */ close(fd); /* Bucle de acceso */ p=org; for (i=0; i<bstat.st_size; i++) if (*p++==caracter) contador++; /* Se elimina la proyeccion */ munmap(org, bstat.st_size); printf(”%d\n”, contador); return 0; } El segundo ejemplo se corresponde con el programa 5.6 que usa la técnica de proyección para realizar la copia de un fichero. Observe el uso del servicio ftruncate para asignar espacio al fichero destino. Programa 5.6 Programa que copia un fichero. #include <sys/types.h> #include <sys/stat.h> #include <sys/mman.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> void main(int argc, char *argv[]) { int i, fdo, fdd; char *org, *dst, *p, *q; struct stat bstat; if (argc!=3) { fprintf (stderr, ”Uso: %s orig dest\n”, argv[0]); return 1; } /* Abre el fichero origen para lectura */ if ((fdo=open(argv[1], O_RDONLY))<0) { perror(”No puede abrirse el fichero origen”); return 1; } /* Crea el fichero destino */ if ((fdd=open(argv[2], O_CREAT|O_TRUNC|O_RDWR, 0640))<0) { perror(”No puede crearse el fichero destino”); close(fdo); return 1; } /* Averigua la longitud del fichero origen */ if (fstat(fdo, &bstat)<0) { perror(”Error en fstat del fichero origen”); close(fdo); close(fdd); unlink(argv[2]); return 1; } 350 Sistemas operativos. Una visión aplicada /* Establece que la longitud del fichero destino es igual a la del origen.*/ if (ftruncate(fdd, bstat.st_size)<0) { perror(”Error en ftruncate del fichero destino”); close(fdo); close(fdd); unlink(argv[2]); return 1; } /* Se proyecta el fichero origen */ if ((org=mmap((caddr_t) 0, bstat.st_size, PROT_READ, MAP_SHARED, fdo, 0)) == MAP_FAILED) { perror(”Error en la proyeccion del fichero origen”); close(fdo); close(fdd); unlink(argv[2]); return 1; } /* Se proyecta el fichero destino */ if ((dst=mmap((caddr_t) 0, bstat.st_size, PROT_WRITE, MAP_SHARED, fdd, 0)) == MAP_FAILED) { perror(”Error en la proyeccion del fichero destino”); close(fdo); close(fdd); unlink(argv[2]); return 1; } /* Se cierran los ficheros */ close(fdo); close(fdd); /* Bucle de copia */ p=org; q=dst; for (i=0; i<bstat.st_size; i++) *q++= *p++; /* Se eliminan las proyecciones */ munmap(org, bstat.st_size); munmap(dst, bstat.st_size); return 0; } 5.6.2. Servicios UNIX de carga de bibliotecas Por lo que se refiere a esta categoría, la mayoría de los sistemas UNIX ofrece las funciones dlopen, dlsym y dlclose. ž void *dlopen(const char *biblioteca, int indicadores); La rutina dlopen realiza la carga y montaje de una biblioteca dinámica. Recibe como argumentos el nombre de la biblioteca y el valor indicadores, que determina diversos aspectos vinculados con la carga de la biblioteca. Como resultado, esta función devuelve un descriptor que identifica dicha biblioteca cargada. En cuanto al segundo parámetro, aunque permite especificar distintas posibilidades a la hora de cargarse la biblioteca, de forma obligatoria, sólo es necesario indicar uno de los dos siguientes valores: RTLD_LAZY, que indica que las referencias a símbolos que estén pendientes de resolver dentro de la biblioteca no se llevarán a cabo hasta que sea estrictamente necesario, o RTLD_NOW, que especifica que durante la propia llamada dlopen se resuelvan todas las referencias pendientes que haya dentro de la biblioteca que se desea cargar. Recuerde que estos dos modos de operación se analizaron cuando se estudiaron los aspectos de implementación de las bibliotecas dinámicas. Gestión de memoria 351 ž void *dlsym(void *descriptor, char *simbolo); La función dlsym permite acceder a uno de los símbolos exportados por la biblioteca. Recibe como parámetros el descriptor de una biblioteca dinámica previamente cargada y el nombre de un símbolo (una variable o una función). Este servicio se encarga de buscar ese símbolo dentro de la biblioteca especificada y devuelve la dirección de memoria donde se encuentra dicho símbolo. Como primer parámetro, en vez de un descriptor de biblioteca, se puede especificar la constante RTLD_NEXT. Si desde una biblioteca dinámica se invoca a la función dlsym especificando esa constante, el símbolo se buscará en las siguientes bibliotecas dinámicas del proceso a partir de la propia biblioteca que ha invocado la función dlsym, en vez de buscarlo empezando por la primera biblioteca dinámica especificada en el mandato de montaje. Esta característica suele usarse cuando se pretende incluir una función de interposición que, además, después invoque la función original. ž int dlclose(void *descriptor); La rutina dlclose descarga la biblioteca especificada por el descriptor. A continuación, se incluye el programa 5.7 que muestra un ejemplo del uso de estas funciones. El programa recibe como argumentos el nombre de una biblioteca dinámica, el nombre de una función y el parámetro que se le quiere pasar a la función. Esta función debe ser exportada por la biblioteca, debe tener un único parámetro de tipo cadena de caracteres y devolver un valor entero. El programa carga la biblioteca e invoca la función pasándole el argumento especificado, imprimiendo el resultado devuelto por la misma. Programa 5.7 Programa que ejecuta una función exportada por una biblioteca dinámica. #include <stdio.h> #include <unistd.h> #include <dlfcn.h> #include <string.h> #include <stdlib.h> int main(int argc, char *argv[]) { void *descriptor_bib; int (*procesar)(char *); int resultado; if (argc!=4) { fprintf(stderr, "Uso: %s biblioteca funcion argumento\n", argv[0]); return 1; } /* Se carga la biblioteca dinámica */ if (!(descriptor_bib=dlopen(argv[1], RTLD_LAZY))) { fprintf(stderr, "Error cargando biblioteca: %s\n", dlerror()); return 1; } /* Busca el símbolo */ if (!(procesar=dlsym(descriptor_bib, argv[2]))) { fprintf(stderr, "Error: biblioteca no incluye la funcion\n"); return 1; } /* Finalmente, llamamos a la función */ resultado=procesar(argv[3]); printf("Resultado: %d\n", resultado); /* Se descarga la biblioteca */ dlclose(descriptor_bib); return 0; } 352 Sistemas operativos. Una visión aplicada 5.6.3. Servicios Windows de proyección de ficheros A diferencia de UNIX, en Windows, la proyección de un fichero se realiza en dos pasos. En primer lugar, hay que crear una proyección del fichero y, posteriormente, se debe crear una región en el proceso que esté asociada a la proyección. ž HANDLE CreateFileMapping(HANDLE fich, LPSECURITY_ATTRIBUTES segur, DWORD prot, DWORD tamaño_max_alta, DWORD tamaño_max_baja, LPCTSTR nombre_proy); Esta función crea una proyección de un fichero. Como resultado de la misma, devuelve un identificador de la proyección. Recibe como parámetros el nombre del fichero, un valor de los atributos de seguridad, la protección, el tamaño del objeto a proyectar (especificando la parte alta y la parte baja de este valor en dos parámetros independientes) y un nombre para la proyección. En cuanto a la protección, puede especificarse de sólo lectura (PAGE_READONLY), de lectura y escritura (PAGE_READWRITE) o privada (PAGE_WRITECOPY). Con respecto al tamaño, en el caso de que el fichero pueda crecer, se debe especificar el tamaño esperado para el fichero. Si se especifica un valor 0, se usa el tamaño actual del fichero. Por último, por lo que se refiere al nombre de la proyección, éste permite a otros procesos acceder a la misma. Si se especifica un valor nulo, no se asigna nombre a la proyección. ž LPVOID MapViewOfFile(HANDLE id_proy, DWORD acceso, DWORD desp_alta, DWORD desp_baja, DWORD tamaño); Esta función crea una región en el mapa del proceso que queda asociada con una proyección previamente creada. Al completarse, esta rutina devuelve la dirección del mapa donde se ha proyectado la región. Recibe como parámetros el identificador de la proyección devuelto por CreateFileMapping, el tipo de acceso solicitado (FILE_MAP_WRITE, FILE_MAP_READ y FILE_MAP_ALL_ACCESS), que tiene que ser compatible con la protección especificada en la creación, el desplazamiento con respecto al inicio del fichero a partir del que se realiza la proyección, y el tamaño de la zona proyectada (el valor cero indica todo el fichero). ž BOOL UnmapViewOfFile(LPVOID dir); Esta función elimina la proyección del fichero. El parámetro indica la dirección de comienzo de la región que se quiere eliminar. A continuación, se muestran los dos mismos ejemplos que se plantearon en la sección dedicada a este tipo de servicios en UNIX. Como se comentó en dicha sección, es necesario conocer los servicios básicos de ficheros para entender completamente los siguientes programas. El primero es el programa 5.8 que cuenta cuántas veces aparece un determinado carácter en un fichero usando la técnica de proyección en memoria. Programa 5.8 Programa que cuenta el número de apariciones de un carácter en un fichero. #include <windows.h> #include <stdio.h> int main (int argc, char *argv[]) { HANDLE hFich, hProy; LPSTR base, puntero; DWORD tam; int contador=0; char caracter; if (argc!=3) { fprintf (stderr, "Uso: %s caracter fichero\n", argv[0]); return 1; } Gestión de memoria 353 /* Para simplificar, se supone que el carácter a contar corresponde con el primero del primer argumento */ caracter=argv[1][0]; /* Abre el fichero para lectura */ hFich = CreateFile(argv[2], GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFich == INVALID_HANDLE_VALUE) { fprintf(stderr, ”No puede abrirse el fichero\n”); return 1; } /* se crea la proyección del fichero */ hProy = CreateFileMapping(hFich, NULL, PAGE_READONLY, 0, 0, NULL); if (hProy == INVALID_HANDLE_VALUE) { fprintf(stderr, ”No puede crearse la proyección\n”); return 1; } /* se realiza la proyección */ base = MapViewOfFile(hProy, FILE_MAP_READ, 0, 0, 0); tam = GetFileSize(hFich, NULL); /* bucle de acceso */ puntero = base; while (puntero < base + tam) if (*puntero++==caracter) contador++; printf("%d\n", contador); /* se elimina la proyección y se cierra el fichero */ UnmapViewOfFile(base); CloseHandle(hProy); CloseHandle(hFich); return 0; } El segundo ejemplo se corresponde con el programa 5.9 que usa la técnica de proyección para realizar la copia de un fichero. Programa 5.9 Programa que copia un fichero. #include <windows.h> #include <stdio.h> int main (int argc, char *argv[]){ HANDLE hEnt, hSal; HANDLE hProyEnt, hProySal; LPSTR base_orig, puntero_orig; LPSTR base_dest, puntero_dest; DWORD tam; if (argc!=3){ fprintf (stderr, "Uso: %s origen destino\n", argv[0]); return 1; } 354 Sistemas operativos. Una visión aplicada /* se abre el fichero origen */ hEnt = CreateFile (argv[1], GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hEnt == INVALID_HANDLE_VALUE) { fprintf(stderr, ”No puede abrirse el fichero origen\n”); return 1; } /* se crea la proyección del fichero origen */ hProyEnt = CreateFileMapping(hEnt, NULL, PAGE_READONLY, 0, 0, NULL); if (hProyEnt == INVALID_HANDLE_VALUE) { fprintf(stderr, ”No puede crearse proyección del fichero origen\n”); return(1); } /* se proyecta el fichero origen */ base_orig = MapViewOfFile(hProyEnt, FILE_MAP_READ, 0, 0, 0); tam = GetFileSize (hEnt, NULL); /* se crea el fichero destino */ hSal = CreateFile(argv[2], GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hSal == INVALID_HANDLE_VALUE) { fprintf(stderr, ”No puede crearse el fichero destino\n”); return 1; } /* se crea la proyección del fichero destino */ hProySal = CreateFileMapping(hSal, NULL, PAGE_READWRITE, 0, tam, NULL); if (hProySal == INVALID_HANDLE_VALUE) { fprintf(stderr, ”No puede crearse proyección del fichero destino”); return 1; } /* se proyecta fichero destino */ base_dest = MapViewOfFile(hProySal, FILE_MAP_WRITE, 0, 0, tam); /* bucle de copia */ puntero_orig = base_orig; puntero_dest = base_dest; for ( ; puntero_orig < base_orig + tam; puntero_orig++, puntero_dest++) *puntero_dest = *puntero_orig; /* se eliminan proyecciones y se cierran ficheros */ UnmapViewOfFile(base_dest); UnmapViewOfFile(base_orig); CloseHandle(hProyEnt); CloseHandle(hEnt); CloseHandle(hProySal); CloseHandle(hSal); return 0; } Gestión de memoria 355 5.6.4. Servicios Windows de carga de bibliotecas En esta categoría, Windows ofrece las funciones LoadLibrary, GetProcAddress y FreeLibrary. ž HINSTANCE LoadLibrary(LPCTSTR biblioteca); La rutina LoadLibrary realiza la carga y montaje de una biblioteca dinámica. Recibe como argumento el nombre de la biblioteca. ž FARPROC GetProcAddress(HMODULE descriptor, LPCSTR simbolo); La función GetProcAddress permite acceder a uno de los símbolos exportados por la biblioteca. Recibe como parámetros el descriptor de una biblioteca dinámica previamente cargada y el nombre de un símbolo. Este servicio se encarga de buscar ese símbolo dentro de la biblioteca especificada y devuelve la dirección de memoria donde se encuentra dicho símbolo. ž BOOL FreeLibrary(HINSTANCE descriptor); La rutina FreeLibrary descarga la biblioteca especificada por el descriptor. A continuación, en el programa 5.10 se muestra el mismo ejemplo que se planteó para UNIX en el programa 5.7. Programa 5.10 Programa que ejecuta una función exportada por una biblioteca dinámica. #include <windows.h> #include <stdio.h> int main(int argc, char *argv[]) { HINSTANCE descriptor_bib; int (*procesar)(LPCTSTR); int resultado; if (argc!=4) { fprintf(stderr, "Uso: %s biblioteca funcion argumento\n", argv[0]); return 1; } /* Se carga la biblioteca dinámica */ descriptor_bib = LoadLibrary(argv[1]); if (descriptor_bib == NULL) { fprintf(stderr, "Error cargando biblioteca %s\n", argv[1]); return 1; } procesar = (int(*)(LPCTSTR))GetProcAddress(descriptor_bib, argv[2]); if (procesar == NULL) { /* La función no existe */ fprintf(stderr, "Error: biblioteca no incluye la funcion\n"); return 1; } /* Finalmente, llamamos a la función */ resultado=procesar(argv[3]); printf("Resultado: %d\n", resultado); /* Se descarga la biblioteca */ FreeLibrary(descriptor_bib); return 0; } 356 Sistemas operativos. Una visión aplicada 5.7. LECTURAS RECOMENDADAS Dada la variedad de temas abordados en este capítulo, al intentar dar una visión integral de la gestión de memoria, se recomiendan lecturas de muy diversa índole. Para profundizar en los aspectos relacionados con el ciclo de vida de un programa, se recomienda al lector interesado la consulta del libro [Levine, 1999]. En cuanto a la gestión del heap y, en general, el problema general de la asignación de espacio, es aconsejable el artículo [Wilson, 1995]. Dada la fuerte dependencia del hardware que existe en el sistema de memoria, es obligado hacer alguna referencia a documentación sobre los esquemas hardware de gestión de memoria ([Jacob, 1998]). Asimismo, se aconseja la revisión de documentación sobre aspectos menos convencionales, ya sea por su carácter novedoso o por ser avanzados, tales como nuevos algoritmos de reemplazo de memoria virtual ([Megiddo, 2004]) o sistemas basados en un espacio de direcciones único ([Chase, 1994]). Todos los libros generales de sistemas operativos (como, por ejemplo, [Silberschatz, 2005], [Stallings, 2005], [Nutt, 2004] y [Tanenbaum, 2001]) incluyen uno o más capítulos dedicados a la gestión de memoria, aunque, en nuestra opinión, seguramente interesada, no usan el enfoque integral de la gestión de memoria que se ha utilizado en este capítulo, por lo que su extensión es menor que la del presente capítulo. Para ampliar conocimientos sobre la implementación de la gestión de la memoria en diversos sistemas operativos, en el caso de Linux, se pueden consultar [Gorman, 2004], [Mosberger, 2002] y [Bovet, 2005], para UNIX BSD, [McKusick, 2004], y, por lo que se refiere a Windows, [Russinovich, 2005]. Por el impacto y la innovación que causaron en su momento en el campo de la gestión de memoria, son de especial interés los sistemas operativos MULTICS [Organick, 1972] y Mach [Rashid, 1988]. En cuanto a los servicios de gestión de memoria, en [Stevens, 1999] se presentan los servicios de UNIX y en [Hart, 2004] los de Windows. 5.8. EJERCICIOS 5.1. 5.2. 5.3. 5.4. 5.5. ¿Cuál de las siguientes técnicas favorece la proximidad de referencias? a) Un programa multithread. b) El uso de listas. c) La programación funcional. d) La programación estructurada. Considere un sistema de paginación con un tamaño de página P. Especifique cuál sería la fórmula que determina la dirección de memoria física F a partir de la dirección virtual D, siendo MARCO(X) una función que devuelve qué número de marco está almacenado en la entrada X de la tabla de páginas. ¿Es siempre el algoritmo LRU mejor que el FIFO? En caso de que sea así, plantee una demostración. En caso negativo, proponga un contraejemplo. Aplique el modelo de coherencia propuesto en la sección 5.5.13 al nivel de registros. En el lenguaje C se define el calificador volatile aplicable a variables. La misión de este calificador es evitar problemas de coherencia en aquellas variables a las que se accede tanto desde el flujo de ejecu- ción normal como desde flujos asíncronos, como por ejemplo una rutina asociada a una señal UNIX. Analice los tipos de problemas que podrían aparecer y proponga un método para resolver los problemas identificados para las variables etiquetadas con este calificador. 5.6. Algunas MMU no proporcionan un bit de referencia para la página. Proponga una manera de simularlo. Una pista: Se pueden forzar fallos de página para detectar accesos a una página. 5.7. Algunas MMU no proporcionan un bit de página modificada. Proponga una manera de simularlo. 5.8. Escriba un programa que use los servicios de proyección de ficheros de UNIX para comparar dos ficheros. 5.9. Escriba un programa que use los servicios de proyección de ficheros de Windows para comparar dos ficheros. 5.10. Determine qué número de fallos de página se producen al utilizar el algoritmo FIFO teniendo 3 marcos y cuántos con 4 marcos. Compárelo con el algoritmo LRU. ¿Qué ca- Gestión de memoria racteriza a los algoritmos de reemplazo de pila? 5.11. La secuencia que se utiliza habitualmente como ejemplo de la anomalía de Belady es la siguiente: 5.19. 1 2 3 4 1 2 5 1 2 3 4 5 5.12. Suponiendo que se utiliza un sistema sin buffering de páginas, proponga ejemplos de las siguientes situaciones: a) Fallo de página sin operaciones de E/S. b) Fallo de página con sólo una operación de lectura. c) Fallo de página con sólo una operación de escritura. d) Fallo de página con una operación de lectura y una de escritura. 5.13. Repita el ejercicio anterior, suponiendo un sistema con buffering de páginas. 5.14. Considere un sistema de memoria virtual sin buffering de páginas. Realice un análisis de cómo evoluciona una página en este sistema dependiendo de la región a la que pertenece. Estudie los siguientes tipos: a) Página de código. b) Página de datos con valor inicial. c) Página de datos sin valor inicial. d) Página de un fichero proyectado. e) Página de zona de memoria compartida. 5.15. Resuelva el ejercicio anterior suponiendo que hay buffering de páginas. 5.16. Como se comentó en la explicación del algoritmo de reemplazo LRU, el tiempo que se debe usar para seleccionar la página menos recientemente usada es el tiempo lógico de cada proceso y no el tiempo real. Modifique la implementación basada en contadores propuesta en el texto para que tenga en cuenta esta consideración. 5.17. En el texto sólo se ha planteado un bosquejo del algoritmo de reemplazo LFU. Explique cómo se podría diseñar una MMU que usara este algoritmo. Acto seguido, describa una aproximación del mismo usando sólo un bit de referencia. 5.18. Complete la rutina de tratamiento de fallo de página presentada en la sección 5.5.7 con todos los aspectos que se han ido añadiendo en las secciones posteriores (caché de páginas y buffering, utilización de preasignación de swap o no, creación de tablas 5.20. 5.21. 5.22. 5.23. 5.24. 5.25. 5.26. 357 de páginas por demanda, expansión implícita de la pila, etc.). Algunas versiones de UNIX realizan la carga de las bibliotecas dinámicas usando el servicio mmap. Explique qué parámetros deberían especificarse para cada una de las secciones de una biblioteca. Acceda en un sistema Linux al fichero /proc/self/maps y analice cuál es su contenido. ¿A qué estructura de datos del sistema de gestión de memoria corresponde el contenido de este fichero? En Windows se pueden crear múltiples heaps. Analice en qué situaciones puede ser interesante esta característica. Algunas versiones de UNIX ofrecen una llamada denominada vfork que crea un hijo que utiliza directamente el mapa de memoria del proceso padre, que se queda bloqueado hasta que el hijo ejecuta una llamada exec o termina. En ese momento el padre recupera su mapa. Analice qué ventajas y desventajas presenta el uso de este nuevo servicio frente a la utilización del fork convencional. En este análisis suponga primero que el fork se implementa sin usar la técnica COW para, a continuación, considerar que sí se utiliza. Analice qué puede ocurrir en un sistema que utiliza paginación por demanda si se recompila un programa mientras se ejecuta. Proponga soluciones a los problemas que pueden surgir en esta situación. En UNIX se define el servicio msync que permite forzar la escritura inmediata de una región en su soporte. ¿En qué situaciones puede ser interesante usar esta función? Analice qué situaciones se pueden producir en el tratamiento de un fallo de TLB en un sistema que tiene una gestión software de la TLB. Con el uso de la técnica de proyección de ficheros se produce una cierta unificación entre el sistema de ficheros y la gestión de memoria. Puesto que, como se verá en el capítulo dedicado a los ficheros, el sistema de ficheros usa una caché de bloques con escritura diferida para acelerar el acceso al disco. Analice qué tipo de inconsistencias pueden producirse si se accede a un fichero utilizando una proyección y los servicios convencionales del sistema de ficheros. 358 Sistemas operativos. Una visión aplicada 5.27. Analice el uso de la técnica del COW para optimizar una función de paso de mensajes entre los procesos de una misma máquina. 5.28. Sea un proceso en cuyo mapa de memoria existe una región privada con soporte en fichero. El proceso va invocar la llamada exec. ¿En qué ubicaciones pueden estar las páginas de la región en el instante previo? ¿Qué tratamiento se realiza sobre esa región durante la llamada exec? 5.29. Basándose en la manera como se utilizan habitualmente las llamadas fork y exec en las aplicaciones, algunos proponen que sería más eficiente que después del fork se ejecutara primero el proceso hijo en vez del padre. Analice en qué puede basarse esta propuesta. 5.30. Considere un proceso Pr1 en cuyo mapa de memoria se incluye una región privada con soporte en fichero, que está formada por 3 páginas. Suponga que se ejecuta la traza que se muestra a continuación y que después de ejecutar esta traza entran a ejecutar otros procesos que expulsan las páginas de estos procesos. Se debe calcular cuántos bloques de swap se dedican en total a esta región y, para cada proceso, cuál es la ubicación de cada página de la región, identificando en qué bloque del fichero o del swap está almacenada (numere los bloques del swap como considere oportuno). a) Pr1: lee de la página 1. b) Pr1: escribe en las páginas 2 y 3. c) Pr1: fork (crea Pr2). d) Pr1: escribe en las páginas 1 y 2. e) Pr2: escribe en la página 2. f) Pr2: fork (crea Pr3). g) Pr2: escribe en las páginas 1 y 3. h) Pr3: lee de las páginas 1, 2 y 3. 5.31. Muchos sistemas operativos mantienen una estadística de cuántos fallos de página se producen en el sistema que no implican una operación de lectura de disco (en Linux se denominan minor faults y en Windows soft faults). Suponga que se utiliza un sistema de memoria virtual basado en paginación por demanda, sin buffering de páginas. Analice si se produciría un fallo de página (ya sea convencional o el asociado al COW) y, en caso afirmativo, si implicaría una lectura de disco o no, para cada una de las situaciones que se plantean a continua- ción. En caso de que haya lectura, especifique si es de un fichero o de swap; en caso de que no la haya, explique si se requiere un marco libre para servir el fallo y qué información se almacena en el mismo. a) Después de reservar memoria dinámica, el proceso escribe en la misma. b) Después de llevar a cabo una llamada a procedimiento que hace que la región de pila se expanda, el proceso modifica una variable local. c) El proceso lee una variable global sin valor inicial que modificó antes, pero cuya página no está actualmente presente. d) Un proceso escribe en una variable global con valor inicial a la que ha accedido previamente sin modificarla, pero cuya página no está actualmente presente. e) Inmediatamente a continuación de que un proceso modifique una variable global con valor inicial y, luego, cree un hijo (fork), el proceso hijo lee esa misma variable. f) Inmediatamente a continuación de que un proceso lea una variable global con valor inicial y, luego, cree un hijo (fork), el hijo escribe en esa misma variable. g) Inmediatamente a continuación de que un proceso modifique una variable global con valor inicial y, luego, cree un thread, el nuevo thread escribe en esa misma variable. h) Analice cómo afectaría a esta estadística el uso del buffering de páginas. ¿Habría nuevas situaciones de fallos sin lectura de disco? ¿Dejaría de haber algunas de las situaciones de fallos sin lectura de disco que existen en un sistema que no usa buffering? 5.32. Considérese una TLB convencional, que no incluye información de proceso, tal que cada entrada tiene la información habitual: número de página y de marco, permisos de acceso, y bits de referencia y de modificado. Supóngase que el procesador incluye dos instrucciones para que el sistema operativo pueda manipular la TLB: una para invalidar la entrada vinculada a una página y otra para invalidar completamente el conte- Gestión de memoria nido de la TLB. Analice de forma razonada si en cada una de las siguientes operaciones, el sistema operativo haría uso de alguna de estas instrucciones, especificando, en el caso de que se invalide una entrada, a qué página correspondería: a) Hay un cambio de contexto entre dos threads de distinto proceso. b) Hay un cambio de contexto entre dos threads del mismo proceso. c) Se crea una nueva región en el mapa del proceso. d) Se elimina una región del mapa del proceso. e) Cambia el tamaño de una región del mapa del proceso. Analice separadamente el caso del aumento de tamaño y el de la disminución. f) Se marca como duplicada una región (por ejemplo, en el tratamiento de una región privada que forma parte de una llamada fork). g) Hay un fallo de página que encuentra un marco libre. h) Hay un fallo de página que no encuentra marcos libres. El sistema usa el algoritmo de reemplazo del reloj y encuentra que la primera página candidata a ser expulsada tiene tanto el bit de referencia como el de modificado a 1, mientras que la segunda tiene ambos bits a 0. i) Se produce un fallo debido al COW y el contador de referencias de la página es mayor que 1. j) Se produce un fallo debido al COW y el contador de Y es igual a 1. 5.33. Supóngase un programa que proyecta un fichero de forma privada y que, luego, crea un proceso mediante fork y este proceso hijo, a su vez, crea un thread. Analice, de manera independiente, qué ocurriría en las siguientes situaciones: a) El thread modifica una determinada posición de memoria asociada al fichero proyectado, luego la modifica el proceso hijo y, por último, el padre. b) El thread elimina la proyección del fichero y, acto seguido, intentan acceder a esa región el proceso hijo y el padre. 5.34. Sea un sistema con memoria virtual, en el que las direcciones lógicas que tienen un 0 en el bit de mayor peso son de usuario y las 359 que tienen un 1 son de sistema. Considérese que el sistema operativo no tiene ningún error de programación y que se produce un fallo de página tal que la dirección que lo provoca no pertenece a ninguna región del proceso. Dependiendo de en qué modo se ejecutaba el proceso cuando ocurrió el fallo y del tipo de la dirección de fallo, se dan los siguientes casos, cada uno de los cuales se deberá analizar para ver si es posible esa situación y, en caso afirmativo, explicar qué tratamiento requiere analizando si influye en el mismo el valor del puntero de pila en el momento del fallo: a) El proceso se estaba ejecutando en modo usuario y la dirección de fallo comienza con un 0. b) El proceso se estaba ejecutando en modo usuario y la dirección de fallo comienza con un 1. c) El proceso se estaba ejecutando en modo sistema y la dirección de fallo comienza con un 0. Distinga entre el caso de que el fallo se produzca durante una llamada al sistema o en una rutina de interrupción. d) El proceso se estaba ejecutando en modo sistema y la dirección de fallo comienza con un 1. Distinga entre el caso de que el fallo se produzca durante una llamada al sistema o en una rutina de interrupción. 5.35. ¿Qué acciones sobre la TLB, la memoria caché y la tabla de páginas se realizan durante un cambio de contexto? Suponga distintos tipos de hardware de gestión de memoria. 5.36. En un sistema con preasignación de swap se implantan dos cuotas máximas de uso de recursos por parte de un proceso: el tamaño máximo del mapa del proceso y el consumo máximo del espacio de swap por el proceso. Analice razonadamente si en las siguientes operaciones es necesario comprobar alguna de estas cuotas: a) Rutina de tratamiento del fallo de página. b) Proyección compartida de un fichero. c) Proyección privada de un fichero. d) Proyección privada de tipo anónima. e) Llamada al sistema fork. f) Rutina de tratamiento del COW. 360 Sistemas operativos. Una visión aplicada g) ¿Habría que comprobar alguna de estas cuotas en la gestión del heap? En caso afirmativo, ¿en qué rutina del sistema operativo? h) ¿Habría que comprobar alguna de estas cuotas en la gestión de la pila? En caso afirmativo, ¿en qué rutina del sistema operativo? 5.37. Una técnica perezosa en la gestión de memoria es la paginación por demanda. Suponiendo que existe suficiente memoria física, explique cuándo es beneficiosa, en cuanto a una ejecución más eficiente del proceso si se compara con la solución no perezosa, y cuándo perjudicial y por qué motivo, en caso de que pueda serlo. 5.38. Responda a la misma cuestión sobre la técnica COW. 5.39. Durante el arranque, Linux almacena en un marco una página de sólo lectura llena de ceros, que se mantendrá así todo el tiempo, y que se usa para implementar una técnica perezosa en la gestión de páginas de una región anónima. Explique cómo sería esta técnica mostrando en qué situaciones puede ser beneficiosa y en cuáles perjudicial, si es que puede serlo.