Memoria en sistemas operativos

La memoria principal es una gran matriz de bytes, tiene un espacio de direcciones en donde cada dirección es un byte.

Todas las instrucciones que se ejecutan y los datos sobre los que se operan tienen que estar en memoria principal (física).

● Un programa debe cargarse en memoria desde disco y colocarse dentro de un proceso para que se ejecute.

● Por otro lado, la memoria principal y los registros son los únicos dispositivos de almacenamiento a los que puede acceder la CPU directamente.

En general, a nivel lógico se hace una diferenciación entre la parte de la RAM que está destinada al Kernel space, en donde se encuentran todas las estructuras administrativas del S.O, y otra parte que es el User space en donde las aplicaciones de usuario van a poder allocar memoria.

Resumiendo, el disco es lento, por ende vamos a tratar de usarlo lo menos posible.

Reasignación de direcciones

La reasignación de instrucciones y datos pueden realizarse en:

Un ejemplo de direcciones simbólicas traducidas en tiempo de compilación es lo que pasaba con los famosos cartuchos de SEGA, en donde la estructura de memoria del cartucho era siempre fija, y si se necesitaba recurrir a alguna dirección se iba directamente a ella sin más. Es decir, si la dirección lógica del mapa era la 10, la dirección física iba a ser también la 10.

La diferencia con el tiempo de carga, es que por ejemplo, si nosotros le pidiéramos la dirección 10 como hicimos arriba, esta podría ir a buscarla a la dirección física 210, es decir, lo que estaría contando en este caso es el desplazamiento de 200, pero al fin y al cabo el concepto sigue siendo el mismo.

Traducción de direcciones

La MMU (Unidad de Administración de Memoria) es un componente de hardware que lo que hace es básicamente leer las direcciones lógicas y traducirlas a físicas para poder ir a buscarlas a la memoria principal.

La traducción en tiempo de ejecución se diferencia de las demás porque cuando la MMU reciba la dirección lógica, irá a buscar una dirección física distinta, y no igual como pasa en compilación o carga.

Antes de continuar con lo que vamos a ver, tenemos que saber que la MMU va tener, por así decirlo, una tabla con 3 columnas: los procesos, sus bases, y sus correspondientes límites (límite = tamaño del proceso).

Asignación contigua

En un modelo de memoria con asignación contigua, todo el espacio lógico de un proceso (Código, Datos, Pila) ha de estar ubicado de forma contigua en memoria principal, es decir en direcciones físicas consecutivas.

Por ejemplo, yo le doy a mi proceso P1 una base P1 física y un límite que va a estar definido por el tamaño de ese proceso (200MB por ej). Si el proceso quiere leer una variable cuya dirección lógica va a ser 10 por ejemplo, la MMU va a buscar esa dirección y traducirla a física sumándole la base.

La asignación contigua hace que el manejo de la memoria sea más simple pero más ineficiente y restrictivo:

Asignación contigua – Particiones fijas

En vez de asignarle a mi proceso una base custom, podría agarrarlo y meterlo en una partición fija, sabiendo que no va a poder solaparse con otros procesos.

Al utilizar particiones fijas es muy importante definir un tamaño de partición adecuado. Todas las particiones fijas son del mismo tamaño.Un problema de las particiones fijas es la fragmentación interna, ¿y por qué este nombre?, porque se produce dentro de un espacio fijo o ya asignado. Este problema podría darse si yo tengo un proceso muy chiquito que podría entrar dentro de la partición que se le fijó al PB, pero nunca va a poder darse porque ese espacio de memoria, aunque no se utilice, ya está reservado para PB.

Asignación contigua – Particiones dinámicas

Lo que me va a pasar en la fragmentación externa va a ser similar a lo que pasa en las particiones fijas con su fragmentación interna, esos pedacitos de memoria que se ven en el gráfico no los voy a poder usar.

La diferencia radica en que la fragmentación externa tiene una solución, y se llama

