CAPITULO 6:
APOYO DEL SISTEMA OPERATIVO

Veremos en este capítulo:

  1. Introducción,
  2. La capa del sistema operativo,
  3. Protección,
  4. Procesos y hebras,
  5. Comunicación e invocación,
  6. Arquitectura de un sistema operativo.

Se describe cómo el middleware se apoya en el sistema operativo de cada uno de los nodos.

El sistema operativo facilita la protección y encapsulación de recursos dentre de servidores, y da apoyo a la comunicación y planificación (scheduling) necesarias para la ``invocación"

Se analizan también las ventajas y desventajas de colocar código en el kernel o el nivel de usuario.

Y por último, se estudia el diseño e implementación de sistemas de comunicación y de proceso multihebra.

6.1 Introducción

Un aspecto importante de los sistemas operativos distribuidos es la compartición de recursos.

A menudo los clientes y los recursos están en nodos (o al menos procesos) distintos. El middleware provee el enlace entre ambos.

Y por debajo del middleware está el sistema operativo, que es el objetivo de este capítulo. Analizaremos la relación entre ambos.

El middleware necesita que el SO le dé acceso eficiente y robusto a los recursos físicos, con la flexibilidad necesaria para implementar distintas reglas (policies) de gestión de recursos.

Todo sistema operativo no hace más que implementar abstracciones del hardware básico: procesos para abstraer el procesador, canales para abstraer las líneas de comunicación, segmentos para abstraer la memoria central, ficheros para abstraer la memoria secundaria, etc.

Es interesante contraponer los sistemas operativos de red frente a los verdaderos sistemas operativos distribuidos.

Tanto UNIX como Windows NT son sistemas operativos de red. Aunque se pueda acceder a ficheros remotos de manera en gran medida ``transparente".

Pero lo que distingue a los sistemas operativos de red frente a los verdaderos sistemas operativos distribuidos es que cada uno de los sistemas operativos locales mantiene su propia autonomía en cuanto a la gestión de sus propios recursos.

Por ejemplo, aunque un usuario pueda hacer rlogin, cada copia del sistema operativo planifica sus propios procesos.

Por contra, se podría tener una única imagen de sistema en el ámbito de toda la red. El usuario no sabría donde ejecutan sus procesos, donde están almacenados sus ficheros, etc.

Por ejemplo, el sistema operativo podría decidir crear un nuevo proceso en el nodo menos cargado.

Sistemas Operativos de red y Middleware

De hecho, no hay sistemas operativos distribuidos de uso genérico, solo sistemas operativos de red

Y es posible que sea así en el futuro, por dos razones principales: Que hay mucho dinero invertido en (aplicaciones) software tradicional que son muy eficientes, y que los usuarios prefieren tener un cierto grado de autonomía sobre sus propias máquinas.

La combinación de middleware y sistema operativo de red es un buen término medio que permite, tanto usar las aplicaciones de la máquina propia, como acceder de manera transparente a recursos de la red.

6.2 La capa del sistema operativo

En cada nodo hay un hardware propio sobre el que ejecuta un SO específico con su kernel y servicios asociados --bibliotecas, por ejemplo--

Y el middleware usa una combinación de los recursos locales para implementar los mecanismos de invocación remota entre objetos (recursos) y procesos (clientes).

- figura 6.1

La figura muestra cómo una capa única de middleware se apoya en distintos S.O. para proveer una infraestructura distribuida para aplicaciones y servicios.

Para que el middleware realice su trabajo, ha de utilizar kernels y procesos servidores. Y ambos deben de ser capaces de ofrecer:

Los recursos se acceden por los programas clientes bien mediante llamada remota a un servidor, o bien mediante llamada al sistema en un kernel. En los dos casos se llama una invocación.

Una combinación de bibliotecas del sistema, kernels y servidores, se encarga de realizar la:

- figura 6.2

La figura muestra la funcionalidad básica que nos interesa: gestor de procesos, gestor de hebras, ...

El software del SO se diseña para que sea transportable en gran medida, y por ello se programa casi todo en algún lenguaje de alto nivel (C, C++, Modula-3, etc.).

