Introducción a los sistemas operativos: la máquina desnuda y la máquina extendida

Por: Artiko
sistemas-operativosadaelixirconcurrenciakernelllamadas-al-sistemaabstraccionhardwaremodo-kernel

Introducción a los sistemas operativos: la máquina desnuda y la máquina extendida

Este es el primer capítulo del curso, así que arranca desde cero: no se asume que sepas qué es un kernel, ni que hayas escrito nunca una línea de C, Ada o Elixir. Lo único que se da por sentado es que has usado un computador, que has abierto archivos y que en algún momento viste una ventana que decía que un programa dejó de responder. Ese último detalle importa más de lo que parece: alguien decidió que ese programa muriera sin llevarse el resto del sistema con él. Ese “alguien” es el sistema operativo, y entender por qué toma esa decisión y cómo puede tomarla es exactamente el objetivo de este curso.

Durante los próximos capítulos vamos a desarmar la idea de “computador” hasta quedarnos con lo que realmente hay debajo: un procesador que ejecuta instrucciones sin ninguna noción de “archivo”, “carpeta”, “ventana” o “usuario”. Todo eso lo inventa el software. Y el software que lo inventa, y que además reparte el hardware entre programas que compiten por él, es el sistema operativo.

Vamos por partes.

Qué significa cada palabra de “sistema operativo”

Antes de definir el conjunto, conviene separar el término. La expresión tiene dos palabras y cada una aporta algo distinto.

Sistema apunta a un conjunto de componentes interconectados cuyo comportamiento conjunto es predecible. No es una pila de piezas: es un arreglo donde cada pieza tiene un contrato con las demás. Si le pides al sistema que abra un archivo, el resultado tiene que ser el mismo hoy y mañana, con disco lleno o vacío, con uno o con cien programas corriendo. Esa predictibilidad no es gratis: se paga con estructura interna.

Operativo apunta a que administra operaciones múltiples y simultáneas: muchas tareas, muchos usuarios, muchos dispositivos. La palabra viene de una época en la que “operar” el computador era literalmente un trabajo humano, y el software que hoy llamamos sistema operativo nació para reemplazar a ese operador.

Juntando ambas ideas, llegamos a la definición de trabajo que usaremos en todo el curso:

Un sistema operativo es una capa de software que se interpone entre el hardware y las aplicaciones, ofreciendo a las aplicaciones un modelo de máquina más simple y más seguro que el hardware real, y repartiendo ese hardware entre todas las aplicaciones que lo piden al mismo tiempo.

Esa definición tiene dos mitades y cada una responde a una pregunta distinta:

  1. Ofrecer un modelo más simple responde a “¿por qué programar sería insoportable sin sistema operativo?”. Esta mitad es la visión de máquina extendida.
  2. Repartir el hardware responde a “¿por qué mi programa no puede simplemente tomarse todo el computador?”. Esta mitad es la visión de gestor de recursos.

Las dos mitades describen el mismo objeto desde ángulos opuestos, y el resto del capítulo las desarrolla una por una.

La máquina desnuda: qué te entrega realmente el hardware

Llamamos máquina desnuda al computador tal como es antes de que cualquier sistema operativo se cargue en memoria. Es útil imaginarla literalmente: enciendes la máquina, el firmware inicializa lo mínimo y te deja frente al procesador. ¿Qué tienes disponible?

Tienes esto y nada más:

  • Un procesador que ejecuta instrucciones de máquina, una tras otra, tomando su siguiente instrucción de una dirección de memoria guardada en un registro.
  • Un conjunto de registros: pequeñas celdas de almacenamiento dentro del procesador, del orden de decenas, cada una de 32 o 64 bits.
  • Una memoria principal direccionable por número. La dirección 0 es real, la dirección 4096 es real, y no hay ningún concepto de “esta zona es mía y esa es tuya”.
  • Un conjunto de controladores de dispositivo: chips que exponen registros propios a través de puertos de entrada/salida o de direcciones de memoria mapeadas.
  • Un mecanismo de interrupciones: los dispositivos pueden pedir la atención del procesador levantando una señal eléctrica.
  • Un temporizador capaz de interrumpir al procesador cada cierto número de ciclos.

Eso es todo. No hay archivos. No hay directorios. No hay procesos. No hay usuarios. No hay memoria “de un programa”. No hay red. Hay bytes, direcciones y señales.

Cómo se ve programar contra la máquina desnuda

Supongamos que quieres guardar un texto en un disco. Sobre la máquina desnuda, la secuencia de trabajo se parece a esto:

  1. Averiguar el modelo exacto del controlador de disco presente en la máquina.
  2. Leer su manual para saber en qué puertos vive y qué significa cada bit de sus registros de control.
  3. Traducir “quiero guardar este texto” a una posición física concreta del medio de almacenamiento.
  4. Programar una transferencia hacia el controlador, byte por byte o mediante acceso directo a memoria.
  5. Escribir el manejador de la interrupción que el controlador levantará cuando termine.
  6. Verificar los códigos de error del controlador, que no son los mismos entre fabricantes.
  7. Reintentar si el medio reportó un error de escritura.

Y todo eso te sirve solo para ese modelo de controlador. Si mañana cambias el disco por otro de otra marca, hay que reescribir el programa.

Este diagrama muestra la ruta real que tendría que recorrer un programa contra la máquina desnuda para hacer algo tan cotidiano como guardar un archivo:

flowchart TD
    A["Programa: quiero guardar 34 bytes de texto"] --> B["Traducir a bloque fisico del medio"]
    B --> C["Escribir parametros en los registros<br/>del controlador de disco"]
    C --> D["Emitir el comando de escritura"]
    D --> E{"El controlador acepto<br/>el comando?"}
    E -->|No| F["Leer registro de estado<br/>e interpretar el codigo del fabricante"]
    F --> G["Reintentar o abortar"]
    E -->|Si| H["Esperar la interrupcion<br/>de fin de transferencia"]
    H --> I["Manejador de interrupcion:<br/>guardar contexto y leer estado"]
    I --> J{"Escritura correcta?"}
    J -->|No| G
    J -->|Si| K["Programa continua"]

    style A fill:#1e3a5f,color:#fff
    style K fill:#1e5f3a,color:#fff
    style G fill:#5f1e1e,color:#fff

