SunOS 5.0 es el sistema operativo que acompaña al entorno Solaris 2.0. SunOS 5.0 (y las versiones posteriores 5.x) se caracteriza por su arquitectura multihilo que contiene soporte kernel para múltiples hilos de control dentro de un solo espacio de direcciones de proceso lo que permite que un mismo proceso solape operaciones de E/S y obtenga las ventajas de emplear un multiprocesador, si está disponible.
SunOS 5.0 es el sistema operativo que Sun Microsystems, Inc desarrolló para su entorno Solaris 2.0 y que ha sido implementado en diversas plataformas hardware, aunque originalmente estaba pensado para las plataformas basadas en los procesadores SPARC. Solaris 2.0 pretende ser un entorno UNIX mejorado, basado en un sistema operativo multitarea, multiproceso, multihilo, con posibilidades de tiempo real y características de sistema distribuido. Su origen se encuentra en versiones anteriores del kernel SunOS 4.x (que ya soportaba hilos, aunque sólo a nivel usuario) con modificaciones importantes necesarias para implementar el soporte de hilos a nivel kernel, así como las nuevas arquitecturas de bus desarrolladas por Sun.
El diseño original del API de hilos de usuario que incluía SunOS 5.0 era sencillo, pero a partir de SunOS 5.2 (que acompaña a Solaris 2.2) el interfaz fue modificado adoptando el estándar UI Threads (Unix International Threads) para implementar hilos de nivel usuario, aunque el kernel del sistema no sufrió grandes cambios.
Inicialmente Solaris 2.3 sólo implementaba un paquete de hilos con el API de UI Threads, pero posteriormente Sun decidió adoptar el estándar POSIX, y el paquete de hilos de Solaris 2.4 ya implementa dos versiones de hilos : Solaris Threads (también conocido como UI-Threads) y POSIX 1003.1c Draft 8. En Solaris 2.5 ya se implementa el estándar IEEE POSIX 1003.1c PThreads.
Las razones para soportar múltiples hilos de control en SunOS 5.x se deben a dos causas principales : la utilización de hardware multiprocesador y la utilización de concurrencia en las aplicaciones.
Se puede aprovechar el hardware multiprocesador haciendo que cada proceso de usuario se ejecute en un procesado distinto, sin que las aplicaciones necesiten modificaciones.
Se establecieron los siguientes objetivos de diseño en SunOS 5.0 por orden de importancia, en relación a la gestión de hilos :
SunOS 5.0 está compuesto de módulos separados, como gestores de dispositivo, sistemas de ficheros, y llamadas al sistema individuales que son cargados dinámicamente en el núcleo cuando son necesarios. El diseño del kernel sigue la filosofía de microkernel, aunque no en el mismo grado que lo hacen otros sistemas operativos, como puede ser Amoeba.
El corazón de SunOS 5.0 es un núcleo en tiempo real que soporta hilos de control a nivel kernel. Todos los flujos de control son hilos, incluyendo las interrupciones. El kernel es en su mayoría desalojable, excepto algunas partes (como el cambio de contexto de hilos). Los hilos del kernel son también empleados para soportar múltiples hilos de control, llamados procesos ligeros o LWPs, dentro de un mismo proceso UNIX. Los hilos kernel son ejecutados por orden de prioridad en la lista de procesadores disponibles. El modelo de programación de hilos en el kernel es similar al empleado por los hilos en el programa de usuario. Existe la siguiente analogía : un hilo de usuario es ejecutado por un LWP de la misma forma que un hilo kernel es ejecutado por un programa de usuario. Existe un hilo kernel por cada LWP, que es ejecutado en el kernel cuando el LWP realiza una llamada al sistema. Otros hilos kernel no tienen LWP asociados, y son empleados para diversos propósitos, como manejo de interrupciones, ejecución de procedimientos de servicio de streams, o proporcionar el servicio NFS (Network File System).
Los hilos de kernel se sincronizan empleando las siguiente primitivas :
Estas primitivas son muy similares a las primitivas
equivalentes de los hilos de usuario. Los mútex y los bloqueos
de escritor soportan un protocolo de herencia de prioridades que
impide que hilos de menor prioridad bloqueen hilos de mayor prioridad,
es el fenómeno conocido como inversiones de prioridad.
SunOS 5.0 se puede ejecutar tanto en sistemas uniprocesador como en sistemas multiprocesador de memoria compartida fuertemente acoplados. El kernel supone que todos los procesadores son equivalentes. Los procesadores seleccionan hilos kernel de las cola de hilos kernel ejecutables. Todos los procesadores ven los mismos datos en memoria. Este modelo es flexible, de forma que las operaciones de memoria realizadas por un procesador pueden ser retrasadas o reordenadas cuando son vistas por otros procesadores. En este entorno, el acceso compartido a memoria debe ser protegido mediante primitivas de sincronización. La excepción es que las primitivas simples pueden ser leídas o actualizadas atómicamente (ej. todos los bytes de un entero cambian al mismo tiempo). Se supone que la memoria compartida es simétrica. El kernel no garantiza que procesos planificados en un procesador particular sean colocados en una zona de memoria concreta que sea más rápida de acceder por dicho procesador. El kernel puede ejecutarse de forma simétrica en un multiprocesador, pero no se permite que más de un procesador ejecute código del kernel. Esta es una estrategia que no favorece el escalado cuando se incrementa el número de procesadores, pero el sistema se diseñó con una estrategia de bloqueo de grano relativamente fino para obtener las ventajas de tantos procesadores como sea posible. Cada subsistema del kernel tiene una estrategia de bloqueo diseñada para permitir un alto grado de concurrencia en operaciones frecuentes. En general, el acceso a conjuntos de datos está protegido por bloqueos. Las operaciones más infrecuentes tienen bloqueos "gruesos" mediante exclusión mutua simple. El sistema tiene cientos de objetos de sincronización distintos estáticamente, y puede tener miles de objetos de sincronización dinámicamente.
El sistema de memoria virtual consta de cuatro subsistemas : el espacio de direcciones, los gestores de segmento, la cache de páginas, y el hardware del nivel de transformación de direcciones. El espacio de direcciones se divide en un conjunto de segmentos cada uno de los cuales representa una parte de la memoria virtual.
SunOS 5.0 soporta dos tipos de gestores de dispositivo : MT-unsafe (inseguro frente a multihilos) y MT-safe (seguro frente a multihilos). Los gestores de dispositivo del tipo MT-safe han sido modificados explícitamente para protegerse en caso de que múltiples hilos de ejecución se encuentren ejecutando código del gestor. Los viejos gestores de dispositivo no son seguros en estos casos :
Como ocurre en la mayoría de servicios de sistema en la actualidad, la visión del programador del servicio de múltiples hilos de control no es la misma que la implementada por el kernel. La visión del software es creada mediante una combinación del kernel, las librerías en tiempo de ejecución, y el compilador. Esta aproximación incrementa la portabilidad de las aplicaciones, ocultando algunos detalles de la implementación, y proporcionando un mejor rendimiento, ya que el código de la librería de nivel usuario realiza parte del trabajo sin involucrar al kernel.
El modelo de programación multihilo tiene dos niveles. El nivel más importante es el interfaz de hilos, que define la mayoría de los aspectos del modelo de programación ; es decir, los programadores escriben programas empleando hilos. El segundo nivel, es el de los procesos ligeros (LWP, light weight process) que está definido por los servicios que proporciona el sistema operativo. Ambos niveles son esenciales.
Un proceso tradicional de UNIX tiene un simple hilo de control. Un hilo de control, o simplemente un hilo, es una secuencia de instrucciones que están siendo ejecutadas en un programa. Un hilo tiene un contador de programa (PC) y una pila que almacena las variables locales y devuelve direcciones. Un proceso multihilo en UNIX está asociado con uno o más hilos. Los hilos se ejecutan independientemente. No existe forma de predecir como se entremezclarán las instrucciones de los diferentes hilos, aunque pueden tener prioridades de ejecución distintas que influyen en la velocidad de ejecución relativa de cada hilo. En general, el número de hilos que un proceso de una aplicación elige para resolver un problema es invisible desde el exterior del proceso. Los hilos pueden verse como recursos de ejecución que pueden aplicarse para la resolución de un problema. Los hilos comparten las instrucciones del proceso y la mayoría de sus datos. Los hilos también comparten la mayor parte del estado del proceso. Cada hilo puede hacer llamadas al sistema e interactuar con otros procesos de forma normal. Algunas operaciones afectan a todos los hilos de un proceso ; por ejemplo, si un hilo realiza la llamada exit() de UNIX, todos los hilos son destruidos. Otros servicios del sistema tienen nuevas interpretaciones ; por ejemplo, una excepción de overflow capturada es aplicada a un hilo particular, no al proceso completo.
La arquitectura proporciona una variedad de facilidades de sincronización para permitir a los hilos cooperar en el acceso a los datos compartidos. Las facilidades de sincronización incluyen bloqueos de exclusión mutua (mútex), variables de condición y semáforos. Para soportar diferentes frecuencias de interacción y diferentes grados de concurrencia, se proporcionan diversos mecanismos de sincronización con diferentes semánticas.
Los hilos de distintos procesos pueden sincronizarse mediante variables de sincronización situadas en memoria compartida, incluso aunque los hilos de diferentes procesos son generalmente invisibles entre sí. Las variables de sincronización pueden ser almacenadas en ficheros, con lo que su vida va más allá de la duración del propio proceso.
Cada hilo tiene su propia máscara de señales, esto permite que un hilo bloquee señales asíncronas mientras modifica datos de información de estado que es también modificada por el gestor de señales. Las señales síncronas son enviadas al hilo que las provocó. Las señales externas son enviadas a uno de los hilos del proceso que las tiene habilitadas. También se pueden enviar señales a hilos concretos del mismo proceso.
Los hilos son un paradigma apropiado para la mayoría de los programas que desean explotar el paralelismo hardware o establecer una estructura concurrente en un programa. Para aquellas situaciones que necesitan más control sobre como el programa es implementado en un hardware paralelo, y necesitan optimizar los costes de la ejecución concurrente y la sincronización, se define un segundo interfaz : los procesos ligeros o LWPs.
En la arquitectura multihilo de SunOS, un proceso UNIX consiste principalmente de un espacio de direcciones y un conjunto de procesos ligeros (LWPs) (que en la práctica se enlazan con hilos de nivel kernel), que comparten el espacio de direcciones. Cada LWP puede ser visto como una CPU virtual que está disponible para ejecutar código o llamadas de sistema. Cada LWP es ejecutado de forma independiente por el kernel, puede realizar llamadas al sistema, provocar faltas de página, todo de forma independiente, y ejecutarse en paralelo en un multiprocesador. Todos los LWPs en el sistema son planificados por el kernel sobre los recursos CPU disponibles de acuerdo a su tipo de planificación y prioridad.
Los hilos de nivel usuario (o simplemente hilos) son implementados empleando LWPs. Los hilos están representados por estructuras de datos en el espacio de direcciones de un programa. Los LWPs de un proceso ejecutan los hilos de usuario :
Cuando un hilo accede a un servicio del sistema mediante una llamada al sistema, o genera una falta de página, o se comunica con otros procesos, lo hace empleando el LWP que le está ejecutando ; y permanece ligado al LWP que le ejecuta hasta que la llamada al sistema finaliza. Si el hilo necesita comunicarse con otros hilos del mismo proceso, puede realizarlo sin involucrar al sistema operativo. La conmutación entre hilos de un mismo proceso se produce sin conocimiento del kernel. Al igual que la mayoría de las rutinas de la librería STDIO de UNIX (fopen(), fread(), etc.) son implementadas usando llamadas al sistema (open(), read(), etc.), el interfaz de hilos es implementado usando el interfaz de LWP, por las mismas razones.
Un LWP puede tener características que no son exportadas directamente a los hilos, como puede ser una clase especial de planificación. Un programador puede usar las ventajas de estas características, y además seguir empleando las características del interfaz de hilos, simplemente especificando que el hilo está ligado permanentemente a un LWP concreto.
Los hilos son el interfaz primario para implementar el paralelismo de las aplicaciones. Algunos programas multihilo pueden emplear el interfaz LWP directamente, pero debe ser por razones justificadas. Algunos lenguajes definen mecanismos de concurrencia que son diferentes de los hilos. Un ejemplo es un compilador Fortran que proporciona paralelismo a nivel de bucle. En estos casos, la biblioteca del lenguaje puede implementar su propia noción de concurrencia empleando LWPs. La mayoría de los programadores pueden programar usando el interfaz de hilos de nivel de usuario y dejar que la biblioteca se encargue de realizar la correspondencia en primitivas del kernel. La decisión de cuantos LWPs se deben crear para ejecutar los hilos de un proceso puede dejarse a la biblioteca, o puede ser especificada por el programador.
Se puede pensar para qué se necesitan dos interfaces que son similares. La respuesta es que la arquitectura multihilo debe estar preparada para distintas expectativas. Algunos programas tienen una gran cantidad de paralelismo lógico, como puede ser un sistema de ventanas que proporciona un objeto visual con un gestor de entradas y un gestor de salidas. Otros programas necesitan un paralelismo más físico. En ambos casos, los programas necesitan acceder a los servicios del sistema de forma sencilla.
Los hilos son implementados por la biblioteca y no son conocidos por el kernel. Así, los hilos pueden ser creados, destruidos, bloqueados, activados, etc. sin involucrar al kernel. Los LWPs son implementados por el kernel. Si un hilo necesita leer de un fichero, el kernel necesita poder conmutar a otro proceso cuando el LWP se bloquea a la espera de la finalización de una E/S. El kernel debe conservar el estado de la operación de lectura y continuar cuando finalice la operación de E/S. Normalmente, si el kernel tuviera conocimiento de cada hilo, debería mantener estructuras de datos para cada uno y se debería involucrar en el cambio de contexto de hilos incluso aunque la mayoría de las interacciones correspondan a hilos dentro del mismo proceso. En otras palabras, el paralelismo soportado por el kernel (LWPs) es relativamente caro comparado con los hilos de nivel usuario. Si todos los hilos fuesen soportados directamente por el kernel, aplicaciones como un sistema de ventanas serían mucho menos eficientes.
A veces, tener más hilos que LWPs es una desventaja. Un algoritmo matricial en paralelo divide las filas de la matriz entre diferentes hilos. Si hay un LWP por procesador, pero múltiples hilos por LWP, cada procesador empleará cierta sobrecarga de tiempo en la conmutación entre hilos. Sería mejor que hubiese un hilo por LWP, dividir las filas entre un pequeño número de hilos, y reducir el número de cambios de contexto de hilos. Especificando que cada hilo esté ligado permanentemente a su propio LWP, un programador puede escribir código multihilo que corresponde realmente a código LWP.
Una mezcla de hilos que están permanentemente ligados a LWPs e hilos no ligados puede ser apropiada en ocasiones para algunas aplicaciones. Un ejemplo puede ser una aplicación de tiempo real que necesite que algunos hilos tengan una prioridad máxima y una planificación en tiempo real, mientras otros hilos pueden realizar cálculos sin necesidad de tiempo real.
Definiendo ambos niveles de interfaz en la arquitectura,
se establece una clara distinción entre lo que el programador
ve y lo que el kernel proporciona. La mayoría de los programadores
emplean los hilos sin pensar en LWPs. Cuando es necesario optimizar
un programa, el programador puede establecer relaciones más
directas entre hilos y LWPs. Esto permite a los programadores
estructurar su aplicación pensando en hilos mientras se
obtiene el nivel de concurrencia adecuado a nivel kernel. Para
algunos casos, el programador puede ver el número de LWPs
usados por la aplicación como el grado de concurrencia
real que la aplicación necesita.
La asignación de hilos con LWPs es controlada por el paquete de hilos o es especificada por el programador. El kernel ve los LWPs y puede planificarlos en los procesadores disponibles.
En el modelo se pueden observar los siguientes procesos de usuario :
Se definieron una serie de principios que ayudaron al diseño de la arquitectura :
La arquitectura multihilo de SunOS proporciona las siguientes ventajas :
Un proceso ligero (LWP) o hilo a nivel núcleo es creado por el kernel cuando un programa es inicializado, y comienza ejecutando el hilo compilado como programa principal. Los hilos adicionales son creados mediante llamadas a la biblioteca especificando un procedimiento a ejecutar por el nuevo hilo y un área de pila para su uso.
Según la implementación, la biblioteca, o los parámetros proporcionados por el programador, un hilo puede ser asociado con el mismo LWP o diferentes LWPs durante su vida. Puede existir una relación uno a uno entre los hilos y los LWPs, o bien, uno o más LWPs pueden ser multiplexados por la biblioteca de hilos entre un conjunto de hilos. Normalmente, un hilo no puede saber que tipo de relación tiene con los LWPs, aunque por razones de rendimiento, o para evitar dead-locks, un programa puede requerir la existencia de un mayor o menor número de LWPs.
Cuando un hilo ejecuta una llamada al sistema, permanece ligado al mismo LWP durante la duración de la llamada al kernel. Si la llamada al kernel bloquea, entonces el hilo y su LWP permanecen bloqueados. Otros LWPs pueden ejecutar otros hilos en el mismo proceso, incluso realizar otras llamadas al sistema. Este mismo principio es aplicable a las faltas de página.
No existe un espacio de nombres en el sistema para los hilos o LWPs. Así, por ejemplo, no es posible dirigir una señal a un LWP particular que no pertenece al proceso, o conocer que LWP envía un mensaje particular.
La siguiente información de estado es única para cada hilo :
El resto de información de estado pertenece al proceso, y es compartida por todos los hilos del proceso.
Cada hilo tiene su propia máscara de señales. Esto permite que un hilo bloquee ciertas señales mientras utiliza información de estado que es también modificada por el gestor de la señal. Todos los hilos en el mismo espacio de direcciones comparte el conjunto de gestores de señales, que son establecidos mediante la función signal() y sus variantes, de forma usual. Si se desea, es posible para una aplicación particular implementar gestores de señales para cada hilo, empleando los gestores de señales por proceso. Por ejemplo, el gestor de señales puede usar el identificador del hilo que gestiona la señal como un índice a una tabla de gestores para cada hilo. Si la biblioteca de hilos fuese a implementar gestores de señales para cada hilo, se debe decidir la semántica correcta cuando varios hilos tienen distintas combinaciones de gestores de señales, SIG_IGN y SIG_DFL, por ejemplo. Además, todos los hilos deberían ser cargados con el estado del gestor. Por esta razón, el soporte de la biblioteca de los gestores de señales para cada hilo fue complejo y confuso de cara al programador de aplicaciones.
Los hilos tienen una zona de almacenamiento privada, además de la pila, llamada espacio de almacenamiento local del hilo. La mayoría de las variables en el programa son compartidas por todos los hilos en ejecución, pero cada hilo tiene su propia copia de las variables locales al hilo. Conceptualmente, el espacio de almacenamiento local corresponde a datos no compartidos y almacenados estáticamente. La variable errno de la biblioteca de C es un buen ejemplo de variable que debe ser colocada en el espacio de almacenamiento local. Esto permite que cada hilo pueda hacer referencia a errno directamente y permitir la ejecución entremezclada de hilos sin miedo a corromper el valor de la variable en otros hilos. El espacio de almacenamiento local es potencialmente costoso en acceso, y debe limitarse a lo esencial, como el soporte de antiguos interfaces no reentrantes.
La sincronización entre hilos se consigue empleando las facilidades que proporciona la implementación, que presenta un conjunto estándar de semánticas. Se soportan los siguientes tipos de sincronización :
La arquitectura permite un rango de implementaciones para cada tipo de sincronización soportada. Por ejemplo, los mútex pueden ser implementados como bucles de comprobación, bloqueos suspendidos, bloqueos aditivos, etc.
Estas facilidades emplean variables de sincronización en memoria. Las variables pueden estar alojadas estáticamente y en direcciones fijas. El programador puede elegir la variante de implementación particular de la semántica de sincronización cuando inicializa la variable de sincronización. Si la variable es inicializada a cero, se emplea una implementación por defecto.
Las variables de sincronización pueden colocarse en memoria compartida por varios procesos. El programador puede seleccionar una variante de implementación de cada tipo de sincronización que permita a la variable sincronizar hilos en distintos procesos compartiendo la variable. Las primitivas de sincronización son aplicables a la variable compartida como parte del "mapeado" del objeto. En otras palabras, las variables de sincronización pueden ser compartidas entre procesos incluso aunque estén mapeadas en diferentes direcciones virtuales.
Las variables de sincronización que no están
en memoria compartida son completamente desconocidas por el kernel.
Las variables de sincronización que están en memoria
compartida o en ficheros son también desconocidas por el
kernel a menos que un hilo se bloquee en ellas, en cuyo caso,
el hilo está ligado temporalmente al LWP que está
bloqueado por el kernel, al igual que ocurre con una llamada al
sistema.