Cuando se construyeron las primeras redes de computadores se vio la potencia que tenían estos sistemas y se desarrolló el paradigma cliente/servidor. Se crearon los sistemas operativos de red con servicios como NFS o FTP para acceder a sistemas de ficheros de otros ordenadores. Xwindow para entorno gráfico, lpd para poder imprimir remotamente además de un conjunto de herramientas que permitían el acceso a recursos compartidos.
En los aurales de la informática el sistema operativo ni siquiera existía: se programaba directamente el hardware. Se vio que era muy pesado y se programó una capa de abstracción que evitaba al programa tener que tener un conocimiento total del hardware: el sistema operativo. Se descubrió también que con un solo proceso se desperdiciaba tiempo de procesador y como tales dispositivos eran carísimos se empezaron a inventar mecanismos que evitasen que el programa esperase bloqueado por ejemplo las operaciones de entrada/salida. Se llegó a la multiprogramación en el ambicioso MULTICS y el posterior UNIX. Estos dos sistemas se desarrollaron a finales de los años 60 y se basaban en el MIT Compatible Timesharing System realizado en 1958.
En los años 70 se añadieron las primeras capacidades de interconexión:
Un sistema operativo distribuido puede acceder a cualquier recurso
transparentemente y tiene grandes ventajas sobre un sistema operativo
de red. Pero es muy difícil de implementar y algunos de sus
aspectos necesitan que se modifique seriamente el núcleo del
sistema operativo, por ejemplo para conseguir memoria distribuida
transparente a los procesos. También se tiene que tener en cuenta
que ahora la máquina sobre la que corre todo el sistema es el
sistema distribuido (un conjunto de nodos) por lo que tareas como
planificación de procesos (scheduling) o los hilos ( threads) y señales entre procesos
toman una nueva dimensión de complejidad. Por todo esto aún no
existe ningún sistema distribuido con todas las características de transparencia necesarias para considerarlo eficiente.
A partir de ahora no hablaremos de computadores, ordenadores ni PC. Cualquier dispositivo que se pueda conectar a nuestro sistema y donde se pueda desarrollar un trabajo útil para el sistema se referenciará como nodo. El sistema operativo tiene que controlar todos estos nodos y permitir que se comuniquen eficientemente.
Aunque tras leer este capítulo el lector podrá intuir esta idea, queremos hacer notar que un sistema distribuido se puede ver como el siguiente nivel de abstracción sobre los sistemas operativos actuales. Como hemos visto la tendencia es a dar más servicios de red y más concurrencia. Si pusiéramos otra capa por encima que aunara ambos, esta capa sería el sistema operativo distribuido. Esta capa debe cumplir los criterios de transparencia que veremos en el próximo apartado.
Podemos comprender esto mejor haciendo una analogía con el SMP: cuando sólo existe un procesador todos los procesos se ejecutan en él, tienen acceso a los recursos para los que tengan permisos, se usa la memoria a la que puede acceder el procesador, etc. Cuando hay varios procesadores todos ellos están corriendo procesos (si hay suficientes para todos) y los procesos migran entre procesadores.
En un sistema multiprocesador al igual que un sistema multicomputador hay una penalización si el proceso tiene que correr en un elemento de proceso (procesador o nodo) distinto al anterior: en multiprocesador por la contaminación de cache y TLB; en multicomputador por la latencia de la migración y pérdidas de cache. En un sistema multicomputador se persigue la misma transparencia que un sistema operativo con soporte multiprocesador, esto es, que no se deje ver a las aplicaciones que realmente están funcionando en varios procesadores.
En este capítulo además se tratarán las zonas de un sistema operativo que se ven más afectadas para conseguirlo. Éstas son:
El scheduling de los procesos ahora puede ser realizado por cada nodo de manera individual, o mediante algún mecanismo que implemente el cluster de manera conjunta entre todos los nodos.
Las técnicas usadas hasta ahora en un nodo local no se pueden generalizar de una forma eficiente en el sistema distribuido.
Hay que disponer de memoria distribuida: poder alojar y desalojar memoria de otros nodos de una forma transparente a las aplicaciones.
Los procesos podrían estar ubicados en cualquier nodo del sistema. Se deben proveer de los mecanismos necesarios para permitir esta intercomunicación de tipo señales, memoria distribuida por el cluster u otros.
Pretenden que todo el sistema distribuido tenga la misma raíz para que no sea necesario explicitar en qué nodo se encuentra la información.
Tendría que ser posible acceder a todos los recursos de entrada/salida globalmente, sin tener que indicar explícitamente a que nodo están estos recursos conectados.
En un sistema de tipo cluster lo que se pretende, como se verá en el capítulo Clusters, es compartir aquellos dispositivos (a partir de ahora serán los recursos) conectados a cualquiera de los nodos. Uno de los recursos que más se desearía compartir, aparte del almacenamiento de datos, es el procesador de cada nodo. Para compartir el procesador entre varios nodos lo más lógico es permitir que las unidades atómicas de ejecución del sistema operativo (procesos, con PID propio) sean capaces de ocupar en cualquier momento cualesquiera de los nodos que conforman el cluster.
En un sistema multiusuario de tiempo compartido la elección de qué proceso se ejecuta en un intervalo de tiempo determinado la hace un segmento de código que recibe el nombre de scheduler, el planificador de procesos. Una vez que el scheduler se encarga de localizar un proceso con las características adecuadas para comenzar su ejecución, es otra sección de código llamada dispatcher la que se encarga de substituir el contexto de ejecución en el que se encuentre el procesador, por el contexto del proceso que queremos correr. Las cosas se pueden complicar cuando tratamos de ver este esquema en un multicomputador. En un cluster, se pueden tener varios esquemas de actuación.
Por supuesto este este es el caso ideal y no se puede dar en la realidad pues siempre se necesita potencia de cálculo para enviar y recibir los procesos así como para tomar las decisiones (overhead).
Este tipo de planteamiento sólo se efectúa en los sistemas SSI de los que se hablará más tarde, ya que necesita un acoplamiento muy fuerte entre los nodos del cluster. Un ejemplo puede ser el requerimiento de un cluster que dependiendo del usuario o grupo de usuario elija un nodo preferente donde ejecutarse por defecto. En openMosix no se hace uso de esta política, ya que el sistema no está tan acoplado como para hacer que otro nodo arranque un programa.
Las causas que hacen que se quiera realizar la migración van a depender del objetivo del servicio de la migración. Así si el objetivo es maximizar el tiempo usado de procesador, lo que hará que un proceso migre es el requerimiento de procesador en su nodo local, así gracias a la información que ha recogido de los demás nodos decidirá si los merece la pena migrar o en cambio los demás nodos están sobrecargados también.
La migración podría estar controlada por un organismo central que tuviera toda la información de todos los nodos actualizada y se dedicase a decidir como colocar los procesos de los distintos nodos para mejorar el rendimiento. Esta solución aparte de ser poco escalable, pues se sobrecargan mucho la red de las comunicaciones y uno de los equipos, es demasiado centralizada ya que si este sistema falla se dejarán de migrar los procesos.
El otro mecanismo es una toma de decisiones distribuida: cada nodo tomará sus propias decisiones usando su política de migración. Dentro de esta aproximación hay dos entidades que pueden decidir cuando migrar un proceso: el propio proceso o el kernel del sistema operativo.
Este problema penaliza a ciertos algoritmos de optimización de nueva generación como las redes neuronales, o los algoritmos genéticos y beneficia a estimaciones estadísticas.
Implica destruirlo en el sistema de origen y crearlo en el sistema de destino. Se debe mover la imagen del proceso que consta de, por lo menos, el bloque de control del proceso (a nivel del kernel del sistema operativo). Además, debe actualizarse cualquier enlace entre éste y otros procesos, como los de paso de mensajes y señales (responsabilidad del sistema operativo). La transferencia del proceso de una máquina a otra es invisible al proceso que emigra y a sus asociados en la comunicación.
El movimiento del bloque de control del proceso es sencillo. Desde el punto de vista del rendimiento, la dificultad estriba en el espacio de direcciones del proceso y en los recursos que tenga asignados. Considérese primero el espacio de direcciones y supóngase que se está utilizando un esquema de memoria virtual (segmentación y/o paginación). Pueden sugerirse dos estrategias:
También es una estrategia obligada cuando lo que se migran son hilos en vez de procesos y no migramos todos los hilos de una vez. Esto está implementado en el sistema operativo Emerald3.2 y otras generalizaciones de este a nivel de usuario.
En este sistema:
Mantiene la parte del contexto del sistema que depende del lugar por lo que se mantiene en el nodo donde se generó el proceso.
Como en Linux la interfície entre el contexto del usuario y el contexto del kernel está bien definida, es posible interceptar cada interacción entre estos contextos y enviar esta interacción a través de la red. Esto está implementado en el nivel de enlace con un canal especial de comunicación para la interacción.
El tiempo de migración tiene una componente fija que es crear el nuevo proceso en el nodo al que se haya decidido migrar; y una componente lineal proporcional al número de páginas de memoria que se vayan a transferir. Para minimizar la sobrecarga de esta migración, de todas las páginas que tiene mapeadas el proceso sólo se van a enviar las tablas de páginas y las páginas en las que se haya escrito.
OpenMosix consigue transparencia de localización gracias a que las llamadas dependientes al nodo nativo que realiza el proceso que ha migrado se envían a través de la red al deputy. Así openMosix intercepta todas las llamadas al sistema, comprueba si son independientes o no, si lo son las ejecuta de forma local (en el nodo remoto) sino la llamada se emitirá al nodo de origen y la ejecutará el deputy. Éste devolverá el resultado de vuelta al lugar remoto donde se continua ejecutando el código de usuario.
También puede ocurrir que se necesite un consenso de un proceso en la máquina origen y otro proceso en la máquina destino, este enfoque tiene la ventaja de que realmente se asegura que la máquina destino va a tener recursos suficientes, esto se consigue así:
|
Sin usar los mecanismos necesarios que controlen la ejecución, ésta es una de las trazas de lo que podría ocurrir:
|
Esto es lo que se llama una condición de carrera y desde que GNU/Linux funciona en máquinas SMP (a partir de la versión 2.0 ) se ha convertido en un tema principal en su implementación. Por supuesto no es un problema único de este sistema operativo sino que atañe a cualquiera con soporte multitarea. La solución pasa por encontrar estos puntos y protegerlos por ejemplo con semáforos, para que sólo uno de los procesos pueda entrar a la vez a la región crítica.
Esta es seguramente la situación más sencilla: compartición de una variable en memoria. Para aprender como solucionar éstas y otras situaciones de conflicto se recomienda al lector consultar los autores de la bibliografía.
En los clusters donde los procesos tienen consciencia de si estan siendo ejecutados locales o remotos, cada nodo tiene las primitivas de comunicación necesarias para enviar toda la comunicación a través de la red. En clusters donde solo el kernel puede conocer este estado de los procesos, estas primitivas se hacen innecesarias pues la transparencia suple esta capa. Éste es el caso de openMosix.
Hay mecanismos de comunicación más problemáticos que otros. Las señales no lo son demasiado pues se pueden encapsular en un paquete qué se envíe a través de la red. Aquí se hace necesario que el sistema sepa en todo momento el nodo dónde está el proceso con el que quiere comunicar.
Otros mecanismos de comunicación entre procesos son más complejos de implementar. Por ejemplo la memoria compartida: se necesita tener memoria distribuida y poder/saber compartirla. Los sockets también son candidatos difíciles a migrar por la relación que tienen los servidores con el nodo.
El sistema de ficheros tradicional tiene como funciones la organización, almacenaje, recuperación, protección, nombrado y compartición de ficheros. Para conseguir nombrar los ficheros se usan los directorios, que no son más que un fichero de un tipo especial que provee una relación entre los nombre que ven los usuarios y un formato interno del sistema de ficheros.
Un sistema de ficheros distribuido es tan importante para un sistema distribuido como para uno tradicional: debe mantener las funciones del sistema de ficheros tradicional, por lo tanto los programas deben ser capaces de acceder a ficheros remotos sin copiarlos a sus discos duros locales. También proveer acceso a ficheros en los nodos que no tengan disco duro. Normalmente el sistema de ficheros distribuido es una de las primeras funcionalidades que se intentan implementar y es de las más utilizadas por lo que su buena funcionalidad y rendimiento son críticas. Se pueden diseñar los sistemas de ficheros para que cumplan los criterios de transparencia, esto es:
Es uno de los primeros sistemas de archivos de red, fue creado por Sun basado en otra obra de la misma casa, RPC y parte en XDR para que pudiese implementarse en sistemas hetereogéneos. El modelo NFS cumple varias de las transparencias mencionadas anteriormente de manera parcial. A este modelo se le han puesto muchas pegas desde su creación, tanto a su eficiencia como a su protocolo, como a la seguridad de las máquinas servidoras de NFS.
El modelo de NFS es simple: máquinas servidoras que exportan directorios y máquinas clientes3.3 que montan este directorio dentro de su árbol de directorio y al que se accederá de manera remota.
De esta manera el cliente hace una llamada a mount especificando el servidor de NFS al que quiere acceder, el directorio a importar del servidor y el punto de anclaje dentro de su árbol de directorios donde desea importar el directorio remoto. Mediante el protocolo utilizado por NFS3.4 el cliente solicita al servidor el directorio exportable (el servidor en ningún momento sabe nada acerca de dónde esta montado el sistema en el cliente), el servidor en caso de que sea posible la operación, concede a el cliente un handler o manejador del archivo, dicho manejador contiene campos que identifican a este directorio exportado de forma única por un i-node, de manera que las llamadas a lectura o escritura utilizan esta información para localizar el archivo.
Una vez montado el directorio remoto, se accede a él como si se tratase del sistema de archivos propio, es por esto que en muchos casos los clientes montan directorios NFS en el momento de arranque ya sea mediante scripts rc o mediante opciones en el fichero /etc/fstab. De hecho existen opciones de automount para montar el directorio cuando se tenga acceso al servidor y no antes, para evitar errores o tiempos de espera innecesarios en la secuencia de arranque.
NFS soporta la mayoría de las llamadas a sistema habituales sobre los sistemas de ficheros locales así como el sistema de permisos habituales en los sistemas UNIX. Las llamadas open y close no están implementadas debido al diseño de NFS. El servidor NFS no guarda en ningún momento el estado de las conexiones establecidas a sus archivos 3.5, en lugar de la función open o close tiene una función lookup que no copia información en partes internas del sistema. Esto supone la desventaja de no tener un sistema de bloqueo en el propio NFS, aunque esto se ha subsanado con el demonio rpc.lockd lo que implicaba que podían darse situaciones en las cuales varios clientes estuviesen produciendo inconsistencias en los archivos remotos. Por otro lado, el no tener el estado de las conexiones, implica que si el cliente cae, no se produce ninguna alteración en los servidores.
De esta manera para el caso de NFS, VFS guarda en el v-node un apuntador al nodo remoto en el sistema servidor,que define a la ruta del directorio compartido y a partir de este todos son marcados como v-nodes que apuntan a r-nodes o nodos remotos.
En el caso de una llamada open() a un archivo que se encuentra en parte de la jerarquía donde está un directorio importado de un servidor NFS, al hacer la llamada, en algún momento de la comprobación de la ruta en el VFS, se llegara al v-node del archivo y se verá que este corresponde a una conexión NFS, procediendo a la solicitud de dicho archivo mediante opciones de lectura, se ejecuta el código especificad o por VFS para las opciones de lectura de NFS.
En cuanto al manejo de las caches que se suelen implementar en los clientes depende de cada caso específico. Se suelen utilizar paquetes de 8KB en las transferencias de los archivos, de modo que para las operaciones de lectura y escritura se espera a que estos 8KB estén llenos antes de enviar nada, salvo en el caso de que se cierre el archivo. Cada implementación establece un mecanismo simple de timeouts para las caches de los clientes de modo que se evite un poco el trafico de la red.
La cache no es consistente, los ficheros pequeños se llaman en una cache distinta a esos 8KB y cuando un cliente accede a un fichero al que accedió poco tiempo atrás y aún se mantiene en esas caches, se accede a esas caches en vez de la versión del servidor. Esto hace que en el caso de ficheros que no hayan sido actualizados se gane el tiempo que se tarda en llegar hasta el servidor y se ahorre tráfico en la red. El problema es que produce inconsistencias en la memoria cache puesto que si otro ordenador modificó esos ficheros, nuestro ordenador no va a ver los datos modificados sino que leerá la copia local.
Esto puede crear muchos problemas y es uno de los puntos que más se le discute a NFS, este problema es el que ha llevado a desarrollar MFS pues se necesitaba un sistema que mantuviera consistencia de caches.
Este es el sistema de ficheros que se desarrolló para openMosix en espera de alguno mejor para poder hacer uso de una de sus técnicas de balanceo, DFSA. Este sistema funciona sobre los sistemas de ficheros locales y permite el acceso desde los demás nodos.
Cuando se instala, se dispone de un nuevo directorio que tendrá varios subdirectorios con los números de los nodos, en cada uno de esos subdirectorios se tiene todo sistema de ficheros del nodo en cuestión3.6. Esto hace que este sistema de ficheros sea genérico pues /usr/src/linux/ podría estar montado sobre cualquier sistema de ficheros; y escalable, pues cada nodo puede ser potencialmente servidor y cliente.
A diferencia de NFS provee una consistencia de cache entre procesos que están ejecutándose en nodos diferentes, esto se consigue manteniendo una sola cache en el servidor. Las caches de GNU/Linux de disco y directorio son sólo usadas en el servidor y no en los clientes. Así se mantiene simple y escalable a cualquier número de procesos.
El problema es una pérdida de rendimiento por la eliminación de las caches, sobre todo con tamaños de bloques pequeños. La interacción entre el cliente y el servidor suele ser a nivel de llamadas del sistema lo que suele ser bueno para la mayoría de las operaciones de entrada/salida complejas y grandes.
Este sistema cumple con los criterios de transparencia de acceso, no cumple
con los demás.
Tiene transparencia de acceso pues se puede acceder a estos ficheros con
las mismas operaciones que a los ficheros locales.
Los datos del cuadro 3.3 son del Postmark3.7 benchmark que simula grandes cargas al sistema de ficheros, sobre GNU/Linux
2.2.16 y dos PCs Pentium 550 MHz3.8:
|
Se basa en que las nuevas tecnologías de redes (como fibra óptica) permiten a muchas máquinas compartir los dispositivos de almacenamiento. Los sistemas de ficheros para acceder a los ficheros de estos dispositivos se llaman sistemas de ficheros de dispositivos compartidos. Contrastan con los sistemas de ficheros distribuidos tradicionales donde el servidor controla los dispositivos (físicamente unidos a él). El sistema parece ser local a cada nodo y GFS sincroniza los acceso a los ficheros a través del cluster. Existen dos modos de funcionamiento, uno con un servidor central (asimétrico) y otro sin él (simétrico).
En el modo que necesita servidor central, éste tiene el control sobre los metadatos (que es un directorio donde están situados fechas de actualización, permisos, etc.), los discos duros compartidos solamente contienen los datos. Por tanto todo pasa por el servidor, que es quien provee la sincronización entre clientes, pues estos hacen las peticiones de modificación de metadata al servidor (abrir, cerrar, borrar, etc.) y leen los datos de los discos duros compartidos, es similar a un sistema de ficheros distribuido corriente. En la figura 3.2 se muestra una configuración típica de este sistema:
Este sistema de ficheros cumple todas las transparencias explicadas al principio de la lección, en el caso de haber un servidor central este es el que no cumple los criterios de transparencia pero en la parte de los clientes si los cumple pues no saben dónde están los ficheros (transparencia de acceso), se podrían cambiar de disco duro sin problema (transparencia de localización), si un nodo falla el sistema se recupera (transparencia a fallos), varios clientes pueden acceder al mismo fichero (transparencia de concurrencia) y se mantienen caches en los clientes (transparencia de réplica).
Por ejemplo es típico en las empresas comprar una única impresora cara para obtener la mejor calidad posible y dejar que esa impresora sea accedida desde cualquier ordenador de la intranet de la empresa, aunque esto significa el desplazamiento físico de los empleados. Puede ser un ahorro considerable a instalar una impresora en cada uno de los ordenadores.
El problema es que para este ejemplo se ha desarrollado una solución específica que necesita un demonio escuchando peticiones en un determinado puerto. Desarrollar una solución general es mucho más complejo y quizás incluso no deseable. Para que cualquier nodo pueda acceder a cualquier recurso de entrada/salida, primero se necesita una sincronización que como ya se ha visto en una sección anterior de este capítulo puede llegar a ser complejo. Pero también se necesita conocer los recursos de entrada/salida de los que se dispone, una forma de nombrarlos de forma única a través del cluster, etc.
Para el caso concreto de migración de procesos el acceso a entrada/salida puede evitar que un proceso en concreto migre, o más convenientemente los procesos deberían migrar al nodo donde estén realizando toda su entrada/salida para evitar que todos los datos a los que están accediendo tengan que viajar por la red. Así por ejemplo un proceso en openMosix que esté muy vinculado al hardware de entrada/salida no migrará nunca (Xwindow, lpd, etc.). Los sockets como caso especial de entrada/salida también plantean muchos problemas porque hay servicios que están escuchando un determinado puerto en un determinado ordenador para los que migrar sería catastrófico pues no se encontrarían los servicios disponibles para los ordenadores que accedieran a ese nodo en busca del servicio.