Fíjate en cuántas cajas del diagrama no tienen nada que ver con el problema original. El programa quería guardar texto; terminó administrando registros de control, códigos de fabricante e interrupciones. Ese desbalance entre “lo que quiero” y “lo que tengo que hacer” es el problema que el sistema operativo viene a resolver.

Hay un segundo problema, todavía peor: si dos programas hicieran esto al mismo tiempo, ambos escribirían en el mismo controlador sin coordinación. El resultado no sería “los dos archivos se guardan”; sería que ambos escriben sobre los mismos bloques y el medio queda corrupto. En la máquina desnuda no existe ningún árbitro.

La máquina extendida: la abstracción que construye el sistema operativo

La máquina extendida es lo que ve un programa cuando corre sobre un sistema operativo. Es una máquina que no existe físicamente, pero que se comporta de manera consistente y es infinitamente más cómoda de programar.

En la máquina extendida, guardar un texto es esto:

#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

int main(void) {
    /* Pedimos al sistema operativo un archivo, sin saber nada del disco. */
    int fd = open("saludo.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) {
        perror("no se pudo abrir el archivo");
        return 1;
    }

    const char *mensaje = "hola desde una llamada al sistema\n";
    ssize_t escritos = write(fd, mensaje, strlen(mensaje));
    if (escritos < 0) {
        perror("no se pudo escribir");
        close(fd);
        return 1;
    }

    printf("se escribieron %zd bytes\n", escritos);
    close(fd);
    return 0;
}

Compílalo y ejecútalo:

gcc -Wall -o guardar guardar.c
./guardar
cat saludo.txt

Tres llamadas: open, write, close. No aparece ningún registro de controlador, ningún bloque físico, ninguna interrupción. Y lo más importante: este programa funciona sin cambios sobre un disco mecánico, sobre un SSD, sobre una memoria USB, sobre un sistema de archivos en red o sobre un disco virtual dentro de una máquina virtual. El sistema operativo se encargó de que todos esos dispositivos, radicalmente distintos entre sí, se comporten como la misma abstracción: un archivo.

Las abstracciones fundamentales

La máquina extendida se construye sobre un puñado pequeño de abstracciones. Cada una toma un recurso físico incómodo y lo convierte en un objeto manejable:

Recurso físicoAbstracción del sistema operativoQué esconde
Sectores y bloques de un medio de almacenamientoArchivo y directorioGeometría del medio, tipo de controlador, tamaño de bloque, sectores dañados
Memoria física direccionable por númeroEspacio de direcciones virtualCuánta memoria hay realmente, quién más la usa, fragmentación física
Procesador único ejecutando instruccionesProceso e hiloQue el procesador se comparte entre decenas de programas
Interfaz de red y sus tramasSocketProtocolos, colas del controlador, fragmentación de paquetes
Teclado, pantalla, puerto serie, impresoraDescriptor de archivoQue cada dispositivo tiene un protocolo eléctrico distinto
Interrupción del temporizadorReloj, temporizadores, planificaciónFrecuencia real del hardware y su manejo de interrupciones
Registros de control de una tarjeta gráficaSuperficie de dibujo, contexto gráficoModelo de tarjeta, tamaño de sus buffers, su lenguaje de comandos

La regla general que se repite en toda la tabla es la misma: el sistema operativo cambia una interfaz difícil, específica del hardware y peligrosa, por una interfaz simple, uniforme y con reglas de acceso.

Comparación directa entre las dos vistas

Esta tabla condensa el contraste. Vale la pena volver a ella al terminar el curso, porque casi todos los capítulos siguientes desarrollan una fila.

AspectoMáquina desnudaMáquina extendida
Unidad de almacenamientoBloque físico numerado en un medio concretoArchivo con nombre, permisos y tamaño variable
Direccionamiento de memoriaDirección física real, compartida por todosEspacio virtual privado, empieza en la misma dirección para todos
EjecuciónUn flujo de instrucciones, sin interrupción lógicaMuchos procesos que aparentan correr a la vez
ErroresUn programa mal escrito corrompe cualquier zona de memoriaUn programa mal escrito muere solo y el resto sigue
PortabilidadPrograma atado a un modelo exacto de hardwarePrograma portable entre máquinas muy distintas
Acceso a dispositivosRegistros de control y puertos por fabricanteLlamadas al sistema uniformes sobre descriptores
ConcurrenciaInexistente; hay que construirla desde ceroProvista y arbitrada por el planificador
SeguridadNinguna; todo el hardware está expuestoPermisos, aislamiento y validación en cada frontera
Tamaño típico del código de aplicaciónEnorme, con drivers incluidos en cada programaPequeño, delegando en las llamadas al sistema

El sistema operativo como gestor de recursos

La visión de máquina extendida explica por qué los programadores quieren un sistema operativo. La visión de gestor de recursos explica por qué los administradores del sistema y los diseñadores de hardware lo quieren.

Desde este ángulo, el sistema operativo no es un embellecedor de interfaces: es un árbitro. Hay un procesador y quince programas que quieren ejecutarse. Hay ocho gigabytes de memoria y programas que en conjunto piden veinte. Hay una interfaz de red y cuarenta conexiones abiertas. Alguien tiene que decidir quién obtiene qué, cuándo y por cuánto tiempo.

El reparto se hace de dos maneras, y toda la administración de recursos que verás en el curso cae en una de ellas.

Multiplexado en el tiempo

Cuando un recurso no se puede partir, se reparte por turnos: cada solicitante lo usa completo durante un intervalo, y luego lo cede.

El caso emblemático es el procesador. Un núcleo ejecuta un flujo de instrucciones a la vez, punto. La ilusión de que tu editor de texto, tu navegador y tu reproductor de música corren simultáneamente se construye alternando entre ellos muchas veces por segundo. El temporizador del hardware interrumpe al procesador periódicamente, el sistema operativo aprovecha esa interrupción para guardar el estado del programa actual y cargar el estado de otro, y así sucesivamente.

Otros recursos multiplexados en el tiempo: la impresora, que atiende un trabajo completo antes del siguiente; el bus de acceso al disco; un canal de comunicación exclusivo.

Multiplexado en el espacio

Cuando un recurso se puede partir, se reparte en porciones simultáneas: cada solicitante recibe un trozo y lo usa al mismo tiempo que los demás usan los suyos.

El caso emblemático es la memoria principal. No hace falta que un programa tenga toda la memoria durante un turno; basta con que tenga una porción, siempre que nadie más pueda tocar esa porción. Lo mismo ocurre con el espacio en disco: cada archivo ocupa bloques distintos y todos existen a la vez.

