Historia de los sistemas operativos: del lote a los sistemas móviles
Historia de los sistemas operativos: del lote a los sistemas móviles
En el capítulo 1 definimos qué es un sistema operativo: una capa de software que administra el hardware, abstrae sus detalles y arbitra el acceso de varios programas a recursos que son limitados. Esa definición, tal como la enunciamos, parece obvia. No lo es. Cada una de sus tres partes —administrar, abstraer, arbitrar— apareció en un momento distinto de la historia, como respuesta a un problema concreto que alguien tenía enfrente. Nadie diseñó el sistema operativo completo de una sola vez.
Este capítulo recorre esa construcción por capas. No es una cronología de anécdotas: es un análisis de qué problema técnico existía en cada momento, qué mecanismo se inventó para resolverlo y qué problema nuevo generó ese mecanismo. Ese ciclo —problema, mecanismo, nuevo problema— es el motor de toda la historia, y entenderlo hace que los conceptos de los capítulos siguientes (procesos, planificación, memoria virtual, sistemas de archivos) dejen de parecer definiciones arbitrarias y pasen a ser respuestas a preguntas que ya te vas a haber hecho.
Por qué la historia es material técnico, no cultural
Hay una razón práctica para estudiar esta secuencia. Los sistemas operativos que usamos hoy conservan, en su código y en su interfaz, decisiones tomadas hace cincuenta o sesenta años. Cuando escribes ls > salida.txt estás usando redirección de flujos, un mecanismo de 1970. Cuando tu programa recibe SIGTERM estás usando señales de UNIX. Cuando una API te obliga a llamar close() sobre un descriptor estás pagando el costo de un modelo de recursos que se fijó cuando la memoria se medía en kilobytes.
Más aún: los problemas vuelven. El procesamiento por lotes desapareció de los escritorios y reapareció intacto en los sistemas de colas de trabajos de clústeres de cómputo y en los pipelines de datos. La multiprogramación de particiones fijas volvió como límites de memoria de contenedores. La virtualización de máquina completa, que IBM inventó en los sesenta con CP/CMS, es hoy la base de la nube. Conocer la versión original de cada idea te permite reconocerla cuando vuelve con otro nombre.
flowchart LR
A["Problema:<br/>tiempo de CPU ocioso<br/>entre trabajos"] --> B["Mecanismo:<br/>monitor residente<br/>y lotes"]
B --> C["Nuevo problema:<br/>la CPU espera a la E/S"]
C --> D["Mecanismo:<br/>interrupciones,<br/>canales, spooling"]
D --> E["Nuevo problema:<br/>un solo trabajo<br/>no llena la CPU"]
E --> F["Mecanismo:<br/>multiprogramacion"]
F --> G["Nuevo problema:<br/>trabajos que se pisan<br/>en memoria"]
G --> H["Mecanismo:<br/>proteccion, modo dual,<br/>reubicacion"]
H --> I["Nuevo problema:<br/>latencia inaceptable<br/>para el usuario"]
I --> J["Mecanismo:<br/>tiempo compartido<br/>y expropiacion"]
Este diagrama es el esqueleto del capítulo. Cada flecha es una sección.
Generación cero: máquinas sin sistema operativo
El contexto de los años cuarenta
Las primeras computadoras electrónicas programables aparecieron durante y justo después de la Segunda Guerra Mundial. En Bletchley Park, el equipo del que formaba parte Alan Turing construyó la máquina BOMBE, un dispositivo electromecánico dedicado a reducir el espacio de búsqueda de las claves de la máquina Enigma, y más tarde Colossus, ya electrónico y basado en válvulas, orientado al criptoanálisis del cifrado de Lorenz. Ninguna de las dos era una computadora de propósito general en el sentido moderno, y ninguna tenía nada parecido a un sistema operativo: la “programación” consistía en cablear conexiones y fijar interruptores.
En las máquinas de propósito general de esa primera generación —ENIAC, EDSAC, UNIVAC I y sus contemporáneas— el modelo de trabajo era el siguiente:
- El programador reservaba un bloque de tiempo de máquina, típicamente horas.
- Bajaba físicamente a la sala, con su programa en tarjetas perforadas o cinta de papel.
- Cargaba el programa, lo ejecutaba, observaba las luces del panel frontal.
- Si fallaba, depuraba ahí mismo, mirando registros en binario.
- Al terminar su bloque, se retiraba y entraba el siguiente.
No había software de sistema. El programa tenía acceso directo y total al hardware: escribía en las direcciones de memoria que quisiera, operaba los dispositivos con instrucciones de entrada/salida crudas y, si se equivocaba, la máquina simplemente hacía algo incorrecto hasta que alguien la detenía.
Los tres problemas de este modelo
Primero, el desperdicio de tiempo de máquina. El hardware costaba millones y se depreciaba rápido. El tiempo entre que un programador terminaba y el siguiente empezaba a ejecutar —recoger tarjetas, montar cintas, ajustar el panel— podía superar el tiempo de cómputo útil. La máquina, el recurso más caro de la organización, pasaba la mayor parte del día apagada en términos productivos.
Segundo, la repetición de código. Todo programa que leyera tarjetas necesitaba una rutina de lectura de tarjetas. Todo programa que imprimiera necesitaba una rutina de impresión. Cada programador escribía la suya, con sus propios errores. Esto llevó a las primeras bibliotecas de rutinas de entrada/salida: colecciones de subprogramas que se copiaban al inicio del programa del usuario. Esas bibliotecas son el ancestro directo de las llamadas al sistema, aunque todavía no había ninguna frontera de protección: eran simplemente código que se ejecutaba con los mismos privilegios que el resto.
Tercero, la ausencia de aislamiento. Un error del programador podía dejar la máquina en un estado que exigía reinicio manual. Con un solo usuario a la vez esto era molesto pero tolerable. En cuanto hubo más de un trabajo en la máquina, se volvió inaceptable, y de ahí salió el modo dual de ejecución que veremos más adelante.
Procesamiento por lotes: el monitor residente
La idea
A mediados de los años cincuenta apareció la primera respuesta sistemática al desperdicio de tiempo: si el problema es la transición manual entre trabajos, hay que automatizarla. En vez de que un operador cargue un trabajo por vez, se agrupan varios trabajos en un lote (batch) y un programa pequeño y permanente en memoria se encarga de ejecutarlos uno tras otro sin intervención humana.
Ese programa permanente es el monitor residente, y es el primer sistema operativo reconocible como tal. Ocupa una zona fija de la memoria —típicamente las direcciones bajas—, nunca se descarga, y su ciclo de vida es un bucle infinito muy simple.
stateDiagram-v2
[*] --> Arranque
Arranque --> LeerTarjetaControl: monitor cargado en memoria baja
LeerTarjetaControl --> CargarPrograma: tarjeta $JOB / $LOAD
CargarPrograma --> Ejecutar: transferir control al area de usuario
Ejecutar --> Ejecutar: el programa corre sin supervision
Ejecutar --> Finalizacion: fin normal o error fatal
Finalizacion --> Contabilizar: registrar tiempo y recursos
Contabilizar --> LeerTarjetaControl: siguiente trabajo del lote
LeerTarjetaControl --> [*]: lote agotado
El lenguaje de control de trabajos
El monitor necesitaba saber, para cada trabajo, qué compilador usar, cuánta memoria reservar, qué cintas montar y cuál era el límite de tiempo. Esa información no podía venir del programa mismo: tenía que estar en una capa por encima. Así nació el lenguaje de control de trabajos o JCL (Job Control Language), un conjunto de tarjetas especiales, marcadas con un carácter distintivo en la primera columna, que el monitor interpretaba en vez de ejecutar.
Un mazo de tarjetas típico se veía conceptualmente así:
$JOB USUARIO=CONTABILIDAD TIEMPO=00:05:00 MEMORIA=32K
$FORTRAN
PROGRAM NOMINA
INTEGER I, TOTAL
TOTAL = 0
DO 10 I = 1, 100
TOTAL = TOTAL + I
10 CONTINUE
WRITE (6, 20) TOTAL
20 FORMAT (' TOTAL = ', I6)
STOP
END
$LOAD
$RUN
$DATA
1 2 3 4 5
$END
Cada línea que empieza con $ es una directiva para el monitor, no código del usuario. $JOB abre el trabajo y declara sus límites; $FORTRAN invoca al compilador; $LOAD y $RUN cargan y ejecutan el binario resultante; $DATA marca dónde empiezan los datos de entrada; $END cierra el trabajo.
Esa separación —metadatos de ejecución fuera del programa— es una de las abstracciones más duraderas de la historia. Es exactamente la misma idea que hay detrás de un Dockerfile, de un manifiesto de Kubernetes, de un archivo .service de systemd o de un script de submission de Slurm. En todos los casos el programa dice qué calcular y una capa externa dice bajo qué condiciones ejecutarlo.
El límite de tiempo y la primera necesidad de hardware especial
TIEMPO=00:05:00 en la tarjeta $JOB no es un adorno. Si un programa entra en un bucle infinito, el monitor tiene que poder recuperar el control. Pero el monitor no está ejecutando: el programa del usuario tiene la CPU. La única forma de que el monitor recupere el control sin colaboración del programa es que el hardware lo interrumpa.
Aquí aparece el temporizador (timer), un circuito que decrementa un contador con cada pulso de reloj y que, al llegar a cero, genera una interrupción que fuerza a la CPU a saltar a una rutina del monitor. Es el primer caso claro de un principio que va a repetirse durante todo el curso: el sistema operativo no puede garantizar nada que el hardware no le permita garantizar. Sin temporizador no hay límite de tiempo; sin límite de tiempo no hay multiprogramación segura; sin eso no hay tiempo compartido.
Lo que el lote mejoró y lo que no
El procesamiento por lotes eliminó el tiempo muerto entre trabajos. Un lote de cincuenta programas podía correr toda la noche sin que nadie tocara la máquina. La utilización de CPU subió de forma dramática.
Pero introdujo un costo nuevo: el tiempo de respuesta. Un programador entregaba su mazo de tarjetas en la ventanilla por la mañana y recibía la impresión de resultados por la tarde o al día siguiente. Si tenía un error de sintaxis en la línea 3, se enteraba doce horas después. Este ciclo de retroalimentación catastrófico es lo que va a motivar, una década más tarde, el tiempo compartido.
Y quedó un problema técnico sin resolver: durante la ejecución de un trabajo, la CPU seguía deteniéndose cada vez que había que leer una tarjeta o imprimir una línea.
El cuello de botella de la entrada/salida
La diferencia de escalas
Los dispositivos mecánicos son varios órdenes de magnitud más lentos que la electrónica. La tabla siguiente da órdenes de magnitud representativos de la época del lote y su equivalente moderno, para que la comparación sea intuitiva.
| Operación | Tiempo típico (años 60) | Equivalente moderno | Ciclos de CPU perdidos |
|---|---|---|---|
| Ejecutar una instrucción | ~2 microsegundos | ~0.3 nanosegundos | 1 |
| Leer una tarjeta perforada | ~60 milisegundos | — | ~30.000 |
| Imprimir una línea | ~100 milisegundos | — | ~50.000 |
| Posicionar cabezal de disco | ~50 milisegundos | ~5 ms (HDD) | ~25.000 |
| Rebobinar una cinta | varios segundos | — | millones |
| Leer un sector de SSD NVMe | — | ~50 microsegundos | ~150.000 |
| Solicitud de red al otro continente | — | ~150 ms | ~500 millones |
La última columna es la que importa. La proporción entre “lo que tarda la CPU” y “lo que tarda el mundo exterior” nunca dejó de ser enorme; lo único que cambió fue la escala absoluta. Por eso los mecanismos inventados en 1960 para no desperdiciar esos ciclos siguen siendo los mismos que usamos hoy con otros nombres.
Espera activa: el modelo ingenuo
El modelo original de entrada/salida era por sondeo o polling: la CPU escribía un comando en el controlador del dispositivo y luego leía repetidamente un registro de estado hasta que el bit “listo” cambiara.
/* Modelo conceptual de E/S por sondeo.
Compilable con: gcc -o sondeo sondeo.c
Simula el registro de estado de un dispositivo lento. */
#include <stdio.h>
#include <stdint.h>
#define ESTADO_OCUPADO 0
#define ESTADO_LISTO 1
/* Simulacion: el dispositivo tarda 30000 "ciclos" en estar listo. */
static long ciclos_restantes = 30000;
static long ciclos_gastados_esperando = 0;
int leer_registro_estado(void) {
if (ciclos_restantes > 0) {
ciclos_restantes--;
return ESTADO_OCUPADO;
}
return ESTADO_LISTO;
}
int main(void) {
printf("Iniciando lectura por sondeo...\n");
/* Espera activa: la CPU no hace nada util aqui. */
while (leer_registro_estado() == ESTADO_OCUPADO) {
ciclos_gastados_esperando++;
}
printf("Dato disponible.\n");
printf("Ciclos de CPU consumidos en espera activa: %ld\n",
ciclos_gastados_esperando);
printf("Trabajo util realizado durante la espera: 0\n");
return 0;
}
El programa imprime treinta mil ciclos gastados y cero trabajo útil. Ese es exactamente el problema. La espera activa no está mal en todos los casos —en secciones críticas de kernel muy cortas se sigue usando, en forma de spinlocks—, pero para dispositivos lentos es un desperdicio inaceptable.
Interrupciones
La solución fue invertir la relación. En vez de que la CPU pregunte al dispositivo, el dispositivo avisa a la CPU cuando terminó. El mecanismo es la interrupción: una señal eléctrica que hace que la CPU, al terminar la instrucción en curso, guarde su estado, consulte una tabla de vectores de interrupción y salte a la rutina de atención correspondiente.
sequenceDiagram
participant P as Programa de usuario
participant CPU as CPU
participant SO as Rutina de interrupcion
participant D as Controlador de disco
P->>CPU: solicita lectura de bloque
CPU->>D: escribe comando en registros del controlador
CPU->>SO: marca el trabajo como bloqueado
SO->>CPU: entrega la CPU a otro trabajo listo
Note over CPU: la CPU ejecuta trabajo util<br/>durante los ~50 ms de la E/S
D-->>CPU: linea de interrupcion activada
CPU->>CPU: termina instruccion en curso
CPU->>CPU: guarda contador de programa y registros
CPU->>SO: salta al vector de interrupcion del disco
SO->>SO: copia el dato, marca el trabajo como listo
SO->>CPU: restaura contexto del trabajo interrumpido
CPU->>P: continua ejecucion
Este diagrama contiene, en miniatura, el concepto central de todo el curso. La secuencia “guardar contexto, atender evento, restaurar contexto” es el cambio de contexto, y todo lo que hace un sistema operativo multitarea es una elaboración de esta idea.
Canales de E/S y DMA
Las interrupciones resolvieron la espera, pero quedaba un residuo: transferir los datos byte por byte seguía consumiendo la CPU. Un bloque de 4 KB copiado a mano son 4096 instrucciones de transferencia.
La respuesta fue delegar la transferencia a hardware dedicado. IBM llamó canales a estos procesadores auxiliares: pequeñas unidades que ejecutan su propio programa de canal, mueven los datos directamente entre el dispositivo y la memoria principal, y solo interrumpen a la CPU al final de la transferencia completa. La versión que sobrevivió en la arquitectura de las computadoras personales se llama DMA (Direct Memory Access).
El efecto sobre el diseño del sistema operativo es profundo: la CPU deja de ser el único motor de la máquina. A partir de aquí hay concurrencia real de hardware, y con ella la necesidad de sincronización, que será el tema de capítulos posteriores.
Spooling
Con canales e interrupciones aparece una técnica que combina las dos: el spooling, de Simultaneous Peripheral Operation On-Line. La idea es interponer el disco entre los programas y los dispositivos lentos.
En vez de que el programa lea directamente de la lectora de tarjetas, un proceso del sistema copia todo el lote de tarjetas al disco por adelantado; el programa lee del disco, que es cientos de veces más rápido. En vez de que el programa escriba directamente en la impresora, escribe en un archivo de disco, y un proceso del sistema lo imprime después, a su ritmo.
flowchart TD
subgraph Entrada
L["Lectora de tarjetas"] -->|proceso de spool-in| DE[("Cola de entrada<br/>en disco")]
end
subgraph Ejecucion
DE --> M["Monitor / planificador"]
M --> T1["Trabajo A"]
M --> T2["Trabajo B"]
M --> T3["Trabajo C"]
T1 --> DS[("Cola de salida<br/>en disco")]
T2 --> DS
T3 --> DS
end
subgraph Salida
DS -->|proceso de spool-out| I["Impresora"]
end
El spooling tuvo dos consecuencias que sobrevivieron intactas.
La primera es que, al tener varios trabajos ya cargados en disco esperando, el sistema puede elegir cuál ejecutar. No está obligado a respetar el orden de llegada. Eso es planificación de trabajos, y es el origen directo de todos los algoritmos de planificación que veremos más adelante.
La segunda es la propia noción de cola de impresión, que sigue existiendo con ese nombre. En un Linux moderno, CUPS es literalmente un sistema de spooling; el comando conserva incluso las siglas históricas:
# Enviar un archivo a la cola de impresion (spool)
lp documento.pdf
# Ver la cola: los trabajos estan en disco, no en la impresora
lpstat -o
# Ubicacion tradicional del area de spool en sistemas tipo UNIX
ls -la /var/spool/
El directorio /var/spool de cualquier distribución actual es un fósil viviente de 1960: ahí siguen viviendo las colas de impresión, de correo y de tareas programadas.
Multiprogramación
El problema que quedaba
Con interrupciones y spooling, un trabajo ya no bloquea la CPU mientras espera. Pero si solo hay un trabajo en memoria, la CPU sigue sin tener nada que hacer durante esa espera. La utilización de un trabajo limitado por E/S puede caer por debajo del 20%.
La solución es evidente en retrospectiva y fue revolucionaria en su momento: mantener varios trabajos en memoria simultáneamente. Cuando uno se bloquea esperando E/S, el sistema le da la CPU a otro.
El modelo formal de utilización
Existe una estimación clásica y sencilla para razonar sobre esto. Si cada trabajo pasa una fracción p de su tiempo esperando E/S, y hay n trabajos en memoria de forma independiente, la probabilidad de que todos estén esperando a la vez es p^n. Por lo tanto la utilización esperada de la CPU es:
utilizacion = 1 - p^n
El modelo asume independencia entre trabajos, cosa que no es exacta en la realidad (compiten por el mismo disco), pero captura bien la forma de la curva. Podemos calcularla:
#!/usr/bin/env python3
"""Utilizacion de CPU segun el grado de multiprogramacion.
Ejecutar con: python3 utilizacion.py
"""
def utilizacion(p: float, n: int) -> float:
"""p: fraccion de tiempo que un trabajo pasa esperando E/S.
n: cantidad de trabajos en memoria (grado de multiprogramacion)."""
return 1.0 - p ** n
def barra(valor: float, ancho: int = 40) -> str:
llenos = int(round(valor * ancho))
return "#" * llenos + "." * (ancho - llenos)
def main() -> None:
perfiles = {
"limitado por CPU (p=0.20)": 0.20,
"mixto (p=0.50)": 0.50,
"limitado por E/S (p=0.80)": 0.80,
"muy limitado (p=0.90)": 0.90,
}
for nombre, p in perfiles.items():
print(f"\n{nombre}")
print(f"{'n':>3} {'utilizacion':>11} grafico")
for n in range(1, 11):
u = utilizacion(p, n)
print(f"{n:>3} {u:>10.1%} {barra(u)}")
print("\nGanancia marginal al pasar de n a n+1 trabajos (p=0.80):")
anterior = utilizacion(0.80, 1)
for n in range(2, 11):
actual = utilizacion(0.80, n)
print(f" {n-1} -> {n}: +{(actual - anterior):.1%}")
anterior = actual
if __name__ == "__main__":
main()
Al ejecutarlo se ve el punto importante: la ganancia marginal decrece rápido. Pasar de 1 a 2 trabajos con p=0.8 sube la utilización del 20% al 36%; pasar de 9 a 10 la sube menos de un punto porcentual. Y cada trabajo adicional consume memoria. Este equilibrio entre grado de multiprogramación y memoria disponible es la tensión que va a dominar el diseño de los sistemas de los años sesenta y setenta, y la que va a producir la memoria virtual.
Los tres problemas nuevos
Poner varios programas en memoria a la vez creó de golpe tres problemas que no existían antes.
Protección de memoria. Si el trabajo A escribe en una dirección que pertenece al trabajo B, lo corrompe. Peor: si escribe sobre el monitor, destruye el sistema. La respuesta fue el hardware de protección: en su forma más simple, un par de registros base y límite. Cada acceso a memoria del programa se compara contra ellos; si cae fuera del rango, la CPU genera una excepción y el control vuelve al sistema operativo.
flowchart TD
A["Instruccion accede a<br/>direccion logica D"] --> B{"D < registro limite?"}
B -->|no| E["Excepcion de<br/>violacion de memoria"]
B -->|si| C["Direccion fisica =<br/>D + registro base"]
C --> D["Acceso a memoria<br/>autorizado"]
E --> F["Trampa al sistema operativo"]
F --> G["El SO aborta el trabajo<br/>y libera sus recursos"]
Este mecanismo, en versiones muchísimo más elaboradas, es la unidad de gestión de memoria o MMU de cualquier procesador actual. El Segmentation fault que ves en tu terminal es el descendiente directo de esa excepción.
Modo dual de ejecución. Los registros base y límite solo protegen si el programa de usuario no puede modificarlos. Por lo tanto la CPU necesita al menos dos modos: modo supervisor o kernel, donde todas las instrucciones son válidas, y modo usuario, donde las instrucciones privilegiadas —cargar registros de protección, manipular la tabla de interrupciones, ejecutar E/S directa— provocan una excepción. El paso de modo usuario a modo supervisor solo puede ocurrir de forma controlada: por interrupción, por excepción o por una instrucción de llamada al sistema que salta a un punto de entrada fijado por el kernel.
Reubicación. Un programa compilado no sabe en qué dirección va a ser cargado, porque depende de qué otros trabajos estén en memoria. Hay que traducir sus direcciones. La solución con base y límite es reubicación en tiempo de ejecución; más adelante llegarían la segmentación y la paginación.
OS/360 y la lección de gestión
El ejemplo canónico de esta era es el IBM System/360, anunciado en 1964, y su sistema operativo OS/360. La ambición era enorme: una única familia de máquinas compatibles entre sí, desde modelos pequeños hasta grandes, con un solo sistema operativo para todas. El System/360 fijó cosas que seguimos usando, como el byte de ocho bits como unidad de direccionamiento y la codificación de caracteres por bytes.
OS/360 se retrasó años, superó ampliamente su presupuesto y llegó con miles de errores conocidos. Fred Brooks, que lo dirigió, escribió después The Mythical Man-Month, donde formuló la observación de que agregar programadores a un proyecto de software atrasado lo atrasa más, porque el costo de comunicación crece cuadráticamente mientras el trabajo se reparte linealmente. Es la primera vez que la ingeniería de software se enfrenta explícitamente a los límites de la complejidad, y el ejemplo que la produjo fue un sistema operativo.
Tiempo compartido
De la utilización a la interactividad
La multiprogramación optimiza la utilización de la CPU. No optimiza el tiempo de respuesta al usuario, que seguía siendo de horas. Un programador que quería probar un cambio de una línea tenía que esperar el ciclo completo del lote.
El tiempo compartido (time-sharing) invierte la prioridad. Acepta perder algo de rendimiento total a cambio de que muchos usuarios en terminales interactivas perciban que la máquina es suya. El mecanismo es una variante de la multiprogramación con dos añadidos decisivos:
- Expropiación por reloj. El sistema no espera a que un proceso se bloquee por E/S: le quita la CPU cuando se le acaba su cuanto o quantum de tiempo, típicamente unas decenas de milisegundos.
- Terminales interactivas. Cada usuario tiene un canal de entrada y salida propio, y el sistema mantiene para él una sesión con estado.
stateDiagram-v2
[*] --> Nuevo
Nuevo --> Listo: admitido por el planificador
Listo --> Ejecutando: el planificador lo elige
Ejecutando --> Listo: expira el cuanto de tiempo<br/>(interrupcion de reloj)
Ejecutando --> Bloqueado: solicita E/S o espera un evento
Bloqueado --> Listo: la E/S termina (interrupcion del dispositivo)
Ejecutando --> Terminado: fin de ejecucion
Terminado --> [*]
Este diagrama de estados es el modelo de proceso que vamos a formalizar en capítulos posteriores. Nació aquí, en el tiempo compartido, porque solo aquí hacen falta los tres estados: sin expropiación no existe la transición de “ejecutando” a “listo”.
CTSS y la demostración de que era posible
El CTSS (Compatible Time-Sharing System), desarrollado en el MIT y demostrado alrededor de 1961, fue el primer sistema de tiempo compartido operativo de forma convincente. Su nombre viene de que era “compatible”: podía seguir corriendo trabajos por lotes convencionales en segundo plano mientras atendía las terminales interactivas, para no desperdiciar la máquina.
CTSS introdujo o popularizó ideas que hoy damos por sentadas: cuentas de usuario con contraseña, archivos privados por usuario, un editor de texto interactivo, y el envío de mensajes entre usuarios de la misma máquina, que es un antepasado directo del correo electrónico.
MULTICS: la ambición y su costo
A partir de 1965, el MIT, General Electric y los Laboratorios Bell emprendieron MULTICS (MULTiplexed Information and Computing Service). La visión era una utilidad de cómputo: una máquina que proveyera cómputo a una ciudad entera como la compañía eléctrica provee electricidad, disponible siempre, para todos, con facturación por uso.
Técnicamente, MULTICS fue extraordinariamente adelantado. Incorporó:
- Memoria virtual segmentada y paginada, con un espacio de direcciones único donde los archivos se mapeaban directamente en memoria.
- Anillos de protección jerárquicos, no solo dos modos, sino ocho niveles de privilegio anidados. Los anillos de x86 vienen de esta idea.
- Sistema de archivos jerárquico con directorios anidados y listas de control de acceso por usuario.
- Enlace dinámico de procedimientos en tiempo de ejecución.
- Escritura del sistema mayoritariamente en un lenguaje de alto nivel, PL/I, en vez de ensamblador.
- Diseño para reconfiguración en caliente: agregar o quitar CPU y memoria sin apagar.
También fue lento, gigantesco y llegó tarde. Bell Labs se retiró del proyecto en 1969. MULTICS terminó existiendo comercialmente y algunas instalaciones funcionaron hasta el año 2000, pero nunca fue masivo. Su influencia real fue indirecta y enorme, porque de la frustración de los ingenieros de Bell Labs salió UNIX.
La visión de 1968
Vale la pena mencionar un hito que no es de sistemas operativos pero que fija el otro extremo de lo que vendría. En diciembre de 1968, Douglas Engelbart presentó en San Francisco una demostración que incluía el ratón, ventanas superpuestas, edición colaborativa en tiempo real, videoconferencia e hipertexto. Ninguna de esas ideas se había visto junta antes, y ninguna era viable comercialmente entonces. Marcó la agenda de las siguientes tres décadas: el sistema operativo tendría que aprender a gestionar no solo trabajos, sino interfaces gráficas y dispositivos de entrada continuos.
UNIX
El origen
Cuando Bell Labs abandonó MULTICS, Ken Thompson quedó sin máquina para un juego que había escrito, Space Travel. Encontró un PDP-7 en desuso y, hacia 1969, escribió para él un sistema mínimo: un sistema de archivos, un intérprete de comandos y un pequeño conjunto de utilidades. Dennis Ritchie se sumó rápidamente. El nombre UNIX fue un juego de palabras a costa de MULTICS: donde MULTICS era múltiple y ambicioso, este sistema era único y modesto.
La decisión que cambió todo llegó alrededor de 1972-1973: reescribir el núcleo en un lenguaje de alto nivel. Ritchie había desarrollado C a partir del lenguaje B, con el objetivo explícito de que fuera lo bastante cercano al hardware para escribir un kernel y lo bastante abstracto para ser portable. UNIX se convirtió en el primer sistema operativo significativo escrito en su mayor parte en un lenguaje de alto nivel.
La consecuencia fue la portabilidad. Hasta entonces, cambiar de máquina significaba reescribir el sistema operativo desde cero en el ensamblador de la nueva arquitectura. Con UNIX en C, portar el sistema significaba escribir un compilador de C y adaptar una capa pequeña dependiente de la máquina.
Las ideas de diseño
UNIX no inventó la mayoría de sus componentes; los combinó con una coherencia que nadie había logrado.
Todo es un archivo. Los dispositivos, los canales de comunicación entre procesos y los archivos regulares se manipulan con las mismas cuatro operaciones: open, read, write, close. Un programa que escribe en la salida estándar no sabe ni le importa si está escribiendo a un terminal, a un archivo o a otro programa.
Composición mediante tuberías. La redirección y las tuberías (pipes) permiten que programas pequeños e independientes se combinen para resolver problemas que ninguno resuelve solo. Esto solo funciona porque existe la abstracción anterior.
#!/usr/bin/env bash
# Filosofia UNIX en accion: cinco programas que no se conocen entre si,
# cooperando a traves de un flujo de bytes.
# Cuenta las diez palabras mas frecuentes de un archivo de texto.
set -euo pipefail
ARCHIVO="${1:?Uso: $0 <archivo.txt>}"
tr '[:upper:]' '[:lower:]' < "$ARCHIVO" \
| tr -cs '[:alpha:]' '\n' \
| grep -v '^$' \
| sort \
| uniq -c \
| sort -rn \
| head -10
Ninguno de esos seis programas fue escrito pensando en los otros. sort no sabe qué es una palabra; uniq no sabe qué es un archivo de texto. La composición funciona porque todos hablan el mismo protocolo mínimo: líneas de texto por la entrada y salida estándar.
Jerarquía única de archivos. Un solo árbol que empieza en /, donde los dispositivos y sistemas de archivos adicionales se montan en puntos del árbol en vez de tener identificadores separados. Esta es la diferencia estructural más visible frente a la tradición de DOS y Windows, con sus letras de unidad.
Procesos con fork y exec. UNIX separó dos operaciones que otros sistemas fusionaban: crear un proceso nuevo (fork, que duplica el actual) y reemplazar la imagen de un proceso por otro programa (exec). Esa separación parece rebuscada hasta que se ve para qué sirve: entre el fork y el exec el proceso hijo puede reconfigurar su entorno —redirigir descriptores, cambiar de usuario, ajustar límites— usando código ordinario en vez de una API especial. Ahí es donde las tuberías del shell se implementan.
/* Implementacion minima de una tuberia entre dos programas,
exactamente como lo hace un shell.
Equivale a ejecutar: ls -1 | wc -l
Compilar con: gcc -o tuberia tuberia.c
Correr con: ./tuberia */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int fd[2]; /* fd[0] lectura, fd[1] escritura */
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
pid_t izquierda = fork();
if (izquierda == -1) {
perror("fork");
return 1;
}
if (izquierda == 0) {
/* Hijo izquierdo: escribe en la tuberia. */
close(fd[0]); /* no lee */
dup2(fd[1], STDOUT_FILENO); /* stdout pasa a ser la tuberia */
close(fd[1]);
execlp("ls", "ls", "-1", (char *)NULL);
perror("execlp ls");
_exit(127);
}
pid_t derecha = fork();
if (derecha == -1) {
perror("fork");
return 1;
}
if (derecha == 0) {
/* Hijo derecho: lee de la tuberia. */
close(fd[1]); /* no escribe */
dup2(fd[0], STDIN_FILENO); /* stdin pasa a ser la tuberia */
close(fd[0]);
execlp("wc", "wc", "-l", (char *)NULL);
perror("execlp wc");
_exit(127);
}
/* Padre: cierra ambos extremos y espera a los dos hijos. */
close(fd[0]);
close(fd[1]);
int estado;
waitpid(izquierda, &estado, 0);
waitpid(derecha, &estado, 0);
return 0;
}
Este programa de cincuenta líneas es, en esencia, lo que hace tu shell cada vez que escribes una barra vertical. Todas las llamadas usadas —pipe, fork, dup2, execlp, waitpid— existen sin cambios de semántica desde los años setenta y están especificadas en POSIX.
La bifurcación: BSD, System V y las guerras UNIX
En los años setenta AT&T no podía vender software por restricciones antimonopolio, así que distribuyó UNIX a universidades con el código fuente y a bajo costo. Berkeley recibió una copia y produjo su propia línea, BSD (Berkeley Software Distribution), que aportó contribuciones fundamentales: el editor vi, el shell csh, la memoria virtual paginada por demanda, el sistema de archivos rápido y, sobre todo, la implementación de TCP/IP con la interfaz de sockets, que es la razón por la que hoy programamos redes como lo hacemos.
Cuando las restricciones antimonopolio cedieron, AT&T comercializó su línea, System V. Los proveedores de hardware crearon sus propias variantes: SunOS y luego Solaris, HP-UX, AIX, IRIX, Ultrix, Xenix. Cada una incompatible con las otras en detalles suficientes como para hacer doloroso el porte de aplicaciones. Ese período se conoce como las guerras UNIX.
La respuesta a la fragmentación fue la estandarización: POSIX (Portable Operating System Interface), una familia de estándares que define la interfaz de llamadas al sistema, la biblioteca de C, el shell y las utilidades básicas. POSIX es la razón por la que un programa escrito para Linux compila en macOS o FreeBSD sin cambios mayores.
El árbol genealógico
graph LR
M["MULTICS<br/>1965"] -.->|influencia| U["UNIX<br/>1969, Bell Labs"]
U --> V6["Research UNIX<br/>V6 / V7"]
V6 --> BSD["BSD<br/>Berkeley"]
V6 --> SYSV["System III / System V<br/>AT&T"]
BSD --> FBSD["FreeBSD"]
BSD --> NBSD["NetBSD"]
NBSD --> OBSD["OpenBSD"]
BSD --> SUN["SunOS"]
SUN --> SOL["Solaris"]
SOL --> ILL["illumos / OpenIndiana"]
SYSV --> AIX["AIX"]
SYSV --> HPUX["HP-UX"]
SYSV --> SCO["SCO UNIX"]
BSD --> NEXT["NeXTSTEP<br/>+ micronucleo Mach"]
NEXT --> DAR["Darwin / XNU"]
DAR --> MAC["macOS"]
DAR --> IOS["iOS, iPadOS,<br/>watchOS, tvOS"]
U -.->|reimplementacion<br/>didactica| MIN["MINIX<br/>1987"]
MIN -.->|inspiracion| LIN["Kernel Linux<br/>1991"]
GNU["Proyecto GNU<br/>1983"] --> LIN
LIN --> DIST["Distribuciones<br/>Debian, Red Hat, Arch..."]
LIN --> AND["Android"]
POSIX["POSIX<br/>estandarizacion"] -.-> SYSV
POSIX -.-> BSD
POSIX -.-> LIN
Hay dos tipos de flecha en este árbol y la diferencia importa. Las flechas continuas indican descendencia de código: FreeBSD contiene código que desciende de BSD. Las flechas punteadas indican influencia de diseño sin herencia de código: Linux no contiene código de MINIX ni de UNIX, reimplementa sus interfaces desde cero. Esa distinción fue jurídicamente crítica en los litigios de los años noventa y dos mil.
La computadora personal y la familia Windows
CP/M y la llegada del microprocesador
En los años setenta el microprocesador hizo económicamente viable poner una computadora en un escritorio. El Altair 8800 de 1975 suele citarse como el punto de partida del mercado de computadoras personales; se vendía en kit y se programaba con interruptores del panel frontal.
Para esas máquinas, Gary Kildall escribió CP/M (Control Program for Microcomputers), que fue el sistema operativo dominante de los microcomputadores de 8 bits. CP/M ya separaba la parte dependiente del hardware (el BIOS) del resto del sistema, lo que permitía que el mismo software corriera en máquinas de distintos fabricantes.
MS-DOS
Cuando IBM preparó su PC en 1980-1981 y necesitó un sistema operativo, terminó licenciando a Microsoft un producto derivado de QDOS, que a su vez replicaba conceptos y buena parte de la interfaz de CP/M. Se convirtió en PC-DOS para IBM y MS-DOS para el resto.
MS-DOS era, en términos de este capítulo, un retroceso deliberado. Era monousuario, monotarea, sin protección de memoria y sin modo dual: cualquier programa podía escribir en cualquier dirección física y manipular el hardware directamente. Eso no fue un descuido, fue una restricción del hardware: el Intel 8088 original no tenía unidad de gestión de memoria ni modo protegido.
La consecuencia práctica es que durante una década el software de PC se acostumbró a acceder al hardware sin intermediarios, y esa costumbre condicionó todo el diseño de Windows durante los quince años siguientes.
La línea de consumo: Windows 1.0 a Windows Me
Las primeras versiones de Windows no eran sistemas operativos: eran entornos gráficos que se ejecutaban sobre MS-DOS. Windows 3.0 y 3.1, a comienzos de los noventa, fueron las primeras que tuvieron éxito comercial masivo. Usaban multitarea cooperativa: un programa conservaba la CPU hasta que voluntariamente devolvía el control al sistema al consultar la cola de mensajes. Si un programa entraba en un bucle sin ceder, congelaba el sistema completo. Es exactamente el problema que el tiempo compartido había resuelto treinta años antes con la expropiación por reloj; el hardware y la compatibilidad hacia atrás obligaron a repetirlo.
Windows 95 introdujo multitarea expropiativa para las aplicaciones de 32 bits y un modelo de memoria protegida parcial, pero conservó código de 16 bits y dependencias de DOS por compatibilidad. Windows 98 y Windows Me continuaron esa línea, que terminó ahí.
La línea NT
En paralelo, Microsoft contrató en 1988 a Dave Cutler, que venía de diseñar VMS en Digital, para construir un sistema operativo nuevo sin deuda con DOS. El resultado fue Windows NT, lanzado en 1993, con un diseño de manual:
- Núcleo híbrido: un ejecutivo con estructura modular sobre una capa de abstracción de hardware o HAL, que permitió portar NT a varias arquitecturas.
- Modo dual estricto y espacio de direcciones protegido por proceso desde el primer día.
- Subsistemas de entorno: la idea de que las APIs de aplicación (Win32, OS/2, POSIX) fueran personalidades implementadas sobre el ejecutivo, no el sistema mismo.
- NTFS, un sistema de archivos con registro por diario (journaling), listas de control de acceso y soporte de archivos grandes.
- Modelo de seguridad con tokens de acceso y descriptores de seguridad integrado en el núcleo, no añadido después.
NT es el ancestro de todo Windows moderno. La línea de consumo y la línea NT se unificaron en Windows XP en 2001, y desde entonces todo —Windows 7, 8, 10, 11, Windows Server— es NT.
Una observación de diseño que conviene retener: la idea de subsistemas de entorno, que se abandonó en su forma original, resucitó décadas después como WSL (Windows Subsystem for Linux). WSL 1 implementaba llamadas al sistema de Linux traduciéndolas al ejecutivo de NT, que es literalmente el concepto de subsistema de entorno; WSL 2 cambió de estrategia y ejecuta un kernel Linux real en una máquina virtual ligera.
Linux
GNU: el sistema sin núcleo
En 1983 Richard Stallman anunció el proyecto GNU, con el objetivo de construir un sistema operativo completo compatible con UNIX y libre, en el sentido de que cualquiera pudiera usarlo, estudiarlo, modificarlo y redistribuirlo. Para 1990 GNU tenía un compilador excelente (GCC), un editor (Emacs), un depurador (GDB), una biblioteca de C, un shell (Bash) y prácticamente todas las utilidades. Lo que no tenía era núcleo: GNU Hurd, basado en un micronúcleo, avanzaba lentamente.
Stallman también produjo el instrumento legal que hizo posible el resto: la GPL, una licencia que usa el derecho de autor para garantizar que las obras derivadas se distribuyan bajo las mismas condiciones. La GPL es la razón estructural por la que el ecosistema de software libre se acumuló en vez de fragmentarse en versiones privativas incompatibles.
MINIX y el kernel de 1991
Andrew Tanenbaum escribió MINIX en 1987 como sistema didáctico para acompañar su libro de texto: un UNIX pequeño, con arquitectura de micronúcleo, cuyo código completo cabía en un apéndice y podía leerse entero.
En 1991, Linus Torvalds, entonces estudiante en Helsinki y usuario de MINIX, empezó a escribir un núcleo propio para explorar el modo protegido del Intel 386. Lo anunció en agosto de ese año como un proyecto pequeño y sin ambición de portabilidad. En pocos meses el núcleo se combinó con las herramientas de GNU, y en 1992 se relicenció bajo GPL. Esa combinación —un núcleo funcional más un espacio de usuario completo y libre— produjo un sistema operativo utilizable donde antes había dos mitades incompletas.
La arquitectura y el debate del micronúcleo
Linux es un núcleo monolítico modular. Monolítico porque los controladores de dispositivos, el sistema de archivos y la pila de red se ejecutan en el mismo espacio de direcciones que el resto del núcleo, en modo privilegiado. Modular porque esos componentes pueden cargarse y descargarse en caliente como módulos.
Esta decisión provocó, en 1992, un intercambio público entre Tanenbaum y Torvalds sobre si el diseño monolítico era una regresión frente al micronúcleo. El argumento del micronúcleo es la robustez y la claridad: si un controlador falla, se reinicia como proceso de usuario sin llevarse el sistema. El argumento monolítico es el rendimiento y la simplicidad de implementación: cada llamada entre subsistemas es una llamada a función y no un intercambio de mensajes.
flowchart TB
subgraph MONO["Nucleo monolitico (Linux)"]
direction TB
UA1["Aplicaciones"] -->|llamada al sistema| K1["Nucleo: planificador,<br/>memoria, VFS, red,<br/>controladores"]
K1 --> HW1["Hardware"]
end
subgraph MICRO["Micronucleo (MINIX 3, Mach)"]
direction TB
UA2["Aplicaciones"] -->|mensaje| S1["Servidor de archivos"]
UA2 -->|mensaje| S2["Servidor de red"]
UA2 -->|mensaje| S3["Controlador de disco"]
S1 -->|mensaje| K2["Micronucleo: IPC,<br/>planificacion basica,<br/>gestion de interrupciones"]
S2 -->|mensaje| K2
S3 -->|mensaje| K2
K2 --> HW2["Hardware"]
end
La diferencia visible en el diagrama es la cantidad de cruces de frontera de privilegio. En el monolítico hay uno por llamada al sistema; en el micronúcleo, una operación de archivo puede requerir varios intercambios de mensajes, cada uno con su cambio de contexto. En la práctica ninguno de los dos extremos ganó: Linux incorporó mecanismos para sacar controladores al espacio de usuario (FUSE para sistemas de archivos, drivers de usuario para dispositivos), y los micronúcleos incorporaron optimizaciones de IPC. Windows NT y XNU son híbridos declarados.
Explorando el sistema hoy
Buena parte de la historia de este capítulo es visible directamente en un Linux actual. Estos comandos son reales y se pueden ejecutar sin privilegios.
#!/usr/bin/env bash
# Recorrido por los fosiles historicos visibles en un Linux moderno.
echo "=== 1. Identificacion del nucleo (llamada uname, POSIX) ==="
uname -a
echo
echo "=== 2. Tiempo de CPU en modo usuario vs modo nucleo (modo dual) ==="
# Las columnas us y sy separan el tiempo en modo usuario del tiempo en kernel.
grep '^cpu ' /proc/stat
echo "campos: usuario nice sistema inactivo espera_io irq softirq ..."
echo
echo "=== 3. Interrupciones atendidas por linea y por CPU ==="
head -5 /proc/interrupts
echo
echo "=== 4. Estados de proceso: el diagrama de tiempo compartido, en vivo ==="
# S=durmiendo, R=ejecutando o listo, D=espera ininterrumpible de E/S, Z=zombi
ps -eo pid,stat,comm --no-headers | awk '{print $2}' | cut -c1 | sort | uniq -c
echo
echo "=== 5. Cambios de contexto desde el arranque ==="
grep -E '^(ctxt|processes)' /proc/stat
echo
echo "=== 6. Colas de spool: el fosil de 1960 ==="
ls -d /var/spool/* 2>/dev/null || echo "sin directorios de spool"
echo
echo "=== 7. Modulos cargados: el nucleo monolitico modular ==="
lsmod | head -5
El punto cuatro merece detenerse. Los estados que devuelve ps en la columna STAT son casi literalmente los del diagrama de estados del tiempo compartido: R corresponde a ejecutando o listo, S y D a bloqueado, Z a terminado pero no cosechado por su padre. Cincuenta años después, el modelo no cambió.
Distribuciones
Linux es solo el núcleo. Una distribución es el núcleo más el espacio de usuario, un sistema de paquetes, una política de versiones y una comunidad. Debian, Red Hat, SUSE, Arch, Gentoo y sus derivadas se diferencian por decisiones de empaquetado y política, no por el núcleo.
El modelo de desarrollo también dejó una herramienta que trasciende el proyecto: Git, escrito por Torvalds en 2005 cuando el sistema de control de versiones que usaba el kernel dejó de estar disponible bajo condiciones aceptables. Un sistema operativo produjo, como efecto colateral, la infraestructura de colaboración de casi toda la industria del software.
La línea de Apple y los micronúcleos en producción
Merece una sección propia porque es el caso más claro de convergencia entre tradiciones.
El Macintosh original de 1984 llevó la interfaz gráfica al mercado masivo, con ideas provenientes en buena parte de los trabajos de Xerox PARC. Pero su sistema operativo clásico, hasta Mac OS 9, era cooperativo y sin protección de memoria: los mismos problemas que Windows 3.x.
Cuando Steve Jobs dejó Apple fundó NeXT, y NeXTSTEP fue un sistema construido sobre el micronúcleo Mach de Carnegie Mellon con un espacio de usuario derivado de BSD y un entorno de desarrollo orientado a objetos en Objective-C. Cuando Apple compró NeXT en 1997, NeXTSTEP se convirtió en la base de Mac OS X.
El núcleo resultante se llama XNU y es explícitamente híbrido: contiene el núcleo Mach para gestión de tareas, memoria virtual e IPC, y componentes de FreeBSD para el modelo de procesos POSIX, la red y el sistema de archivos, todo en el mismo espacio de direcciones para evitar el costo de los mensajes. Encima va Darwin, el sistema base de código abierto, y sobre él las capas propietarias.
Esto significa que macOS, iOS, iPadOS, watchOS y tvOS son todos, en el nivel del núcleo, descendientes de BSD y de Mach. La terminal de un Mac es una terminal UNIX auténtica, con certificación UNIX incluida.
Sistemas operativos móviles
El problema nuevo
Los teléfonos introdujeron restricciones que ningún sistema de escritorio había enfrentado con esa severidad:
- Energía: la batería es el recurso escaso. Un proceso que despierta la CPU cada segundo tiene un costo medible en horas de autonomía.
- Memoria sin intercambio: los dispositivos móviles tradicionalmente no usan área de intercambio en disco por el desgaste de la memoria flash y la latencia, así que el sistema debe matar procesos cuando falta memoria en vez de paginar.
- Modelo de seguridad hostil: las aplicaciones se instalan desde tiendas, no las escribe el administrador de la máquina. Hay que asumir que cada aplicación es potencialmente maliciosa.
- Interfaz de una sola aplicación visible, lo que permite políticas de ciclo de vida mucho más agresivas.
Symbian, iOS y Android
Symbian dominó los teléfonos inteligentes antes de 2007. Descendía de EPOC de Psion, con un diseño orientado a memoria muy escasa y sin memoria virtual paginada, y una arquitectura basada fuertemente en eventos asíncronos para ahorrar energía. Su declive fue de plataforma y ecosistema, no de núcleo.
iOS, presentado en 2007, es Darwin y XNU con un espacio de usuario y una interfaz propios. Introdujo el modelo de sandbox por aplicación con permisos explícitos, firma obligatoria de código y un ciclo de vida donde el sistema suspende las aplicaciones que pasan a segundo plano en vez de dejarlas ejecutar libremente.
Android, cuya primera versión comercial es de 2008, usa el núcleo Linux con modificaciones significativas para el caso móvil, entre ellas mecanismos propios de comunicación entre procesos y de gestión de energía y de memoria baja. Sobre el núcleo va un espacio de usuario que no es GNU: una biblioteca de C propia (Bionic), un entorno de ejecución gestionado (Dalvik y después ART) y un modelo de aplicaciones basado en componentes con ciclo de vida controlado por el sistema.
El punto arquitectónicamente interesante de Android es su uso del modelo de usuarios de UNIX en un sentido para el que no fue diseñado: cada aplicación instalada recibe su propio identificador de usuario, y el aislamiento entre aplicaciones se apoya en los permisos de archivos de UNIX más SELinux. Un mecanismo pensado en 1970 para separar a Juan de María en una máquina compartida se reutilizó para separar dos aplicaciones del mismo dueño.
flowchart TB
subgraph AND["Android"]
AA["Aplicaciones<br/>cada una con su UID"] --> ART["Entorno de ejecucion ART<br/>+ framework Java"]
ART --> BIO["Bionic libc + HAL"]
BIO --> KL["Nucleo Linux modificado:<br/>IPC propio, low memory killer,<br/>gestion de energia"]
KL --> HWA["Hardware"]
end
subgraph IOS["iOS"]
IA["Aplicaciones<br/>en sandbox firmado"] --> CO["Cocoa Touch / Foundation"]
CO --> DAR["Darwin: espacio de usuario BSD"]
DAR --> XNU2["XNU: Mach + BSD<br/>nucleo hibrido"]
XNU2 --> HWI["Hardware"]
end
Las dos pilas tienen la misma forma en tres capas —aplicaciones aisladas, marco de trabajo gestionado, núcleo derivado de UNIX— y linajes completamente distintos. Es una buena ilustración de que las decisiones de arquitectura convergen cuando las restricciones son las mismas.
Tabla comparativa de generaciones
| Generación | Período aproximado | Problema dominante | Mecanismo clave | Soporte de hardware requerido | Métrica optimizada | Rastro actual |
|---|---|---|---|---|---|---|
| Sin sistema operativo | 1945-1955 | operar la máquina | operador humano, bibliotecas de E/S | ninguno | nada explícito | firmware de sistemas empotrados |
| Lote simple | 1955-1965 | tiempo muerto entre trabajos | monitor residente, JCL | temporizador | utilización de CPU | Slurm, cron, pipelines de datos |
| Lote con spooling | 1960s | espera de dispositivos lentos | interrupciones, canales, DMA, spool | interrupciones, DMA | rendimiento total | /var/spool, CUPS, colas de mensajes |
| Multiprogramación | 1965-1975 | CPU ociosa durante la E/S | varios trabajos en memoria | base/límite, modo dual | utilización de CPU | límites de memoria de contenedores |
| Tiempo compartido | 1962-1980 | tiempo de respuesta al usuario | expropiación por reloj, terminales | reloj programable, MMU | latencia interactiva | planificador de cualquier SO actual |
| Computador personal | 1975-1995 | costo y accesibilidad | monousuario, interfaz gráfica | microprocesador barato | comodidad de uso | modelo de escritorio |
| Red y distribuidos | 1980s-2000s | compartir recursos remotos | sockets, sistemas de archivos en red, RPC | interfaz de red | transparencia de ubicación | NFS, gRPC, nube |
| Móvil | 2007- | energía y confianza cero | sandbox, ciclo de vida gestionado, sin swap | SoC, gestión fina de energía | autonomía y seguridad | iOS, Android |
| Virtualización y nube | 2000- | densidad y aislamiento | hipervisores, contenedores, espacios de nombres | extensiones de virtualización | densidad y elasticidad | KVM, Xen, Docker, Kubernetes |
Fíjate en la columna “problema dominante”: nunca es el mismo dos veces, y sin embargo casi todos los mecanismos siguen presentes. Los sistemas operativos no reemplazan sus capas anteriores, las acumulan.
Tabla de linajes: quién viene de quién
| Sistema actual | Tipo de núcleo | Linaje | Espacio de usuario | Certificación / estándar |
|---|---|---|---|---|
| Linux (distribuciones) | monolítico modular | reimplementación independiente | GNU mayoritariamente | POSIX en gran medida, sin certificar |
| Android | monolítico modular | núcleo Linux modificado | Bionic + ART, no GNU | parcialmente POSIX |
| macOS | híbrido (XNU) | Mach + FreeBSD vía NeXTSTEP | BSD + capas de Apple | UNIX certificado |
| iOS / iPadOS | híbrido (XNU) | mismo que macOS | BSD reducido + Cocoa Touch | no certificado |
| FreeBSD / OpenBSD / NetBSD | monolítico modular | descendencia directa de BSD | BSD propio | POSIX en gran medida |
| Windows 11 / Server | híbrido (ejecutivo NT) | NT, con influencia de VMS | Win32 y sucesores | subsistema POSIX histórico |
| illumos / OpenIndiana | monolítico | System V vía Solaris | GNU y SVR4 | descendiente de UNIX certificado |
| z/OS | especializado | descendencia de OS/360 | JCL y utilidades propias | conformidad POSIX opcional |
| MINIX 3 | micronúcleo | reimplementación didáctica | NetBSD | parcial |
La fila de z/OS es la más llamativa: el sistema operativo de los mainframes actuales de IBM desciende, con continuidad de compatibilidad, del OS/360 de 1964. Sigue procesando lotes con JCL. El procesamiento por lotes no es historia antigua, es producción en bancos y aerolíneas hoy.
La concurrencia como hilo conductor
Hay una lectura transversal de todo lo anterior que conviene hacer explícita, porque conecta con los lenguajes que este curso usa para ilustrar los conceptos.
Cada avance de la historia consistió en hacer coexistir más actividades independientes sobre menos recursos, y en cada etapa hubo que decidir dos cosas: quién decide cuándo cambia el actor que ejecuta, y cómo se comunican los actores entre sí.
En el lote hay un actor por vez y el cambio ocurre al terminar. En la multitarea cooperativa el actor decide cuándo ceder, y basta uno que no ceda para colgar el sistema. En el tiempo compartido decide el sistema, apoyado en el reloj, y la expropiación es forzosa.
Esa misma decisión reaparece dentro de los lenguajes de programación. Ada, diseñado a fines de los setenta con concurrencia en el propio lenguaje, expone tareas y una forma de comunicación sincronizada llamada rendezvous, donde dos tareas se citan en un punto de encuentro explícito:
-- Concurrencia con expropiacion y comunicacion sincronizada en Ada.
-- Compilar con: gnatmake tiempo_compartido.adb
-- Ejecutar con: ./tiempo_compartido
with Ada.Text_IO; use Ada.Text_IO;
procedure Tiempo_Compartido is
-- Un objeto protegido: el equivalente a una seccion critica
-- administrada por el sistema, no por el programador.
protected Contador is
procedure Incrementar;
function Valor return Natural;
private
Total : Natural := 0;
end Contador;
protected body Contador is
procedure Incrementar is
begin
Total := Total + 1;
end Incrementar;
function Valor return Natural is
begin
return Total;
end Valor;
end Contador;
-- Tipo de tarea: cada instancia es un flujo de control independiente,
-- planificado por el runtime igual que el SO planifica procesos.
task type Trabajo (Id : Natural; Vueltas : Natural);
task body Trabajo is
begin
for I in 1 .. Vueltas loop
Contador.Incrementar;
-- Cede el control voluntariamente: multitarea cooperativa.
-- Sin esta linea el runtime igual expropia por cuanto de tiempo.
delay 0.001;
end loop;
Put_Line ("Tarea" & Natural'Image (Id) & " termino.");
end Trabajo;
T1 : Trabajo (1, 100);
T2 : Trabajo (2, 100);
T3 : Trabajo (3, 100);
begin
-- El bloque principal espera a que todas las tareas terminen
-- antes de continuar: es la semantica de Ada, no una llamada explicita.
null;
end Tiempo_Compartido;
Elixir, sobre la máquina virtual BEAM de Erlang, tomó el otro camino: procesos ligeros completamente aislados, sin memoria compartida, que se comunican exclusivamente por mensajes, con un planificador expropiativo dentro de la máquina virtual. Es, en el nivel del lenguaje, una reencarnación fiel del modelo de micronúcleo.
# Concurrencia por paso de mensajes: el modelo de micronucleo,
# implementado dentro de una maquina virtual.
# Ejecutar con: elixir spooler.exs
defmodule Spooler do
@moduledoc """
Reimplementacion didactica de un spooler de impresion de 1960:
un proceso servidor mantiene la cola, los clientes encolan trabajos
y nadie comparte memoria con nadie.
"""
def iniciar do
spawn(fn -> bucle(:queue.new(), 0) end)
end
defp bucle(cola, atendidos) do
receive do
{:encolar, remitente, trabajo} ->
send(remitente, {:encolado, trabajo})
bucle(:queue.in(trabajo, cola), atendidos)
{:imprimir, remitente} ->
case :queue.out(cola) do
{{:value, trabajo}, resto} ->
# La "impresion" es lenta, como en 1960.
Process.sleep(50)
send(remitente, {:impreso, trabajo})
bucle(resto, atendidos + 1)
{:empty, _} ->
send(remitente, :cola_vacia)
bucle(cola, atendidos)
end
{:estado, remitente} ->
send(remitente, {:estado, :queue.len(cola), atendidos})
bucle(cola, atendidos)
end
end
end
# --- Programa principal ---
spooler = Spooler.iniciar()
# Tres "trabajos por lotes" se encolan sin esperar a la impresora.
for n <- 1..3 do
send(spooler, {:encolar, self(), "informe_#{n}.txt"})
receive do
{:encolado, trabajo} -> IO.puts("Encolado: #{trabajo}")
end
end
send(spooler, {:estado, self()})
receive do
{:estado, pendientes, atendidos} ->
IO.puts("Cola: #{pendientes} pendientes, #{atendidos} atendidos")
end
# Ahora se drena la cola: el spool-out ocurre a su propio ritmo.
for _ <- 1..4 do
send(spooler, {:imprimir, self()})
receive do
{:impreso, trabajo} -> IO.puts("Impreso: #{trabajo}")
:cola_vacia -> IO.puts("Cola vacia, nada que imprimir")
end
end
Los dos ejemplos resuelven versiones del mismo problema con las dos filosofías que la historia enfrentó: estado compartido con exclusión mutua, y aislamiento total con mensajes. Ninguna ganó definitivamente, dentro ni fuera del sistema operativo.
Errores comunes al estudiar esta historia
| Error | Por qué aparece | Consecuencia práctica | Corrección |
|---|---|---|---|
| Creer que “Linux” es un sistema operativo completo | El uso coloquial confunde núcleo y distribución | No se entiende por qué Android, siendo Linux, no ejecuta programas de escritorio | Linux es el núcleo; la distribución añade libc, utilidades y gestor de paquetes. Android usa Bionic, no glibc |
| Pensar que multiprogramación y tiempo compartido son sinónimos | Ambos tienen varios programas en memoria | Se atribuye la interactividad a la multiprogramación y no se entiende para qué existe el cuanto de tiempo | La multiprogramación cambia de tarea al bloquearse; el tiempo compartido además expropia por reloj |
| Suponer que UNIX inventó todo lo que popularizó | Es el ancestro visible del ecosistema actual | Se pasa por alto MULTICS, CTSS y OS/360, donde muchas ideas ya existían | UNIX simplificó y combinó ideas previas; su aporte original fue la coherencia y la implementación en C |
| Creer que Linux desciende del código de UNIX o de MINIX | El parecido de interfaces es total | Se malinterpretan los litigios de licencias y el rol de POSIX | Linux reimplementa las interfaces desde cero. La compatibilidad viene del estándar, no del código |
| Confundir MS-DOS con un sistema multitarea limitado | La palabra “sistema operativo” sugiere las tres funciones | Se subestima cuánto costó a Windows llegar a la protección de memoria | MS-DOS era monotarea y sin modo dual: el hardware del 8088 no lo permitía |
| Pensar que Windows moderno desciende de Windows 95 | La continuidad de nombre y de interfaz | Se buscan explicaciones equivocadas de comportamientos del sistema | Windows XP y posteriores descienden de NT; la línea 95/98/Me se descontinuó |
| Suponer que macOS deriva de Linux porque tiene terminal | Ambos son “tipo UNIX” y comparten comandos | Fallan supuestos sobre herramientas: ls, sed y date de macOS son BSD, no GNU | macOS deriva de BSD y Mach vía NeXTSTEP; sus utilidades tienen banderas distintas a las de GNU |
| Dar el procesamiento por lotes por extinto | No se ve en el escritorio | No se reconocen sus algoritmos al trabajar con colas de trabajos o ETL | Sigue vivo en z/OS, en planificadores de clúster y en todo pipeline de datos programado |
| Creer que el micronúcleo “perdió” | Linux domina el escritorio y el servidor | Se descarta un diseño que está en producción masiva | XNU es híbrido con Mach dentro; hay micronúcleos en sistemas críticos y en el procesador seguro de muchos teléfonos |
| Tomar las fechas como precisas y únicas | Los libros citan años distintos | Discusiones estériles sobre cuál es “la” fecha | Casi todo sistema tiene fecha de inicio, de primer uso interno, de publicación y de versión comercial. Importa el orden y la causa, no el año exacto |
Ejercicios propuestos
1. Reproducir la curva de utilización. Modifica el script utilizacion.py para que reciba p y el máximo de n por línea de comandos y para que informe a partir de qué n la ganancia marginal cae por debajo de un punto porcentual. Responde: para un servidor cuyos procesos pasan el 95% del tiempo esperando red, ¿cuál es el grado de multiprogramación mínimo para superar el 90% de utilización?
2. Medir el costo de la espera activa. Escribe dos versiones de un programa que espere un segundo: una con espera activa en un bucle que consulta el reloj, y otra que use una llamada de suspensión del sistema. Mide ambas con time o /usr/bin/time -v y compara el tiempo de CPU consumido frente al tiempo transcurrido. Explica la diferencia en términos de la sección de interrupciones.
3. Un monitor residente en miniatura. Implementa en el lenguaje que prefieras un intérprete de un JCL simplificado que lea un archivo de “mazo de tarjetas” con directivas $JOB, $RUN, $DATA y $END, ejecute cada trabajo como un subproceso con límite de tiempo, y produzca un informe final con el tiempo consumido por cada uno. Los trabajos que excedan su límite deben ser terminados y marcados como fallidos.
4. Instrumentar un cambio de contexto. Usando /proc/stat, escribe un script que muestre cuántos cambios de contexto por segundo ocurren en tu máquina en reposo y bajo carga. Genera carga con varios procesos que compitan por CPU y con varios que hagan E/S intensa, y explica por qué los números difieren.
5. Comparar linajes en la práctica. Ejecuta ls --help en Linux y ls -h o man ls en macOS o FreeBSD si tienes acceso. Identifica tres banderas presentes en la versión GNU y ausentes en la BSD, o con significado distinto. Relaciona el resultado con la sección de las guerras UNIX y con el rol de POSIX.
6. El árbol genealógico ampliado. Toma el diagrama Mermaid del árbol UNIX y añade cinco sistemas que no aparecen, distinguiendo con flecha continua la herencia de código y con flecha punteada la influencia de diseño. Justifica cada arista con una fuente.
7. Spooler concurrente. Extiende el ejemplo de Elixir para que existan tres procesos impresores consumiendo de la misma cola, con velocidades distintas, y para que el servidor lleve estadísticas de cuántos trabajos atendió cada uno. Observa qué pasa con el reparto y relaciónalo con el problema de planificación que veremos en capítulos posteriores.
8. Traducción de mecanismos. Elige tres mecanismos modernos —un contenedor Docker, un trabajo de cron y un grupo de control de memoria— y escribe para cada uno cuál es su antepasado histórico en este capítulo, qué problema resolvía el original y qué cambió en la versión actual.
9. Ada frente a Elixir. Ejecuta ambos ejemplos del capítulo. Describe, en no más de una página, qué garantiza cada modelo respecto de la corrupción de estado compartido, y qué costo paga cada uno por esa garantía. Conecta la respuesta con el debate monolítico frente a micronúcleo.
10. Investigación guiada. Elige un sistema operativo que no aparezca en este capítulo —por ejemplo VMS, Plan 9, QNX, seL4, Fuchsia o TempleOS— y escribe una ficha de una página con: el problema que buscaba resolver, el mecanismo distintivo que aportó, y si esa idea sobrevivió en algún sistema actual.
Lo que viene
Este capítulo recorrió sesenta años de acumulación de mecanismos y dejó, sin definirlos formalmente, casi todos los conceptos que sostienen la disciplina: proceso, cambio de contexto, interrupción, modo dual, planificación, protección de memoria, llamada al sistema, aislamiento. Cada uno apareció aquí como respuesta a una necesidad histórica concreta, que es la mejor forma de recordarlos.
En el capítulo 3 vamos a dar vuelta la perspectiva: en vez de mirar cómo se construyeron los mecanismos a lo largo del tiempo, los vamos a organizar por función. Qué significa exactamente que el sistema operativo sea una máquina extendida y qué significa que sea un administrador de recursos; qué gestiona en concreto sobre procesos, memoria, almacenamiento, dispositivos y protección; y dónde está la frontera entre lo que hace el núcleo y lo que hace el espacio de usuario. Con la historia de este capítulo en la cabeza, cada rol va a tener un porqué además de una definición.
Si quieres revisar el recorrido completo del curso o saltar a otro tema, el índice está en /tecnologias/sistemas-operativos/00-indice/.