Amoeba es un ejemplo de sistema operativo distribuido que permite que un conjunto de CPUs y sistemas de E/S se comporten como una única computadora. Además proporciona elementos para la programación en paralelo, basándose para ello en hilos de nivel kernel.
A continuación se presenta una breve historia de Amoeba, los objetivos de su desarrollo, su arquitectura, la estructura de su núcleo y la gestión de procesos e hilos que realiza.
Amoeba nació como un proyecto de investigación de cómputo distribuido y paralelo en Vrije Universiteit, Amsterdam (Holanda) en 1981, diseñado por Andrew S. Tanenbaum y varios estudiantes de doctorado. En 1983 apareció el primer prototipo inicial, Amoeba 1.0, a nivel operacional. A partir de 1984 el proyecto se dividió en varios lugares de estudio en toda la Comunidad Europea, y evolucionó a un sistema distribuido de área amplia, Amoeba 3.0. Las evoluciones posteriores adquirieron nuevas características, como la emulación parcial de UNIX, la comunicación en grupo y un nuevo protocolo de bajo nivel, hasta llegar a la versión Amoeba 5.0, que es la que se describe aquí.
La mayoría de los proyectos de investigación de sistemas operativos distribuidos han partido de sistemas ya existentes, como UNIX, y les han añadido nuevas características, como el uso de redes y sistemas compartidos de ficheros, para hacerles más distribuidos. Amoeba, sin embargo, tomó un camino distinto, partiendo de un desarrollo limpio y nuevo, desde cero. El objetivo era crear nuevas ideas sin preocuparse por la compatibilidad con los sistemas existentes, aunque más tarde se añadió un paquete de emulación de UNIX para aprovechar el software existente. Los objetivos principales del diseño del sistema operativos Amoeba fueron :
Amoeba se diseñó con dos hipótesis en relación al hardware : se suponía que funcionaría en un sistema con un gran número de CPUs, y, cada CPU tendría cientos de megabytes de memoria. Estas hipótesis se cumplen en muchas de las organizaciones actuales, y serán más probables en el futuro. El objetivo es aprovechar al máximo los recursos de computación continuamente, de forma que si existen usuarios que no utilizan su computadora, ésta pueda ser utilizada por el resto de miembros de la red.
Amoeba emplea un modelo de pilas de procesadores, donde se concentra el poder de cómputo. Una pila de procesadores consta de un gran número de CPU, cada una con su propia memoria local y conexión a la red (también es válido un conjunto de ordenadores personales aunque no estén localizados físicamente en el mismo lugar). No se necesita la existencia de memoria compartida, aunque si existe se aprovecha para optimizar la transferencia de mensajes, empleando en ese caso copiado de memoria a memoria, en vez de transferencias vía red. Las CPU pueden tener diferentes arquitecturas mezcladas (386, 68030, SPARC y VAX), ya que Amoeba se diseñó para trabajar con varios sistemas heterogéneos.
Cuando un usuario lanza un proceso, el sistema selecciona uno o más procesadores de la pila de forma dinámica. Al terminar el proceso, se liberan los procesadores asignados. Si no quedan procesadores libres en la pila, se asigna aquel procesador con menor carga. Está claro que esta filosofía es distinta de la utilizada actualmente, donde cada usuario realiza todas las acciones de cómputo en su estación de trabajo, aunque exista una interconexión entre todas las estaciones de trabajo.
Otro elemento de la arquitectura es el terminal, a través del cual el usuario tiene acceso al sistema como un todo. Además otro elemento son los servidores especializados (como los servidores de archivos), que por razones de rendimiento deben ejecutarse en procesadores específicos, aunque no es obligatorio. Varios servidores pueden ofrecer el mismo servicio, de este modo el sistema es más tolerante a fallos.
Amoeba consta de dos partes :
El microkernel se ejecuta en todas las máquinas del sistema: procesadores de la pila, servidores especializados, terminales (si son computadoras en vez de terminales X). El microkernel tiene cuatro funciones básicas:
1) Gestión de Procesos : Amoeba soporta el concepto de proceso, pero también soporta el concepto de hilo. Soporta varios hilos de control dentro de un mismo espacio de direcciones. Así un proceso con un único hilo es equivalente a un proceso UNIX, con un único espacio de direcciones, un conjunto de registros, una pila y un contador de programa. Sin embargo, un proceso con varios hilos, tiene un único espacio de direcciones compartido por todos los hilos del proceso, y cada uno de los hilos tiene, desde un punto de vista lógico, sus propios registros, contador de programa y pila. Un conjunto de hilos de un proceso es equivalente a un conjunto de procesos independiente de UNIX, excepto por el hecho de que comparten el mismo espacio de direcciones y otra serie de recursos.
Una aplicación típica de los hilos en Amoeba es la de servidor de archivos, de tal forma que cada solicitud recibida es procesada por un único hilo : comienza el proceso de la solicitud, se puede bloquear a la espera de disco y después continuar su trabajo. El servidor se divide en varios hilos, siendo cada uno secuencial, aunque se pueden bloquear a la espera de E/S. Sin embargo, todos los hilos pueden acceder a un bloque de memoria común que funcione como cache. Los hilos se pueden sincronizar mediante semáforos o mútex para acceder al bloque cache compartido.
2) Gestión de la memoria de bajo nivel : Los hilos pueden poseer o liberar bloques de memoria, llamados segmentos. Los segmentos se pueden leer o escribir y ser asociados o desasociados al espacio de direcciones del proceso al que pertenece el hilo. Un proceso posee al menos un segmento. Los segmentos pueden almacenar datos, texto o pila, sin ningún patrón específico.
3) Gestión de la comunicación entre procesos : Existen dos formas de comunicación entre procesos en Amoeba :
- Comunicación Puntual: Se basa en el modelo cliente/servidor. El cliente envía un mensaje al servidor y se bloquea hasta que recibe la respuesta del servidor.
- Comunicación en Grupo: Permite la comunicación de mensajes de una fuente a varios destinos mediante protocolo software fiables y tolerantes a fallos (mensajes perdidos u otros errores).
Los dos tipos de comunicación emplean un protocolo especializado llamado FLIP (Fast Local Internet Protocol), que es un protocolo de capas de la red diseñado específicamente para cubrir las necesidades del cómputo distribuido.
4) Gestión de la E/S de bajo nivel : Todo dispositivo de E/S conectado a la máquina tiene un gestor de dispositivo asociado en el kernel. Este gestor controla toda la E/S del dispositivo. Los gestores están enlazados con el kernel y no se pueden cargar de forma dinámica. Los procesos se comunican con los gestores de dispositivos mediante comunicación puntual (cliente/servidor), enviando solicitudes y recibiendo respuestas. Los procesos no necesitan saber que se comunican con un gestor de dispositivo, sino que simplemente se comunican con un cierto hilo y un determinado lugar.
Los procesos servidores de Amoeba realizan todas las funciones que no se realizan en el kernel. El objetivo de este diseño es minimizar el tamaño del kernel, mejorando su flexibilidad. De esta forma el sistema de archivos y otros dispositivos no se integran en el núcleo lo que permite su fácil modificación, así como la ejecución simultánea de varios tipos de sistemas según las necesidades.
Amoeba se basa en un modelo cliente/servidor. El concepto de objeto es central en el diseño del software. Un objeto es un tipo abstracto de datos, que posee ciertos datos encapsulados junto con ciertas operaciones definidas sobre ellos. Por ejemplo, un objeto archivo dispone de una operación READ para su lectura. Los objetos son gestionados por los servidores. Los objetos son pasivos. Cada objeto es controlado por un proceso servidor. Para realizar una operación sobre un objeto, un proceso cliente realiza una llamada RPC al servidor, especificando el objeto, la operación a realizar, y los parámetros necesarios. El servidor realiza el trabajo y devuelve una respuesta. Las operaciones son sincronizadas, tras realizar la RPC, el hilo cliente se bloquea hasta que el servidor responde, pero mientras tanto se pueden seguir ejecutando otros hilos en el mismo proceso. Los procesos clientes no necesitan conocer la localización de los objetos o los servidores, que pueden ser locales o remotos, de usuario o de kernel, pero todo esto es transparente para el proceso cliente, puesto que el protocolo RPC es idéntico para todos los casos.
Los objetos tienen un nombre y una protección asociados, que reciben mediante etiquetas especiales llamadas posibilidades. Para crear un objeto, un proceso cliente realiza una llamada RPC al servidor adecuado. El servidor crea el objeto y devuelve una posibilidad al cliente. El cliente usará esta posibilidad para identificar el objeto en sucesivas operaciones.
Un proceso en Amoeba consiste en un espacio de direcciones y un conjunto de hilos que se ejecutan en dicho espacio. En Amoeba un proceso es un objeto más. Cuando se crea un proceso, el proceso padre obtiene una posibilidad para el proceso hilo, como en cualquier otro objeto. Empleando esta posibilidad, el proceso hilo se puede suspender, reiniciar o destruir.
La creación de procesos en Amoeba es distinta de la de UNIX. El modelo de empleado en UNIX no es adecuado en un sistema distribuido debido al enorme costo que supone crear una copia de un proceso existente (fork), para casi inmediatamente reemplazarla por un nuevo programa (exec). Amoeba utiliza una técnica parecida a la empleada en MS-DOS, pero con la salvedad de que aquí se puede establecer una árbol de procesos mediante relaciones padre-hijo. Se puede crear un nuevo proceso en un procesador específico, con la imagen de memoria situada justo al principio.
La administración de procesos se realiza a tres niveles:
1) Nivel de servidores de procesos : Son los hilos del núcleo que se ejecutan en cada una de las máquinas del sistema. Para crear un proceso en una determinada máquina, el proceso padre realiza una llamada RPC al servidor de procesos de dicha máquina que le proporciona la información necesaria.
2) Nivel de procedimientos de biblioteca : Proporcionan una interfaz apropiada para los programas de usuario. Realizan el trabajo mediante llamadas a los procedimientos de interfaz de bajo nivel.
3) Nivel de servidor de ejecución : Es la forma más sencilla de crear procesos, ya que es éste servidor el que determina el lugar donde ejecutar el nuevo proceso, sin que nosotros nos preocupemos por ello.
Muchas de las llamadas relativas a la administración de procesos emplean el descriptor de proceso, que es una estructura de datos donde se almacena la siguiente información relativa al proceso :
- Arquitectura : Existe un campo en el descriptor que indica el tipo de arquitecturas donde se puede ejecutar el proceso. Esto es de enorme importancia en un sistema Amoeba con diferente arquitecturas (386, SPARC, VAX).
- Posibilidad para estado de salida: Otro campo contiene la posibilidad, para comunicar el estado de salida al padre. Al terminar el proceso se realiza una RPC empleando esta posibilidad para informar del evento.
- Descriptores de los segmentos del proceso : De forma conjunta definen el espacio de direcciones del proceso.
- Descriptores de los hilos del proceso: Existe un descriptor por cada hilo del proceso.
El contenido del descriptor de hilo depende de cada arquitectura, pero al menos contendrá el contador de programa y el puntero a la pila del hilo, aunque también suele tener información adicional para la ejecución del hilo (valores de los registros, estado del hilo y una serie de banderas de señalización).
La interfaz de procesos del nivel de servidor de ejecución no necesita un descriptor de proceso completo, sino que existen una llamada similar a UNIX, para la creación de procesos :
- newproc(nombre_fichero, puntero_args, puntero_entorno)
Amoeba presenta un modelo sencillo de hilos. Cuando se inicia un proceso, éste consta de al menos un hilo, aunque puede estar formado por más. Durante la ejecución, el proceso puede crear más hilos y los hilos anteriores pueden finalizar su labor. El número de hilos, es pues, dinámico. Para crear un nuevo hilo, la llamada necesita que se especifiquen el nombre del procedimiento a ejecutar y el tamaño de pila inicial para el hilo. Todos los hilos del proceso comparten el mismo texto de programa y los datos globales, pero cada hilo tiene su propia pila, puntero de pila y registros de máquina.
Existen procedimientos de biblioteca para crear variables globales a todos los procedimientos de un hilo pero ocultas al resto de hilos, son las variables llamadas glocales. El procedimiento de biblioteca lo que hace es asignar un bloque de memoria glocal del tamaño necesario y devolver un puntero a él.
Existen tres formas de sincronización para los hilos: señales, mútex y semáforos.
a) Señales: Son interrupciones asíncronas que se envían de un hilo a otro en el mismo proceso. Conceptualmente son como las señales de UNIX, salvo que se envían entre hilos en vez de entre procesos. Las señales se pueden enviar, capturar o ignorar.
b) Mútex: Es como un semáforo binario. Su funcionamiento es el de un mútex típico. Puede tener un estado entre dos posibles, cerrado o no cerrado. Si se intenta cerrar un mútex no cerrado, se cierra, y el hilo que realizó la llamada continúa su ejecución. Pero si se intenta cerrar un mútex ya cerrado, el hilo que realizó la llamada se bloquea hasta que otro hilo libere la cerradura del mútex. Cuando más de un hilo espera bloqueado por un mútex, cuando éste se libera, se desbloquea a un único hilo. También existen una llamada en la que se intenta cerrar el mútex, pero si no se puede hacer durante un determinado intervalo de tiempo, finaliza y devuelve un error al hilo que realizó la llamada.
c) Semáforos: Los semáforos son más lentos que los mútex, pero su funcionamiento es similar, y existen ocasiones en que son necesarios. Su funcionamiento es el habitual descrito por Dijkstra. Existen operaciones P y V para el semáforo, pero además existe una operación DOWN que si no tiene éxito durante cierto intervalo de tiempo, termina.
El kernel controla todos los hilos, de esta forma
cuando un hilo realiza una RPC, el kernel lo bloquea y planifica
la ejecución de otro hilo del mismo proceso o de otro.
La planificación de los hilos se realiza empleando un sistema
de prioridades, de tal forma que los hilos del núcleo tienen
mayor prioridad que los hilos de nivel usuario.