flowchart LR
    subgraph tiempo["Multiplexado en el TIEMPO"]
        direction TB
        T1["Turno 1: proceso A usa el nucleo"]
        T2["Turno 2: proceso B usa el nucleo"]
        T3["Turno 3: proceso C usa el nucleo"]
        T4["Turno 4: proceso A usa el nucleo"]
        T1 --> T2 --> T3 --> T4
    end

    subgraph espacio["Multiplexado en el ESPACIO"]
        direction TB
        E1["Proceso A: paginas 0 a 511"]
        E2["Proceso B: paginas 512 a 1023"]
        E3["Proceso C: paginas 1024 a 1535"]
    end

    R["Recurso escaso"] --> D{"Se puede<br/>partir?"}
    D -->|No: procesador,<br/>impresora| tiempo
    D -->|Si: memoria,<br/>disco| espacio

    style R fill:#1e3a5f,color:#fff
    style D fill:#5f4a1e,color:#fff

Las tres preguntas del gestor

Cada vez que un recurso escasea, el sistema operativo debe responder tres preguntas, y las respuestas se llaman políticas:

  1. ¿A quién se lo doy? Es la política de asignación. ¿Al que lleva más tiempo esperando? ¿Al de mayor prioridad? ¿Al que pidió menos?
  2. ¿Por cuánto tiempo? Es la política de retención. ¿Hasta que termine? ¿Hasta que se acabe su turno? ¿Hasta que alguien más urgente lo pida?
  3. ¿Qué hago si nadie cede? Es la política de recuperación, y es donde aparecen los interbloqueos, la inanición y las decisiones difíciles que veremos en los capítulos de concurrencia.

Un detalle que se suele pasar por alto: el sistema operativo separa mecanismo de política. El mecanismo es cómo se cambia de un proceso a otro; la política es a quién se elige. Esa separación permite cambiar la política sin reescribir el mecanismo, y es una de las razones por las que un mismo kernel puede servir para un servidor y para un teléfono.

Modo usuario y modo kernel: la frontera que hace posible todo lo anterior

Hasta aquí hemos dicho que el sistema operativo arbitra. Falta explicar con qué autoridad. Si el sistema operativo es simplemente otro programa corriendo en el mismo procesador, ¿qué le impide a tu programa ignorarlo y escribir directamente en los registros del controlador de disco?

La respuesta es que el hardware colabora. Los procesadores modernos tienen al menos dos modos de ejecución:

  • Modo kernel (también llamado modo supervisor o anillo 0): todas las instrucciones del procesador están permitidas. Se pueden tocar registros de control, cambiar la tabla de páginas, deshabilitar interrupciones, acceder a cualquier puerto.
  • Modo usuario (anillo 3 en la arquitectura x86): un subconjunto de instrucciones está prohibido. Si el programa intenta ejecutar una instrucción privilegiada, el procesador no la ejecuta: genera una excepción y transfiere el control al sistema operativo.

Tu código corre siempre en modo usuario. El kernel corre en modo kernel. Y aquí está lo importante: el paso de modo usuario a modo kernel no se puede hacer de forma arbitraria. Solo hay tres formas de cruzar esa frontera, y las tres están controladas por el hardware:

  1. Una llamada al sistema: el programa ejecuta una instrucción especial de trampa que salta a una dirección fija predefinida por el kernel.
  2. Una excepción: el programa hizo algo inválido, como dividir por cero o acceder a una dirección de memoria que no le pertenece.
  3. Una interrupción de hardware: un dispositivo o el temporizador pidió atención.

En los tres casos, el procesador cambia a modo kernel y al mismo tiempo salta a una dirección que el kernel eligió de antemano. Nunca puedes cambiar de modo y seguir ejecutando tu propio código. Esa combinación es la clave de toda la seguridad del sistema.

stateDiagram-v2
    [*] --> ModoUsuario

    ModoUsuario: Modo usuario
    ModoUsuario: Instrucciones privilegiadas prohibidas
    ModoUsuario: Solo memoria propia

    ModoKernel: Modo kernel
    ModoKernel: Todas las instrucciones permitidas
    ModoKernel: Toda la memoria fisica visible

    ModoUsuario --> ModoKernel: llamada al sistema<br/>instruccion de trampa
    ModoUsuario --> ModoKernel: excepcion<br/>division por cero, fallo de pagina
    ModoUsuario --> ModoKernel: interrupcion de hardware<br/>temporizador, disco, red

    ModoKernel --> ModoUsuario: retorno de trampa<br/>con el resultado
    ModoKernel --> ElProcesoMuere: la excepcion no es recuperable

    ElProcesoMuere: El proceso termina
    ElProcesoMuere: El resto del sistema sigue intacto

    ElProcesoMuere --> [*]

Esta separación explica de una vez la anécdota del comienzo del capítulo. Cuando un programa “deja de responder” o se cierra inesperadamente, lo que ocurrió fue que intentó algo inválido, el hardware generó una excepción, el control saltó al kernel, y el kernel decidió terminar ese proceso. El resto del sistema no se enteró porque ese proceso nunca tuvo acceso a la memoria de los demás.

Anatomía de una llamada al sistema

La llamada al sistema es el punto de contacto entre la máquina extendida y la máquina desnuda. Vale la pena verla en cámara lenta, porque casi todo lo que estudiaremos después pasa por aquí.

Tomemos la línea write(fd, mensaje, 34) del ejemplo en C y veamos qué ocurre realmente:

sequenceDiagram
    participant App as Programa en modo usuario
    participant Lib as Biblioteca del sistema
    participant CPU as Procesador
    participant Ker as Kernel en modo kernel
    participant Drv as Controlador de disco

    App->>Lib: write(fd, mensaje, 34)
    Note over Lib: coloca el numero de la<br/>llamada y los argumentos<br/>en los registros acordados
    Lib->>CPU: instruccion de trampa
    Note over CPU: cambia a modo kernel<br/>y salta a la direccion<br/>fija del despachador
    CPU->>Ker: entrada al despachador de llamadas
    Ker->>Ker: valida el numero de llamada
    Ker->>Ker: valida que fd este abierto y sea escribible
    Ker->>Ker: valida que el puntero apunte a<br/>memoria del propio proceso
    Ker->>Ker: copia los 34 bytes a un buffer del kernel
    Ker->>Drv: encola la escritura en el dispositivo
    Note over Ker: el proceso queda bloqueado;<br/>el planificador entrega el<br/>nucleo a otro proceso
    Drv-->>Ker: interrupcion: transferencia completa
    Ker->>Ker: marca el proceso como listo
    Ker->>CPU: retorno de trampa con el valor 34
    CPU->>App: vuelve a modo usuario
    Note over App: write devuelve 34