compactación:

En las particiones dinámicas vamos a tomar todos esos pedacitos de memoria con el fin de compactarlos todos contiguos para posteriormente asignarle esa partición al PD, en este caso.

A pesar de que solucione el problema, está a la vista que pueden llegar a surgir problemas como vimos anteriormente, como por ejemplo el solapamiento de procesos.

La compactación es una tarea que dura bastante en realizarse. Algoritmos para la asignación de particiones dinámicas:

Paginación (Simple)

La paginación simple es un caso especial de partición fija.

La idea de la paginación es dividir tanto la memoria principal en particiones fijas chicas, todas del mismo tamaño, denominadas “marcos” o “frames”, como también a los procesos en páginas del mismo tamaño (si los marcos son de 4KB, entonces las páginas también). Con esto lo que tratamos de hacer es que la asignación no sea contigüa.

● Características y ventajas:

Lo bueno de la paginación en parte es la facilidad de realocación, hacer crecer al proceso es fácil, se crea otra/s página/s, y listo.

Proceso de traducción de paginaciónDIRECCIÓN LÓGICA = NRO PÁGINA * TAMAÑO PÁGINA + OFFSET DIRECCIÓN FÍSICA= NRO MARCO * TAMAÑO MARCO + OFFSETSi la MMU tuviera una tabla con todas las páginas y los marcos de cada proceso, pesaría demasiado, y eso requeriría de un hardware demasiado costoso. Por este motivo, normalmente las tablas de páginas de cada proceso se encuentran en la RAM.Cada vez que nosotros queramos hacer una traducción nos va a inferir 2 accesos a memoria, una porque tengo que acceder primero a la tabla de páginas del proceso que se encuentra en la RAM, y otra porque con esa información recién voy a saber a dónde ir a otra vez a RAM pero para encontrar el dato que me pidieron.

¿Y cómo accedemos a la tabla de página de cada proceso? Bien, accediendo al PCB de cada proceso, el cual posee el puntero a la tabla de página correspondiente.

Todos los procesadores modernos que usan paginación, poseen un hardware muy rápido llamado caché de tipo TLB. Dicha caché no cachea datos, información, sino que cachea estas traducciones de la tabla de página.

Básicamente la función de la TLB es que se agilice el proceso de tener que ir a buscar X páginas que van a ser de alta concurrencia, como por ejemplo, al tener en código una variable que esté guardada en una página dentro del stack y se utilice mucho. Por tal motivo, cada vez que se tenga que ir a buscar esa página con dicha variable, se va a fijar primero si está en la caché.

La búsqueda no va a ser secuencial, no va a ir primero a ver si está en caché y si no está ir a memoria, sino que va a ser una búsqueda en paralelo, con el fin de que si la página no se encuentra en la caché, inmediatamente entre a memoria.

Gráfico de la TLB:

Protección y compartir memoria entre procesos

Estructura de tablas de páginas

La forma de compartir memoria va a lograrse cuando haya páginas que compartan el mismo marco. Dichas páginas van a tener el mismo contenido, o bien van a poder compartir el mismo.

¿Con qué problema nos encontramos acá? Que si yo tengo procesos muy grandes, cada tabla de páginas para cada proceso estarían ocupando una locura de espacio, tan solo con el ejemplo chiquito de arriba pudimos ver que una sola tabla de páginas puede ocupar 16MB.

Por este motivo, lo que se usa es la paginación jerárquica, o multinivel:Con la paginación jerárquica, yo voy a poder paginar a su vez la tabla de páginas. El sentido de esto es tener una tabla de páginas de primer nivel que me diga a su vez donde van a estar el resto de tablas de páginas de ese proceso.

La gracia de esto es que si yo tengo por ejemplo, un proceso chiquito que solamente necesite una tabla de páginas chiquita, va a usar solamente la tabla de páginas de primer nivel y la de segundo nivel, mientras que las demás tablas de páginas m van a quedar solo para los procesos que la necesiten, de esta manera vamos a lograr que no todas las tablas de páginas pesen 16MB siguiendo el ejemplo anterior.

