Planificación de CPU

"El planificador es la cuerda invisible que reparte el segundo entre todos."

Qué vas a aprender en este capítulo

Tu computadora puede tener decenas o cientos de procesos listos al mismo tiempo, pero solo unos pocos núcleos. ¿Quién decide qué proceso corre cuándo? Eso es el planificador (scheduler) del kernel. En este capítulo vas a entender qué métricas usa para evaluar buenas decisiones, vas a comparar los algoritmos clásicos (FCFS, SJF, Round Robin, prioridad), vas a calcular a mano sus tiempos, y al final vas a ver cómo planifica Linux en la realidad — un algoritmo más sofisticado llamado CFS.

3.1 La idea: repartir un recurso escaso

💡 Intuición

Imaginate una sola caja registradora en una pupusería con cola de 30 personas. ¿Quién atiende primero? Hay varias políticas:

  • Por orden de llegada. Justo, predecible, pero alguien que solo quiere un refresco espera detrás de uno que pide 50 pupusas.
  • El pedido más rápido primero. Maximizás clientes por hora, pero alguien con un pedido grande puede esperar eternamente si nuevos clientes simples siguen llegando.
  • Turnos de 5 minutos. Repartís atención. Si tu pedido es de 1 minuto, lo terminan rápido. Si es de 1 hora, te atienden a pedazos.
  • Prioridad (clientes VIP, ancianos primero). Justo en cierta lógica, injusto en otra.

Cada política tiene ventajas y costos. La cola "por llegada" es justa pero ineficiente. "Pedido más corto" es eficiente pero injusta. Turnos son justos pero generan overhead (cambiar de cliente lleva tiempo).

El planificador del SO enfrenta exactamente este dilema, miles de veces por segundo. Y como hay muchos procesos con perfiles distintos (algunos cortos y CPU-bound, otros largos y I/O-bound), no existe un algoritmo perfecto — existen trade-offs.

3.2 Métricas para evaluar un planificador

📐 Fundamento

Para comparar algoritmos hace falta medir. Cinco métricas clásicas:

Métrica Definición Querés...
Throughput Procesos completados por unidad de tiempo Maximizar
Utilización CPU % del tiempo que la CPU está trabajando Maximizar
Tiempo de retorno (turnaround) Desde que llega hasta que termina Minimizar
Tiempo de espera Tiempo en estado "listo", esperando CPU Minimizar
Tiempo de respuesta Desde llegada hasta primera ejecución Minimizar

Definiciones formales. Para un proceso que llega en tllegadat_{\text{llegada}}, comienza a ejecutarse en tiniciot_{\text{inicio}} y termina en tfint_{\text{fin}}:

  • Tiempo de retorno: Tr=tfintllegadaT_r = t_{\text{fin}} - t_{\text{llegada}}.
  • Tiempo de espera: Te=TrTraˊfagaT_e = T_r - T_{\text{ráfaga}} (donde la ráfaga es el tiempo de CPU que el proceso necesita).
  • Tiempo de respuesta: Tresp=tiniciotllegadaT_{\text{resp}} = t_{\text{inicio}} - t_{\text{llegada}}.

Trade-offs. Algunos objetivos chocan:

  • Maximizar throughput suele aumentar tiempo de espera (priorizás eficiencia, no equidad).
  • Minimizar tiempo de respuesta requiere preempción frecuente (más overhead).
  • Equidad y eficiencia son a menudo opuestos.

Por eso hay distintos algoritmos: cada uno optimiza distinto.

Apropiativo (preemptive) vs no apropiativo. Un planificador es apropiativo si puede interrumpir un proceso que está ejecutándose para darle el CPU a otro. No apropiativo si una vez que un proceso empieza, sigue hasta que termine o se bloquee voluntariamente.

  • Apropiativo: más justo, mejor respuesta, pero más overhead por cambios de contexto.
  • No apropiativo: simple, sin overhead, pero un proceso largo monopoliza la CPU.

Los SO modernos son apropiativos.

3.3 Algoritmo 1: FCFS (First-Come First-Served)

📐 Fundamento