Hay cuatro momentos en ese diagrama que conviene subrayar, porque son los que hacen que el sistema sea seguro y no solo cómodo:

Uno: la validación del descriptor. El kernel no confía en que fd sea un número válido. Comprueba que ese proceso tenga efectivamente ese descriptor abierto y que se haya abierto con permiso de escritura. Si le pasas un número inventado, la llamada falla, no corrompe nada.

Dos: la validación del puntero. El kernel no confía en que mensaje apunte a memoria legítima. Comprueba que la dirección pertenezca al espacio del proceso que llama. Si no, la llamada falla con un error de dirección inválida. Sin esta validación, cualquier programa podría pedirle al kernel que leyera memoria del kernel y se la copiara.

Tres: la copia. El kernel copia los datos a su propio espacio antes de usarlos. Si trabajara directamente sobre el buffer del proceso, ese proceso podría modificar los datos entre la validación y el uso.

Cuatro: el bloqueo. Mientras el disco trabaja, el proceso no ocupa el procesador. El planificador se lo entrega a otro. Esto es multiplexado en el tiempo aplicado exactamente en el momento en que más se nota.

Ver las llamadas al sistema con tus propios ojos

En Linux puedes observar todas las llamadas al sistema que hace un programa:

strace -f ./guardar

La salida incluirá líneas como estas, entre muchas otras del arranque del proceso:

