⚙️ Sistemas operativos
Qué es un sistema operativo, procesos y threads, planificación, memoria virtual y paging, concurrencia y locks, sistemas de archivos y el kernel. Teoría, código y práctica.
Sistemas operativos
El sistema operativo (SO) es el programa que gestiona el hardware y reparte sus recursos: CPU, memoria, disco, red. Cuando tu backend levanta un proceso, cuando tu base de datos escribe en disco, cuando Docker aísla contenedores… todo pasa por el SO.
La pieza central es el kernel (el núcleo, el único software con privilegios para tocar el hardware). Lo que verás aquí —procesos, threads, memoria virtual, archivos— son las abstracciones que el kernel construye sobre el hardware crudo.
¿Qué hace un sistema operativo?
Cinco responsabilidades:
| Responsabilidad | Qué hace | Abstracción que te da |
|---|---|---|
| Procesos | Repartir la CPU entre programas | proceso = un programa en ejecución |
| Memoria | Repartir la RAM sin que se pisen | espacio de direcciones virtuales |
| Persistencia | Gestionar el disco | archivos y directorios |
| Concurrencia | Coordinar acceso a recursos compartidos | locks, semáforos |
| Seguridad | Aislar usuarios y programas | usuarios, permisos, syscalls |
💡 La idea central: el SO crea ilusiones. Ilusión de que cada programa tiene la CPU entera (los reparte rápido). Ilusión de que cada programa tiene memoria infinita y privada (memoria virtual). Ilusión de que hay archivos (cuando en el disco solo hay bloques).
Procesos y threads
Proceso
Un proceso es un programa en ejecución con su propio espacio de memoria aislado (código, datos, stack, heap) y sus propios recursos (archivos abiertos, etc.). Dos procesos no pueden tocar la memoria del otro (protección del kernel).
Proceso A Proceso B
┌───────────────┐ ┌───────────────┐
│ código │ │ código │
│ datos │ aislados │ datos │
│ stack/heap │ ◀────────▶ │ stack/heap │
│ archivos │ │ archivos │
└───────────────┘ └───────────────┘
Cuando ejecutas node app.js en la terminal, creas un proceso. Cuando nginx lanza workers, cada worker es un proceso.
ps aux # ver todos los procesos
top / htop # ver procesos en tiempo real
kill -9 1234 # matar un proceso
Thread (hilo)
Un thread es un flujo de ejecución dentro de un proceso. Un proceso puede tener muchos threads que comparten la misma memoria (por eso se comunican fácil) pero compiten por los recursos.
Un proceso con 3 threads:
┌─────────────────────────────┐
│ código común (compartido) │
│ datos globales (compartido)│
│ ┌─────┬─────┬─────┐ │
│ │ T1 │ T2 │ T3 │ │ ← cada uno con su stack y registros
│ │stack│stack│stack│ │
│ └─────┴─────┴─────┘ │
└─────────────────────────────┘
| Proceso | Thread | |
|---|---|---|
| Memoria | Aislada | Compartida (dentro del mismo proceso) |
| Creación | Cara | Barata |
| Comunicación | IPC (pipes, sockets, archivos) | Variables compartidas |
| Fallo | Un proceso cae sin matar a otros | Un thread malo puede romper el proceso |
| Uso típico | Aislamiento fuerte | Paralelismo dentro de una app |
⚠️ Concurrencia ≠ paralelismo: concurrencia es manejar varias tareas intercaladas (una CPU puede ser concurrente); paralelismo es ejecutarlas a la vez (varias CPU/cores). Python con GIL es concurrente pero no paralelo para CPU; Go con goroutines lo gestiona todo.
Estados de un proceso
┌──────────┐
new ──▶│ ready │──▶ running ──▶ terminated
└────┬─────┘ │ │
│ │ └──▶ blocked (esperando I/O, disco, red)
└───────────┘ ◀── (vuelve a ready cuando termina la espera)
- Ready: listo pero esperando CPU.
- Running: usando la CPU.
- Blocked/waiting: esperando algo (lectura de disco, red, otro proceso).
Planificación (scheduling)
El kernel decide qué proceso/thread usa la CPU y cuándo. La pieza que hace esto es el scheduler.
Algoritmos de planificación
| Algoritmo | Cómo funciona | Nota |
|---|---|---|
| FIFO / FCFS | Primero en llegar, primero en ejecutarse | Simple, pero el corto espera al largo (convoy) |
| SJF | El más corto primero | Óptimo en teoría, pero no conoces la duración |
| Round Robin | Cada uno recibe un time slice (quantum), rotan | Justo y con respuesta rápida; el estándar de los sistemas interactivos |
| Priority | El de mayor prioridad primero | Riesgo de starvation (los de baja nunca corren) → se añade aging |
Round Robin (quantum = 2):
T1 T1 T2 T2 T1 T1 T3 T3 T2 T2 ...
(cada thread recibe 2 unidades y rota)
💡 Context switch (cambio de contexto): guardar el estado del thread actual y cargar el siguiente. Cuesta tiempo (guardar registros, estados). Por eso no puedes tener infinitos threads: cada cambio de contexto es overhead.
Memoria virtual y paging
El SO da a cada proceso la ilusión de tener toda la memoria para sí mediante la memoria virtual. Cada proceso tiene su espacio de direcciones virtuales, que se traduce a direcciones físicas reales mediante las tablas de páginas.
Espacio virtual del proceso (lo que "ve")
┌──────────────────────────────┐
│ código / datos / heap │
│ stack │
└──────────────────────────────┘
│ traducción (MMU + tabla de páginas)
▼
Memoria física (RAM) + swap en disco (páginas no usadas)
Beneficios:
- Aislamiento: cada proceso cree tener la memoria desde
0x00000000. Nunca pisan a otro. - Protección: la tabla de páginas marca qué está permitido (leer/escribir/ejecutar).
- Memoria que no cabe: páginas poco usadas se intercambian (swap) al disco; el proceso ni se entera.
- Compartición limpia: dos procesos pueden compartir páginas (librerías).
⚠️ Terminología: página (unidad de memoria virtual, 4 KB típicamente), tabla de páginas (mapeo virtual→físico), page fault (la página no está en RAM → hay que cargarla del disco, lento), TLB (caché de la tabla de páginas en la CPU), swap (intercambio con disco), ASLR (aleatoriza direcciones para seguridad).
free -h # ver RAM y swap
cat /proc/meminfo # detalles de memoria en Linux
vmstat 1 # ver swapping y actividad en vivo
Concurrencia y sincronización
Cuando dos threads leen y escriben la misma variable, hay una carrera de datos (race condition): el resultado depende del orden en que se intercalan. Ejemplo clásico:
// Dos threads ejecutan esto al mismo tiempo sobre el mismo `contador`:
contador++; // esto NO es atómico: es leer, sumar, escribir
Si dos threads hacen contador++ a la vez, el contador puede incrementar solo 1 en vez de 2 (ambos leyeron el mismo valor). Para evitarlo se usan mecanismos de sincronización:
| Mecanismo | Qué hace |
|---|---|
| Mutex / lock | Solo un thread entra en la sección crítica a la vez |
| Semáforo | Contador que permite N accesos a la vez |
| Monitor | Lock + condición (el patrón moderno) |
| Condition variable | Un thread espera a que otro lo despierte |
| Atómico | Operación que no se puede interrumpir (hardware) |
import threading
contador = 0
lock = threading.Lock()
def incrementar():
global contador
for _ in range(100_000):
with lock: # sección crítica protegida
contador += 1
t1 = threading.Thread(target=incrementar)
t2 = threading.Thread(target=incrementar)
t1.start(); t2.start()
t1.join(); t2.join()
print(contador) # 200000 (correcto gracias al lock)
⚠️ Los 3 problemas clásicos de la concurrencia: race condition (resultado depende del orden), deadlock (dos threads esperan cada uno un lock que tiene el otro — se bloquean para siempre), starvation (un thread nunca consigue el recurso). El deadlock necesita 4 condiciones (mutua exclusión, espera y retención, no apropación, espera circular); se rompe eliminando una.
# Deadlock: A espera el lock2, B espera el lock1
# A: lock1 → lock2
# B: lock2 → lock1 ← ambos bloqueados
Primitivas en el plan de backend
- Go: goroutines + channels (
go,make(chan)) — ver Go: concurrencia. - Python: threads con GIL (limitados para CPU) +
asynciopara I/O — ver Python: asyncio. - Node.js: event loop con un solo thread + worker threads — ver Node: event loop.
Sistemas de archivos (filesystems)
Los datos en disco son bloques de bytes. El SO construye la ilusión de archivos y directorios encima de esos bloques.
Conceptos
| Término | Significado |
|---|---|
| Archivo | Secuencia de bytes con nombre |
| Directorio | Colección de archivos (y otros directorios) |
| Inode | La «ficha» que guarda metadatos de un archivo (permisos, tamaño, dónde están los bloques) |
| Descriptor (fd) | Número que usa tu proceso para referirse a un archivo abierto |
| Hard link / symlink | Más de un nombre para el mismo inode / un nombre que apunta a otro nombre |
| Mount | Colgar un sistema de archivos en un directorio |
Permisos
drwxr-xr-x 2 ana dev 4096 mar 3 12:00 proyecto
││└──────┘ └────┘ └─ dueño y grupo
││ permisos: r=leer(4) w=escribir(2) x=ejecutar(1)
│└─ tipo: d=directorio, -=archivo, l=link
chmod 755 archivo # rwxr-xr-x (dueño rwx, grupo r-x, otros r-x)
chmod 644 archivo # rw-r--r-- (dueño rw, resto solo lectura)
chown ana:dev archivo # cambiar dueño:grupo
Cómo escribe el sistema (buffering y journaling)
- Write-back caching: el SO no escribe a disco cada
write(); acumula en caché y escribe en tandas (rápido, pero riesgo de perder datos si se cae). - fsync(): fuerza la escritura a disco (las bases de datos lo usan para no perder transacciones confirmadas).
- Journaling: antes de hacer cambios en el filesystem, registra «lo que va a hacer» en un journal; si se cae a mitad, al arrancar repliega (por eso ext4 y NTFS se recuperan).
Syscalls y el modelo de anillos
El kernel no deja que tu programa toque el hardware directamente. La interfaz es la syscall (system call): tu programa pide al kernel hacer algo privilegiado.
Tu programa (modo usuario, anillo 3)
│ open("/etc/passwd", O_RDONLY)
▼
syscall (trap / interrupción)
│ el CPU cambia a modo kernel (anillo 0)
▼
Kernel (modo kernel, anillo 0)
│ valida permisos → ejecuta → devuelve
▼
Tu programa (con el resultado: un fd)
Syscalls que usas todo el día sin saberlo: read, write, open, close, fork, exec, mmap, socket, send, recv, stat.
💡 En Nivel Élite profundizamos: construyes un kernel (xv6), haces tus propias syscalls y lees el código del kernel de Linux. También está la ruta DevOps de Linux con los comandos.
Cheatsheet de conceptos
| Término | En una frase |
|---|---|
| Kernel | El núcleo del SO con privilegios sobre el hardware |
| Proceso | Programa con memoria aislada |
| Thread | Flujo de ejecución que comparte memoria con su proceso |
| Scheduler | Decide quién usa la CPU |
| Context switch | Cambio de un thread a otro (cuesta tiempo) |
| Memoria virtual | Ilusión de memoria privada e infinita por proceso |
| Página / table de páginas | Unidad de memoria virtual / el mapeo |
| Page fault | Página no presente en RAM (lento, va a disco) |
| Race condition | Resultado depende del orden de ejecución |
| Mutex / lock | Solo un thread a la vez en la sección crítica |
| Deadlock | Bloqueo mutuo entre threads |
| Inode | Ficha de metadatos de un archivo |
| fd (descriptor) | Número con el que tu proceso usa un archivo |
| Syscall | Llamada de tu programa al kernel |
| Usermode / kernelmode | Anillo 3 (sin privilegios) / anillo 0 (privilegiado) |
Práctica propuesta
- Ejecuta
ps auxytop. Identifica los procesos, sus IDs (PID) y cuánta CPU/RAM usa cada uno. Mata un proceso tuyo conkill. - Escribe un programa que cree 2 threads que incrementan un contador sin lock y con lock. Compara el resultado.
- Investiga en tu SO: ¿cuánta RAM y swap tienes? (
free -h). Observa la memoria de un proceso contopohtop. - Crea un deadlock a propósito con 2 threads y 2 locks. Descríbelo y luego arréglalo (adquiere siempre los locks en el mismo orden).
- Explora tus archivos con
ls -la,stat,ln(crea un hard link y un symlink),mount. Observa los permisos. - Con
strace(Linux) odtruss(macOS), ve las syscalls que hace un programa simple al ejecutarse.
Para profundizar
- OSTEP — Operating Systems: Three Easy Pieces: el libro moderno de SO, gratis en PDF. Capítulos de virtualización, concurrencia y persistencia.
- MIT 6.S081 — xv6: construyes y modificas un kernel real (RISC-V). Labs de syscalls, páginas, traps.
- Berkeley CS162: el curso de SO de Berkeley.
- OSTEP Projects: shell, scheduler, web server, file server.
- Linux — comandos esenciales: la wiki de este sitio.
- Sigue con 📡 Redes.