FCFS es la política más simple: los procesos se atienden en el orden en que llegan. Una cola FIFO, sin más.

  • Apropiativo: No.
  • Implementación: trivial — una sola cola.
  • Justicia: alta (orden de llegada).

Ejemplo.

Proceso Llegada Ráfaga
P1 0 24
P2 0 3
P3 0 3

Diagrama de Gantt:

| P1 (0-24) | P2 (24-27) | P3 (27-30) |
  • TesperaT_{\text{espera}}: P1 = 0, P2 = 24, P3 = 27. Promedio: 17.
  • TretornoT_{\text{retorno}}: P1 = 24, P2 = 27, P3 = 30. Promedio: 27.

Problema célebre — efecto convoy. Si llegan en orden P1 (largo), P2 (corto), P3 (corto), los cortos esperan al largo. Si fuera al revés (P2, P3, P1), el promedio sería mucho menor:

  • P2 espera 0, P3 espera 3, P1 espera 6. Promedio espera: 3.

Mismo trabajo total, distinto promedio. La política importa.

Cuándo usar FCFS: sistemas batch sin urgencia, donde la simplicidad pesa más que el rendimiento. Casi nunca en sistemas interactivos.

3.4 Algoritmo 2: SJF (Shortest Job First) y SRTF

📐 Fundamento

SJF ejecuta primero el proceso más corto entre los que están listos.

  • Apropiativo: versión apropiativa = SRTF (Shortest Remaining Time First).
  • Justicia: baja (los largos pueden esperar mucho — starvation).
  • Óptimo: demostrablemente minimiza tiempo de espera promedio.

Ejemplo (no apropiativo).

Proceso Llegada Ráfaga
P1 0 7
P2 2 4
P3 4 1
P4 5 4

Diagrama:

| P1 (0-7) | P3 (7-8) | P2 (8-12) | P4 (12-16) |

(En t=7 P1 termina; entre los que llegaron, P3 es el más corto.)

Tiempo de espera promedio: (0+6+3+7)/4=4(0 + 6 + 3 + 7)/4 = 4.

Versión SRTF (apropiativa):

| P1 (0-2) | P2 (2-4) | P3 (4-5) | P2 (5-7) | P4 (7-11) | P1 (11-16) |
  • En t=2 llega P2 (ráfaga 4 < restante de P1 = 5). Cambia.
  • En t=4 llega P3 (ráfaga 1 < 2 restantes de P2). Cambia.
  • En t=5 llega P4 (ráfaga 4 > 2 restantes de P2). P2 sigue.
  • En t=7 termina P2. Quedan P1 (5), P4 (4). P4 es más corto.
  • En t=11 termina P4. P1 corre hasta 16.

Tiempo de espera promedio: 3.0\approx 3.0.

El problema de SJF: cómo predecir la ráfaga. Es imposible saber cuánto va a durar un proceso antes de ejecutarlo. La práctica usa promedio histórico (estimación exponencial):

τn+1=αtn+(1α)τn\tau_{n+1} = \alpha t_n + (1 - \alpha) \tau_n

donde tnt_n es la ráfaga real anterior y τn\tau_n la estimación previa, α[0,1]\alpha \in [0, 1] pesa cuánto cuenta el reciente.

Starvation. Si llegan procesos cortos sin parar, los largos nunca corren. Es un problema serio. Una solución: aging — aumentar la prioridad del proceso a medida que espera.

3.5 Algoritmo 3: Round Robin (RR)

📐 Fundamento

Round Robin asigna a cada proceso un cuanto (quantum) de tiempo fijo. Cuando expira, el proceso vuelve al final de la cola y sigue el siguiente.

  • Apropiativo: sí, por timer.
  • Diseñado para: sistemas interactivos, donde la respuesta importa.
  • Justicia: alta (todos reciben CPU regularmente).

Ejemplo con cuanto = 4.

Proceso Llegada Ráfaga
P1 0 24
P2 0 3
P3 0 3
| P1 (0-4) | P2 (4-7) | P3 (7-10) | P1 (10-14) | P1 (14-18) | P1 (18-22) | P1 (22-26) | P1 (26-30) |

Tiempo de espera promedio: (6+4+7)/3=5.66(6 + 4 + 7)/3 = 5.66.

