Ir al contenido

8. Sistemas operativos por su estructura

Cómo se clasifican los sistemas operativos según cómo organizan su núcleo, comparando el diseño monolítico tradicional con arquitecturas en capas, microkernel y modulares.

19 min de lectura

Ya hemos discutido anteriormente acerca de los componentes más comunes en un sistema operativo. En esta sección comentaremos cómo se clasifican los distintos sistemas operativos según la organización e interconexión de sus componentes.

Clasificación según funcionalidad en el núcleo

Sección titulada «Clasificación según funcionalidad en el núcleo»

Antes de entrar en el detalle de cada tipo de estructura conviene fijar una clasificación más general y de uso común, que responde a una pregunta distinta: cuánta funcionalidad del sistema operativo se ejecuta en el núcleo, en modo privilegiado, frente a la que se ejecuta como procesos de usuario.

  • Monolítico. Casi toda la funcionalidad del sistema operativo —planificación, gestión de memoria, sistemas de archivos, controladores de dispositivo, etc.— se ejecuta en el núcleo. Es un enfoque muy eficiente, porque los componentes se llaman directamente entre sí sin necesidad de ningún mecanismo de comunicación, pero también el menos seguro y fiable, porque un fallo en cualquier componente puede comprometer todo el sistema.

    Ejemplos de sistemas operativos con núcleo monolítico son el UNIX original y buena parte de sus descendientes, como FreeBSD, OpenBSD, Solaris o los sistemas Linux. También es común entre los sistemas operativos para dispositivos empotrados, como los de control industrial o IoT.

  • Microkernel. Solo las funciones mínimas imprescindibles —gestión básica de procesos y memoria, comunicación entre procesos— se ejecutan en el núcleo. El resto de los servicios se implementan como procesos de usuario que se comunican con el núcleo y entre sí mediante paso de mensajes. Es un enfoque más seguro y portable, pero con más sobrecarga y, por tanto, peor rendimiento, debido a la comunicación entre procesos.

    Ejemplos de sistemas operativos con núcleo microkernel son QNX, MINIX 3 o ThreadX, además de las primeras versiones de Windows NT.

  • Híbrido. Combina ambos enfoques: parte de los servicios se ejecutan en el núcleo por motivos de rendimiento, mientras que el resto se mantienen como procesos de usuario. Suele ser el resultado de que un sistema microkernel que evolucionó para mejorar su rendimiento —como veremos que ocurrió con Windows NT— o de que un sistema monolítico adopte mecanismos propios de los microkernel para aislar ciertos componentes y ganar en seguridad y fiabilidad. Por eso algunas fuentes1 consideran que estos no son diferentes a los monolíticos, sino que los fabricantes adoptan este término con fines de marketing, para distinguir sus sistemas de los monolíticos tradicionales y no reconocer el fracaso de su apuesta inicial por el microkernel.

    Sistemas operativos como los Windows actuales y macOS son ejemplos de sistemas híbridos. En particular, el caso de macOS es especialmente paradigmático de este tipo de sistemas, porque su núcleo, denominado XNU, combina un microkernel (Mach) con un núcleo monolítico (FreeBSD) y un framework específico para el desarrollo de controladores de dispositivo. La idea detrás de este diseño es que el núcleo monolítico basado en FreeBSD proporcione la mayor parte de la funcionalidad del sistema operativo garantizando la compatibilidad con los estandares de los sistemas UNIX.

Esta clasificación es ortogonal a la que usaremos en el resto del capítulo —sencilla, en capas, microkernel y modular—, que atiende a cómo se organiza internamente el código dentro del núcleo, no a dónde se ejecuta. De hecho, como veremos, las estructuras sencilla, en capas y modular son todas variantes de diseño monolítico, que difieren en cómo compartimentan el código del núcleo, pero todas mantienen la inmensa mayoría de su funcionalidad dentro de él.

Los primeros sistemas operativos eran de estructura sencilla, como el MS-DOS o el UNIX original, porque realmente no tenían una estructura bien definida: los componentes no estaban bien separados ni las interfaces entre ellos eran estables ni estaban bien definidas. Lógicamente, la mayor parte de la funcionalidad del sistema operativo se proporcionaba desde el núcleo, que era un bloque de código único. Se trata, por tanto, de una de las variantes de sistemas monolíticos.

