Conceptos de Sistemas Operativos de 0 a Hero: indice del curso
Conceptos de Sistemas Operativos de 0 a Hero: indice del curso
Un sistema operativo es el programa que decide qué se ejecuta, cuándo se ejecuta, con cuánta memoria y con qué permisos. Está debajo de todo lo demás: del navegador, del compilador, del servidor de base de datos, del juego que corre a sesenta cuadros por segundo. Cuando ese programa hace bien su trabajo es invisible, y esa invisibilidad es justamente lo que hace difícil estudiarlo. Este curso lo vuelve visible: desarma las abstracciones una por una, muestra el mecanismo de hardware que hay debajo de cada una y lo reconstruye con código que se compila y se ejecuta.
El recorrido son diecisiete capítulos. Los primeros cinco construyen el vocabulario y la base de hardware. Los siguientes seis desarrollan los mecanismos centrales: procesos, planificación, concurrencia, semáforos, monitores, paso de mensajes y sistemas de archivos. Después vienen tres capítulos de Ada, un lenguaje que expresa la concurrencia como parte del idioma y no como una biblioteca externa, lo que permite leer el mecanismo en el código en lugar de adivinarlo. Y los últimos tres son proyectos completos: una minishell, un procesador concurrente de un millón de registros y un servidor de videojuegos multijugador.
A quién va dirigido
El curso está escrito para gente que sabe programar algo y quiere entender qué pasa debajo. No supone conocimientos previos de sistemas operativos, arquitectura de computadores ni concurrencia: cada uno de esos temas se levanta desde cero dentro del curso.
| Perfil | Punto de partida esperado | Qué obtiene del curso |
|---|---|---|
| Estudiante de un ramo de sistemas operativos | Programación básica en cualquier lenguaje | Cobertura del temario clásico con código ejecutable en vez de solo teoría |
| Desarrollador de aplicaciones | Experiencia web, móvil o de escritorio | Entender por qué un proceso se cuelga, por qué un dato se corrompe entre hilos y qué mide realmente el monitor del sistema |
| Persona autodidacta sin formación formal | Curiosidad y capacidad de seguir instrucciones de terminal | Una ruta ordenada que no da por sabido nada y llega hasta la implementación |
| Alguien que prepara entrevistas técnicas | Estructuras de datos y algoritmos | Los temas que aparecen una y otra vez: procesos frente a hilos, deadlock, planificación, sistemas de archivos |
| Programador de sistemas embebidos o críticos | C o C++ | Ada como alternativa con concurrencia integrada y verificación en tiempo de compilación |
Lo que sí se supone: saber abrir una terminal, moverse entre directorios, editar un archivo de texto y ejecutar un comando. Si alguna de esas cuatro cosas es nueva, conviene resolverla antes de empezar, porque todos los capítulos las usan.
Qué sabrás hacer al terminar
Al final del recorrido, sin ayuda y desde una terminal en blanco, deberías poder:
- Explicar qué ocurre, instrucción por instrucción, cuando un programa llama a una función de biblioteca que termina en una llamada al sistema, y por qué el procesador cambia de modo para atenderla.
- Leer la salida de las herramientas de diagnóstico de un sistema real y saber qué significa cada columna: estado del proceso, memoria residente frente a memoria virtual, descriptores abiertos, tiempo de CPU en usuario y en kernel.
- Reconocer una condición de carrera en un fragmento de código antes de ejecutarlo, señalar la sección crítica y elegir el mecanismo de sincronización adecuado.
- Implementar desde cero un semáforo, un monitor con variables de condición y un esquema de paso de mensajes, y justificar cuál corresponde a cada problema.
- Escribir un programa concurrente en Ada usando tareas, entradas, cita y objetos protegidos, y el equivalente en Elixir con procesos y mensajes.
- Construir un intérprete de comandos que soporte redirecciones, tuberías, ejecución en segundo plano y manejo de señales.
- Diseñar pruebas que hagan fallar de manera reproducible un error de concurrencia, en lugar de esperar a que aparezca en producción.
- Levantar un servidor concurrente sobre sockets TCP con estado compartido bajo un único dueño, un ciclo de actualización de frecuencia fija y medición de latencia.
Qué se necesita para partir
Un computador con Linux, macOS o Windows con WSL2. Los ejemplos están escritos y verificados sobre Linux; en macOS casi todo funciona igual salvo detalles de las herramientas de diagnóstico, que se indican en cada capítulo cuando corresponde.
Las herramientas se instalan a medida que se necesitan, no todas de entrada:
| Herramienta | Aparece a partir del capítulo | Para qué |
|---|---|---|
Un compilador de C (gcc o clang) | 4 | Ejemplos de llamadas al sistema, procesos, hilos y la minishell |
| Herramientas de diagnóstico del sistema | 3 | Observar procesos, memoria, archivos abiertos y llamadas al sistema |
| GNAT y Alire | 12 | Compilar y ejecutar todo el material de Ada |
| Elixir y Erlang/OTP | 8 | Modelo de actores, paso de mensajes y el servidor final |
| Python 3 | 16 | Versión secuencial de referencia del proyecto de concurrencia |
| Un editor de texto con resaltado de sintaxis | 1 | Todo el curso |
En una distribución basada en Debian o Ubuntu, la base mínima para los primeros capítulos se instala así:
sudo apt update
sudo apt install -y build-essential gdb strace ltrace man-db manpages-dev
gcc --version
Para el bloque de Ada, la vía recomendada es Alire, el gestor de paquetes del ecosistema, porque instala una cadena de herramientas propia y evita mezclar versiones con las del sistema:
sudo apt install -y gnat gprbuild
gnatmake --version
gprbuild --version
Y para el bloque de Elixir, cualquier instalación que deje disponibles elixir,
iex y mix:
sudo apt install -y erlang elixir
elixir --version
iex --version
Cada capítulo repite la instrucción de instalación de lo que usa, con la verificación correspondiente, así que no hace falta preparar todo de antemano. Lo único imprescindible para abrir el capítulo 1 es una terminal.
Los lenguajes del curso y por qué está cada uno
El curso usa cuatro lenguajes y ninguno está por gusto. Cada uno muestra una parte del tema que los otros esconden.
| Lenguaje | Qué muestra que los otros no | Dónde aparece |
|---|---|---|
| C | La llamada al sistema sin capas intermedias: fork, exec, dup2, pipe y las señales tal como las expone el kernel | Capítulos 4, 6 y 15 |
| Ada | La concurrencia como parte de la sintaxis: tareas, citas y objetos protegidos escritos como enunciados del lenguaje | Capítulos 8, 12, 13, 14, 16 y 17 |
| Elixir | El modelo de actores puro: procesos livianos aislados que solo se comunican por mensajes, sin memoria compartida posible | Capítulos 6, 8, 10, 16 y 17 |
| Python | Una versión secuencial de referencia contra la cual comparar, escrita en un lenguaje que casi todos leen | Capítulo 16 |
El mismo problema resuelto en tres estilos distintos es el recurso didáctico central del bloque de concurrencia. Un contador compartido incrementado por varios flujos se ve así en cada uno.
En C, con hilos POSIX y un mutex, la sincronización es una llamada a biblioteca que rodea al dato:
#include <pthread.h>
#include <stdio.h>
static long contador = 0;
static pthread_mutex_t candado = PTHREAD_MUTEX_INITIALIZER;
static void *sumar(void *arg)
{
(void)arg;
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&candado);
contador++;
pthread_mutex_unlock(&candado);
}
return NULL;
}
int main(void)
{
pthread_t a, b;
pthread_create(&a, NULL, sumar, NULL);
pthread_create(&b, NULL, sumar, NULL);
pthread_join(a, NULL);
pthread_join(b, NULL);
printf("contador = %ld\n", contador);
return 0;
}
Se compila y se ejecuta así:
gcc -O2 -pthread -o contador contador.c
./contador
En Ada, la exclusión mutua no se pide: se declara. El objeto protegido garantiza que solo un flujo esté dentro de sus operaciones a la vez, y eso es parte del tipo:
with Ada.Text_IO; use Ada.Text_IO;
procedure Contador is
protected Cuenta is
procedure Incrementar;
function Valor return Long_Integer;
private
Total : Long_Integer := 0;
end Cuenta;
protected body Cuenta is
procedure Incrementar is
begin
Total := Total + 1;
end Incrementar;
function Valor return Long_Integer is
begin
return Total;
end Valor;
end Cuenta;
task type Sumador;
task body Sumador is
begin
for I in 1 .. 100_000 loop
Cuenta.Incrementar;
end loop;
end Sumador;
A, B : Sumador;
begin
null;
end Contador;
Se compila y se ejecuta así:
gnatmake contador.adb
./contador
En Elixir no hay dato compartido que proteger: el contador vive dentro de un proceso y los demás le envían mensajes, de modo que la exclusión mutua es una consecuencia del diseño y no un mecanismo añadido:
defmodule Contador do
def iniciar do
spawn(fn -> ciclo(0) end)
end
defp ciclo(total) do
receive do
:incrementar -> ciclo(total + 1)
{:valor, quien} ->
send(quien, {:total, total})
ciclo(total)
end
end
def sumar(pid, cuantas) do
Enum.each(1..cuantas, fn _ -> send(pid, :incrementar) end)
end
def valor(pid) do
send(pid, {:valor, self()})
receive do
{:total, total} -> total
end
end
end
pid = Contador.iniciar()
tareas =
for _ <- 1..2 do
Task.async(fn -> Contador.sumar(pid, 100_000) end)
end
Enum.each(tareas, &Task.await(&1, :infinity))
IO.puts("contador = #{Contador.valor(pid)}")
Se ejecuta directamente con el intérprete:
elixir contador.exs
Los tres programas hacen lo mismo y llegan al mismo resultado, pero cada uno responde la pregunta “¿quién impide que dos flujos escriban a la vez?” en un lugar distinto: en C la respuesta está en las dos llamadas que rodean la línea, en Ada está en el tipo del objeto y en Elixir está en que no existe la línea compartida. Los capítulos 8, 9 y 10 desarrollan exactamente esa diferencia.
La ruta de aprendizaje
El curso avanza en cinco bloques temáticos. Cada bloque cierra un tema completo y deja instalado el vocabulario que usa el siguiente. El diagrama muestra las dependencias reales entre capítulos, no solo el orden de lectura.
flowchart TD
subgraph B1["Bloque 1 · Fundamentos"]
C1["Cap 1<br/>Qué es un SO"]
C2["Cap 2<br/>Historia"]
C3["Cap 3<br/>Roles del SO"]
C4["Cap 4<br/>Arquitectura"]
C5["Cap 5<br/>El kernel"]
end
subgraph B2["Bloque 2 · Procesos y recursos"]
C6["Cap 6<br/>Procesos y planificación"]
C11["Cap 11<br/>Sistemas de archivos"]
end
subgraph B3["Bloque 3 · Concurrencia"]
C7["Cap 7<br/>Problemas de concurrencia"]
C8["Cap 8<br/>Semáforos"]
C9["Cap 9<br/>Monitores"]
C10["Cap 10<br/>Paso de mensajes"]
end
subgraph B4["Bloque 4 · Ada"]
C12["Cap 12<br/>Ada desde cero"]
C13["Cap 13<br/>Ada práctico"]
C14["Cap 14<br/>Tareas y concurrencia"]
end
subgraph B5["Bloque 5 · Proyectos"]
C15["Cap 15<br/>Minishell"]
C16["Cap 16<br/>Concurrencia real"]
C17["Cap 17<br/>Servidor de juegos"]
end
C1 --> C2 --> C3 --> C4 --> C5
C5 --> C6
C5 --> C11
C6 --> C7
C7 --> C8 --> C9 --> C10
C6 --> C12
C12 --> C13 --> C14
C10 --> C14
C6 --> C15
C11 --> C15
C14 --> C16
C10 --> C16
C16 --> C17
Las flechas que cruzan bloques son las que importan al momento de saltarse capítulos. El capítulo 15 necesita procesos, descriptores de archivo y señales, es decir los capítulos 6 y 11. El capítulo 16 necesita los tres mecanismos de sincronización y las tareas de Ada. El capítulo 17 no se sostiene sin el 16.
Los cinco bloques en detalle
Bloque 1: fundamentos (capítulos 1 a 5)
Responde tres preguntas: qué problema resuelve un sistema operativo, cómo llegó a resolverlo de esta manera y sobre qué hardware lo hace. El capítulo 4 es el pivote técnico del bloque, porque introduce los mecanismos físicos —interrupciones, modos de ejecución, jerarquía de memoria— que después se invocan en todos los capítulos posteriores. El capítulo 5 los junta en la figura del kernel.
El resultado concreto de este bloque es poder seguir el camino completo de una petición de un programa al kernel, que es la operación que se repite en todo lo que viene después:
sequenceDiagram
participant P as Programa (modo usuario)
participant B as Biblioteca del sistema
participant K as Kernel (modo privilegiado)
participant H as Dispositivo
P->>B: llamada a una función de lectura
B->>B: coloca número de servicio y argumentos en registros
B->>K: instrucción que cambia el modo del procesador
K->>K: valida argumentos y permisos
K->>H: solicita los datos
K-->>P: bloquea el proceso mientras espera
H-->>K: interrupción: datos disponibles
K->>K: copia los datos al espacio del proceso
K->>P: retorna al modo usuario con el resultado
Bloque 2: procesos y recursos (capítulos 6 y 11)
El proceso es la abstracción central del área y el sistema de archivos es la abstracción de almacenamiento equivalente. Ambos capítulos comparten una estructura: qué estructura de datos mantiene el kernel, qué operaciones expone al programa y qué se ve desde afuera con herramientas de diagnóstico.
Bloque 3: concurrencia (capítulos 7 a 10)
Es el corazón teórico del curso y el bloque más largo en tiempo de práctica. El capítulo 7 plantea los problemas; los capítulos 8, 9 y 10 presentan las tres familias históricas de solución, en el mismo orden en que aparecieron: semáforos, monitores y paso de mensajes. Cada mecanismo resuelve las limitaciones del anterior, así que leerlos en desorden hace perder el hilo del argumento.
flowchart LR
P["Problema:<br/>dos flujos tocan<br/>el mismo dato"] --> EA["Espera activa<br/>consume CPU<br/>sin avanzar"]
EA --> S["Semáforo<br/>contador + cola<br/>wait / signal"]
S --> LS["Limitación:<br/>el orden de las<br/>operaciones queda<br/>disperso en el código"]
LS --> M["Monitor<br/>exclusión encapsulada<br/>+ variables de condición"]
M --> LM["Limitación:<br/>requiere memoria<br/>compartida"]
LM --> PM["Paso de mensajes<br/>send / receive<br/>sin estado común"]
PM --> A["Ada: cita y<br/>objetos protegidos"]
PM --> E["Elixir: procesos<br/>y buzones"]
Bloque 4: Ada (capítulos 12 a 14)
Ada aparece en el curso por una razón concreta: su modelo de concurrencia está en el lenguaje, con sintaxis propia para tareas, citas y objetos protegidos. Eso permite escribir un monitor sin biblioteca externa y leerlo como texto en lugar de reconstruirlo mentalmente a partir de llamadas a funciones. Los capítulos 12 y 13 enseñan el lenguaje desde el primer programa; el 14 es el que conecta con el bloque de concurrencia.
Bloque 5: proyectos (capítulos 15 a 17)
Tres programas completos, de dificultad creciente, que obligan a usar todo lo anterior junto. No son ejercicios ilustrativos: cada uno tiene un enunciado, una implementación completa, pruebas y una discusión de qué falla y por qué.
Tabla resumen del curso
| Capítulo | Qué aprendes | Con qué sales |
|---|---|---|
| 1. Introducción | Qué es un sistema operativo, la máquina desnuda frente a la máquina extendida, el rol de gestor de recursos | Vocabulario base y un mapa mental de las capas entre tu programa y el hardware |
| 2. Historia | Lotes, monitor residente, spooling, multiprogramación, tiempo compartido, UNIX, Windows, Linux, móviles | Capacidad de explicar por qué los sistemas actuales tienen la forma que tienen |
| 3. Roles del SO | Gestión de procesos, memoria, archivos y dispositivos; protección; interfaz de llamadas al sistema | Entender qué pide un programa al kernel y cómo lo pide |
| 4. Arquitectura | Ciclo de la CPU, registros, jerarquía de memoria, cache, stack y heap, interrupciones, DMA, modo usuario y kernel | La base de hardware que explica el costo de cada operación del sistema |
| 5. El kernel | Monolítico, microkernel e híbrido; frontera usuario-kernel; módulos; planificador | Saber qué hay dentro del núcleo y cómo se extiende sin reiniciar |
| 6. Procesos y planificación | PCB, estados, árbol de procesos, fork, exec, wait, hilos, cambio de contexto, FCFS, SJF, Round Robin, colas multinivel | Crear procesos e hilos, y calcular a mano el resultado de un planificador |
| 7. Concurrencia | Concurrencia frente a paralelismo, condición de carrera, sección crítica, exclusión mutua, deadlock, inanición, problemas clásicos | Detectar el error antes de ejecutarlo y nombrar con precisión qué falla |
| 8. Semáforos | Semáforos binarios y contadores, wait y signal, atomicidad, patrones de uso y errores típicos | Implementar y usar semáforos en Ada y en Elixir sin espera activa |
| 9. Monitores | Encapsulamiento de la exclusión mutua, invariante, variables de condición, semánticas de Hoare y Mesa, la regla del while | Escribir un buffer acotado correcto y saber por qué el if no basta |
| 10. Paso de mensajes | send y receive, bloqueo y no bloqueo, direccionamiento directo e indirecto, buzones, cita de Ada, actores de Elixir, cifrado entre procesos | Comunicar procesos sin memoria compartida y proteger el contenido del canal |
| 11. Sistemas de archivos | Archivos, directorios, inodos, punteros indirectos, asignación de bloques, journaling, FAT, ext4, btrfs, ZFS, permisos, montaje | Explicar qué ocurre en disco al crear, escribir y borrar un archivo |
| 12. Ada desde cero | Historia del lenguaje, instalación de GNAT y Alire, archivos .ads, .adb y .gpr, primeros programas | Compilar y ejecutar programas Ada propios |
| 13. Ada práctico | Arreglos restringidos y no restringidos, contenedores, cadenas, UTF-8, argumentos de línea de comandos | Un programa interactivo completo que conversa con el shell |
| 14. Ada concurrente | Tareas y tipos de tarea, entradas, accept, select con guardas, objetos protegidos, lanzamiento de procesos del sistema | Escribir concurrencia correcta con sintaxis del lenguaje, no con bibliotecas |
| 15. Minishell | Ciclo leer-parsear-ejecutar, tokenizador, fork y exec, códigos de salida, redirección con descriptores, tuberías, señales | Un intérprete de comandos propio que ejecuta programas reales |
| 16. Concurrencia real | Un millón de registros procesados secuencialmente y de forma concurrente, los tres mecanismos aplicados, pruebas que exponen la carrera | Una batería de pruebas que hace fallar el error de forma reproducible |
| 17. Servidor de juegos | Sockets TCP, estado autoritativo, tick loop de frecuencia fija, latencia y percentiles, predicción y reconciliación | Un servidor multijugador funcionando y la bibliografía para seguir solo |
Los diecisiete capítulos
Capítulo 1
Introducción a los sistemas operativos: la máquina desnuda y la máquina extendida
Punto de partida del curso. Define qué es exactamente un sistema operativo, qué problema resuelve y por qué apareció, contrastando la máquina desnuda que entrega el hardware con la máquina extendida que construye el software de sistema. Introduce el doble rol de máquina extendida y gestor de recursos, la separación entre modo usuario y modo kernel, y deja armado el mapa que recorren los dieciséis capítulos siguientes.
Capítulo 2
Historia de los sistemas operativos: del lote a los sistemas móviles
Recorrido por la evolución del área, desde las máquinas que no tenían software de sistema hasta los teléfonos actuales. Cubre el procesamiento por lotes y el monitor residente, el spooling y las interrupciones, la multiprogramación, el tiempo compartido, el nacimiento de UNIX con su árbol genealógico, la familia Windows, la aparición de Linux y los sistemas móviles. Sirve para entender que casi ninguna decisión de diseño actual es arbitraria: cada una responde a una limitación concreta de su época.
Capítulo 3
Los roles del sistema operativo: árbitro, ilusionista y pegamento
Desarrolla una por una las funciones que cumple el sistema operativo: gestión de procesos, gestión de memoria, sistemas de archivos, control de dispositivos, protección y aislamiento. La segunda mitad del capítulo se dedica a la interfaz de llamadas al sistema: cómo un programa formula una petición, cómo viaja hasta el kernel y cómo vuelve el resultado. Es el capítulo que convierte la palabra “sistema operativo” en una lista concreta de responsabilidades.
Capítulo 4
Arquitectura de computadoras para entender el sistema operativo
El hardware que el sistema operativo administra, explicado desde cero: el ciclo de la CPU, los registros, la jerarquía de memoria con sus cachés, el stack y el heap de un proceso, las interrupciones, el acceso directo a memoria y la separación entre modo usuario y modo kernel. Cada mecanismo se conecta de inmediato con la decisión de diseño del sistema operativo que lo aprovecha, de modo que no queda como teoría suelta de arquitectura.
Capítulo 5
El kernel por dentro: arquitecturas, llamadas al sistema, módulos y planificación
Entra al núcleo del sistema. Explica la frontera entre espacio de usuario y espacio de kernel y qué instrucción del procesador la cruza, las diferencias reales entre kernel monolítico, microkernel e híbrido con sus compromisos de rendimiento y aislamiento, el ciclo de vida de los módulos cargables y el papel del planificador dentro del núcleo. Cierra el bloque de fundamentos.
Capítulo 6
Procesos y planificación: del bloque de control al reparto del procesador
Anatomía completa del proceso: qué guarda el bloque de control, cómo se mueve por
sus estados, cómo se forma el árbol de procesos y qué hacen exactamente fork, exec
y wait. Estudia los hilos frente a los procesos, el mecanismo real del cambio de
contexto y los algoritmos clásicos de planificación —FCFS, SJF, SRTF, Round Robin,
prioridades y colas multinivel— con trazas calculadas paso a paso e implementaciones
en C, Ada y Elixir.
Capítulo 7
Concurrencia y sus problemas: condiciones de carrera, sección crítica, interbloqueo e inanición
Separa concurrencia de paralelismo y muestra por qué un programa correcto en secuencial deja de serlo cuando dos flujos tocan el mismo dato. Desarma una condición de carrera hasta el nivel de instrucciones individuales, define sección crítica y exclusión mutua con sus cuatro requisitos, y desarrolla el interbloqueo con sus condiciones necesarias, la inanición y los tres problemas clásicos: productor-consumidor, lectores-escritores y filósofos comensales.
Capítulo 8
Semáforos: el primer mecanismo que resolvió la exclusión mutua sin espera activa
Estudio completo del semáforo: la estructura de contador más cola de espera, las
operaciones wait y signal, la diferencia entre semáforos binarios y contadores,
y cómo el kernel garantiza que esas operaciones sean atómicas. Se implementan
semáforos desde cero en Ada con objetos protegidos y se resuelven con ellos los
problemas clásicos, además de catalogar los errores de uso que producen bloqueos
permanentes.
Capítulo 9
Monitores: exclusión mutua encapsulada, variables de condición y las semánticas de Hoare y Mesa
Presenta el monitor como construcción de sincronización de alto nivel: qué encapsula,
qué es su invariante y por qué una variable de condición no es un semáforo. Explica
en detalle las tres semánticas de señalización —Hoare, Mesa y señalar-y-salir— y la
regla de esperar dentro de un while en lugar de un if, con el buffer acotado como
caso central y una comparación directa contra la solución del capítulo anterior.
Capítulo 10
Paso de mensajes: buzones, rendezvous de Ada, actores de Elixir y cifrado entre procesos
Tercera familia de sincronización, la que no necesita memoria compartida. Cubre las
operaciones send y receive con sus cuatro combinaciones de bloqueo, el
direccionamiento directo e indirecto, los buzones con disciplina FIFO y por prioridad,
la cita de Ada y el modelo de actores de Elixir. Incluye además el cifrado del contenido
de los mensajes que viajan entre procesos.
Capítulo 11
Sistemas de archivos: del bloque crudo al árbol de directorios
Cómo un sistema operativo convierte un disco de bloques anónimos en archivos con nombre. Recorre la anatomía del archivo y del directorio, los inodos con sus punteros indirectos, las estrategias de asignación de bloques —contigua, enlazada, indexada y por extensiones—, el journaling y sus modos, y las familias reales: FAT, ext4, btrfs y ZFS. Termina con permisos, montaje y el efecto de todo eso sobre lo que ve un programa cuando abre un archivo.
Capítulo 12
El lenguaje Ada: historia, instalación de GNAT y primeros programas
Abre el bloque de Ada explicando por qué el Departamento de Defensa estadounidense
encargó el lenguaje en 1974, cómo se eligió el diseño ganador y qué principios quedaron
grabados en el estándar. Cubre la instalación de GNAT y Alire en Linux, la estructura
de archivos .ads, .adb y .gpr, la etapa real de compilación, y tres programas
completos explicados línea a línea: hola mundo, raíz cuadrada y conversor de temperatura.
Capítulo 13
Ada práctico: arreglos, contenedores, cadenas y argumentos de línea de comandos
Segunda parte práctica del lenguaje: arreglos restringidos y no restringidos, los
contenedores estándar Vectors, Doubly_Linked_Lists y Ordered_Maps, las tres
familias de cadenas de Ada con el manejo de UTF-8, y cómo un programa recibe argumentos
del sistema operativo mediante Ada.Command_Line. Todo desemboca en un juego de
adivinar el número con entrada validada, historial y código de salida hacia el shell.
Capítulo 14
Concurrencia en Ada: tareas, cita, objetos protegidos y procesos del sistema
El capítulo que une el bloque de Ada con el bloque de concurrencia. Desarrolla las
tareas y los tipos de tarea, las entradas y la cita, el enunciado select con sus
guardas y alternativas, los objetos protegidos con barreras, y el uso de GNAT.OS_Lib
para lanzar y supervisar procesos del sistema operativo desde varias tareas. Compara
el modelo con los actores de Elixir y con los hilos POSIX.
Capítulo 15
Proyecto 1: construir una minishell desde cero con fork, exec, pipes y señales
Primer proyecto integrador: escribir un intérprete de comandos propio. Construye el
ciclo leer-parsear-ejecutar, el tokenizador que reconoce redirecciones, tuberías y
ejecución en segundo plano, la pareja fork/exec con su espera y sus códigos de
salida, la redirección con dup2 sobre la tabla de descriptores y el manejo de señales
para que la shell no muera con el proceso que lanzó.
Capítulo 16
Segundo proyecto: generar un archivo de un millón de registros y procesarlo, primero de forma secuencial en Python y después de forma concurrente en Elixir y en Ada. El mismo punto de contención se resuelve con las tres familias estudiadas —semáforo contador, monitor y paso de mensajes— y se acompaña de pruebas diseñadas para que la condición de carrera falle de manera reproducible, con la versión rota a propósito, el test que la caza y la medición comparativa de tiempos.
Capítulo 17
Proyecto final: un servidor de videojuegos concurrente y la bibliografía del curso
Cierre del curso: un servidor multijugador sobre sockets TCP que parte del cachipún
por turnos y llega a un servidor autoritativo con tick loop de frecuencia fija, estado
compartido bajo un único dueño y control explícito de la latencia. Se implementa
completo en Elixir con :gen_tcp, procesos y supervisores, y en Ada con GNAT.Sockets,
tareas y objetos protegidos, con protocolo interoperable, percentiles de latencia,
predicción del cliente y reconciliación. Termina con la bibliografía recomendada de
los diecisiete capítulos.
Cómo recorrer el curso
El orden numérico es el orden diseñado y funciona para cualquier punto de partida. Aun así, hay tres rutas alternativas que sirven según el objetivo.
flowchart TD
START["¿Cuál es tu objetivo?"]
START --> R1["Entender el área completa"]
START --> R2["Resolver concurrencia<br/>en el trabajo"]
START --> R3["Preparar una prueba<br/>o entrevista"]
R1 --> P1["Ruta completa<br/>1 → 17 en orden"]
R2 --> P2["4 → 5 → 6 → 7 → 8 → 9 → 10 → 14 → 16"]
R3 --> P3["1 → 3 → 5 → 6 → 7 → 11<br/>+ tablas comparativas<br/>de 8, 9 y 10"]
P1 --> FIN["Proyectos 15, 16 y 17"]
P2 --> FIN
P3 --> REV["Revisión con los<br/>ejercicios del final<br/>de cada capítulo"]
Ruta completa. Del 1 al 17 en orden. Es la única que garantiza que ningún concepto aparezca antes de haber sido explicado, y la única en que los tres proyectos finales se pueden resolver sin volver atrás.
Ruta de concurrencia. Para quien ya trabaja con hilos o procesos y necesita el marco conceptual rápido. Empieza en el capítulo 4 para tener la base de hardware, salta la historia y los sistemas de archivos, y desemboca en el proyecto 16, que es el que pone a prueba lo aprendido. El capítulo 12 se puede leer en diagonal si el único interés en Ada es la sintaxis de tareas del capítulo 14.
Ruta de repaso. Para quien tiene una evaluación cerca. Prioriza los capítulos con más contenido conceptual evaluable y usa las tablas comparativas de los capítulos 8, 9 y 10 como resumen de los mecanismos de sincronización.
Recomendaciones de práctica
- Escribe el código a mano. Copiar y pegar los ejemplos hace que compilen, no que se entiendan. Teclear el mismo programa obliga a leer cada línea.
- Rompe los ejemplos a propósito. Quita el
whilede una variable de condición, invierte el orden de doswaitsobre semáforos distintos, borra elwaitdel padre en unfork. Los capítulos indican qué debería fallar en cada caso; comprobarlo vale más que leerlo. - Ejecuta cada programa concurrente muchas veces seguidas. Un error de sincronización puede no aparecer en veinte ejecuciones y aparecer en la vigésimo primera. El capítulo 16 desarrolla esta idea hasta convertirla en pruebas.
- Observa el sistema mientras corre. Casi todos los capítulos incluyen comandos para mirar desde afuera lo que está haciendo el programa. Tener esa ventana abierta en paralelo convierte la teoría en algo medible.
- No avances con un capítulo a medias. Los mecanismos se apilan: el monitor del capítulo 9 supone entendido el semáforo del 8, y el proyecto 16 supone entendidos los tres.
- Haz los ejercicios del final antes de pasar al siguiente capítulo. Están diseñados para exponer justamente los malentendidos que se arrastran hacia adelante.
Ritmo sugerido
Una sesión de estudio productiva termina con algo ejecutándose. Conviene dividir cada capítulo en dos pasadas: una primera de lectura completa sin tocar el teclado, para tener el mapa, y una segunda con la terminal abierta reproduciendo los ejemplos. Los capítulos de proyecto —15, 16 y 17— no se leen: se construyen, y conviene abordarlos en sesiones dedicadas, porque partir un proyecto por la mitad obliga a reconstruir el contexto cada vez.
Errores comunes al recorrer el curso
| Error | Causa | Solución |
|---|---|---|
| Saltar del capítulo 1 directo a semáforos | La concurrencia parece el tema “interesante” y los fundamentos parecen relleno | Leer al menos los capítulos 4, 6 y 7: sin cambio de contexto ni sección crítica, el semáforo queda como una receta memorizada |
| Leer el bloque de concurrencia sin ejecutar nada | Los ejemplos se entienden al leerlos y da la sensación de que basta | Ejecutar cada programa varias veces; la condición de carrera es un fenómeno de ejecución, no de lectura |
| Tratar Ada como un obstáculo antes de llegar a la concurrencia | Sintaxis desconocida y verbosa comparada con lenguajes de uso diario | Los capítulos 12 y 13 existen para eso; con hacer sus ejemplos alcanza para leer el capítulo 14 sin fricción |
Empezar el proyecto 15 sin dominar fork y descriptores | La minishell parece un ejercicio de parseo de texto | Volver al capítulo 6 para procesos y al 11 para descriptores; el parseo es la parte fácil del proyecto |
| Empezar el proyecto 17 sin haber hecho el 16 | El servidor de juegos suena más atractivo que procesar un archivo | El 16 instala la disciplina de pruebas y la elección de mecanismo que el 17 da por resuelta |
| Copiar y pegar todos los ejemplos | Ahorra tiempo en la sesión | Transcribir a mano al menos los ejemplos de sincronización; el error típico está en un detalle de una línea |
| Confundir concurrencia con paralelismo durante todo el curso | Ambos términos se usan como sinónimos fuera del ámbito técnico | El capítulo 7 los separa explícitamente; si esa distinción no queda firme, los capítulos 8 a 10 se leen mal |
| Estudiar los algoritmos de planificación solo de memoria | Las tablas de FCFS, SJF y Round Robin parecen fáciles de recordar | Calcular a mano las trazas del capítulo 6 con datos propios: el tiempo de espera promedio se equivoca sistemáticamente al hacerlo de memoria |
Dar por entendido el while de las variables de condición | En el ejemplo funciona igual con if la mayoría de las veces | Reproducir el caso de despertar espurio del capítulo 9 y ver el resultado incorrecto |
Glosario de arranque
Estos términos aparecen desde los primeros capítulos y se definen con precisión en el lugar que corresponde. La tabla sirve como referencia rápida mientras el vocabulario se asienta, no como reemplazo de la explicación completa.
| Término | Definición breve | Capítulo donde se desarrolla |
|---|---|---|
| Máquina desnuda | El hardware tal como es antes de que exista software de sistema sobre él | 1 |
| Máquina extendida | La vista simplificada del hardware que el sistema operativo presenta a los programas | 1 |
| Modo usuario / modo kernel | Dos niveles de privilegio del procesador; en modo usuario ciertas instrucciones están prohibidas | 1, 4, 5 |
| Llamada al sistema | Petición de un programa al kernel para hacer algo que no puede hacer por su cuenta | 3, 5 |
| Interrupción | Señal del hardware que desvía la ejecución del procesador hacia una rutina de atención | 4 |
| DMA | Transferencia de datos entre dispositivo y memoria sin pasar por el procesador | 4 |
| Proceso | Programa en ejecución con su propio espacio de direcciones y su estado asociado | 6 |
| PCB | Estructura del kernel que guarda todo lo que define a un proceso | 6 |
| Hilo | Flujo de ejecución dentro de un proceso, que comparte memoria con los demás hilos del mismo proceso | 6 |
| Cambio de contexto | Guardar el estado de un flujo y restaurar el de otro para que el procesador siga con este último | 4, 6 |
| Planificador | Componente del kernel que decide cuál de los procesos listos usa el procesador | 5, 6 |
| Condición de carrera | Resultado que depende del orden en que dos flujos ejecutan sus instrucciones | 7 |
| Sección crítica | Fragmento de código que accede a un recurso compartido y no debe ejecutarse en paralelo | 7 |
| Exclusión mutua | Garantía de que solo un flujo está dentro de la sección crítica a la vez | 7 |
| Interbloqueo | Situación en que dos o más flujos se esperan entre sí de forma permanente | 7 |
| Inanición | Un flujo listo para ejecutarse que nunca obtiene el recurso porque otros se le adelantan siempre | 7 |
| Semáforo | Contador con cola de espera y dos operaciones atómicas, wait y signal | 8 |
| Monitor | Construcción que encapsula datos, operaciones y exclusión mutua en una sola unidad | 9 |
| Variable de condición | Cola de espera dentro de un monitor asociada a una condición lógica sobre su estado | 9 |
| Cita (rendezvous) | Sincronización de Ada en que dos tareas se encuentran para intercambiar datos | 10, 14 |
| Buzón | Destino intermedio de mensajes que desacopla al emisor del receptor | 10 |
| Inodo | Estructura que guarda los metadatos y los punteros a bloques de un archivo | 11 |
| Journaling | Registro previo de las operaciones del sistema de archivos para poder recuperarse tras un corte | 11 |
| Descriptor de archivo | Número entero con que un proceso se refiere a un archivo o canal abierto | 11, 15 |
| Objeto protegido | Tipo de Ada que garantiza acceso exclusivo a su estado interno sin escribir bloqueos | 8, 14 |
| Tick loop | Ciclo de servidor que avanza el estado del mundo a una frecuencia fija | 17 |
Qué no cubre este curso
Delimitar el alcance evita frustraciones. Estos temas se mencionan cuando hacen falta para entender otra cosa, pero no se desarrollan:
- Memoria virtual en profundidad. El curso explica la jerarquía de memoria, el stack, el heap y el aislamiento entre procesos, pero no entra en el detalle de la paginación, la tabla de páginas multinivel ni los algoritmos de reemplazo.
- Escribir un kernel propio. El capítulo 5 explica la estructura interna y los módulos, sin llegar a la implementación de un sistema operativo desde cero.
- Administración de sistemas. No hay configuración de servicios, gestión de usuarios ni afinamiento de rendimiento del sistema.
- Redes más allá de los sockets. El capítulo 17 usa TCP como herramienta; no desarrolla la pila de protocolos ni el enrutamiento.
- Virtualización y contenedores. Aparecen mencionados como consecuencia del aislamiento, sin capítulo propio.
- Seguridad ofensiva. El curso cubre protección, permisos y cifrado de mensajes entre procesos, no técnicas de explotación.
Preguntas frecuentes
¿Hay que saber C para empezar? No. El capítulo 4 introduce lo que se necesita y los ejemplos en C están comentados línea a línea. Sí ayuda haber programado antes en cualquier lenguaje.
¿Por qué Ada y no C++ o Java? Porque en Ada la concurrencia es sintaxis del lenguaje. Un objeto protegido se lee como lo que es —un monitor— sin tener que reconstruirlo mentalmente a partir de una clase con un candado adentro. Eso hace que los capítulos 8, 9 y 14 muestren el mecanismo en vez de una implementación de él.
¿Sirve el curso si trabajo en desarrollo web? Sí, para la parte que suele quedar como caja negra: por qué el servidor se queda sin descriptores, qué significa que un proceso esté en estado de espera, por qué dos peticiones simultáneas corrompen un contador, qué hace realmente un pool de conexiones.
¿Se puede hacer el curso en Windows? Con WSL2 sí, y es la vía recomendada. Sin WSL2, las herramientas de diagnóstico y varias llamadas al sistema de los capítulos 6, 11 y 15 no tienen equivalente directo.
¿Los proyectos finales sirven como portafolio? Los tres son programas completos y ejecutables. La minishell y el servidor de juegos, en particular, son piezas que se pueden mostrar y explicar.
¿Qué pasa si un capítulo me queda a medias? Conviene resolverlo antes de avanzar. El diagrama de la ruta de aprendizaje muestra qué capítulos dependen de cuál; si el capítulo pendiente tiene flechas salientes hacia el siguiente, seguir sin él implica arrastrar el hueco.
Ejercicios previos de diagnóstico
Antes de abrir el capítulo 1, estas preguntas sirven para medir el punto de partida. No hay que saber responderlas: el objetivo es volver a ellas al terminar el curso y notar la diferencia.
- Cuando escribes un comando en la terminal y presionas Enter, ¿cuántos programas distintos participan hasta que aparece la salida?
- Un programa reserva memoria y nunca la libera. ¿Qué pasa con esa memoria cuando el programa termina, y quién se encarga?
- Dos hilos incrementan la misma variable un millón de veces cada uno. ¿Cuál es el valor final, y por qué la respuesta no es dos millones?
- ¿Qué diferencia hay entre borrar un archivo y liberar el espacio que ocupaba?
- Si el procesador tiene cuatro núcleos y hay cuarenta procesos en ejecución, ¿qué significa exactamente que los cuarenta estén “corriendo”?
- ¿Por qué un programa no puede escribir directamente en la memoria de otro?
Fuente y créditos
El temario de este curso está basado en la guía de sistemas operativos publicada por Ninjas.cl, disponible en https://ninjascl.github.io/sistemas-operativos/. De ahí provienen la selección de temas, el orden general de los bloques, la decisión de usar Ada y Elixir como lenguajes de trabajo para la concurrencia y la idea de los tres proyectos integradores que cierran el recorrido.
Este material es una reelaboración propia y extendida: los textos, las explicaciones, los diagramas, los ejemplos de código, las tablas comparativas, las secciones de errores frecuentes y los ejercicios fueron escritos para este curso. No se reproduce texto literal de la fuente. Cada capítulo desarrolla los temas con mayor extensión y con código ejecutable y verificado, y agrega material que no está en la guía original, como el bloque de arquitectura de computadoras, el detalle de las semánticas de señalización de monitores y la parte de medición de latencia del proyecto final.
El crédito por el diseño del temario corresponde a sus autores; los errores, omisiones y decisiones de redacción de esta versión son responsabilidad de este sitio. Las referencias bibliográficas específicas —libros, artículos y documentación de cada tema— están reunidas al final del capítulo 17.
Por dónde empezar
El primer paso es el capítulo 1, que define qué es un sistema operativo partiendo de lo que hace el hardware cuando no hay ninguno. Ahí queda planteada la distinción entre la máquina desnuda y la máquina extendida, que es el hilo conductor de todo lo que viene después: cada capítulo del curso agrega una capa a esa máquina extendida, hasta que al final, en el servidor del capítulo 17, se usan todas juntas sin pensar en ellas.