Trade-off del cuanto:

  • Cuanto muy chico → muchos cambios de contexto. El SO gasta más tiempo cambiando que ejecutando.
  • Cuanto muy grande → degenera en FCFS (un proceso ocupa la CPU una eternidad).
  • Cuanto típico: 10–100 ms en sistemas modernos.

Regla práctica: el cuanto debe ser un poco más grande que la ráfaga típica de un proceso interactivo, así la mayoría termina antes de ser interrumpida.

Costo del cambio de contexto. Aproximadamente 1–10 µs en hardware moderno: guardar registros del proceso saliente, cargar los del entrante, invalidar TLB y caché parcialmente. Si el cuanto fuera 10 µs y el cambio cuesta 10 µs, la mitad del tiempo se va en overhead. Por eso los cuantos son del orden de ms, no µs.

3.6 Algoritmo 4: Prioridad

📐 Fundamento

Cada proceso tiene un número de prioridad. El planificador siempre elige al de mayor prioridad entre los listos.

  • Apropiativo o no.
  • SJF es un caso particular (prioridad = inversa de la ráfaga).
  • Las prioridades pueden ser estáticas (fijas) o dinámicas (cambian con el tiempo).

Quién asigna la prioridad:

  • Sistema: el kernel decide según comportamiento (CPU-bound vs I/O-bound).
  • Usuario: comandos como nice y renice en Unix.
$ nice -n 10 ./mi_programa     # arranca con prioridad baja
$ renice -n -5 -p 1234         # mejorá la prioridad del PID 1234 (root)

En Linux, la "prioridad de niceness" va de -20 (la más alta, solo root) a +19 (la más baja). El default es 0.

Problema: starvation. Igual que SJF — procesos de baja prioridad pueden no ejecutarse nunca si siguen llegando otros prioritarios.

Solución: aging. Aumentar la prioridad efectiva en función del tiempo de espera:

prioridadefectiva=prioridadbase+tiempo de esperaconstante\text{prioridad}_{\text{efectiva}} = \text{prioridad}_{\text{base}} + \frac{\text{tiempo de espera}}{\text{constante}}

Así un proceso que esperó mucho eventualmente "gana" su turno.

3.7 Multilevel Feedback Queue (MLFQ)

📐 Fundamento

MLFQ es el algoritmo "inteligente" que combina varios anteriores. La idea: aprender del comportamiento de cada proceso.

Estructura:

  • Hay varias colas (típicamente 3–8), con distintas prioridades.
  • Cuanto más alta la cola, más corto el cuanto.
  • Procesos nuevos entran en la cola más alta.
  • Si un proceso usa todo su cuanto, baja una cola (parece CPU-bound, "castiguelo").
  • Si un proceso libera CPU antes de su cuanto (I/O), se queda o sube (parece I/O-bound, "favorezcalo").
  • Periódicamente, todos los procesos vuelven arriba (evitar starvation).

Por qué funciona:

  • Procesos interactivos (editores, navegadores) hacen mucha I/O y rara vez consumen su cuanto entero. Se quedan en colas altas → respuesta rápida.
  • Procesos batch (compilación, simulación) consumen cuantos enteros. Bajan a colas más bajas → no entorpecen a los interactivos.
  • Sin saber a priori qué tipo es cada proceso, el sistema lo clasifica observando.

Variaciones. Los detalles cambian: cuántas colas, qué cuanto en cada una, cuándo subir, cuándo bajar, qué tan seguido "resetear". UNIX tradicional, Windows NT, Solaris — todos usan variantes de MLFQ.

3.8 Cómo planifica Linux: CFS

🛠️ En la práctica

Desde 2007, el planificador estándar de Linux es CFS (Completely Fair Scheduler), de Ingo Molnár. Su filosofía es novedosa: en lugar de prioridades discretas, busca equidad perfecta — cada proceso recibe proporcionalmente el CPU.

La idea (simplificada). En un mundo ideal con nn procesos listos, cada uno debería recibir 1/n1/n del tiempo de CPU. Si un proceso recibió menos de su parte justa, debería correr; si recibió más, debería esperar.

CFS mantiene una variable por cada proceso: el virtual runtime (vruntime), que es básicamente "cuánto CPU ha usado, escalado por su prioridad". El planificador siempre elige al proceso con el vruntime más chico.