A veces, el nodo es multiprocesador (con memoria compartida) y tiene un kernel especial que es capaz de ejecutar en él.

Puede haber también algo de memoria privada por procesador. Y lo más usual es la arquitectura simétrica, con todos los procesadores ejecutando el mismo kernel y compartiendo estructuras de datos claves como la cola de procesos listos para ejecutar.

En sistemas distribuidos, la alta capacidad de cómputo de un multiprocesador viene bien para implementar servidores de altas prestaciones (una base de datos con acceso compartido y simultáneo por parte de muchos clientes, por ejemplo).

Las partes básicas de un SO son pues:

6.3 Protección

Tanto si es por malicia como si es por error.

Y tanto a operaciones no permitidas como a operaciones ``inexistentes" (datos internos).

Lo segundo podría evitarse programando en lenguajes de alto nivel con comprobación de tipos (control de acceso), pero no suele ser éste el caso.

Kernels y protección

El kernel o núcleo es un programa que ejecuta con control total de la máquina. Y suele impedir que otro código no privilegiado lo use, pero a veces deja que los servidores accedan a ciertos recursos físicos (registros de dispositivos periféricos, por ejemplo).

La mayor parte de los procesadores tiene un registro cuyo contenido determina si se pueden ejecutar instrucciones privilegiadas en ese momento o no. El kernel trabaja en modo privilegiado y asegura que el resto del código trabaje en modo usuario

El kernel establece también espacios de direcciones para garantizar la protección, y maneja la ``unidad de gestión de memoria" (tabla de páginación).

Un espacio de direcciones es un conjunto de zonas contiguas de memoria lógica, cada una con sus propios derechos de acceso (lectura, escritura, ejecución, ...). Y un proceso no puede acceder fuera de su espacio de direcciones.

Cuando un proceso pasa de ejecutar en modo usuario a ejecutar código del kernel, su espacio de direcciones cambia.

El paso de modo usuario a modo kernel se hace de manera segura ejecutando una instrucción especial TRAP de llamada al sistema que:

La protección obliga a pagar un precio en eficiencia. La llamada al sistema es más costosa que la llamada a una subrutina

6.4 Procesos y hebras

El proceso de UNIX resultó ``caro" en tiempo de creación y sobre todo, multiplexación.

Un segundo nivel de multiplexación: hebras. Comparten el mismo espacio de memoria.

Ahora un proceso es hebras más un entorno de ejecución (espacio de memoria más mecanismos de sincronización y de comunicación, como puertos).

La concurrencia (de hebras) permite alcanzar más eficiencia (quitar cuellos de botella en servidores, por ejemplo), y también simplificar la programación.

Confusión terminológica: procesos, tareas, hebras, procesos ligeros (y pesados).

``La jarra cerrada con comida y aire y con moscas."

6.4.1 Espacios de direcciones

Lo más caro de crear y gestionar de un entorno de ejecución.

Suele ser grande (32 y hasta 64 bits de dirección), y formado por varias regiones, trozos contiguos de memoria lógica separados por áreas de memoria lógica inaccesibles.

- Figura 6.3

Es una ampliación de la memoria paginada tradicional, no de la segmentación.

Pero de alguna manera simula la segmentación, a base de usar un espacio lógica ``discontinuo".

Tiene implicaciones en el hardware (TLB): Tablas de páginas muy grandes y "dispersas".

Cada región consta de un número completo de páginas y tiene:

Permite dejar ``huecos" suficientemente grandes para que las regiones crezcan. Pero hay límites (no como en segmentación).

De todas maneras, se va a direcciones de 64 bits ...Meditar qué significa esto.

Tradicionalmente, en UNIX había código, ``montón" y pila.

