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 , comienza a ejecutarse en y termina en :
- Tiempo de retorno: .
- Tiempo de espera: (donde la ráfaga es el tiempo de CPU que el proceso necesita).
- Tiempo de respuesta: .
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) |
- : P1 = 0, P2 = 24, P3 = 27. Promedio: 17.
- : 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: .
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: .
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):
donde es la ráfaga real anterior y la estimación previa, 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: .
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
niceyreniceen 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:
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 procesos listos, cada uno debería recibir 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 . - 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
nicebajo (alta prioridad) avanzan suvruntimemás despacio → corren más. - I/O-bound. Cuando un proceso se bloquea, su
vruntimequeda 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:
- ¿Cada núcleo tiene su propia cola o hay una cola global?
- ¿Si un núcleo está ocioso, roba trabajo de otros?
- ¿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 | Sí | Tiempo medio espera | (idem, apropiativo) |
| Round Robin | Sí | Tiempo de respuesta | Sistema interactivo |
| Prioridad | Variantes | Lo que la prioridad codifique | Necesitás control fino |
| MLFQ | Sí | Mezcla — auto-clasifica | Carga heterogénea |
| CFS (Linux) | Sí | 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?
Solución
FCFS (orden A, B, C, D):
- A: espera 0, retorno 5.
- B: espera 5, retorno 8.
- C: espera 8, retorno 16.
- D: espera 16, retorno 18.
Promedio espera: . Retorno: .
Orden óptimo (SJF): D, B, A, C.
- D: espera 0, retorno 2.
- B: espera 2, retorno 5.
- A: espera 5, retorno 10.
- C: espera 10, retorno 18.
Promedio espera: . Retorno: .
Lección: poner cortos primero rebaja drásticamente el promedio.
✏️ 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.
Solución
Cola dinámica (uso convención: a la cola se entra al final, "agregamos" después de descontar):
t=0: solo P1. Ejecuta P1 (0-2). Restante P1=4. t=1: llega P2 (entra a cola). t=2: llega P3. Cola: [P2, P3, P1]. Ejecuta P2 (2-4). Restante P2=1. t=3: llega P4. Cola al final del cuanto: [P3, P1, P4, P2]. t=4: ejecuta P3 (4-6). Restante P3=6. Cola: [P1, P4, P2, P3]. t=6: ejecuta P1 (6-8). Restante P1=2. Cola: [P4, P2, P3, P1]. t=8: ejecuta P4 (8-10). Restante P4=2. Cola: [P2, P3, P1, P4]. t=10: ejecuta P2 (cuanto=2 pero solo necesita 1). P2 (10-11). Termina. Cola: [P3, P1, P4]. t=11: ejecuta P3 (11-13). Restante P3=4. Cola: [P1, P4, P3]. t=13: ejecuta P1 (13-15). Restante P1=0. P1 termina. Cola: [P4, P3]. t=15: ejecuta P4 (15-17). Restante P4=0. P4 termina. Cola: [P3]. t=17: ejecuta P3 (17-19). Restante 2. t=19: ejecuta P3 (19-21). Termina.
Fines: P1=15, P2=11, P3=21, P4=17. Retornos: 15, 10, 19, 14. Promedio: 14.5. Esperas: : P1=9, P2=7, P3=11, P4=10. Promedio: 9.25.
✏️ 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é?
Solución
MLFQ o CFS son las opciones razonables.
Por qué MLFQ:
- Los interactivos quedan en colas altas (cuantos chicos) → respuesta rápida.
- Los batch bajan a colas inferiores → no entorpecen interactivos.
- Los I/O-bound quedan en colas medianas/altas (no consumen su cuanto) → buena respuesta cuando despiertan.
- Aging evita que los batch mueran de inanición.
Una alternativa simple: prioridades estáticas con tres niveles. Pero MLFQ se autoajusta y resiste mejor que un sistema rígido cuando la mezcla cambia.
Por qué CFS: equidad proporcional natural — los I/O-bound automáticamente "ganan" tiempo cuando despiertan (su vruntime está bajo). Es lo que usa Linux y funciona excelente en cargas mixtas reales.
✏️ 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.
Solución
Sin contención: tiempos similares con o sin nice.
Con CPU saturada:
nice -n 0: el scheduler reparte equitativo entre tu programa y losyes. Tiemporealaumenta moderado.nice -n 19: tu programa es de prioridad mínima — losyeslo dejan casi sin CPU. Tiemporealaumenta mucho.
Lección: la prioridad solo importa cuando hay contención. Sin contención, todos corren igual.
Acordate de matar los yes: kill %1 (o pkill yes).
3.14 Para profundizar
- Tanenbaum & Bos cap. 2.4 (planificación).
- OSTEP capítulos sobre scheduling: gratis online, súper claros.
- Robert Love, Linux Kernel Development — capítulo sobre CFS, técnico pero accesible.
- Próximo capítulo: Sincronización — el verdadero problema de tener varios hilos: cómo evitar que se pisen.
Definiciones nuevas: planificador, scheduler, throughput, tiempo de retorno, espera, respuesta, FCFS, SJF, SRTF, Round Robin, cuanto, prioridad, aging, MLFQ, CFS, vruntime, afinidad, work stealing.