Detalles fascinantes:

  • Estructura de datos: un árbol rojo-negro (red-black tree) ordenado por vruntime. Insertar y extraer mínimo en O(logn)O(\log n).
  • Sin cuantos fijos. El "cuanto" se calcula dinámicamente: latencia / n_procesos, con un mínimo (3 ms típicamente).
  • Niceness afecta el peso. Procesos con nice bajo (alta prioridad) avanzan su vruntime más despacio → corren más.
  • I/O-bound. Cuando un proceso se bloquea, su vruntime queda congelado. Al despertar, suele ser el más bajo del árbol → corre inmediatamente, dando la respuesta rápida que los usuarios esperan.

Comandos para inspeccionar:

$ cat /proc/<pid>/sched      # parámetros del scheduler para ese proceso
$ chrt -p <pid>              # ver política y prioridad
$ chrt -f -p 50 <pid>        # cambiar a SCHED_FIFO con prioridad 50 (root)

Más allá de CFS. Linux tiene varios "schedulers" especializados:

Política Para qué
SCHED_OTHER (CFS) Procesos normales, default
SCHED_BATCH Procesos largos no interactivos
SCHED_IDLE Solo cuando no hay nada más
SCHED_FIFO Tiempo real, sin cuanto
SCHED_RR Tiempo real, Round Robin
SCHED_DEADLINE Garantías por deadline (autos, robots)

Las últimas tres son tiempo real y se usan en sistemas con plazos estrictos.

3.9 Planificación en multinúcleo

📐 Fundamento

Hasta acá hablamos como si hubiera una CPU. La realidad: tu computadora tiene 4, 8, 16 o más núcleos. Eso agrega complicaciones:

Decisiones nuevas:

  1. ¿Cada núcleo tiene su propia cola o hay una cola global?
  2. ¿Si un núcleo está ocioso, roba trabajo de otros?
  3. ¿Mantener un proceso en el mismo núcleo (afinidad) o moverlo libremente?

Afinidad de CPU. Cuando un proceso corre en el núcleo 0, deja datos en su caché L1/L2. Si lo movés al núcleo 3, el caché está frío — todo el trabajo cuesta más. Por eso los planificadores modernos prefieren mantener procesos en el mismo núcleo (afinidad), aunque a veces hay que reequilibrar.

Linux: taskset permite forzar afinidad.

$ taskset -c 0,1 ./mi_programa   # corré solo en núcleos 0 y 1
$ taskset -p <pid>                # consultar afinidad actual

Work stealing. Cuando un núcleo se queda sin trabajo, busca en las colas de otros núcleos y "roba" tareas. Es la técnica clásica de schedulers paralelos modernos (Go, Rust Tokio, .NET ThreadPool).

3.10 Cómo medir la planificación en tu computadora

🛠️ En la práctica

perf sched (Linux):

$ sudo perf sched record sleep 5
$ sudo perf sched latency

Muestra la latencia de cada proceso — cuánto tardó entre quedar listo y conseguir CPU. Útil para diagnosticar lentitud.

pidstat:

$ pidstat 1 5    # cada 1 s, 5 veces; muestra CPU por proceso

Probando trade-offs con un script. Generá dos procesos: uno CPU-bound (loop matemático) y uno I/O-bound (sleep + lectura). Mirá con top cómo se reparten el CPU. Cambiá las prioridades con nice y observá.

Observación pedagógica. Cuando ejecutás make -j8 para compilar con 8 hilos, el scheduler está orquestando: distribuye los compiladores entre núcleos, balancea, mueve cuando hace falta. Sin él, tendrías que hacerlo a mano.

3.11 Ejemplo integrador completo

🛠️ En la práctica

Considerá esta carga de trabajo:

Proceso Llegada Ráfaga
P1 0 8
P2 1 4
P3 2 9
P4 3 5

Calculamos tiempos de espera y retorno para 4 algoritmos.

FCFS

Orden de ejecución: P1 (0-8), P2 (8-12), P3 (12-21), P4 (21-26).

