Examen final de Ampliación de Sistemas Operativos Solución 5 de Febrero de 2003. Tercer curso de Ingenierías Técnicas de Informática de Sistemas y de Gestión, URJC. Problema 1 (3 puntos) Tenemos una aplicación que tiene un cliente con una estructua de datos. El cliente se limita a utilizar un algoritmo como el que sigue cada vez que modifica dicha estruc­ tura: modificar_adt( val : Adt ) { backup_adt(adt); adt = val; } La idea es que backup_adt realice un backup del valor del adt en un servidor que se encuentra en otra máquina. Se pide responder razonadamente a las siguientes cuestiones: a) ¿Usarías RPCs o simplemente paso de mensajes para implementar backup_adt ? ¿Qué usarías si quieres que modificar_adt no tenga que esperar a que se complete el backup para retornar? ¿Te supone esto algún problema? b) Escribe en pseudo-código todo el código involucrado desde que el cliente llama a backup_adt hasta que dicho procedimiento retorna en el caso en que se utilicen RPCs para dicha operación. c) ¿Qué semántica te parece más correcta para esta aplicación, at-most-once o atleast once? ¿Cómo implementas dicha semántica? d) Quieres poder arrancar el servidor en la máquina que desees sin necesidad de rear­ rancar el cliente. ¿Qué supone esto para tu mecanismo de RPCs? ¿Cómo lo haces? e) ¿Podrías implementar un servidor sin estado empleando RMI? Solución: a) Daría igual en realidad utilizar RPCs o paso de mensajes. Tanto en un caso como en otro el cliente tendría que enviar una petición al servidor y el servidor enviar una respuesta (posiblemente vacía) al cliente para notificar que se hizo el backup. Dado que el enunciado plantea backup_adt como un procedimiento podríamos optar por RPCs (lo que tiene la ventaja de que si utilizamos un compilador de IDLs obtenemos los stubs automáticamente y nos podemos olvidar del aplanado y desa­ planado de adt). Si no queremos esperar a que se complete el backup podríamos eliminar el receive en el lado del cliente (por ej. editando los stubs) y continuar con la ejecución una vez solicitado el backup. Dada la simplicidad de backup_adt no tiene sentido utilizar ARPC (RPC asíncrona), tardaríamos prácticamente lo mismo que con una síncrona. ­2­ El problema es que eliminar el receive nos dejaría en la ignorancia respecto a si el servidor realmente ha realizado el backup o no. b) En el lado del cliente tendríamos: # Código del cliente modificar_adt(val: Adt){ backup_adt(val); adt = val; } # Stub del cliente. 1 parametro de entrada backup_adt(val: Adt){ # el mensaje y la respuesta. msg, repl : array of byte # resolvemos el servidor si no lo hicimos antes if ( serveraddr == nil) resolve(serveraddr, "backups"); # el msg tiene el id del proc y # el adt aplanado (en formato de red). msg = append(aplanar_id("backup_adt"), aplanar_adt(adt)); send(serveraddr, msg); recv(nil, repl); # no hay parametros de salida. Nada que desaplanar. return; } En el lado del servidor tendríamos un proceso atendiendo las peticiones: main() { # notificamos al binder quienes somos y escuchamos # peticiones bind("backups", NetAddr); listen(NetAddr); for(;;){ # recibimos un mensaje y lo servimos recv(cliaddr, msg); spawn service(cliaddr, msg); } } Para cada petición, se demultiplexa según el identificador de procedimiento y se llama al stub del servidor: ­3­ service(cliaddr: string; msg: array of byte){ op := desaplanar_id(msg); switch(op){ case "backup_adt" => msg = backup_adt_stub(msg); case * => msg = aplanar_id("error"); } send(cliaddr, msg); } backup_adt_stub(msg: array of byte) : array of byte { # hacemos el unmarshalling de los parametros adt := desaplanar_adt(msg); # llamamos al procedimiento backup_adt(adt); # no hay resultados, nada de lo que hacer # marshaling. Usamos un mensaje vacio. return aplanar_id(""); } Nos hemos inventado el interfaz de las operaciones para enviar, recibir y escuchar peticiones en la red. Sólo decir que recv devuelve tanto la dirección del remitente del mensaje como el mensaje que recibimos. Suponemos que listen informa al sistema de la dirección en que queremos recibir mensajes (dejando que el sistema escoja la dirección en otro caso). c) At least once parece apropiada, dado que la operación es idempotente y queremos que en caso de errores al menos se haga una vez. La forma de implementarla es retransmitir la petición hasta estar seguro de que el servidor la ha procesado. Por ejemplo, utilizando esto como parte del stub del cliente: backup_adt(val: Adt){ ... do { send(serveraddr, msg); recv(nil, repl); } while (geterror() == "timed out"); ... } d) Supone que habría que resolver la dirección del servidor cada vez que vamos a efectuar la RPC. Una sóla vez no basta, dado que la dirección antigua puede que no corresponda a la nueva dirección del servidor. Otra opción sería resolver de nuevo cuando se presentan errores y reintentar la operación. e) Dado que, en general, el objeto sujeto de la RMI posee estado, es imposible. Sólo podríamos si utilizamos los objetos como si fuesen interfaces a procedimientos y nos aseguramos de que ninguno mantenga estado. ­4­ Problema 2 (3 puntos) La realización de backups ha tenido tanto éxito que ahora disponemos de tres pro­ tocolos distintos para hacer backups. El servidor dispone de una única estructura de datos que es considerada el backup de la estructura del cliente. Naturalmente, se puede emplear cualquiera de los tres protocolos para terminar haciendo el backup sobre dicha estructura. Además, nuestro servidor desea permitir que un número indeterminado de clientes pueda realizar sus backups simultáneamente. Se pide responder razonadamente a las siguientes cuestiones: a) Escribe en pseudocódigo un servidor multithread que atienda dichos protocolos, prestando atención a la concurrencia en el servidor. b) ¿Tiene sentido limitar el número de threads que dicho servidor puede arrancar? c) ¿Tiene sentido utilizar procesos en lugar de threads en este servidor? Solución a) En realidad, el servidor mostrado en el problema anterior ya utiliza múltiples threads (uno para atender a cada llamada). Para resolver las condiciones de carrera que afectan a la actualización del backup, podríamos utilizar un semáforo para implementar una sección crítica con exclusión mutua. También habría que modi­ ficar la demultiplexación en el servidor para atender a los distintos protocolos. El código que cambia respecto al problema anterior quedaría como sigue: sem := Semaphore(1); service(cliaddr: string; msg: array of byte){ op := desaplanar_id(msg); down(sem); switch(op){ case "backup_adt1" => msg = backup_adt1_stub(msg); case "backup_adt2" => msg = backup_adt2_stub(msg); case "backup_adt3" => msg = backup_adt3_stub(msg); case * => msg = aplanar_id("error"); } up(sem); send(cliaddr, msg); } b) Una posible razón para hacerlo es evitar problemas de sobrecarga en el servidor. Dado que cada thread se va a pasar la mayor parte del tiempo en la región crítica, podríamos incluso limitarlo a uno y no usar multiples threads en absoluto. Pero la mejor opción es mantener los múltiples threads aunque sólo sea para evitar que conexiones lentas de determinados clientes afecten a otros clientes. c) No hay ninguna razón por la que fuese más conveniente el uso de procesos en lugar de threads. Por otro lado, a no ser que tengamos problemas serios de rendi­ miento tampoco hay ninguna razón que lo desaconseje. ­5­ Problema 3 (2 puntos) Sea un sistema de ficheros distribuidos con un sistema de nombrado único para todo el sistema. El tipo de nombrado que usa es de tipo UNIX, esto es, mediante una estructura de directorios permite nombrar a partir de un directorio raiz a cualquier directorio o fichero del sistema usando una lista de nombres de subdirectorios. Supóngase que el sistema permite enlaces físicos tipo UNIX, esto es, a partir de un nom­ bre que referencia a un identificador de fichero o de directorio, se pueden crear nuevos nombres que referencian a ese mismo identificador de fichero o directorio. Se pide responder razonadamente a las siguientes cuestiones: a) ¿Qué tipo de grafo forma este sistema de nombrado? b) ¿Se pueden utilizar contadores de referencia para eliminar ficheros o directorios sin nombre desde la raiz? c) ¿Se puede utilizar "marcado y barrido" ( mark and sweep) para eliminar ficheros o directorios sin nombre desde la raiz? d) ¿Se necesitaría alguna de las dos técnicas mencionadas en b) y c) para el caso en que en el sistema de nombrado sólo se permitiera enlazado simbólico tipo UNIX en vez del enlazado físico antes mencionado? Solución: a) Un grafo general del que ni siquiera podemos asegurar que sea un DAG. Por supuesto podría tener ciclos, debido a la presencia de enlaces duros. b) No podríamos utilizar contadores de referencia (RCs) dado que la presencia de cic­ los hace que algunos nodos tengan un número de referencias mayor que cero (no sean recolectados) a pesar de no ser alcanzables desde ningún nombre. Nos quedarían pues nodos sin recolectar que en realidad no son útiles. c) Este tipo de recolección de basura si podríamos emplearla dado que procesa ade­ cuadamente nodos inalcanzables tengan o no referencias. d) En ese caso el grafo es en realidad un árbol, por lo que no sería precisa ninguna de las dos técnicas anteriores. En ninguno de los casos hemos tenido en cuenta la presencia de fallos en los nodos involucrados. Problema 4 (2 puntos) El algoritmo de elección de coordinador del abusón (bully algorithm) funciona porque el sistema de comunicación es síncrono, esto es, el tiempo de entrega de los mensajes está acotado y es conocido. Se pide responder razonadamente y con un ejem­ plo de ejecución por qué podría no funcionar en un sistema asíncrono, esto es, el tiempo de entrega de los mensajes no está acotado. Solución: La asincronía hace imposible saber si un nodo que ha de responder a un mensaje está muerto o tan sólo tiene una pésima conectividad. No podríamos emplear un time­ out para decidir si un nodo esta muerto o no. No tiene sentido esperar indefinidamente, puesto que la muerte del coordinador actual bloquearía todo el sistema (todos estarían esperando). Podríamos entonces elegir un plazo de tiempo arbitrariamente largo para decidir que no nos han contestado a una petición. Incluso en tal caso, supongamos que el nodo 1 inicia el algoritmo enviando mensajes a los nodos 2 y 3. El 1 puede recibir una respuesta del 2, quien a su vez ejecuta del nuevo el algoritmo y no recibe respuesta alguna. En un sistema síncrono el 2 sería el nuevo coordinador, en uno asíncrono puede ­6­ que además el nodo 3 considere que él es el coordinador (basta con que sus respuestas se retrasen lo suficiente como para que los otros nodos lo consideren muerto).