Estos primeros sistemas se diseñaron así en gran medida debido a las limitaciones del hardware de su época. Al crecer más allá de las previsiones originales, rápidamente se hizo evidente que en este tipo de estructura no es sencillo añadir nuevas funcionalidades y que son difíciles de mantener. En la actualidad, este tipo de estructura ya no se usa en sistemas de propósito general, sino en sistemas que deben ejecutarse en hardware muy limitado, como en sensores conectados, termostatos, sistemas de control o electrodomésticos.

Uno de los ejemplos paradigmáticos de sistema de escritorio de este tipo es MS-DOS, que se diseñó para ejecutarse en el Intel 8086 y sus descendientes. Como el Intel 8086 no proporcionaba un modo dual de operación, los diseñadores del sistema no podían evitar que los programas de usuario accedieran directamente al hardware ni tenían forma de proteger las distintas partes del sistema operativo.

Esquema de la estructura de MS-DOS.

En la figura anterior se puede observar un esquema de la estructura de MS-DOS. En teoría las aplicaciones de usuario solo deberían utilizar las funciones del programa residente del sistema —el núcleo de MS-DOS— que, a su vez, se comunicaba con el hardware a través de los controladores de dispositivo. Pero en la práctica podían acceder directamente a cualquier parte de la memoria y a cualquier dispositivo. Por ejemplo, muchos juegos de la época accedían directamente a la memoria de vídeo para dibujar en pantalla, e implementaban sus propios controladores de sonido para obtener un mejor rendimiento, sin pasar por el núcleo ni por los controladores de dispositivo del sistema operativo. También eran comunes las herramientas que accedían directamente a los discos para repararlos, recuperar archivos borrados o incluso formatearlos de forma no estándar para aumentar su capacidad. Disponiendo de esa libertad, un programa erróneo cualquiera podía corromper el sistema completo.

Otro ejemplo es el de UNIX original, donde sí había una separación clara entre procesos de usuario y código del sistema, dado que los mainframes de la época ya proporcionaban un modo dual de operación. Sin embargo, agrupaba mucha funcionalidad en el núcleo del sistema, como se puede observar en la figura siguiente.

Esquema de la estructura de UNIX.

El núcleo proporciona la planificación de CPU, la gestión de la memoria, el soporte de los sistemas de archivos y muchas otras funcionalidades del sistema operativo. En general se trata de una enorme cantidad de funcionalidad que es difícil de implementar y mantener, si no se compartimenta adecuadamente. Por su parte, las aplicaciones accedían a estas funcionalidades desde el modo usuario a través de llamadas al sistema, de forma similar a como lo hacen los sistemas actuales.

Los sistemas con estructura en capas organizan el núcleo dividiéndolo en capas, buscando aislar mejor sus componentes que en la estructura sencilla. Se caracterizan por:

  • La funcionalidad se divide en capas, de tal forma que una capa solo utiliza funciones y servicios de la capa inmediatamente inferior y lo hace a través de una interfaz bien definida.

  • Como en la programación orientada a objetos, cada capa oculta a la capa superior los detalles de su implementación. Por ejemplo, las estructuras de datos internas que usa y las operaciones o el hardware de la capa inferior que utiliza.

  • Son más fáciles de extender que los sistemas con estructura sencilla porque las capas hacen que el código esté mejor compartimentado. Por ejemplo, al corregir un bug o añadir una nueva funcionalidad solo hay que preocuparse de su efecto en la capa a la que afecta y no en todo el código del núcleo —siempre que no se altere la interfaz de la capa con el exterior—.

  • Son algo menos eficientes que los sistemas de estructura sencilla. En cada capa los argumentos son transformados y los datos necesarios deben de ser transferidos al invocar operaciones en la capa inferior, por lo que cada una añade cierto nivel de sobrecarga al funcionamiento del sistema.

  • Son sistemas monolíticos, dado que gran parte de la funcionalidad del sistema se implementa en el núcleo, aunque ahora este esté compartimentado en capas.

Esquema de la estructura de IBM OS/2.