P Llegada Fin Retorno Espera
1 0 8 8 0
2 1 12 11 7
3 2 21 19 10
4 3 26 23 18

Promedio retorno: 15.25. Promedio espera: 8.75.

SJF no apropiativo

Mismo orden FCFS hasta P1 (0-8). Después en t=8, listos: P2 (4), P3 (9), P4 (5). Más corto: P2.

P1 (0-8), P2 (8-12), P4 (12-17), P3 (17-26).

P Fin Retorno Espera
1 8 8 0
2 12 11 7
4 17 14 9
3 26 24 15

Promedio retorno: 14.25. Promedio espera: 7.75.

SRTF (apropiativo)

Iteramos el tiempo en el cual cambia algo (llega un proceso o termina uno).

  • t=0: solo P1, ráfaga 8. Ejecuta P1.
  • t=1: llega P2, ráfaga 4 < 7 (P1 restante). Cambia a P2.
  • t=2: llega P3 (9), restante P2=3. P2 sigue.
  • t=3: llega P4 (5), restante P2=2. P2 sigue.
  • t=5: P2 termina. Restantes: P1=7, P3=9, P4=5. Ejecuta P4.
  • t=10: P4 termina. P1=7, P3=9. Ejecuta P1.
  • t=17: P1 termina. Ejecuta P3.
  • t=26: P3 termina.
P Fin Retorno Espera
1 17 17 9
2 5 4 0
3 26 24 15
4 10 7 2

Promedio retorno: 13.0. Promedio espera: 6.5. (Mejor de los tres hasta acá.)

Round Robin con cuanto = 2

Bastante laborioso. Resultado:

P Fin Retorno Espera
1 22 22 14
2 18 17 13
3 26 24 15
4 24 21 16

Promedio retorno: 21. Promedio espera: 14.5.

RR es peor que SJF aquí, pero mucho mejor en tiempo de respuesta (cada proceso recibe CPU temprano). En sistemas interactivos eso vale más que el throughput.

3.12 Resumen visual

Algoritmo Apropiativo Optimiza Pega cuando
FCFS No Simplicidad Procesos uniformes
SJF Variantes Tiempo medio espera Conocés ráfagas
SRTF Tiempo medio espera (idem, apropiativo)
Round Robin Tiempo de respuesta Sistema interactivo
Prioridad Variantes Lo que la prioridad codifique Necesitás control fino
MLFQ Mezcla — auto-clasifica Carga heterogénea
CFS (Linux) Equidad proporcional Uso general moderno
Conceptos clave
Cambio de contexto = costo
Starvation = peligro de prioridades fijas
Aging = solución típica
Cuanto = balance entre overhead y respuesta
Afinidad = conviene mantener proceso en mismo núcleo

3.13 Ejercicios

✏️ Ejercicio 3.1 — Cálculo FCFS

Dada la carga:

P Llegada Ráfaga
A 0 5
B 0 3
C 0 8
D 0 2

Calculá tiempo de espera y retorno promedio bajo FCFS. ¿Qué orden minimizaría el promedio de espera?

✏️ Ejercicio 3.2 — Round Robin

P Llegada Ráfaga
P1 0 6
P2 1 3
P3 2 8
P4 3 4

Cuanto = 2. Calculá tiempos de espera y retorno.

✏️ Ejercicio 3.3 — Diseñar un planificador

Diseñás un SO para una mezcla de procesos: 70% interactivos cortos, 20% I/O-bound moderados, 10% batch CPU-bound de horas. ¿Qué algoritmo elegís y por qué?

✏️ Ejercicio 3.4 — Experimento real

Ejecutá:

$ time ./prog                     # tiempo normal
$ nice -n 19 time ./prog          # con prioridad mínima
$ # en otra terminal: cargá la CPU con `yes > /dev/null &`
$ time ./prog                     # con CPU saturada y prio normal
$ nice -n 19 time ./prog           # con CPU saturada y prio min

Observá las diferencias en real, user, sys.

3.14 Para profundizar


Definiciones nuevas: planificador, scheduler, throughput, tiempo de retorno, espera, respuesta, FCFS, SJF, SRTF, Round Robin, cuanto, prioridad, aging, MLFQ, CFS, vruntime, afinidad, work stealing.