Práctica 4

Anuncio
Sistemas Operativos
Ingenierı́a de telecomunicaciones
Sesión 4: Memoria
Calendario
Comienzo: Lunes 16 de noviembre grupo A y miércoles 18 de octubre grupo B.
Entrega: 14 de diciembre grupo A y 16 de diciembre grupo B, hasta la hora de
clase.
1.
Objetivos
Al finalizar esta sesión:
Interpretarás correctamente la información proporcionada por el comandos top referente al uso de la memoria virtual.
Utilizarás correctamente las funciones para gestionar memoria: malloc() y free().
Utilizarás correctamente los punteros en C.
2.
Memoria virtual
Como ya sabéis de otras sesiones y de teorı́a, UNIX, como la gran mayorı́a de sistemas
operativos modernos, es un sistema multitarea. Esto quiere decir que, de alguna manera,
el sistema se las ingenia para que varias aplicaciones compartan recursos mientras se
ejecutan “a la vez”.
Vamos a ver esto con un ejemplo. Asegúrate de entenderlo bien antes de continuar:
#include <e r r n o . h>
#include <s y s / t y p e s . h>
#include <u n i s t d . h>
1
#include <s t d i o . h>
#include < s t d l i b . h>
int main ( )
{
int x ;
int y ;
p i d t pid = f o r k ( ) ;
switch ( p i d ) {
case −1:
perror (” fork () ”) ;
e x i t ( −1) ;
break ;
case 0 : // Soy e l h i j o
x = y = 5;
p r i n t f ( ” Soy e l h i j o , l a v a r i a b l e \” x \” v a l e %d y e s t a en l a
p o s i c i o n : %p\n” , x , &x ) ;
p r i n t f ( ” Soy e l h i j o , l a v a r i a b l e \” y \” v a l e %d y e s t a en l a
p o s i c i o n : %p\n” , y , &y ) ;
break ;
default : // Soy e l padre
x = y = 3;
p r i n t f ( ” Soy e l padre , l a v a r i a b l e \” x \” v a l e %d y e s t a en l a
p o s i c i o n : %p\n” , x , &x ) ;
p r i n t f ( ” Soy e l padre , l a v a r i a b l e \” y \” v a l e %d y e s t a en l a
p o s i c i o n : %p\n” , y , &y ) ;
break ;
}
return 0 ;
}
En este ejemplo, un primer proceso crea un proceso hijo mediante la llamada al sistema
fork(). Como sabes, esto crea una copia exacta del proceso padre, pero en una zona de
memoria nueva. Puedes comprobar que esto es ası́ fijándote en la asignación que cada
proceso hace a las variables x e y. Después del fork(), cada proceso tiene una copia
independiente de esas variables y por lo tanto puede asignarle valores distintos. Aunque
en nuestro programa se llamen igual, en realidad son dos zonas de memoria distintas.
2
¿O no? ¿Cómo puede ser que sean independientes, se puedan asignar por separado, y
sin embargo cada proceso las ve en la misma posición? Para cada proceso, la memoria va
desde la posición cero a la posición infinito. Es una ilusión creada por el sistema operativo,
es la memoria virtual (en contraposición a la real). IMPORTANTE: los procesos en
UNIX siempre manejan direcciones virtuales. Es el sistema operativo el que se encarga
de almancenarlas en direcciones fı́sicas o reales diferentes. Es decir, la posición virtual
0xbfbfe81c de un proceso corresponderá, por regla general, a una posición completamente
distinta de memoria real, por ejemplo la 0x00000034. Más aún, la misma posición virtual
0xbfbfe81c de otro proceso distinto, como es este caso, estará almacenada en otra posición
fı́sica distinta, por ejemplo la 0xbc804598.
2.1.
Memoria de intercambio (swap)
Con el comando top podéis ver información sobre el estado de la memoria virtual.
En la cabecera veréis información sobre la memoria fı́sica y la de intercambio. Podéis ver
incluso información sobre cómo está distribuida la memoria de un proceso especı́fico. Para
ello pulsad la tecla ’f’. Esto os permitirá definir los campos que queréis visualizar. Con
la tecla ’p’ activaréis el de memoria de intercambio. Ahora pulsad, por ejemplo ’3’ para
volver a la zona de información. Veréis que para algunos procesos este nuevo campo tiene
un valor de ’0’, pero para otros el valor es distinto. Eso quiere decir que parte de sus datos
y/o código están almacenados en la memoria de intercambio.
2.2.
Accediendo a memoria desde nuestros programas
Ya sabemos que los procesos son las entidades a las que se les asignan recursos en
un sistema operativo moderno. Para que un proceso pueda acceder a un recurso hay que
seguir una estrategia general: obtener permiso y/o reservar el recurso apropiado, utilizarlo
y, finalmente, liberarlo.
Es importante que veas que la memoria también es un recurso, y por tanto antes de
utilizarla tienes que asegurarte de que tienes permiso para hacerlo. Podemos clasificar la
forma de obtener permisos para utilizar la memoria de dos maneras: estática y dinámica.
La memoria estática es la que se conoce a la hora de compilar el programa: variables y
matrices que se declaren en el programa. El compilador puede determinar sin problemas
el tamaño que han de tener y en qué zona de la memoria residirán. Se llama estática
porque una vez compilado el programa no se podrá ampliar, ni cambiar de sitio ni liberar.
La memoria dinámica es aquella que se va adquiriendo en tiempo de ejecución en función de las necesidades del programa. Dado que al escribir un programa no podemos prever
todas las decisiones que tomará nuestro usuario, tampoco podemos predecir exactamente
cuánta memoria necesitaremos. Para resolver este problema tenemos algunas llamadas al
sistema que seguro ya conoces: malloc() para reservar memoria, realloc() para cambiar
de tamaño y/o de lugar una zona de memoria ya reservada y free() para liberarla.
NOTA: Los mayores problemas surgen al utilizar punteros. Antes de seguir,
asegúrate de que sabes manejar los siguientes conceptos:
3
Cadenas de caracteres.
La relación entre un vector (array) y un puntero, y como acceder a los elementos
del primero utilizando el segundo.
Reservar memoria utilizando malloc() y acceder a esa zona como un vector (ver
punto anterior).
2.2.1.
Memoria compartida
Una caracterı́stica muy importante del fork() que hemos visto es que los hijos son una
copia del padre, pero cada uno tiene su espacio de memoria independiente. De hecho, como
también hemos visto, direcciones virtuales iguales de dos procesos no se corresponden
con direcciones fı́sicas iguales. Vamos a ver un mecanismo que permite que dos procesos
compartan información a través de la memoria virtual. Un ejemplo:
#include
#include
#include
#include
#include
#include
#include
#include
<s y s / t y p e s . h>
<s y s / s t a t . h>
<s y s / i p c . h>
<s y s /shm . h>
< f c n t l . h>
<s t d i o . h>
< s t d l i b . h>
<u n i s t d . h>
void h i j o ( int ∗ s h a r e d v a r i a b l e )
{
sleep (3) ;
∗ shared variable = 3;
exit (0) ;
}
int main ( )
{
k e y t key = 3 4 ;
int shmid = shmget ( key , s i z e o f ( int ) , IPC CREAT | 0 6 0 0 ) ;
int ∗ s h a r e d v a r i a b l e = shmat ( shmid , 0 , 0 ) ;
∗ shared variable = 5;
p i d t pid = f o r k ( ) ;
i f ( p i d == 0 ) {
hijo ( shared variable ) ;
}
4
/∗ Por a q u i pasa s o l o e l padre ∗/
for ( int i = 0 ; i < 1 0 ; i ++){
p r i n t f ( ‘ ‘ V a r i a b l e compartida : %d\n ’ ’ , ∗ s h a r e d v a r i a b l e ) ;
sleep (1) ;
}
return 0 ;
}
En este ejemplo el padre imprime el valor de la variable *shared variable cada
segundo. El hijo, por su parte, espera tres segundos y lo cambia.Si lo compiláis y ejecutáis
deberı́ais comprobar cómo, sin que el padre toque nada, su variable cambia de valor.
Las llamadas al sistema clave son shmget() y shmat(). La primera nos devuelve el
identificador de un objeto de tipo “memoria compartida”. La opción IPC CREAT le dice que
lo cree si no existe ya (probad a quitarle la opción y utilizad una clave nueva). Los otros
parámetros son una clave asociada al objeto IPC y el tamaño de la memoria que queremos
compartir. La segunda (SHared Memory ATtach) mapea el objeto IPC externo en una
zona de memoria propia del proceso. Esto asocia un puntero a esa memoria compartida
externa, de tal forma que cada vez que modifiquemos la memoria a la que apunta el
puntero modificaremos el objeto IPC, compartido por otros procesos.
PROBLEMA: esta nueva forma de comunicación tiene el problema de que no es
posible enterarse de cuándo se produce un cambio, por lo que necesitaremos mecanismos
auxiliares para realizar la sincronización. Que entren los semáforos.
3.
Ejercicios
3.1.
Conocimiento
Cada pregunta vale 1 punto. Serán valoradas como correctas o incorrectas.
1. ¿Qué llamada al sistema permite conocer el tamaño de una página de memoria
virtual?
2. Indica un comando de Linux que no sea top que permita obtener información variada
sobre el estado de la memoria virtual de todo el sistema.
3. Existe un problema asociado con la reserva de memoria en distintos tamaños llamado
“fragmentación”. Explica cómo se origina y qué consecuencias tiene.
4. ¿Qué es la memoria de intercambio? ¿Qué relacion tiene con la memoria principal?
¿Es la misma, es distinta, es una parte de ella, ...?
5
5. ¿Qué comando permite conocer la cantidad de memoria libre? ¿Qué significa cada
campo?
3.2.
Experimentación
1. Vamos a ver cómo el sistema atiende a las peticiones de reserva de memoria. Haz
un programa que acepte dos argumentos por lı́nea de comandos: un número entero
que indique el número de bloques a reservar y otro número entero que indique el
tamaño de cada reserva.
Ahora el programa deberá ir haciendo reservas del tamaño indicado usando malloc().
Deberá hacer tantas reservas consecutivas como se haya indicado con el primer argumento.
Lo que nos interesa es ir viendo cómo el proceso va consumiendo memoria del sistema. Para ello usaremos la funcionalidad del pseudo-sistema de ficheros /proc. En él
encontraremos información relacionada con la máquina y todos los procesos. Si tu
proceso tiene como identificador el número PID, entonces en el directorio /proc/PID/
encontrarás toda la información relacionada con él. Usando la página man de la sección 5 de proc, averigua qué fichero de ese directorio contiene información sobre el
uso de memoria del proceso.
Ahora en tu programa, después de cada reserva, deberás averiguar cuánta memoria
está ocupando tu proceso e imprimirla por pantalla.
Representa gráficamente la ocupación de memoria de un proceso en función del
tamaño de las reservas y el número de ellas.
Contesta ahora razonadamente a las siguientes preguntas:
a) Desde el punto de vista del sistema, no del proceso, ¿cuándo aumenta la memoria ocupada por un proceso? (1 punto)
b) ¿Cuánto tamaño aumenta? (0,25 puntos)
c) Reservando la misma cantidad de memoria, ¿se tiene siempre la misma cantidad
ocupada? Intenta explicar el comportamiento del sistema en este punto. (1
punto)
4.
Programación
1. Retoma el ejercicio de la sesión 3. Ya tenemos un proceso con hilos que imprimen
sus identificadores. Ahora los hilos no imprimirán por pantalla, si no que deberán
comunicar sus identificadores a otro proceso mediante una zona de memoria compartida de tamaño variable. Tanto el proceso con los hilos como el que escribe en
pantalla deberán aceptar por lı́nea de comandos el número de enteros que cabrán
en la zona de memoria a compartir.
Habrá que implementar un esquema productor/consumidor entre los dos procesos.
Para ello usaremos una estrategia de buffer circular : cada proceso mantendrá un
6
puntero al principio de la zona de memoria, otro al final, y otro que indicará la
posición en la que se está trabajando actualmente. Cada vez que el productor escriba
un número, avanzará el puntero de trabajo. Si llega al final de la zona de memoria,
lo pondrá otra vez al principio. El consumidor hará lo mismo cada vez que lea
un número. Tienes que garantizar que el productor no va a pisar números que el
consumidor todavı́a no ha leı́do.
Al final deberá mantenerse la restricción del ejercicio anterior, en cada ronda deberá aparecer el identificador de cada hilo, sin imponer limitaciones al orden en el
que lo hagan. (4 puntos)
7
Descargar