Lo primero, ahora, que se pueden crear nuevas regiones para pilas de flujos concurrentes (problema de las llamadas a subrutinas y la ``pila cactus"). Así se controla mejor si se desbordan, que poniéndolas en el montón del proceso.

También, al tener número variable de regiones, se pueden asociar ficheros de datos (no sólo de código y datos binarios) en memoria lógica (idea también original de MULTICS, curiosamente ...).

Por último, se pueden tener regiones compartidas para:

6.4.2 Creación de un nuevo proceso

Tradicionalmente, en UNIX hay fork y exec: Explicarlas.

En un sistema distribuido se puede diferenciar entre:

Elegir el computador explícitamente, o que se escoja automáticamente (bien para equilibrar la carga, o siempre el local).

La explícita no es transparente, pero puede necesitarse para tolerancia a fallos o para usar computadores específicos.

La implícita puede usar reglas estáticas o reglas dinámicas. Y las reglas estáticas pueden ser deterministas o probabilistas.

El reparto de carga puede ser centralizado, jerárquico o descentralizado.

Y hay también algoritmos iniciados por el emisor e iniciados por el receptor

Los segundos son mejores para hacer migración de procesos.

Que en todo caso se utiliza muy poco por su gran complejidad de implementación (sobre todo, por la gran dificultad de la recolección del estado de un proceso en un kernel tradicional).

(Cluster: conjunto de hasta miles de computadores estándar conectados por una red local de alta velocidad: Caso de www.Google.com).

Creación de un nuevo entorno de ejecución

Espacio de direcciones con valores iniciales (y quizás otras cosas como ficheros abiertos predefinidos).

Iniciado explícitamente, dando valores para las regiones (normalmente sacados de ficheros), o heredado del ``padre". El caso de UNIX (explicarlo) se puede generalizar para más regiones, controlando cuáles se heredan y cuáles no.

La herencia puede ser por copia o por compartición.

Si es por compartición, simplemente se ajustan las tablas de páginas.

Cuando es por copia, hay una optimización importante: ``copia en la escritura"

- Figura 6.4

Explicarlo con las regiones RA y RB. Suponemos que las páginas residen en memoria.

Viene de UNIX de Berkeley (vfork).

La herencia de puertos da problemas, sin embargo. Se suele arrancar con un conjunto de ellos predefinido, que comunican con ``ligadores" de servicios.

6.4.3 Hebras

- Figura 6.5

Fijémonos primero en el servidor multihebra.

Supongamos inicialmente 2 ms. de CPU y 8 de disco, lo cual da 100 servicios (1000/(2+8)) por segundo con una única hebra.

Si ahora ponemos dos hebras, sube a 125 (1000/8) servicios. Importante resaltar que, en realidad, ya tenemos un ``multiprocesador" aunque solo haya una CPU.

Si ahora tenemos cache del disco, con un 75% de aciertos, ¡500 servicios! (hace falta que siempre haya hebras listas).

En realidad, puede que el tiempo de CPU haya subido ahora. Supongamos que sea de 2,5 ms. Aún tenemos entonces 400 servicios.

Si ahora ponemos otra CPU, con las hebras ejecutadas por cualquiera de las dos CPUs, volvemos a 444 servicios con dos hebras, y a 500 servicios con tres hebras o más.

Las hebras son útiles también para los clientes, no sólo para los servidores.

Arquitecturas de servidores multihebra

Una posibilidad es la arquitectura del banco de trabajadores

- Figura 6.5

La cola puede tener prioridades (o puede haber varias colas).

Otras, las de hebra por petición, hebra por conexión y hebra por objeto

- Figura 6.6

Las dos últimas ahorran en gastos de creación y destrucción de hebras, pero pueden dar lugar a esperas.

Los modelos han sido expuestos en el contexto de CORBA, pero son generales.

Son modelos ``estándar" en programación concurrente, en todo caso.

Hebras dentro de los clientes

Las hebras son útiles también para los clientes, no sólo para los servidores.

- Figura 6.5

El cliente no necesita respuesta.

Pero la llamada remota suele bloquear al cliente, incluso cuando no hace falta esperar.

Con este esquema sólo se le bloquea cuando se llena el buffer.

Otro ejemplo típico de clientes multihebra son los hojeadores (browsers) web, donde es esencial que se puedan gestionar varias peticiones de páginas al mismo tiempo debido a lo lento que se suelen recibir.

Hebras frente a múltiples procesos

Las hebras son mucho más eficientes en multiplexación (y creación y destrucción) y además permiten compartición eficiente de recursos (memoria y otros).

Estados de procesos y hebras

- Figura 6.7

Al ser el estado de las hebras menor, su creación y, sobre todo, su multiplexación, es menos costosa. (Pero programar con hebras es más difícil y produce más errores --¡desde luego!--).

En la creación y destrucción, 11 ms. frente a 1 ms.

El cambio de contexto supone el cambio de estado (registros) del procesador y el cambio de dominio (espacio de direcciones y modo de ejecución del procesador).

Lo que más cuesta es el cambio de dominio.

Si son hebras del mismo proceso, o llamada al kernel estando éste en el mismo espacio de memoria, no hay cambio de dominio. Si es entre diferentes procesos (o entre espacio de usuario y de kernel), sin embargo, es mucho más costoso.

1,8 ms. entre procesos y 0,4 ms. entre hebras del mismo proceso. Diez veces menos si no hay que ir al kernel (planificador de hebras en espacio de usuario).

Y está también el problema de las tablas de páginas y la cache. Explicar caches direccionadas lógica y físicamente. Importante.

También eficiencia adicional por comunicación a través de memoria compartida (¡y peligro!).

Programación con hebras

Es la programación concurrente tradicional, sólo que en un nivel de abstracción muy bajo. Hay lenguajes que permiten subir el nivel: Ada95, Modula-3, Java, etc.

Los mismos problemas y soluciones clásicos: carreras, regiones críticas, semáforos, cerrojos, condiciones, etc.

Varias versiones: C threads (Mach), ``Procesos ligeros" de SunOS, Solaris threads, Java threads, ...

En Java hay métodos para crear, destruir y sincronizar hebras.

- Figura 6.8

Vida de las hebras

Las hebras nacen en la misma máquina virtual (JVM, Java es un lenguaje interpretado) que su progenitor, y en el estado SUSPENDED.

El método start() les arranca, y empiezan a ejecutar el código del método run().

Tanto JVM como las hebras que tiene ejecutan sobre el SO subyacente.

Hay prioridades (setPriority)... ¿hay prioridades realmente?...

El final de run() o destroy() acaba la vida de las hebras.

Las hebras pueden agruparse. Los grupos son útiles para protección y para gestión de prioridades (``techo" de prioridad por grupo).

Sincronización de las hebras

Las variables globales del proceso son compartidas por las hebras. Solo tienen copias propias de la pila y de las variables locales de las subrutinas.

En Java, los métodos puede ser synchronized y entonces se ejecutan con exclusión mutua de otros métodos sincronizados para el mismo objeto (o clase, si son métodos de clase).

También se pueden sincronizar (solo) bloques dentro de un método, y no el método entero.

Hay también ``señales" para realizar sincronización ``productor/consumidor". Con métodos predefinidos wait y notify (y notifyAll).

Y también join e interrupt

- Figura 6.9

Otros mecanismos como semáforos pueden implementarse encima.

También se pueden mezclar métodos sincronizados y no sincronizados dentro de la misma clase. Esto no es lectores/escritores, ¡ojo!

Y solo hay una condición por objeto.

Y no se usa el modelo de ``cáscara de huevo"...

Planificación de hebras

Expulsiva (preemptive) o no expulsiva (non-preemptive).

La segunda garantiza exclusión mutua. La primera es necesaria para tiempo real y multiprocesadores.

Con la segunda se suele usar yield para evitar el monopolio.

Hay una nueva versión de Java para tiempo real (crítico, Hard Real-time programming).

En tiempo real suele hacer falta un mayor control de la planificación: planificador como parte de la aplicación (al estilo de Modula-2).

Implementación de Hebras

Una forma, como parte de la biblioteca del sistema. Ejecuta en el nivel de usuario. Es lo que se hace en los procesos ligeros de SunOS. El kernel no sabe de hebras, sólo de procesos.

Además, no necesita cambiar el kernel original del SO

Pero no hay prioridades globales ni se pueden usar múltiples CPUs dentro del mismo proceso. También, el bloqueo de una hebra bloquea al proceso (la E/S asíncrona complica mucho los programas, y si es un fallo de página no se puede hacer nada).

Se pueden combinar ambos esquemas. En Mach, el kernel recibe hints del nivel de aplicación (número y tipo de procesadores, gang scheduling, etc.).

En Solaris 2 hay planificación jerárquica. Un proceso puede crear crear uno o más ``procesos ligeros" (hebras del kernel) y hay también hebras de nivel de usuario.

Y un planificador de nivel de usuario asigna cada hebra de nivel de usuario a una hebra del kernel.

Permite combinar las ventajas (¡y desventajas!) de ambos modelos.

En otros sistemas, la planificación jerárquica es más complicada: hay colaboración estrecha entre el kernel y el planificador (del nivel de usuario).

Verlo primero con una sola CPU.

El kernel avisa al planificador del nivel de usuario (mediante una interrupción software) del bloqueo de una hebra al iniciar una operación de E/S, así como de cuando se finaliza esa misma operación de E/S.

Esto se llama un upcall y es otra técnica muy utilizada para disminuir el tamaño del kernel.

Puede que el planificador entre en el estado equivalente a wait, y entonces hay que notificarlo al kernel.

Hay comunicación con el kernel a través de memoria compartida.

Para multiprocesadores, existe el concepto de procesador virtual.

- Figura 6.10

Se piden procesadores virtuales y el kernel informa de su concesión.

El kernel avisa también de procesadores virtuales que se asignan y de procesadores virtuales que se desasignan.

Y también de bloqueos de hebras por E/S y de finalización posterior

El kernel simula una ``máquina virtual" al planificador.

La forma de hacerlo es sutil. En realidad, el kernel solo fuerza la ejecución de cierto código en el planificador.

Y una instrucción especial da el número de procesador en el que se está ejecutando; y las interrupciones, del reloj por ejemplo, son privativas de cada procesador.

En todo caso, con este esquema el kernel sigue controlando la planificación de (asignación de tiempo a) los procesos.

6.5 Comunicación e invocación

La comunicación se usa normalmente para invocar o solicitar servicios.

Se puede hablar de tipos de primitivas, protocolos soportados y flexibilidad (openness), eficiencia de la comunicación, y del soporte que pueda existir o no para funcionamiento con desconexión o con alta latencia.

Primitivas de comunicación

Algunos kernel tienen operaciones específicas ajustadas a la invocación remota. Amoeba, por ejemplo, tiene DoOperation/GetRequest--SendReply.

Es más eficiente que el simple Send-Receive (y más fiable y legible).

Amoeba y otros sistemas tienen también comunicación con grupos o radiado (parcial) (broadcast).

Es importante para tolerancia de fallos, mejora de rendimiento y reconfigurabilidad.

Diversas variantes: como mensajes, como múltiples RPCs, con un sólo valor devuelto, con varios valores devueltos (todos juntos o pidiendo uno a uno), etc.

En la práctica, mecanismos de comunicación de alto nivel tales como RPC/RMI, radiado y notificación de sucesos (parecido a los manejadores de interrupciones), se implementan en middleware y no en el kernel.

Normalmente, sobre un nivel TCP/IP, por razones de transportabilidad, (aunque resulta ``caro").

Protocolos y ``apertura"

Los protocolos se organizan normalmente como una pila (``torre") de niveles.

Conviene que se tengan los niveles normalizados en la industria (TCP/IP), y que además se puedan soportar otros, incluso dinámicamente.

Es lo que apareció (por primera vez) con los streams de UNIX.

Por ejemplo, para portátiles que se mueven por lugares distintos y así ajustan la comunicación, bien en LAN, o bien en WAN.

Esto es lo que se entiende como ``apertura", desde los tiempos de UNIX (hacer un dibujo).

Para algunas aplicaciones, TCP/IP es poco eficiente y conviene saltarlo (por ejemplo, HTTP no debería establecer una conexión por petición).

En el caso más dinámico, el protocolo en particular se decide ``al vuelo" para cada mensaje. por ejemplo en base a técnicas de programación mediante objetos (``dynamic binding" de subrutinas).

6.5.1 Eficiencia en la invocación de servicios

Es un factor crítico en sistemas distribuidos, pues hay muchas invocaciones.

A pesar de los avances en redes, los tiempos de invocación no disminuyen proporcionalmente.

Los costes en tiempo más importantes son de software, no de comunicación.

Costes de invocación

Una llamada al núcleo o una RPC son ejemplos de invocación de servicios.

También puede ser llamada síncrona o asíncrona (no hay respuesta).

Todos suponen ejecutar código en otro dominio, y pasar parámetros en los dos sentidos. A veces, también acceder a la red.

Lo más importante es el cambio de dominio (espacio de direcciones), la comunicación por la red y el coste de la planificación (multiplexación) de flujos de control (hebras).

- Figura 6.11

Invocación a través de la red

Una RPC nula tarda del orden de décimas de milisegundo con una red de 100 Mbits/s. y PCs a 500 Mhz, frente a una fracción de microsegundo en una llamada a procedimiento local.

El coste de la transmisión por la red es sólo de una centésima de milisegundo (unos 100 bytes). Pero hay costes fijos muy importantes que no dependen del tamaño del mensaje.

Para una RPC que solicita datos a un servidor, hay pues un retraso fijo importante cuando los datos son pequeños.

- Figura 6.12

Hay una solución de continuidad cuando el mensaje supera el tamaño del paquete.

Con una red ATM de 150 Mbits/s., el máximo ancho de banda que se ha conseguido, trasmitiendo 64 Kb, es del orden de 80 Mbits/s.

Aparte del tiempo de transmisión por la red, hay otros retrasos software:

Compartición de memoria

Se usan regiones compartidas entre procesos o entre proceso y kernel. Ya se ha mencionado.

Mach lo usa para enviar mensajes localmente, usando automáticamente copy-on-write. Los mensajes están en grupos completos (y adyacentes) de páginas. Muy eficiente y seguro.

El enviador coloca el mensaje en una región aparte.

De vuelta del receive el receptor se encuentra con una nueva región, donde está el mensaje recibido.

Las regiones compartidas (sin copy-on-write) se pueden usar también para comunicar grandes masas de datos con el kernel o entre procesos de usuario. Hace falta entonces sincronización explícita para evitar ``condiciones de carrera".

Elección de protocolo

UDP suele ser más eficiente que TCP, excepto cuando los mensajes son largos.

Pero en general los buffer de TCP puede suponer bajas prestaciones, al igual que el coste fijo de establecer las conexiones.

Lo anterior está muy claro en HTTP, que al ir sobre TCP establece una nueva conexión para cada petición.

Y además, TCP tiene un arranque lento, pues al principio usa una ventana de datos pequeña por si hay congestión en la red.

Por eso, HTTP 1.1 utiliza ``conexiones persistentes", que permanecen a través de varias invocaciones.

También se han hecho experimentos para evitar el buffering automático, a base de juntar varios mensaje pequeños y enviarlos juntos (pues es lo que va hacer el SO operativo en todo caso, pero con mayor coste).

Se ha experimentado incluso cambiando el SO para que no haga buffering (con peticiones HTTP 1.1) y así evitar el coste importante que suponen los plazos (time-outs).

Invocación dentro de un mismo computador

Se presentan de hecho con mucha frecuencia: por uso de servidores en microkernels, y debido a caches grandes.

A diferencia de lo mostrado en la figura 6.11, hay una LRPC (Lightweight Remote Procedure Call) para esos casos. Se ahorra copia de datos, y multiplexación de hebras.

Para cada cliente hay una región compartida, donde hay una o más pilas A (de argumentos).

Se pasa la pila del resguardo llamante, directamente al resguardo del procedimiento llamado.

- Figura 6.13

En una llamada al núcleo no suele haber cambio de hebra. Lo mismo se puede hacer con la LRPC.

El servidor, en vez de crear un conjunto de hebras que escuchan, sólo exporta un conjunto de rutinas (como un monitor clásico). Los clientes se ``ligan" con las rutinas del servidor. Cuando el servidor responde afirmativamente al kernel, éste pasa capabilities al cliente (esto no se muestra en la figura).

Se hace, de nuevo, una llamada ``ascendente" (upcall).

Análisis de LRPC

Del orden de 3 veces más rápida que una RPC local normal. Compromete la migración dinámica, sin embargo.

Pero un bit puede indicar al stub si la llamada es local o remota.

Es complicado, y hay aún otras optimizaciones (para multiprocesadores, por ejemplo).

6.5.2 Operación asíncrona

Internet tiene con frecuencia retrasos grandes y velocidades bajas, así como desconexiones y reconexiones (y computadores portátiles con acceso esporádico, por ejemplo por radio --GSM--).

Una posible solución al problema es operar asíncronamente. Bien con invocaciones concurrentes, o bien con invocaciones asíncronas (no bloqueantes).

Son mecanismos que se usan principalmente en el nivel de middleware, no en el del sistema operativo.

Invocaciones concurrentes

En este primer modelo, el middleware solo tiene operaciones bloqueantes, pero las aplicaciones arrancan hebras múltiples para realizar las invocaciones bloqueantes concurrentemente.

Es el caso de un hojeador de web típico, cuando pide varias imágenes de una misma página concurrentemente usando peticiones HTTP GET (y el hojeador suele también hacer la presentación concurrentemente con la petición).

- Figura 6.14

En el caso concurrente, el cliente tiene dos hebras, cada una de las cuales hace una petición bloqueante (síncrona).

Se aprovecha mejor la CPU del cliente.

Algo parecido ocurre si se hacen peticiones concurrentes a servidores diferentes.

Y si el cliente es multiprocesador, aún se puede obtener más mejora al poder ejecutar sus hebras en paralelo.

Invocaciones asíncronas

Es una invocación no bloqueante que devuelve control tan pronto como el mensaje de invocación se ha creado y está listo para envío.

Hay peticiones que no requieren respuesta. Por ejemplo, las invocaciones CORBA de tipo ``un solo sentido" (oneway), que tienen semántica ``quizás" (maybe).

En otro caso, el cliente utiliza una llamada distinta para recoger los resultados. Es el caso de las ``promesas" del sistema Mercury de Barbara Liskov.

Las promesas son handles que se devuelven inmediatamente con la invocación, y que pueden usarse más adelante para recoger los resultados, mediante la operación primitiva claim.

La operación claim ya sí es bloqueante, si bien existe otra, ready, que tampoco lo es.

Invocaciones asíncronas persistentes

Invocaciones tradicionales como las de un solo sentido en CORBA y las de Mercury van sobre conexiones TCP y fallan si la conexión se rompe. Es decir, si falla la red o se cae el nodo destino.

Para funcionar en modo desconectado, cada vez se usa más un nuevo modelo de invocación asíncrona llamada Invocación asíncrona persistente.

Básicamente, se dejan de usar los plazos (timeouts) que cuando vencen abortan las invocaciones remotas. Y solo las aborta la aplicación cuando lo estima oportuno.

El sistema QRPC (Queued RPC) pone las peticiones en una cola ``estable" en el cliente cuando no hay conexión disponible con el servidor y las envía cuando la conexión se reestablece.

Y pone también las respuestas en una cola en el servidor cuando no hay conexión con el cliente.

Adicionalmente, puede comprimir las peticiones y las respuestas para cuando la conexión se realiza con poco ancho de banda.

Puede usar también enlaces de comunicación diferentes (¡y ``esperar" al cliente con la respuesta almacenada en el sitio siguiente más probable!).

Un aspecto interesante es que puede ordenar la cola de peticiones pendientes por prioridades asignadas por las aplicaciones.

Y esas prioridades las utiliza también a la hora de extraer los resultados de las invocaciones remotas.

6.6 Arquitectura de un sistema operativo

Veremos cual es la arquitectura adecuada para un kernel de sistema distribuido.

Lo más importante es que sea, de nuevo, ``abierto".

Y por que sea ``abierto" (otra vez la figura clásica de UNIX) entendemos ahora que sea flexible (o adaptable):

El principio básico de diseño de SO durante ya mucho tiempo ha sido separar ``reglas" de ``mecanismos"

Por ello, lo ideal es que el kernel implemente solo los mecanismos básicos, permitiendo así que las reglas se implementen sobre él mediante servidores (que se cargan dinámicamente).

Microkernels frente a kernels monolíticos

La diferencia está en cuanta funcionalidad está dentro del kernel, o fuera del mismo en servidores.

Los microkernels (y nanokernels :-) son de hecho poco usados en la práctica, pero su estudio es instructivo.

Con kernels tradicionales como el de UNIX y servidores con RPCs se puede hacer algo en esa línea (DCE, CORBA).

Mejor usar microkernels, que tienen sólo el denominador común.

Son más pequeños y más fáciles de entender y sólo proveen los servicios mínimos para soportar los otros: procesos e IPC local, y espacios de direcciones (y posiblemente gestión básica de periféricos).

- figura 6.15

Los servidores se cargan dinámicamente según se necesiten, y se invocan sus servicios mediante paso de mensajes (principalmente, RPCs).

Los servidores se pueden cargar en espacio de usuario (lo más frecuente), o incluso como procesos del mismo espacio del kernel (caso de Chorus).

Los kernel monolíticos mezclan todo el código y datos, no están bien estructurados.

Los microkernels suelen también emular sistemas operativos tradicionales

En conjunto, se tiene:

- figura 6.16

Los programas de aplicación suelen usar los servicios del kernel a través de subsistemas, bien mediante compiladores de un lenguaje de programación y sistemas de soporte de ejecución (run-time systems), o bien llamando al subsistema de emulación de un S.O. en particular.

Puede haber incluso más de un sistema operativo ejecutando encima del microkernel (casos de MACH y NT). Una idea antigua de IBM (sistema VM).

Comparación

El microkernel es más flexible y simple. Muy importante esto último.

El kernel monolítico es más eficiente. Hablar de números: petición de disco, por ejemplo, y de sincronización.

El monolítico se puede organizar en capas, pero es fácil cometer errores en lenguajes como C y C++.

Si se modifica, es complicado probarlo (todo) de nuevo.

Ineficiencia en microkernels también por cambios de espacios de direcciones, no sólo por comunicación.

Soluciones mixtas

Dos microkernels, Mach y Chorus, empezaron con los servidores ejecutando solo como procesos de nivel de usuario.

De esa forma, la modularidad viene garantizada por los espacios de direcciones.

Con excepción hecha del acceso directo a registros de dispositivos y buffers, que se obtiene mediante llamadas al kernel especiales. Y el kernel también transforma las interrupciones en mensajes.

Pero, por razones de eficiencia, ambos sistemas cambiaron para permitir la carga dinámica de servidores tanto en un espacio de direcciones de usuario como dentro del kernel.

Los clientes interaccionan en los dos casos de igual forma con los servidores, lo cual permite una depuración sencilla del código servidor.

Aunque sigue siendo un riesgo meter los servidores en el kernel.

El sistema SPIN usa un método más sutil: programa en un lenguaje de alto nivel, Modula-3, y el compilador provee el control de acceso (y usa ``notificación de sucesos" para reducir la interacción entre componentes software al mínimo).

Otros sistemas como Nemesis usan un solo espacio de direcciones para el kernel y todos los programas de aplicación (con direcciones de 64 bits es posible) y así no evacúan las caches (lógicas y de la MMU).

L4 ejecuta los servidores en el nivel de usuario pero optimiza la comunicación entre procesos.

Exokernel usa rutinas de biblioteca en vez de rutinas dentro del kernel o servidores en el nivel de usuario, de ahí su nombre. Es más rápido.


Angel Alvarez 2001-10-18