openat(AT_FDCWD, "saludo.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 3
write(3, "hola desde una llamada al sistem"..., 34) = 34
close(3)                                = 0

Ese = 3 es el descriptor de archivo que el kernel te asignó. Ese = 34 es el número de bytes que efectivamente aceptó. Si repites el experimento con un programa más grande, verás que incluso escribir en pantalla, reservar memoria o mirar la hora pasan por esta misma frontera.

Prueba también con un comando cualquiera para dimensionar cuántas llamadas hace un programa trivial:

strace -c ls

La opción -c imprime un resumen contando llamadas por tipo. Un simple listado de directorio hace decenas de ellas.

En macOS el equivalente aproximado es dtruss, que requiere permisos elevados. En sistemas donde no dispongas de estas herramientas, el concepto sigue siendo el mismo aunque no puedas observarlo directamente.

La misma llamada, tres lenguajes

El curso usa Ada y Elixir además de C, y hay una razón para cada uno. Vale la pena verlos ya en el primer capítulo para que las abstracciones no queden atadas a un solo lenguaje.

C: la vista más cercana al kernel

Ya vimos el ejemplo. En C, las funciones de la biblioteca estándar son envoltorios finísimos sobre las llamadas al sistema. write en C corresponde casi uno a uno con la llamada write del kernel. Es el lenguaje en el que están escritos la mayoría de los kernels, precisamente porque no interpone capas.

Ada: tipos fuertes y concurrencia en el lenguaje

Ada nació de un encargo del Departamento de Defensa de Estados Unidos en los años setenta para unificar el desarrollo de sistemas embebidos críticos. Su rasgo distintivo para este curso es que la concurrencia forma parte del lenguaje, no de una biblioteca. Eso permite estudiar tareas, sincronización y comunicación sin depender de ninguna API externa.

El equivalente del ejemplo anterior:

with Ada.Text_IO;

procedure Guardar is
   Archivo : Ada.Text_IO.File_Type;
begin
   Ada.Text_IO.Create (File => Archivo,
                       Mode => Ada.Text_IO.Out_File,
                       Name => "saludo_ada.txt");

   Ada.Text_IO.Put_Line (Archivo, "hola desde Ada");

   Ada.Text_IO.Close (Archivo);
   Ada.Text_IO.Put_Line ("archivo escrito");
end Guardar;

Guárdalo como guardar.adb y compílalo con el compilador GNAT:

gnatmake guardar.adb
./guardar
cat saludo_ada.txt

Y aquí está lo que hace a Ada interesante para un curso de sistemas operativos: lanzar dos flujos concurrentes no requiere ninguna biblioteca.

with Ada.Text_IO; use Ada.Text_IO;

procedure Dos_Tareas is

   task Contador_A;
   task Contador_B;

   task body Contador_A is
   begin
      for I in 1 .. 5 loop
         Put_Line ("A:" & Integer'Image (I));
         delay 0.1;
      end loop;
   end Contador_A;

   task body Contador_B is
   begin
      for I in 1 .. 5 loop
         Put_Line ("B:" & Integer'Image (I));
         delay 0.1;
      end loop;
   end Contador_B;

begin
   Put_Line ("principal: las dos tareas ya estan activas");
end Dos_Tareas;
gnatmake dos_tareas.adb
./dos_tareas

Las tareas declaradas dentro de la parte declarativa de un procedimiento comienzan a ejecutarse cuando empieza el cuerpo del procedimiento. La salida entremezclará las líneas de A y B, y el orden exacto no está garantizado. Ejecútalo varias veces: verás órdenes distintos. Esa no determinación es el tema central de los capítulos de concurrencia.

Elixir: procesos ligeros y paso de mensajes

Elixir corre sobre la máquina virtual de Erlang, que implementa su propio modelo de procesos: miles de procesos ligeros, aislados entre sí, que no comparten memoria y solo se comunican enviándose mensajes. Es, en efecto, un pequeño sistema operativo dentro de un proceso del sistema operativo real, y por eso resulta un laboratorio excelente: puedes estudiar planificación, aislamiento y comunicación sin arriesgar tu máquina.

defmodule Guardar do
  def ejecutar do
    contenido = "hola desde Elixir\n"

    case File.write("saludo_elixir.txt", contenido) do
      :ok ->
        IO.puts("archivo escrito: #{byte_size(contenido)} bytes")

      {:error, razon} ->
        IO.puts("fallo la escritura: #{:file.format_error(razon)}")
    end
  end
end

Guardar.ejecutar()
elixir guardar.exs
cat saludo_elixir.txt

Y el equivalente concurrente del ejemplo de Ada:

defmodule Reloj do
  def contar(nombre, 0) do
    IO.puts("#{nombre}: fin")
  end

  def contar(nombre, n) do
    IO.puts("#{nombre}: #{n}")
    Process.sleep(100)
    contar(nombre, n - 1)
  end
end

spawn(fn -> Reloj.contar("A", 5) end)
spawn(fn -> Reloj.contar("B", 5) end)

# El proceso principal espera a que ambos terminen antes de salir.
Process.sleep(1000)
elixir reloj.exs

Igual que en Ada, el orden de las líneas varía entre ejecuciones.

Un detalle que conecta directo con el tema del capítulo: puedes preguntarle a Elixir por el sistema operativo sobre el que está corriendo, y verás que la máquina virtual también depende de la máquina extendida:

IO.inspect(:os.type(), label: "familia de sistema operativo")
IO.inspect(:erlang.system_info(:logical_processors), label: "procesadores logicos")
IO.inspect(System.pid(), label: "identificador de proceso asignado por el kernel")

Ese último valor es el PID que el kernel del sistema operativo le dio a la máquina virtual completa, sin importar cuántos miles de procesos ligeros haya dentro.

Comparación de los tres enfoques

DimensiónCAdaElixir
Distancia al kernelMínima; envoltorios directosMedia; biblioteca estándar tipadaAlta; máquina virtual intermedia
Modelo de concurrenciaHilos vía biblioteca externaTareas integradas en el lenguajeProcesos ligeros de la máquina virtual
Compartición de memoriaTotal y sin controlControlada por objetos protegidosNinguna; solo mensajes
Detección de erroresEn ejecución, muchas veces tardeFuerte en compilación y en ejecuciónEn ejecución, con supervisión y reinicio
Rol en este cursoVer el mecanismo realEstudiar sincronización clásicaEstudiar paso de mensajes y aislamiento

Por qué existe: la presión que lo hizo aparecer

Los sistemas operativos no se diseñaron en una pizarra: aparecieron porque el hardware se volvió demasiado caro para desperdiciarlo y demasiado complejo para programarlo directamente. El capítulo 2 recorre esta historia en detalle, pero conviene fijar ahora la lógica que la explica.

En 1972, Gordon Bell observó que cada década aparecía una nueva clase de computador, más pequeña y más barata, que multiplicaba por diez o por cien la cantidad de máquinas en el mundo. La secuencia aproximada es esta:

DécadaClase dominanteConsecuencia para el sistema operativo
1960MainframesEl tiempo de máquina es carísimo; nacen los sistemas por lotes y luego el tiempo compartido
1970MinicomputadoresMuchas máquinas por organización; portabilidad y sistemas de archivos jerárquicos
1980Computadores personalesUna máquina por persona; interfaces gráficas y sistemas de archivos simples
1990InternetTodo se conecta; pila de red integrada en el kernel y seguridad como problema central
2000Cómputo en la nubeMuchos inquilinos por máquina física; virtualización y aislamiento reforzado
2010 en adelanteMóviles y dispositivos conectadosEnergía como recurso escaso; permisos por aplicación y aislamiento estricto

El patrón que se repite en cada fila: cuando cambia qué recurso es escaso, cambia lo que el sistema operativo tiene que administrar. En los sesenta lo escaso era el tiempo de procesador. En los noventa, el ancho de banda. En los dos mil, el aislamiento entre inquilinos. En los teléfonos, la batería. Cada vez que leas una decisión de diseño que te parezca arbitraria, pregúntate qué recurso era escaso cuando se tomó.

La otra presión: la complejidad

La segunda razón por la que existe el sistema operativo es de escala. La cantidad de código necesaria para manejar el hardware moderno es tan grande que ningún programa de aplicación podría cargarla. Un kernel de propósito general contiene millones de líneas, y la mayor parte son controladores de dispositivo: código específico para cada modelo de tarjeta de red, de controlador de almacenamiento, de sensor. El software de un vehículo moderno supera los cien millones de líneas contando todos sus subsistemas.

Sin un sistema operativo, cada aplicación tendría que incluir su propia copia de todo ese código. Con él, ese código se escribe una vez, vive en el kernel, y todas las aplicaciones lo aprovechan a través de las mismas llamadas.

El hardware que el sistema operativo debe conciliar

Hay una tercera dificultad que la máquina extendida esconde, y es de las más brutales: los componentes de un computador operan en escalas de tiempo que difieren en órdenes de magnitud enormes. El sistema operativo tiene que coordinarlos todos.

Estos son órdenes de magnitud aproximados, útiles para desarrollar intuición, no cifras exactas de ningún equipo en particular:

OperaciónTiempo aproximadoEn escala humana, si 1 ns fuera 1 segundo
Acceso a caché de nivel 10,5 nsMedio segundo
Predicción de salto fallida5 ns5 segundos
Acceso a caché de nivel 27 ns7 segundos
Bloqueo y liberación de un cerrojo25 nsMedio minuto
Acceso a memoria principal100 ns2 minutos
Enviar 1 KB por una red de 1 Gbps10 µs3 horas
Lectura aleatoria en almacenamiento de estado sólido150 µs2 días
Ida y vuelta dentro de un centro de datos500 µs6 días
Búsqueda en un disco mecánico10 ms4 meses
Paquete de California a Europa y de vuelta150 ms5 años

Lee esa tabla dos veces. La columna de la derecha es la que da la intuición correcta: si un acceso a caché es medio segundo, esperar una respuesta intercontinental es esperar cinco años. Ningún diseño razonable deja el procesador parado durante cinco años equivalentes.

De ahí sale una de las decisiones más importantes del sistema operativo: cuando un proceso pide algo lento, el kernel lo bloquea y entrega el procesador a otro. Todo el capítulo de procesos y el de planificación giran alrededor de esta tabla.

El ciclo del procesador y dónde entra el sistema operativo

Para que las interrupciones dejen de ser una palabra abstracta, hay que mirar cómo funciona un procesador en su nivel más básico. Un procesador hace, eternamente, tres cosas en secuencia:

  1. Buscar la instrucción que está en la dirección indicada por el contador de programa.
  2. Decodificar esa instrucción: averiguar qué operación es y sobre qué operandos.
  3. Ejecutar la operación y avanzar el contador de programa a la siguiente instrucción.

Este ciclo no tiene pausa ni noción de “programa”. Solo hay direcciones e instrucciones. Si el contador de programa apunta a tu código, tu código corre. Si apunta al código del kernel, corre el kernel. Es exactamente el mismo procesador haciendo exactamente lo mismo.

Lo que convierte ese bucle ciego en un sistema multitarea es un cuarto paso que el hardware inserta:

  1. Antes de buscar la siguiente instrucción, revisar si hay alguna interrupción pendiente. Si la hay, guardar el contador de programa actual y saltar a la dirección del manejador correspondiente, cambiando además a modo kernel.

Ese paso número cuatro es todo el poder que tiene el sistema operativo sobre los programas. Sin él, un programa con un bucle infinito se quedaría con el procesador para siempre y ningún kernel podría quitárselo, porque el kernel no estaría corriendo para intentarlo.

sequenceDiagram
    participant PC as Contador de programa
    participant CPU as Ciclo del procesador
    participant TMR as Temporizador
    participant KER as Kernel
    participant PLA as Planificador

    Note over CPU: proceso A ejecutando su bucle
    loop mientras no haya interrupcion
        CPU->>PC: buscar instruccion
        CPU->>CPU: decodificar
        CPU->>CPU: ejecutar
        CPU->>PC: avanzar
    end

    TMR-->>CPU: interrupcion de temporizador
    CPU->>CPU: guardar contador de programa<br/>y registros del proceso A
    CPU->>CPU: cambiar a modo kernel
    CPU->>KER: saltar al manejador del temporizador
    KER->>PLA: se agoto el turno de A
    PLA->>PLA: elegir el siguiente proceso segun la politica
    PLA-->>KER: elegido: proceso B
    KER->>CPU: restaurar registros del proceso B
    KER->>CPU: retorno de interrupcion, modo usuario
    Note over CPU: proceso B ejecutando<br/>sin haber notado nada

La operación de guardar los registros de un proceso y cargar los de otro se llama cambio de contexto, y es uno de los conceptos que más aparecerán en el resto del curso. Tiene un costo medible: hay que escribir decenas de registros en memoria, leer otros tantos, y además las cachés del procesador quedan llenas de datos del proceso anterior que ya no sirven. Ese costo es la razón por la que “más procesos” no significa automáticamente “más rápido”.

Interrupción, excepción y trampa: tres nombres para el mismo mecanismo

Las tres palabras describen entradas al kernel, pero se originan en lugares distintos y conviene no mezclarlas:

TérminoQuién la provocaEs síncrona con el programaEjemplo típicoQué hace el kernel
InterrupciónUn dispositivo externoNo; llega en cualquier momentoEl temporizador vence; el disco terminó una transferenciaAtiende el dispositivo y decide si cambiar de proceso
ExcepciónEl propio programa, por accidenteSí; siempre en la misma instrucciónDivisión por cero; acceso a una dirección inválidaRepara si puede, o termina el proceso
TrampaEl propio programa, a propósitoSí; es una instrucción explícitaUna llamada al sistemaEjecuta el servicio solicitado y devuelve el resultado

Un caso interesante que borra la línea entre excepción y servicio útil es el fallo de página. Cuando un programa accede a una dirección virtual que todavía no tiene memoria física asociada, el hardware genera una excepción. Pero el kernel no mata al proceso: asigna una página física, actualiza la tabla de traducción y reinicia la instrucción que falló. El programa no se entera de nada. Ese mecanismo es la base de la memoria virtual y de por qué puedes ejecutar programas que piden más memoria de la que hay instalada.

Las cuatro garantías que ofrece la máquina extendida

Toda la maquinaria descrita hasta aquí existe para sostener cuatro promesas. Vale la pena nombrarlas explícitamente, porque cada una tiene un costo y todas se contradicen entre sí en algún punto.

Aislamiento. Un proceso no puede leer ni modificar la memoria de otro, ni sus archivos abiertos, ni su estado. Se paga con el hardware de memoria virtual y con el costo de traducir cada dirección. Es lo que permite ejecutar código que no escribiste tú sin apostar el sistema entero.

Equidad. Todos los procesos que pueden ejecutarse llegan a ejecutarse en un plazo acotado; ninguno se queda esperando indefinidamente. Se paga con cambios de contexto frecuentes, y entra en tensión directa con el rendimiento total: cambiar menos sería más eficiente, pero menos justo.

Eficiencia. El hardware no debe quedarse ocioso mientras hay trabajo pendiente. Es lo que motiva bloquear a un proceso que espera al disco en vez de dejarlo ocupando el procesador. Entra en tensión con la equidad y con el aislamiento, porque toda verificación cuesta ciclos.

Portabilidad. El mismo programa corre sobre hardware distinto sin cambios. Se paga con capas de indirección: el sistema de archivos no habla con el disco, habla con un controlador que habla con el disco. Cada capa agrega latencia a cambio de independencia.

La tensión entre estas cuatro garantías es el motivo real de casi todas las decisiones de diseño que vamos a estudiar. Cuando en el capítulo de planificación aparezcan varios algoritmos distintos, no será porque unos sean mejores que otros: será porque cada uno privilegia una garantía distinta.

GarantíaMecanismo principal que la sostieneCon qué garantía compite
AislamientoMemoria virtual, modo usuario, validación en llamadas al sistemaEficiencia: cada verificación cuesta ciclos
EquidadPlanificador con desalojo por temporizadorEficiencia: cada cambio de contexto es tiempo perdido
EficienciaBloqueo de procesos en espera, cachés, escritura diferidaAislamiento y previsibilidad
PortabilidadAbstracciones uniformes y capa de controladoresEficiencia: la indirección agrega latencia

Qué NO es un sistema operativo

Es tan útil delimitar por fuera como definir por dentro. Hay tres cosas que se confunden habitualmente con el sistema operativo y que no lo son:

El intérprete de comandos no es el sistema operativo. Cuando escribes ls en una terminal, estás hablando con un programa llamado shell, que corre en modo usuario como cualquier otro. El shell traduce lo que escribes en llamadas al sistema, pero es reemplazable: puedes cambiar de shell sin cambiar de kernel.

La interfaz gráfica no es el sistema operativo. El escritorio, las ventanas y el gestor de archivos son aplicaciones. En muchos sistemas es perfectamente posible arrancar sin ninguna interfaz gráfica y el sistema operativo funciona igual.

Las bibliotecas del lenguaje no son el sistema operativo. Cuando llamas a una función que reserva memoria, esa función normalmente administra un bloque que ya le pidió al kernel, y solo vuelve al kernel cuando se le acaba. Buena parte del trabajo ocurre completamente en modo usuario.

Lo que sí es indiscutiblemente sistema operativo es el código que corre en modo kernel. Esa es la frontera limpia. Todo lo demás es discusión de dónde poner la línea, y de hecho es la discusión que separa a los diseños monolíticos de los micronúcleos, tema que abordaremos más adelante.

Componentes internos: el mapa del kernel

Para cerrar la parte conceptual, este es el esqueleto de un sistema operativo de propósito general. Cada bloque corresponde, aproximadamente, a un capítulo o a un grupo de capítulos de este curso.

flowchart TB
    subgraph usuario["Modo usuario"]
        APP1["Aplicaciones"]
        SHELL["Interprete de comandos"]
        LIBS["Bibliotecas del sistema"]
        APP1 --> LIBS
        SHELL --> LIBS
    end

    FRONTERA["Interfaz de llamadas al sistema"]
    LIBS --> FRONTERA

    subgraph kernel["Modo kernel"]
        PROC["Gestion de procesos<br/>creacion, estados, planificacion"]
        MEM["Gestion de memoria<br/>espacios virtuales, paginacion"]
        FS["Sistema de archivos<br/>nombres, permisos, bloques"]
        IPC["Comunicacion y sincronizacion<br/>semaforos, monitores, mensajes"]
        NET["Pila de red<br/>sockets, protocolos"]
        DRV["Controladores de dispositivo"]
        INT["Manejo de interrupciones"]
    end

    FRONTERA --> PROC
    FRONTERA --> MEM
    FRONTERA --> FS
    FRONTERA --> IPC
    FRONTERA --> NET

    PROC <--> MEM
    PROC <--> IPC
    FS --> DRV
    NET --> DRV
    DRV <--> INT
    INT --> PROC

    HW["Hardware: procesador, memoria,<br/>almacenamiento, red, temporizador"]
    DRV --> HW
    INT --> HW

    style FRONTERA fill:#5f4a1e,color:#fff
    style HW fill:#3a3a3a,color:#fff

Observa las flechas que cruzan dentro del kernel. La gestión de procesos y la de memoria se hablan constantemente, porque cada proceso tiene su espacio de direcciones. Los controladores y el manejo de interrupciones son inseparables. Y todo lo que entra desde arriba pasa por un único punto controlado: la interfaz de llamadas al sistema. Esa estrangulación deliberada es lo que hace auditable al sistema.

Experimentos para hacer ahora mismo

La teoría de este capítulo se puede verificar en pocos minutos. Estos comandos son de sistemas tipo Unix; si usas otro sistema, busca el equivalente.

Ver los procesos que el sistema operativo está multiplexando en el tiempo:

ps -eo pid,ppid,stat,comm --sort=-pid | head -20

La columna STAT te dice el estado: R para ejecutable, S para dormido esperando algo, D para bloqueado en entrada/salida. La mayoría estará dormida: eso confirma que los procesos pasan casi todo su tiempo esperando, no calculando.

Ver la máquina extendida de un proceso concreto:

ls -l /proc/self/fd

Ese directorio muestra los descriptores de archivo abiertos por el propio comando. Verás al menos tres: entrada estándar, salida estándar y error estándar. Los tres son “archivos” aunque ninguno sea un archivo en disco.

Ver el espacio de direcciones virtual:

cat /proc/self/maps

Cada línea es una región del espacio virtual con sus permisos: lectura, escritura, ejecución. Nota que las direcciones son grandes y ordenadas, nada que ver con la disposición física real de la memoria.

Ver cuánto tiempo se pasa en modo usuario y cuánto en modo kernel:

time ./guardar

La salida distingue real (tiempo transcurrido), user (tiempo de procesador en modo usuario) y sys (tiempo de procesador en modo kernel). Ese último número es literalmente cuánto tiempo estuvo el sistema operativo trabajando por ti.

Comparar un programa que calcula contra uno que hace entrada/salida:

# Mucho tiempo en modo usuario, casi nada en modo kernel
time bash -c 'x=0; for i in $(seq 1 200000); do x=$((x+i)); done'

# Mucho tiempo en modo kernel: cada escritura cruza la frontera
time bash -c 'for i in $(seq 1 20000); do echo "$i" > /dev/null; done'

Errores comunes al empezar

Estos son los malentendidos que más se repiten en este punto del curso, con su origen y su corrección.

Error frecuentePor qué se produceCómo corregirlo
Creer que el sistema operativo “corre” todo el tiempo en paralelo con los programasSe lo imagina como un programa más que estuviera siempre activoEl kernel es código reactivo: solo se ejecuta cuando lo despierta una llamada al sistema, una excepción o una interrupción. El resto del tiempo el procesador está en tu código
Confundir el intérprete de comandos con el sistema operativoLa terminal es la vía habitual de contacto con el sistemaEl shell es una aplicación en modo usuario; se puede reemplazar sin tocar el kernel. Compruébalo ejecutando otro shell dentro del actual
Pensar que un archivo es un objeto físico continuo en el discoEl nombre y el tamaño sugieren un objeto únicoUn archivo es una estructura lógica que puede estar repartida en bloques dispersos. El sistema de archivos mantiene el mapa
Creer que la dirección de memoria que imprime un programa es una dirección física realEl número parece concreto y verificableEs una dirección virtual, válida solo dentro de ese proceso. Dos procesos pueden imprimir la misma dirección apuntando a memoria física distinta
Suponer que dos tareas concurrentes producen siempre la misma salidaEl programa es determinista línea por línea, así que se espera determinismo globalEl planificador decide el intercalado y no es reproducible. Ejecuta el ejemplo de Ada o Elixir varias veces y observa órdenes distintos
Asumir que más procesos siempre significa más rendimientoSe equipara concurrencia con velocidadCada cambio de contexto cuesta tiempo. Pasado cierto punto, agregar procesos reduce el rendimiento total
Pensar que un programa puede leer la memoria de otro si conoce la direcciónEn la máquina desnuda sería ciertoEl hardware de memoria virtual bloquea el acceso y genera una excepción. Sin cooperación explícita del kernel, no hay lectura cruzada
Creer que write garantiza que los datos ya están en el discoLa llamada devuelve un número de bytes y parece completaLos datos pueden estar en un buffer del kernel. Para forzar la escritura al medio hay una llamada específica de sincronización, y aun así los detalles dependen del sistema de archivos
Suponer que “sistema operativo” y “kernel” son sinónimos exactosSe usan indistintamente en el habla comúnEl kernel es el código en modo kernel. La distribución completa incluye además bibliotecas, servicios y utilidades en modo usuario
Pensar que el sistema operativo elige la política de planificación por razones técnicas purasSe asume que hay una respuesta óptima únicaLa política depende del objetivo: latencia baja en un escritorio, rendimiento total en un servidor, previsibilidad en un sistema de tiempo real. No hay una que gane siempre

Mapa del curso

Este curso avanza desde la definición conceptual hasta escribir código concurrente correcto. Estos son los bloques y lo que aporta cada uno:

flowchart TD
    B1["BLOQUE 1: Fundamentos<br/>Que es un sistema operativo<br/>Historia y roles"]
    B2["BLOQUE 2: Kernel y procesos<br/>Arquitectura de computadoras<br/>Conceptos del kernel<br/>Procesos y planificacion"]
    B3["BLOQUE 3: Sincronizacion<br/>Concurrencia y sus problemas<br/>Semaforos, monitores, mensajes"]
    B4["BLOQUE 4: Almacenamiento<br/>Sistemas de archivos"]
    B5["BLOQUE 5: Practica en Ada y Elixir<br/>Del hola mundo a lanzar procesos"]
    B6["BLOQUE 6: Proyectos<br/>Minishell, concurrencia,<br/>servidor de videojuegos"]

    B1 --> B2 --> B3 --> B4
    B3 --> B5
    B4 --> B6
    B5 --> B6

    style B1 fill:#1e3a5f,color:#fff
    style B6 fill:#1e5f3a,color:#fff

Y este es el detalle de qué pregunta responde cada bloque:

BloquePreguntas que responde
Fundamentos¿Qué es un sistema operativo, por qué apareció, qué roles cumple?
Kernel y procesos¿Cómo funciona el procesador por dentro? ¿Qué es exactamente un proceso? ¿Cómo se decide quién corre?
Sincronización¿Qué sale mal cuando dos flujos tocan lo mismo? ¿Cómo se coordina sin bloquear todo?
Almacenamiento¿Cómo se convierten bloques de un medio físico en archivos con nombre y permisos?
Práctica en Ada y Elixir¿Cómo se escribe todo lo anterior en código que compila y corre?
Proyectos¿Puedo construir un intérprete de comandos, un programa concurrente correcto y un servidor?

El hilo conductor es siempre el mismo: cada capítulo toma una parte de la máquina desnuda y muestra cómo se convierte en una parte de la máquina extendida.

Ejercicios propuestos

Los primeros son de observación, los últimos de escritura de código. Ninguno requiere hardware especial.

Ejercicio 1 — Inventario de abstracciones. Elige tres programas que uses a diario. Para cada uno, enumera al menos cuatro abstracciones del sistema operativo que necesita para funcionar y explica en una frase qué haría si esa abstracción no existiera.

Ejercicio 2 — Contar cruces de frontera. Ejecuta strace -c sobre tres comandos distintos: uno que solo calcule, uno que lea archivos y uno que use la red. Compara los conteos totales de llamadas al sistema y explica el orden que obtuviste.

Ejercicio 3 — Direcciones virtuales. Escribe un programa en C que imprima la dirección de una variable local. Ejecútalo dos veces al mismo tiempo desde dos terminales. Anota si las direcciones coinciden y justifica el resultado en términos de espacios de direcciones virtuales.

Ejercicio 4 — Medir la frontera. Escribe dos programas: uno que llame write con un byte, un millón de veces; otro que acumule un millón de bytes en memoria y llame write una sola vez. Mide ambos con time y compara la columna sys. Explica la diferencia usando el diagrama de la llamada al sistema.

Ejercicio 5 — No determinismo en Ada. Toma el programa dos_tareas.adb de este capítulo, súbelo a veinte iteraciones por tarea y ejecútalo diez veces guardando la salida en archivos distintos. Compáralos y describe qué varía y qué se mantiene constante.

Ejercicio 6 — No determinismo en Elixir. Repite el ejercicio anterior con reloj.exs, pero además lanza cinco procesos en vez de dos. Anota si alguna ejecución produjo un orden estrictamente alternado y explica por qué eso es posible pero no garantizado.

Ejercicio 7 — La lista de la compra del gestor. Para cada uno de estos recursos, indica si se multiplexa en el tiempo o en el espacio, y justifica: núcleo de procesador, memoria principal, ancho de banda de red, espacio en disco, impresora, caché de nivel 1.

Ejercicio 8 — Escala de latencias. Usando la tabla de latencias, calcula cuántos accesos a memoria principal cabrían en el tiempo de una sola búsqueda en disco mecánico. Explica qué decisión de diseño del sistema operativo justifica ese número.

Ejercicio 9 — Delimitar el kernel. Argumenta en un párrafo si el gestor de ventanas debería correr en modo kernel o en modo usuario. Nombra un beneficio y un costo de cada opción.

Ejercicio 10 — Máquina desnuda hipotética. Describe paso a paso qué tendría que hacer un programa para mostrar la palabra “hola” en pantalla si no existiera ningún sistema operativo. Compáralo con la única línea que hace falta en Elixir. El objetivo no es la exactitud técnica de cada paso, sino dimensionar la distancia entre ambas máquinas.

Lo que viene

Este capítulo dejó fijadas las dos vistas que sostienen todo lo demás: el sistema operativo como máquina extendida, que reemplaza una interfaz de hardware hostil por abstracciones cómodas y portables, y como gestor de recursos, que reparte hardware escaso entre programas que compiten por él. También quedó claro con qué autoridad lo hace: la separación entre modo usuario y modo kernel, impuesta por el propio procesador, y la llamada al sistema como única puerta controlada entre ambos mundos.

Lo que todavía falta es entender por qué las cosas se hicieron así y no de otra manera. Casi todas las decisiones que estudiaremos tienen una fecha y un motivo: alguien tenía un problema concreto con un hardware concreto, y la solución que inventó sigue viva en el sistema operativo que estás usando ahora mismo. En el capítulo 2 recorremos esa línea de tiempo: del procesamiento por lotes en tarjetas perforadas al tiempo compartido, de los mainframes a los micronúcleos, de Unix a los sistemas móviles actuales. Verás aparecer, una por una, las ideas que hoy damos por evidentes, y entenderás qué escasez las hizo necesarias.

Si en algún momento quieres ubicarte dentro del recorrido completo, el temario del curso está en el índice general.