Un ejemplo paradigmático de este tipo de sistemas operativos es OS/2. Al compararlo con el esquema de la estructura de UNIX en la figura anterior, se observa que la funcionalidad del núcleo de OS/2 en modo privilegiado está claramente compartimentada en tres capas: subsistemas, núcleo y controladores de dispositivo. Ahora bien, ¿por qué solo 3 capas y no más? Como veremos en el siguiente apartado, la definición de las capas y sus funcionalidades debe ser planificada cuidadosamente debido a la restricción de que una capa solo puede utilizar los servicios de las capas inferiores. Por eso, ya no existen sistemas operativos de propósito general que utilicen puramente este tipo de estructura en capas. En su lugar, es preferible utilizar la estructura modular, que presenta las mismas ventajas y evita estas dificultades en el diseño.

Es importante tener en cuenta que diseñar un sistema con estructura en capas no es tan sencillo como pudiera parecer. La definición de las capas y sus funcionalidades debe ser planificada cuidadosamente debido a la restricción, comentada anteriormente, de que una capa solo puede utilizar los servicios de las capas inferiores. Veamos un ejemplo de cómo esta restricción puede complicar el diseño de un sistema operativo:

  1. El planificador de la CPU suele tener información de los procesos que están en la memoria. Parte de esa información puede ser almacenada en el disco para aumentar la memoria principal disponible. Esto nos debería llevar a pensar que la gestión del almacenamiento secundario debe ir en una capa inferior a la del planificador de la CPU, para que así el segundo pueda pedir al primero que guarde los datos en disco.

  2. Sin embargo, el planificador de la CPU debe asignar la CPU a otro proceso cuando el proceso que actualmente la ocupa solicita alguna operación de E/S —lo típico en multiprogramación—. Como es la gestión del almacenamiento secundario el que debe pedir una operación al planificador de la CPU, ahora el primero debe estar sobre el segundo.

La solución a esta dependencia circular es hacer que ambos componentes estén en la misma capa. Este tipo de dependencias no son raras. Ocurre en muchos otros casos, ya que los componentes del sistema operativo suelen depender mucho unos de otros. Al final, la solución de compromiso es tender hacia sistemas con muy pocas capas donde cada una tiene mucha funcionalidad. Esto limita mucho las ventajas de esta técnica porque no permite compartimentar el núcleo tanto como sería deseable.

Los sistemas con estructura microkernel rompen con el enfoque monolítico de las estructuras anteriores, manteniendo en el núcleo solo la funcionalidad imprescindible y trasladando el resto a procesos de usuario. Se caracterizan por:

  • Eliminar todos los componentes no esenciales del núcleo e implementarlos como procesos de usuario.

  • Estrictamente hablando, un núcleo microkernel solo proporciona funciones mínimas de gestión de procesos y de memoria y algún mecanismo de comunicación entre procesos. Sin embargo, hay que tener en cuenta que hay poco consenso a este respecto, por lo que algunos microkernel reales incluyen en el núcleo algunas funcionalidades adicionales.

  • Dado que los componentes del sistema están aislados unos de otros —ya que se implementan como procesos de usuario— necesitan un mecanismo de comunicación entre procesos que permite a los procesos de los usuarios solicitar servicios a los otros componentes del sistema, o a los propios componentes del sistema comunicarse entre sí y solicitar servicios. Generalmente esta comunicación se implementa mediante paso de mensajes (ver el capítulo «Comunicación entre procesos»), pero algunas implementaciones utilizan memoria compartida (ver el apartado «Memoria compartida») buscando mejorar el rendimiento.

En la siguiente figura, por ejemplo, se puede observar un esquema del sistema microkernel de MINIX 3. Este núcleo es muy pequeño —apenas tiene 5000 líneas de código— por lo que la mayor parte de la funcionalidad reside en los procesos de servicios y de controladores de dispositivo que se ejecutan aislados en modo usuario.

Esquema de la estructura microkernel de MINIX 3.

MINIX 3 es un sistema compatible POSIX, así que soporta las funciones de la librería del sistema definidas por este estándar. Sin embargo, la librería del sistema no hace llamadas directas al núcleo para solicitar la operación correspondiente —es decir, la función open() no realiza directamente una llamada al sistema equivalente—, sino que usa las llamadas al sistema de paso de mensajes proporcionadas por el núcleo para enviar un mensaje al servidor de sistema de archivos, que es el que realmente implementa la operación abrir archivos —por ejemplo, usando ipc_sendrec() para solicitar la apertura del archivo y esperar la respuesta del servidor del sistema de archivos—. Este comportamiento descrito puede parecer igual que en los sistemas monolíticos, pero hay una diferencia importante: en los sistemas monolíticos la petición de abrir archivos es atendida directamente por el núcleo, mientras que en los sistemas microkernel es atendida por un proceso de usuario que implementa el servicio de sistema de archivos. El núcleo proporciona las llamadas al sistema que permiten usar el mecanismo de paso de mensajes para enviar y recibir mensajes entre procesos, pero no implementa la funcionalidad de los servicios del sistema operativo.