En la paginación jerárquica van a ocurrir 3 accesos a memoria: primero porque voy a tener que ir a la tabla de páginas de primer nivel, segundo porque voy a tener que ir al cachito de tabla de páginas del segundo nivel que contiene la página a la que quiero acceder, y por último para ir al dato que estoy buscando.

Si tenemos la TLB para los casos recurrentes, los accesos se reducen Tabla de páginas invertida

Una tabla de páginas invertida es mucho más lenta, por el hecho de que si tengo que ir a buscar una página X, voy a tener que ir ‘preguntándole’ a cada frame si contiene dicha página para poder entrar a ella.

Tabla de páginas invertida con tabla de hash

La idea de usar una función de hash, es acortar el tiempo de ir a buscar la página que quiero fijándome en que marco está. Sin hash nosotros empezamos a buscar desde 0, no tenemos un punto de partida, nos vamos fijando en cada marco.

Lo que sucede usando una función de hash es que yo le voy a pasar por parámetro el PID del proceso y la página, devolviéndome la función un número de marco; entonces cuando vuelva de nuevo el proceso y me pida la página, yo voy a poner los mismos parámetros y me va a tener que devolver el mismo valor.

Si bien con esta alternativa se pueden generar colisiones, aun así mi búsqueda va a tener un mejor punto de partida.

Segmentación (Simple)

La idea de la segmentación es dividir a mi proceso en segmentos. Esta idea va a soportar la visión que tiene el “usuario” del programa

Tipos de segmentos:

Segmento de Código (CS)

Segmento de Datos (DS)

Segmento de Pila (SS)

Los segmentos asignados son de tamaño variable, ajustándose al tamaño requerido.

También, por este mismo motivo, la segmentación va a sufrir de fragmentación externa pero no interna.

Por otro lado, cada segmento se almacena en forma contigua en memoria principal. Además cada uno puede presentar distintos permisos (R (Read), W (Write), X (Execute))

La dirección lógica está compuesta por:

NRO SEGMENTO + OFFSET

La dirección física está compuesta por:

BASE DEL SEGMENTO + OFFSET

A nivel traducción es igual a la de particiones dinámicas, con la diferencia de que la tabla de segmentos, en vez de tener para cada proceso una base y un límite, voy a tener una base y un límite para cada segmento.

Segmentación paginada

Nosotros vamos a querer que la memoria esté dividida en marcos, que pueda agarrar cualquier página de un proceso y ponerla en cualquier marco, queremos compartir memoria, pero al mismo tiempo no quiero tener tanta granularidad con el tema seguridad, y quiero tener al proceso dividido en segmentos que tengan sentido, que yo sepa si son cachos de código (CS) o no.

La idea de la segmentación paginada va a ser segmentar al proceso en segmentos de código, pila o datos, los cuales van a tener los bits de protección de lectura, escritura, etc. Una vez segmentado el proceso, voy a darle a cada segmento una tabla de páginas.

De esta manera yo voy a saber por ejemplo, si cada página es de código o no, o si es de datos o no.

Con la segmentación paginada, puede que llegue a tener un poco de fragmentación interna, pero no voy a tener fragmentación externa.

Buddy-System

Buddy-System es un mecanismo de asignación continua. Tiene las dos fragmentaciones, interna y externa, pero por como funcionan, ninguna es un problema.

Fundamento

Funcionamiento

  • Inicialmente, a la MP se la considera como un gran bloque libre.
  • Pedido memoria -> se busca el bloque potencia de dos más chico que permita satisfacer la petición.
  • Si no existe el bloque entonces se divide en dos al bloque más cercano en tamaño al deseado y se repite la búsqueda.
  • Esos 2 nuevos bloques producto de la división serán buddies.