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