Como el servidor del sistema de archivos también está aislado de los otros servicios del sistema y del hardware, para poder abrir o leer un archivo puede necesitar enviar mensajes a otros servidores o controladores de dispositivo. Incluso es muy probable que alguno de los servidores y controladores tenga que usar otras llamadas al sistema para solicitar al núcleo alguna operación privilegiada que no se implementa en el modo usuario, como la reserva de memoria, el acceso real al hardware de los dispositivos —fundamental para que los controladores de dispositivo puedan hacer su trabajo— o acceso a información del sistema —como la lista de procesos—.

Entre los beneficios de estos sistemas operativos se incluyen:

  • Facilidad a la hora de añadir nuevas funcionalidades. Los nuevos servicios son añadidos como aplicaciones de nivel de usuario, por lo que no es necesario hacer modificaciones en el núcleo. Desarrollar en el modo privilegiado siempre es más peligrosos que en el modo usuario porque los errores pueden ser catastróficos: bloqueo o caída del sistema, corrupción de datos, etc.

  • Facilidad a la hora de llevar el sistema a otras plataformas. Puesto que el núcleo es muy pequeño, resulta muy sencillo de portar a otras plataformas.

  • Más seguridad y fiabilidad. Puesto que la mayor parte de los servicios se ejecutan como procesos separados de usuario, un servicio que falla no puede afectar a otros ni puede ser utilizado para ganar acceso a otros servicios o al núcleo. Además se pueden implementar estrategias para mejorar la tolerancia a fallos, como reiniciar un servicio que ha fallado, como si fuera un programa cualquiera.

El principal inconveniente es el pobre rendimiento que puede tener. Este está causado por la sobrecarga que introduce el intercambio de mensajes mediante el mecanismo de comunicación y la correspondiente conmutación de tareas en la CPU, necesaria para que el proceso que atiende cada solicitud pueda resolver la petición del proceso de usuario.

Por ejemplo, Microsoft Windows NT —del que recordemos que derivan los Windows actuales— nació con una estructura de microkernel en capas donde una parte importante de los servicios eran proporcionados por unos procesos de usuario llamados subsistemas, mientras que el término en capas se refiere a la organización interna de los componentes en el modo privilegiado, que estaba compartimentado en capas, separando componentes como la gestión de memoria y de procesos del núcleo y este, a su vez, de una capa que se encargaba del acceso al hardware —llamada HAL, de Hardware Abstraction Layer—.

Aprovechando esta arquitectura, el sistema operativo podía mostrar diferentes personalidades o entornos operativos a través del uso de subsistemas ambientales —que también se ejecutaban como procesos de usuario— y que permitían ejecutar aplicaciones de diferentes sistemas operativos, como Microsoft Windows, POSIX o OS/2. Las aplicaciones se comunicaban con estos subsistemas y con otros servicios utilizando un mecanismo de comunicación denominado LPC (Local Inter-Process Communication).

Lamentablemente, se trataba de una arquitectura muy interesante, pero con un rendimiento muy pobre, especialmente en lo relativo a las operaciones gráficas. La pérdida de rendimiento respecto a Microsoft Windows 95 era tan importante que los diseñadores se vieron obligados a mover más servicios al espacio del núcleo en la versión 4.0 de Windows NT. El resultado es que Windows NT 4.0 y sus sucesores tienen una arquitectura más monolítica que microkernel, ya que aunque muchos servicios siguen siendo proporcionados por procesos de usuario, esto solo ocurre con aquellos donde el rendimiento no es un factor crítico. Por este motivo actualmente se considera que los Windows actuales son sistemas híbridos, a medio camino entre un sistema microkernel y uno monolítico.

Aunque se puede utilizar en cualquier ámbito, la estructura microkernel se utiliza, principalmente, en sistemas empotrados críticos donde la seguridad y la fiabilidad son especialmente importantes, como en la automoción o los equipos médicos. A diferencia de la estructura sencilla, que actualmente se utiliza en sistemas empotrados con hardware muy limitado, la estructura microkernel necesita un hardware algo más potente que admita modo dual de operación, para así poder ejecutar los procesos de usuario y el núcleo en espacios de memoria separados. A cambio ofrece el aislamiento y la tolerancia a fallos que estos sistemas críticos requieren.

