530 likes | 876 Views
Sistemas Distribuidos. El Modelo Cliente-Servidor. Armando Arce O. arce@ic-itcr.ac.cr Febrero 2000. Desventajas del modelo OSI. La existencia de todos los protocolos OSI genera un costo excesivo Cada vez que se envía un mensaje se deben procesar 7 capas de OSI
E N D
Sistemas Distribuidos El Modelo Cliente-Servidor Armando Arce O. arce@ic-itcr.ac.cr Febrero 2000
Desventajas del modelo OSI • La existencia de todos los protocolos OSI genera un costo excesivo • Cada vez que se envía un mensaje se deben procesar 7 capas de OSI • Cada capa genera y añade su propio encabezado • Todo el trabajo requiere tiempo
El modelo OSI en redes LAN • En una WAN se pueden usar todos los protocolos OSI, porque ya de por sí la red es lenta • Pero en una LAN no se puede hacer esto, pues requiere de mucho tiempo de procesamiento. • Además el modelo OSI no dice nada acerca de la forma de estructurar el sistema distribuido.
El modelo cliente-servidor • Se trata de estructurar el sistema distribuido como un grupo de procesos en cooperación, llamados servidores, que ofrezcan servicios a usuarios, llamados clientes. • Los clientes y servidores se ejecutan como procesos del usuario. • Una máquina puede ejecutar un único proceso a varios clientes, varios servidores o combinaciones de ambos.
Protocolo solicitud-respuesta • Para evitar un gasto excesivo el modelo cliente-servidor se basa en un protocolo solicitud/respuesta sencillo y sin conexión: • El cliente envía un mensaje de solicitud al servidor para pedir cierto servicio. • El servidor hace el trabajo y regresa los datos o un código de error.
Protocolo solicitud-resp. (cont.) • Este protocolo es sencillo y eficiente: • No se tiene que establecer conexión • La pila de protocolo es mas corta y por tanto más eficiente. • Sólo se necesitan tres niveles de protocolos. • Las capas física y de enlace se encargan de llevar el mensaje (hardware) • La capa 5 es el protocolo solicitud/respuesta
Primitivas de comunicación • Este enfoque reduce en mucho los servicios de comunicación. • Basta con dos primitivas de comunicación: • send(dest, &mptr): envía y bloquea el proceso • receive(addr, &mptr): recibe el mensaje
Direccionamiento • Antes de enviar un mensaje al servidor hay que conocer su dirección. • La dirección podría ser el # de máquina en una red (dirección física) • Problema: • Utilizando este esquema, solo se puede ejecutar un servicio en cada máquina
Esquema máquina-proceso • Otro esquema utiliza nombres con dos partes, el identificador de máquina junto con el identificador de proceso. • Cada máquina puede numerar sus procesos • Problema: • Este esquema no es transparente, puesto que el usuario debe conocer al identificador del proceso.
Direccionamiento por transmisión • Otro enfoque consiste en dejar que cada proceso elija un propio identificador de un espacio de direcciones grande y disperso: • La probabilidad de utilizar el mismo # es pequeña • Problema: • El cliente no sabe el # de servidor • El cliente puede enviar un mensaje de localización a todas las máquinas en la red; luego el servidor responde con su identificador.
Servidor de nombres • Otro esquema utiliza una máquina adicional para la asociación de alto nivel de los nombres de los servicios con las direcciones de las máquinas • Cada vez que se ejecuta un cliente, se envía una solicitud al servidor de nombres, para pedirle el # de máquina en donde se localiza el servidor
Servidor de nombres (cont.) • Problema: • Es un componente centralizado, por lo que puede causar cuellos de botella. • Una solución sería duplicarlo, sin embargo, puede haber problemas de consistencia entre las diferentes copias.
Primitivas de comunicación • Las dos primitivas de transferencia (comunicación) de mensajes utilizadas en el modelo cliente-servidor son: • send: enviar un mensaje • receive: recibir un mensaje • Estas primitivas pueden ser: • sincrónicas: con bloqueo • asíncronas: sin bloqueo
Send con bloqueo • La instrucción que sigue a la llamada SEND no se ejecuta sino hasta que el mensaje se envía en su totalidad • Una llamada a receive no regresa el control sino hasta que realmente se reciba un mensaje. • El proceso se suspende hasta que llegue un mensaje
Send con bloqueo (cont.) • Si send llama a un bloqueo y no existe respuesta el emisor se bloqueará para siempre • Solución: • Quien hace la llamada puede especificar un intervalo de tiempo para esperar una respuesta. • Si nada llega en ese intervalo, send termina con error
Send sin bloqueo • Una alternativa a las primitivas con bloqueo son las primitivas sin bloqueo • SEND regresa de inmediato el control a quién hizo la llamada, antes de enviar el mensaje • Ventaja: El proceso emisor puede continuar su cómputo en forma paralela con la transmisión del mensaje • Desventaja: El emisor no puede modificar el buffer de mensajes sino hasta que se envíe el mensaje
Send sin bloqueo (cont.) • El proceso emisor no tiene idea de cuando termina la transmisión, y no sabe cuando es seguro reutilizar el buffer. • Varias soluciones: • Send sin bloqueo, con copia • Send sin bloqueo, con interrupción • Disfrazar la interrupción
Send sin bloqueo, con copia • El núcleo copia el mensaje de salida a un buffer interno del núcleo, y permite que el proceso continúe • Problema: • Desperdicia CPU para la copia adicional del espacio del usuario al núcleo
Send sin bloqueo, con interrupción • Interrumpir al emisor cuando se envíe el mensaje, para informarle que el buffer está ya disponible • Problema: • Las interrupciones a nivel de usuario dificultan la programación
Disfrazar la interrupción • La interrupción puede ser disfrazada mediante un nuevo hilo de control. • Problema: • Se requieren múltiples hilos de control
La primitiva receive • Así como send puede tener o no bloqueo, también receive. • Un receive sin bloqueo solo le indica al núcleo la localización del buffer y regresa el control de forma inmediata.
La primitiva receive (cont.) • Sin embargo, de nuevo cómo sabe que la operación ya terminó. • Existen varias soluciones: • Primitiva wait • Primitiva test • Primitiva receive condicional • Interrupción • Múltiples hilos
Las primitivas wait, test y receive condicional • Primitiva wait • Se proporciona una primitiva wait, para que el receptor se bloquee • Primitiva test • Se proporciona una primitiva test que permita hacer un muestreo del núcleo para verificar su estado • Primitiva receive condicional • La primitiva receive, obtiene el mensaje y señale un error, pero regresa el control de forma inmediata.
Interrupciones e hilos de control • Manejo de interrupciones • Se utilizan interrupciones para señalar el fin del proceso. • Múltiples hilos de control • La llegada de un mensaje puede provocar la creación espontánea de un nuevo hilo de control.
Primitivas con almacenamiento • Hasta ahora se han visto únicamente primitivas no almacenadas. • Esto se refiere a que una dirección se refiere a un proceso específico. • En estos casos se dispone de un único buffer de mensajes, al que apunta m, cuando el mensaje llega, el núcleo receptor lo copia al buffer y elimina el bloqueo del proceso receptor.
Primitivas con almacen. (cont.) • Este mecanismo sirve solo si el servidor llama a receive antes que el cliente llame a send. • Problema: • ¿ Cómo sabe el núcleo cúal de sus procesos utiliza la dirección en el mensaje recién llegado. • ¿ Cómo sabe donde copiar el mensaje
Primitivas con almacen. (cont.) • Existen varias soluciones: • Descartar los mensajes • Mensajes perdidos • Buzones • No envío del mensaje
Descartar los mensajes • Consiste en descartar los mensajes del cliente y esperar que el servidor llame a receive antes que el cliente vuelva a intentar • Este se puede implantar con facilidad • Problema • El cliente se puede cansar de hacer intentos y darse por vencido
Mensajes pendientes • El núcleo receptor mantiene pendientes los mensajes por un tiempo. • Cuando llega un mensaje se inicializa un cronómetro, si no se utiliza se descarta • Esto reduce la probabilidad de que un mensaje se pierda • Problema • Hay que almacenar los mensaje que llegan en forma prematura (requiere espacio)
Buzones • Un proceso le indica al núcleo que cree un “buzón” y especifica una dirección. • En cada receive se elimina un mensaje del buzón, o se bloquea si no hay. • Los mensajes se guardan en buzones particulares • Problema • Los buzones son finitos y cuando se llenan el núcleo decidir que hacer con los mensajes
No envío del mensaje • Este consiste en no dejar que un proceso envíe un mensaje si no existe espacio para su almacenamiento en el destino. • El emisor guarda el mensaje anterior y se suspende. • Cuando hay espacio disponible en el buzón, se hace que el emisor intente de nuevo.
Primitivas confiables • Los mensajes se pueden perder, lo que afecta el modelo de transferencia de mensajes. • No existe seguridad que se haya entregado un mensaje. • Existen varias soluciones: • Send no confiable • Utilizar reconocimiento • Respuesta como reconocimiento
Send no confiable • El sistema no da garantía alguna acerca de la entrega de mensajes. • La comunicación confiable la implanta el usuario
Utilizar reconocimiento • El núcleo receptor envía un reconocimiento al núcleo emisor • Cuando se recibe el reconocimiento el núcleo libera el proceso • Cada solicitud con respuesta consta de cuatro mensajes
Respuesta como reconocimiento • La respuesta funciona como reconocimiento • Si la respuesta tarda demasiado el núcleo puede volver a enviar la solicitud. • El último reconocimiento a veces se envía y aveces no
Referencias • Andrew S. Tanenbaum. Sistemas Operativos Distribuidos, Primera Edición, Prentice Hall, 1996
Ejemplo de cliente-servidor • Un ejemplo útil es un servicio de copiado de archivos. • El cliente y el servidor deben compartir definiciones comunes: • Tamaños de buffer, direcciones en la red • Números de las operaciones (que ejecuta el servidor) • La definición del mensaje mismo
Ejemplo: Códigos de resultado • Cada respuesta contiene el código de resultado: • Si la operación tiene éxito, el código contiene información útil (# bytes transferidos) • Si no existe valor para regresar, se utiliza el valor OK. • Si la operación fracasa, el código resultante indica porqué, mediante un código.
Ejemplo: Definiciones generales /* Definiciones necesarias para los clientes y servidores. */ #define MAX_PATH 255 /* tamaño del nombre del archivo */ #define BUF_SIZE 1024 /* tamaño del buffer a transferir */ #define FILE_SERVER 243 /* dirección en la red del servidor */ /* Definiciones de las operaciones permitidas */ #define CREATE 1 /* Crea un nuevo archivo */ #define READ 2 /* retorna una parte de un archivo */ #define WRITE 3 /* escribe una parte de un archivo */ #define DELETE 4 /* elimina un archivo existente */ /* Códigos de error */ #define OK 0 /* operación correcta */ #define E_BAD_OPCODE -1 /* operación desconocida */ #define E_BAD_PARAM -2 /* error en un parámetro */ #define E_IO -3 /* error de E/S */
Ejemplo: Formato del mensaje /* Definición del formato del mensaje */ struct message { long source; /* identidad del emisor */ long dest; /* identidad del receptor */ long opcode; /* operación: CREATE, READ, etc. */ long count; /* número de bytes por transferir */ long offset; /* Inicio de lectura o la escritura */ long extra1; /* campo adicional */ long extra2; /* campo adicional */ long result; /* resultado de la operación */ char name[MAX_PATH]; /* nombre del archivo */ char data[BUF_SIZE]; /* datos por leer o escribir */ }
Ejemplo: Código del servidor # include <header.h> void main(void) { struct message m1, m2; /* mensajes de entrada y salida */ int r; while (1) { /* ejecuta un ciclo infinito */ receive(FILE_SERVER, &m1) /*espera de un mensaje*/ switch (m1.opcode) { /* tipo de solicitud */ case CREATE; r=do_crear(&m1,&m2); break; case READ; r=do_leer(&m1, &m2); break; case WRITE; r=do_escribe(&m1,&m2); break; case DELETE; r=do_borrar(&m1,&m2); break; default: r=E_BAD_OPCODE; } m2.result = r; /* regresa el resultado */ send(m1.source, &m2); /* envia la respuesta */ } }
Ejemplo: Código del cliente int copy(char scr, char dst) { /* rutina de copia de archivo */ struct message m1; /* buffer del mensaje */ long position; /* posición del archivo */ long client = 110; /* dirección del cliente */ initialize(); /* se prepara para la ejecución */ position = 0; do { m1.opcode = READ; /* operación de lectura */ m1.offset = position; /* posición actual */ m1.count = BUF_SIZE; /* bytes a leer */ strcopy(&m1.name, src); /* copia del mensaje */ send(FILE_SERVER, &m1); /* envia el mensaje al servidor */ receive(client, &m1); /* espera la respuesta */ m1.opcode = WRITE; /* operación de escritura */ m1.offset = position; /* posición actual */ m1.count = m1.result; /* bytes por escribir */ strcp(&m1.name, dst); /* envia el mensaje al servidor */ receive(client,&m1); /* espera la respuesta */ position += m1.result; /* obtiene bytes escritos */ } while (m1.result > 0); /* continua el ciclo */ return(m1.result >= 0 ¿ OK: m1.result); } /* regresa OK o error */