🔎 Buscar

⚙️ 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.

Fundamentos CS📖 Contenido

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:

  1. Aislamiento: cada proceso cree tener la memoria desde 0x00000000. Nunca pisan a otro.
  2. Protección: la tabla de páginas marca qué está permitido (leer/escribir/ejecutar).
  3. Memoria que no cabe: páginas poco usadas se intercambian (swap) al disco; el proceso ni se entera.
  4. 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) + asyncio para 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

  1. Ejecuta ps aux y top. Identifica los procesos, sus IDs (PID) y cuánta CPU/RAM usa cada uno. Mata un proceso tuyo con kill.
  2. Escribe un programa que cree 2 threads que incrementan un contador sin lock y con lock. Compara el resultado.
  3. Investiga en tu SO: ¿cuánta RAM y swap tienes? (free -h). Observa la memoria de un proceso con top o htop.
  4. 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).
  5. Explora tus archivos con ls -la, stat, ln (crea un hard link y un symlink), mount. Observa los permisos.
  6. Con strace (Linux) o dtruss (macOS), ve las syscalls que hace un programa simple al ejecutarse.

Para profundizar

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.