Buenos ejemplos son QNX, el ya mencionado MINIX 3 y ThreadX. Los tres son sistemas operativos de tiempo real que basan en la estructura microkernel su estabilidad para tareas críticas. Fuera de ese ámbito, Google ha apostado también por el microkernel en Fuchsia, pensado como sistema operativo de propósito general y posible sustituto de Android.

Actualmente se puede afirmar que la mayor parte de los sistemas operativos de propósito general, que se instalan tanto en sistemas de escritorio como en servidores, son de estructura modular. También en los smartphones, smart TV y otros dispositivos «inteligentes» y, en general, en muchos sistemas empotrados.

Los sistemas con estructura modular se caracterizan por:

  • Dividir las funcionalidades del núcleo en módulos o subsistemas, cada uno de los cuales implementa funciones y servicios concretos y se comunican entre sí a través de una interfaz bien definida.

  • Como en la programación orientada a objetos, cada módulo oculta al resto los detalles de su implementación.

  • Todos los módulos pueden llamar a funciones de la interfaz de cualquier otro módulo, a diferencia de los sistemas operativos con estructura en capas, donde una capa solo podía usar a la inmediatamente inferior.

  • Suelen disponer de un mecanismo para cargar y descargar módulos en tiempo de ejecución, lo que permite añadir o eliminar funcionalidades del sistema sin necesidad de reiniciarlo. Por lo general, el núcleo dispone de un pequeño conjunto de componentes fundamentales que se cargan siempre durante el arranque y luego se pueden cargar otros módulos adicionales según se necesiten.

  • Suelen ser sistemas monolíticos, dado que gran parte de la funcionalidad del sistema se implementa en el núcleo, aunque ahora este esté compartimentado en módulos. Sin embargo, también hay sistemas modulares híbridos y microkernel, donde se pueden cargar funcionalidades adicionales como módulos en el núcleo.

En la siguiente figura se puede observar un esquema de la estructura modular del núcleo Linux, donde se pueden observar distintos módulos y subsistemas que implementan funcionalidades concretas del sistema operativo y cómo se comunican entre sí. En este caso, no existen restricciones de comunicación entre módulos, como ocurría en los sistemas en capas, por lo que cualquier módulo puede usar a cualquier otro. El único requisito es que la comunicación se haga a través de la interfaz que cada módulo expone al resto del sistema. De esta forma, el núcleo de Linux puede ser extendido con nuevos módulos que implementen nuevas funcionalidades y los módulos existentes pueden ser actualizados sin afectar al resto del sistema, siempre que no se cambie la interfaz que exponen.

Esquema de la estructura del núcleo Linux.

Por tanto, la descomposición en módulos permite que estos sistemas tengan características muy similares a los sistemas microkernel, siendo más fáciles de mantener y extender que los sistemas con estructura sencilla o en capas, ya que cada módulo puede ser desarrollado y probado de forma independiente. Sin embargo, los núcleos modulares, respecto a los microkernel:

  • Son mucho más eficientes, al no necesitar ningún mecanismo de comunicación entre procesos, puesto que los módulos se cargan en la memoria del núcleo y pueden llamarse directamente entre sí. Sin embargo, siguen siendo algo menos eficientes que los sistemas con estructura sencilla, ya que cada llamada entre módulos pasa por la interfaz bien definida que exponen, lo que añade cierta sobrecarga frente a las llamadas directas y sin restricciones de la estructura sencilla.

  • Son menos seguros y fiables, puesto que gran parte de su funcionalidad se ofrece desde el modo privilegiado. Un error en cualquier componente puede comprometer o hacer caer el sistema.

Este tipo de estructura es la utilizada en los sistemas tipo UNIX modernos, como FreeBSD o GNU/Linux. Mientras que sistemas denominados híbridos como macOS y los Windows actuales pueden considerarse modulares, ya que internamente se organizan en componentes modulares y subsistemas, y permiten la carga y descarga de módulos en tiempo de ejecución.

  1. Linus Torvalds, «Hybrid kernel, not NT», Real World Tech Forums, 9 de mayo de 2006.