Introducción a los sistemas operativos: la máquina desnuda y la máquina extendida
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:
- 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.
- 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:
- Averiguar el modelo exacto del controlador de disco presente en la máquina.
- Leer su manual para saber en qué puertos vive y qué significa cada bit de sus registros de control.
- Traducir “quiero guardar este texto” a una posición física concreta del medio de almacenamiento.
- Programar una transferencia hacia el controlador, byte por byte o mediante acceso directo a memoria.
- Escribir el manejador de la interrupción que el controlador levantará cuando termine.
- Verificar los códigos de error del controlador, que no son los mismos entre fabricantes.
- 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ísico | Abstracción del sistema operativo | Qué esconde |
|---|---|---|
| Sectores y bloques de un medio de almacenamiento | Archivo y directorio | Geometría del medio, tipo de controlador, tamaño de bloque, sectores dañados |
| Memoria física direccionable por número | Espacio de direcciones virtual | Cuánta memoria hay realmente, quién más la usa, fragmentación física |
| Procesador único ejecutando instrucciones | Proceso e hilo | Que el procesador se comparte entre decenas de programas |
| Interfaz de red y sus tramas | Socket | Protocolos, colas del controlador, fragmentación de paquetes |
| Teclado, pantalla, puerto serie, impresora | Descriptor de archivo | Que cada dispositivo tiene un protocolo eléctrico distinto |
| Interrupción del temporizador | Reloj, temporizadores, planificación | Frecuencia real del hardware y su manejo de interrupciones |
| Registros de control de una tarjeta gráfica | Superficie de dibujo, contexto gráfico | Modelo 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.
| Aspecto | Máquina desnuda | Máquina extendida |
|---|---|---|
| Unidad de almacenamiento | Bloque físico numerado en un medio concreto | Archivo con nombre, permisos y tamaño variable |
| Direccionamiento de memoria | Dirección física real, compartida por todos | Espacio virtual privado, empieza en la misma dirección para todos |
| Ejecución | Un flujo de instrucciones, sin interrupción lógica | Muchos procesos que aparentan correr a la vez |
| Errores | Un programa mal escrito corrompe cualquier zona de memoria | Un programa mal escrito muere solo y el resto sigue |
| Portabilidad | Programa atado a un modelo exacto de hardware | Programa portable entre máquinas muy distintas |
| Acceso a dispositivos | Registros de control y puertos por fabricante | Llamadas al sistema uniformes sobre descriptores |
| Concurrencia | Inexistente; hay que construirla desde cero | Provista y arbitrada por el planificador |
| Seguridad | Ninguna; todo el hardware está expuesto | Permisos, aislamiento y validación en cada frontera |
| Tamaño típico del código de aplicación | Enorme, con drivers incluidos en cada programa | Pequeñ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:
- ¿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?
- ¿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?
- ¿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:
- Una llamada al sistema: el programa ejecuta una instrucción especial de trampa que salta a una dirección fija predefinida por el kernel.
- 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.
- 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ón | C | Ada | Elixir |
|---|---|---|---|
| Distancia al kernel | Mínima; envoltorios directos | Media; biblioteca estándar tipada | Alta; máquina virtual intermedia |
| Modelo de concurrencia | Hilos vía biblioteca externa | Tareas integradas en el lenguaje | Procesos ligeros de la máquina virtual |
| Compartición de memoria | Total y sin control | Controlada por objetos protegidos | Ninguna; solo mensajes |
| Detección de errores | En ejecución, muchas veces tarde | Fuerte en compilación y en ejecución | En ejecución, con supervisión y reinicio |
| Rol en este curso | Ver el mecanismo real | Estudiar sincronización clásica | Estudiar 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écada | Clase dominante | Consecuencia para el sistema operativo |
|---|---|---|
| 1960 | Mainframes | El tiempo de máquina es carísimo; nacen los sistemas por lotes y luego el tiempo compartido |
| 1970 | Minicomputadores | Muchas máquinas por organización; portabilidad y sistemas de archivos jerárquicos |
| 1980 | Computadores personales | Una máquina por persona; interfaces gráficas y sistemas de archivos simples |
| 1990 | Internet | Todo se conecta; pila de red integrada en el kernel y seguridad como problema central |
| 2000 | Cómputo en la nube | Muchos inquilinos por máquina física; virtualización y aislamiento reforzado |
| 2010 en adelante | Móviles y dispositivos conectados | Energí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ón | Tiempo aproximado | En escala humana, si 1 ns fuera 1 segundo |
|---|---|---|
| Acceso a caché de nivel 1 | 0,5 ns | Medio segundo |
| Predicción de salto fallida | 5 ns | 5 segundos |
| Acceso a caché de nivel 2 | 7 ns | 7 segundos |
| Bloqueo y liberación de un cerrojo | 25 ns | Medio minuto |
| Acceso a memoria principal | 100 ns | 2 minutos |
| Enviar 1 KB por una red de 1 Gbps | 10 µs | 3 horas |
| Lectura aleatoria en almacenamiento de estado sólido | 150 µs | 2 días |
| Ida y vuelta dentro de un centro de datos | 500 µs | 6 días |
| Búsqueda en un disco mecánico | 10 ms | 4 meses |
| Paquete de California a Europa y de vuelta | 150 ms | 5 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:
- Buscar la instrucción que está en la dirección indicada por el contador de programa.
- Decodificar esa instrucción: averiguar qué operación es y sobre qué operandos.
- 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:
- 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érmino | Quién la provoca | Es síncrona con el programa | Ejemplo típico | Qué hace el kernel |
|---|---|---|---|---|
| Interrupción | Un dispositivo externo | No; llega en cualquier momento | El temporizador vence; el disco terminó una transferencia | Atiende el dispositivo y decide si cambiar de proceso |
| Excepción | El propio programa, por accidente | Sí; siempre en la misma instrucción | División por cero; acceso a una dirección inválida | Repara si puede, o termina el proceso |
| Trampa | El propio programa, a propósito | Sí; es una instrucción explícita | Una llamada al sistema | Ejecuta 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ía | Mecanismo principal que la sostiene | Con qué garantía compite |
|---|---|---|
| Aislamiento | Memoria virtual, modo usuario, validación en llamadas al sistema | Eficiencia: cada verificación cuesta ciclos |
| Equidad | Planificador con desalojo por temporizador | Eficiencia: cada cambio de contexto es tiempo perdido |
| Eficiencia | Bloqueo de procesos en espera, cachés, escritura diferida | Aislamiento y previsibilidad |
| Portabilidad | Abstracciones uniformes y capa de controladores | Eficiencia: 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 frecuente | Por qué se produce | Cómo corregirlo |
|---|---|---|
| Creer que el sistema operativo “corre” todo el tiempo en paralelo con los programas | Se lo imagina como un programa más que estuviera siempre activo | El 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 operativo | La terminal es la vía habitual de contacto con el sistema | El 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 disco | El nombre y el tamaño sugieren un objeto único | Un 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 real | El número parece concreto y verificable | Es 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 salida | El programa es determinista línea por línea, así que se espera determinismo global | El 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 rendimiento | Se equipara concurrencia con velocidad | Cada 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ón | En la máquina desnuda sería cierto | El 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 disco | La llamada devuelve un número de bytes y parece completa | Los 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 exactos | Se usan indistintamente en el habla común | El 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 puras | Se asume que hay una respuesta óptima única | La 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:
| Bloque | Preguntas 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.