Sistemas Operativos I Práctica 2: Memoria Compartida y Semáforos Práctica 2 Grupos Lunes Martes Miércoles Viernes Realización 22 de Marzo, 12 y 19 de Abril 23 de Marzo, 13 y 20 de Abril 24 de Marzo, 14 y 21 de Abril 26 de Marzo, 9 y 16 de Abril Entrega/Evaluación 26 de Abril 27 de Abril 28 de Abril 23 de Abril Memoria Compartida y Semáforos Teoría En esta segunda práctica de Sistemas Operativos, vamos a aprender a comunicar datos entre varios procesos en ejecución mediante la memoria compartida. La memoria compartida es una zona de memoria común a la que todos los procesos pueden conseguir acceso y de esta forma, lo que un proceso escribe en la memoria, es accesible al resto de procesos. Las funciones para trabajar con memoria compartida en UNIX en C están incluidas en los includes <sys/types.h>, <sys/ipc.h> y <sys/shm.h>. En particular, las funciones que usaremos son: shmget, shmat, shmdt y shmctl. Todas ellas documentadas en línea (comando man). Hay que tener también en cuenta e investigar, una consideración que no está documentada. En algunos sistemas UNIX, para utilizar la función shmget es necesario usar los flags SHM_W y SHM_R además del flag IPC_CREAT. Estos flags definen la memoria compartida como un área de memoria donde los procesos pueden escribir y de la que pueden leer, respectivamente. Veremos cómo si estos flags no se definen, la memoria compartida se crea pero los procesos no pueden escribir o leer de ella. Además, en esta práctica estudiaremos los riesgos que tiene el acceso concurrente por varios procesos a la memoria compartida, y como evitarlos mediante el uso de semáforos. Aprenderemos dos tipos de semáforos: – Los semáforos binarios: sólo pueden tomar dos valores (0 ó 1). Cuando el semáforo está a 1 permite el acceso del proceso a la sección crítica, mientras que con 0 bloquea el acceso a ella. Este tipo de semáforos es especialmente útil para garantizar la exclusión mutua a la hora de realizar una tarea crítica en la memoria compartida. Por ejemplo, para controlar la escritura de variables en memoria compartida, de forma que sólo se permita que un proceso esté en la sección crítica mientras que se están modificando los datos. – Los semáforos N­arios: pueden tomar valores desde 0 hasta N. El funcionamiento es similar al de los semáforos binarios. Esto es, cuando el semáforo está a 0, está cerrado y no permite el acceso a la sección crítica. La diferencia está en que puede tomar cualquier otro valor positivo además de 1. De hecho, este tipo de semáforos son muy útiles para permitir que un determinado número de procesos trabajen concurrentemente en alguna tarea no crítica en la memoria compartida. Por ejemplo, varios procesos pueden estar leyendo simultáneamente de la memoria compartida, mientras que ningún otro proceso intente modificar datos. También, nos familiarizaremos con dos operaciones fundamentales que debemos conocer a la hora de trabajar con semáforos y sus ventajas al ser operaciones atómicas: – Down, p, wait o toma_semaforo: Consiste en la petición del semáforo por parte de un Escuela Politécnica Superior Universidad Autónoma de Madrid 1 Sistemas Operativos I Práctica 2: Memoria Compartida y Semáforos proceso que quiere entrar en la sección crítica. Internamente el sistema operativo comprueba el valor del semáforo. De forma que si está a 1, se le concede el acceso a la sección crítica (por ej. escribir un dato en la memoria compartida) y de forma atómica decrementa el valor del semáforo para bloquear el paso a cualquier otro proceso que intente acceder a la sección crítica. Por lo tanto, si el semáforo está a 0, el proceso queda bloqueado (sin consumir tiempo de CPU) hasta que el valor del semáforo vuelva a ser 1 y obtenga el acceso a la sección crítica. – Up, v, signal o libera_semaforo: Consiste en la liberación del semáforo por parte del proceso que ya ha terminado de trabajar en la sección crítica y por lo tanto de forma atómica incrementa el valor del semáforo en una unidad. De forma, por ejemplo, que si el semáforo fuera binario y estuviera a 0 pasaría a valer 1, y se permitiría el acceso a cualquier otro proceso que estuviera bloqueado en espera de conseguir acceso a la sección crítica. Las funciones para trabajar con semáforos en UNIX en C están incluidas en las librerías <sys/types.h>, <sys/ipc.h> y <sys/sem.h>. En particular, las funciones son semget, semop y semctl. Al igual que las otras funciones, están documentadas en líneas (comando man). Estas funciones, aunque muy potentes, son habitualmente complicadas de usar, por lo que se recomienda utilizarlas en su versión más básica, siempre que sea posible. Entre las complicaciones introducidas por estas funciones, es importante prestar atención a las siguientes: – En vez de un solo semáforo clásico representado por un entero no negativo, en realidad semget define un array de semáforos del tamaño especificado. Hay que trabajar por tanto con conjuntos de semáforos, aunque para crear un sólo semáforo necesitemos crear un array de un solo elemento. – La creación de un semáforo es independiente de su inicialización, lo cual es especialmente peligroso ya que no se puede crear un semáforo inicializado de forma atómica. Es el programador el que debe tener cuidado de inicializar siempre los semáforos que cree. Es fundamental a la hora de colocar semáforos, intentar seguir algún algoritmo ya diseñado para resolver casos tipo como: lectores/escritores, productor/consumidor, el problema de la barbería, o la cena de los filósofos y evitar, entre otras problemas, interbloqueos entre procesos. Por último, comentaros dos comandos muy útiles, no sólo para esta práctica sino también para la práctica 3 (y en general siempre que trabajéis en programación concurrente en UNIX). Son los comandos ipcs e ipcrm y nos permiten inspeccionar los recursos compartidos (memoria compartida, semáforos y mensajes) de un usuario y eliminarlos en caso de que sea necesario. Ejercicios Dado el siguiente código de creación de memoria compartida: #include #include #include #include #include <sys/ipc.h> <sys/shm.h> <sys/types.h> <errno.h> <stdlib.h> #define CLAVE_MEMORIA 2555 int main() { int shm; Escuela Politécnica Superior Universidad Autónoma de Madrid 2 Sistemas Operativos I Práctica 2: Memoria Compartida y Semáforos shm = shmget(CLAVE_MEMORIA, sizeof(int), IPC_CREAT); if (shm == -1) { perror("shmget:"); exit(-1); } exit(0); } 1. Ejecuta el anterior programay comprueba qué ha tenido lugar en el sistema. Ejecuta los comandos de Shell necesarios para restaurar el sistema a su estado original (0,5 puntos) 2. Modifícalo en un programa1.c para que cree la memoria compartida sólo en caso de que no exista previamente. Si ya existiese sólo debe mostrar un mensaje indicando que ya está creada (1,5 puntos). 3. Modifícalo de nuevo en un programa2.c para que libere la memoria compartida al salir sólo si fue él quien la creó. En caso contrario debe mostrar un mensaje en pantalla indicando el identificador de la memoria. (2 puntos). Compila ahora el siguiente código y ejecútalo. #include #include #include #include #include <sys/ipc.h> <sys/shm.h> <sys/types.h> <errno.h> <stdlib.h> #define CLAVE_MEMORIA 1555 int main() { int shm; int * dir = NULL; int numero = 2; shm = shmget(CLAVE_MEMORIA, sizeof(int), IPC_CREAT|SHM_R|SHM_W); if (shm == -1) { perror("shmget:"); exit(-1); } dir = (int *) shmat (shm, NULL, 0); (*dir) = numero; exit(0); } 4. Explica qué hace el anterior código. Explica qué pasa si lanzas una segunda vez el programa (0,5 puntos) 5. Modifica el código anterior en un programa3.c para engancharse a la memoria Escuela Politécnica Superior Universidad Autónoma de Madrid 3 Sistemas Operativos I Práctica 2: Memoria Compartida y Semáforos compartida aunque esta exista previamente. Controla todos los errores posibles y realiza una salida limpia del programa liberando todos los recursos utilizados. (2 puntos) 6. Modifica el código anterior en otro programa3b.c en el que una vez enganchado a la memoria el proceso en vez de asignar a la memoria compartida el valor de la variable “numero” simplemente incremente su contenido y lo imprima por la salida estándar. Compílalo y ejecútalo varias veces. Explica los resultados. Controla todos los errores posibles y realiza una salida limpia del programa liberando todos los recursos utilizados. (0,5 puntos) Cuando un segmento de memoria compartida es borrado en realidad sólo se marca para borrado. La eliminación definitiva del segmento se realiza cuando todos los procesos enganchados a él se desenganchan. 7. Realiza un programa sencillo (programa4.c) que demuestre esto y explica como lo hace1. (3 puntos) Compila el siguiente código y ejecútalo. #include #include #include #include #include #include <sys/ipc.h> <sys/shm.h> <sys/sem.h> <sys/types.h> <errno.h> <stdlib.h> #define CLAVE_SEMAFORO 2000 int main() { int semid; } semid = semget(CLAVE_SEMAFORO, 1, IPC_CREAT|IPC_EXCL|SHM_R|SHM_W); if (semid == -1 && errno == EEXIST) semid = semget(CLAVE_SEMAFORO, 1, SHM_R|SHM_W); if (semid == -1) { perror("semget:"); exit(errno); } exit(0); 8. Modifícalo en un programa5.c para crear 10 semáforos e inicializarlos con valores desde 1 hasta 10. (2 puntos). 1 Puedes usar fork e ipcs. Escuela Politécnica Superior Universidad Autónoma de Madrid 4 Sistemas Operativos I Práctica 2: Memoria Compartida y Semáforos 9. Modifícalo de nuevo en un programa6.c para liberar correctamente los semáforos antes de salir. (1 punto) 10. Escribe una función de prototipo: int down (int id, int num_sem) que llame a semop para bloquear el semáforo en la posición num_sem de un array de semáforos con identificador id. Si todo es correcto devolverá 0 y otro valor en caso de error. (1 punto) 11. Escribe una función de prototipo: int up (int id, int num_sem) que llame a semop para liberar el semáforo en la posición num_sem del array de semáforos con identificador id. Si todo es correcto devolverá 0 y otro valor en caso de error. (1 punto) Analiza el siguiente código y fíjate que está incompleto en algunos puntos. #include <sys/ipc.h> // Completar las librerías #include <stdlib.h> #define CLAVE_SEMAFORO 2000 // Completar las definiciones de constantes #define SEM_MUTEX 0 // Poner aquí el código de la función down desarrollada en el ejercicio 7 // Poner aquí el código de la función up desarrollada en el ejercicio 8 int main() { int semid, shm; union semun carg; unsigned short int array[NUM_SEMAFOROS]; int * dir = NULL; semid = semget(CLAVE_SEMAFORO, 1, IPC_CREAT|IPC_EXCL|SHM_R|SHM_W); if (semid == -1 && errno == EEXIST) semid = semget(CLAVE_SEMAFORO, 1, SHM_R|SHM_W); if (semid == -1) { perror("semget:"); exit(errno); } // inicializar el semáforo mutex a 1 // crear una zona de memoria compartida de identificar shm para guardar un // entero y obtener el puntero para acceder a ella down(semid, SEM_MUTEX); (*dir) = 2; up(semid, SEM_MUTEX); // liberar la memoria compartida y los semáforos exit(0); } 12. Completa este código en un programa7.c y no olvides controlar posibles errores y realizar Escuela Politécnica Superior Universidad Autónoma de Madrid 5 Sistemas Operativos I Práctica 2: Memoria Compartida y Semáforos una correcta liberación de recursos. (3 puntos) 13. ¿Qué problema hay si falla la función down? (1 punto) 14. ¿Y si falla la función up? (1 punto) Cuando un proceso está bloqueado en un semáforo y recibe una señal el proceso queda libre del semáforo tras atender a la señal. Esto puede ser causa de errores no deseados. 15. Modifica la función de down anterior en un programa8.c para tener esto en cuenta y volver a realizar la espera en caso de haber recibido una señal. (1 punto) 16. Si habíamos realizado un down y recibimos una señal que nos haga finalizar el proceso, SIGKILL por ejemplo, dejaríamos inutilizado el semáforo. Modifica el código del down y el up para evitar esto en un programa9.c (2 puntos) 17. ¿Cómo se puede saber cuántos procesos están esperando a coger un semáforo? Escribe un programa10.c que demuestre tu respuesta. (3 puntos) 18. ¿Cómo se puede saber el valor de un semáforo? Escribe un programa11.c que demuestre tu respuesta. (3 puntos) Ejercicio final de la practica 2 Realizar dos programas que implementan un sistema de SMS trivial: 1. El primero se llama “envía” y toma dos parámetros: el destinatario (una cadena de caracteres) y el texto del mensaje (otra cadena de caracteres). Este programa se usa para enviar un mensaje a un destinatario 2. El segundo se llama “recibe” y toma un único parámetro que es el nombre del destinatario. Este programa lee todos los mensajes pendientes para el destinatario especificado y los muestra por pantalla. También los elimina para que no se puedan leer la siguiente vez que se invoque este programa. El funcionamiento de ambos será como sigue. Ambos programas: 1. Crearán una memoria compartida si esta no existe o se engancharán a ella si ya existiese. En cualquier caso mostrará un mensaje por pantalla indicando la creación o el enganche (1 punto). 2. Crearán los semáforos necesarios para controlar el acceso a dicha memoria indicando también si ya existían o no. (1 punto) 3. La memoria compartida se usará para guardar los mensajes que consistirán cada mensaje en dos Escuela Politécnica Superior Universidad Autónoma de Madrid 6 Sistemas Operativos I Práctica 2: Memoria Compartida y Semáforos campos de tipo cadena de caracteres (el destinatario y el mensaje). Es decir, si se teclea en una consola: > ./envía pepe “Esto es un ejemplo” guardará en la memoria compartida la estructura con tres campos: el destinatario: “pepe”, el mensaje: “Esto es un ejemplo”, y un entero que indica si el mensaje ha sido leído ya o no. Al insertarse como no se ha leído todavía valdrá “0”. Posteriormente cuando se lea habrá que cambiar ese valor a “1” Esta anotación se añadirá a lo que ya hubiera en dicha memoria. (1,5 puntos) 4. Del mismo modo el programa “recibe” sirve para buscar en la memoria compartida todos los mensajes asociados al destinatario que se pasa como parámetro, imprimirlos por pantalla y eliminarlos de la memoria compartida.. Así si se escribe en la consola lo siguiente: > ./recibe pepe Buscará en la memoria compartida todas las estructuras de mensaje que tengan como destinatario “pepe”, y para cada una de ellas, imprimirá el texto del mensaje en pantalla y marcará el mensaje como leído, para eso cambiará el tercer campo del mensaje a “1”. Es decir por pantalla se vería: > Mensaje 1 (no leído): “Esto es un ejemplo” Si ahora volvemos a ejecutar > ./recibe pepe El resultado ahora será (ya que el mensaje se ha leído anteriormente): > Mensaje 1 (leído anteriormente): “Esto es un ejemplo” (2,5 puntos) 5. Si se invoca el programa “envía” con un signo – se imprimirá por pantalla una estadística indicando cuantos mensajes han sido enviados, cuantos han sido leídos y cuantos no. De seguido se liberarán todos los recursos esperando previamente a que nadie esté usando dicha memoria. Es decir, si se teclea en una consola: > ./envia – eliminará la memoria compartida, los semáforos y cualquier otro recurso que use el programa. (2 puntos, uno de la espera y otro de la eliminación) 6. La estructuración de la memoria compartida es importante para hacer un uso eficiente de la misma, por lo que se deberán definir estructuras de tamaño fijo que permitan almacenar en la memoria compartida una lista de mensajes donde cada mensaje es una estructura de tres campos (de tamaño fijo): el destinatario, el texto del mensaje y el entero que indica si está leído o no. (2 puntos). 7. Puede darse el caso de que varias instancias del programa estén funcionando a la vez por lo que el acceso a la memoria compartida debe ser protegido mediante los semáforos por algún algoritmo a elección del alumno que debe justificar adecuadamente su decisión. (2 puntos, uno de las funciones up y down y otro por el uso correcto de ellas) Escuela Politécnica Superior Universidad Autónoma de Madrid 7 Sistemas Operativos I Práctica 2: Memoria Compartida y Semáforos 8. Deben controlarse correctamente todos los errores durante el manejo de las llamadas al sistema (creación de memoria, semáforos, etc.). (2 puntos) 9. Todos los errores propios de la aplicación, como por ejemplo intentar anotar algo cuando no hay espacio, deben ser correctamente manejados e informados al usuario. (2 puntos) 10. El código debe ser claro (desde ­1 hasta 1 punto dependiendo de la claridad). 11. El código debe estar estructurado adecuadamente (desde ­2 hasta 2 puntos dependiendo de la calidad). 12. Las funciones bloqueantes deben estar preparadas para el caso de que reciban una señal. (1 punto) 13. Cualquier mejora que se proponga y justifique adecuadamente (hasta 5 puntos). Algunas aclaraciones sobre el ejercicio final: ­ El programa debe ser entregado con un makefile e instrucciones de compilación. Si no fuese así o no compilase la nota en esta parte sería un cero. ­ El orden de creación de memoria y semáforos lo decide el alumno. ­ La organización interna de la memoria compartida y su tamaño lo decide el alumno pero debe ser suficiente como para guardar 1000 caracteres de anotaciones. ­ El programa se ejecuta y tras realizar su tarea se sale. ­ Cualquier cambio sobre estas especificaciones debe ser consultado antes con el profesor. En caso contrario se considerará un fallo. ­ Cualquier mejora que decidan realizar debe ser comentada en la memoria y será tenida en cuenta. Escuela Politécnica Superior Universidad Autónoma de Madrid 8 Sistemas Operativos I Práctica 2: Memoria Compartida y Semáforos Criterios de evaluación de la práctica 1. Todos los ejercicios son obligatorios. La puntuación se obtiene aplicando una regla de tres directa, en la que la suma del total de puntos de todos los ejercicios de la práctica equivale a obtener un 10. 2. Aquellos ejercicios que no contienen una respuesta con código, se evaluarán atendiendo a la explicación realizada por el alumno/a. Para que el razonamiento sea considerado válido debe ser suficientemente detallado y se puede apoyar en documentación externa. En ningún caso, se considerará una respuesta válida una captura de pantalla sin explicación, o un código sin explicación. 3. Los ejercicios que solicitan la realización de un código, deben ser también explicados, tanto a nivel de comentarios en el propio código (que se debe entregar junto con la memoria, inserto en los ejercicios o en apéndices), como en texto como respuesta al ejercicio. Esto es, especialmente relevante en el caso del ejercicio final que debe ir acompañado de una descripción textual del diseño realizado y la solución propuesta en el código. 4. No se tendrá en cuenta código que no compila. Recibiendo una fuerte penalización programas que no realizan una correcta liberación de los recursos solicitados al Sistema Operativo (por ej. dejar procesos zombie), o que no tienen un correcto control de errores implementado (con especial interés en el control de errores de las llamadas del sistema operativo tratadas en esta práctica). 5. Por último, se valorará la correcta aplicación de los principios de la programación estructurada (esto es, estructurar el código en funciones en la medida de lo posible, separar las definiciones y estructuras en un archivo .h, no usar “números mágicos”, etc.) Escuela Politécnica Superior Universidad Autónoma de Madrid 9