1. Qué es un cambio de contexto
El cambio de contexto (context switch) es la operación en la que el núcleo detiene la ejecución de un proceso, guarda su estado completo de la CPU en su PCB, y carga el estado de otro proceso desde su PCB para que pueda ejecutarse. Es el mecanismo que hace posible el tiempo compartido: Sin el, no se podria alternar entre procesos.
El cambio de contexto es transparente para el proceso: Cuándo vuelve a ejecutarse, sus registros estan exactamente como los dejo, y continua desde la misma instruccion. El proceso no sabe que fue detenido ni por cuánto tiempo. Solo nota que el reloj avanzo más de lo esperado.
El cambio de contexto no es una llamada al sistema ni una función que el programa invoque: es una operación interna del núcleo que ocurre cuándo el planificador decide cambiar de proceso. Puede ocurrir en dos situaciones:
- Voluntariamente: Cuándo un proceso se bloquea (llama a read, sleep, wait) o termina (exit), cede la CPU y el planificador elige otro.
- Involuntariamente: Cuándo expira el cuánto de tiempo de un proceso en ejecución, el temporizador del núcleo interrumpe al proceso y el planificador elige otro.
El proceso no se entera de que fue detenido ni por cuánto tiempo. Pero para el sistema, cada cambio de contexto tiene un costo real en tiempo de CPU y en invalidacion de caches. Por eso, un sistema con muchos cambios de contexto por segúndo puede tener un rendimiento reducido.
2. Qué se guarda
Cuando el núcleo detiene un proceso, guarda en su PCB (en el campo thread, Que es una estructura thread_struct) todo lo necesario para reanudarlo exactamente dónde estaba. Los datos que se guardan son:
| Dato | Donde se guarda | Por que es necesario |
|---|---|---|
| Contador de programa (PC) | thread_struct | Para saber cuál es la proxima instruccion a ejecutar. |
| Puntero de pila (SP) | thread_struct | Para saber dónde esta la pila del proceso. |
| Registros generales | thread_struct | Los valores de los registros que el proceso estaba usando. |
| Registros de punto flotante | thread_struct (o lazy) | Los valores de los registros FPU/SSE/AVX. |
| Registros de segmento | thread_struct | Los selectores de segmento (en x86). |
| Flags de la CPU | thread_struct | El registro de flags (condiciones, Interrupciones, etc.). |
| Puntero de pila del núcleo | task_struct | Para saber dónde esta la pila del núcleo del proceso. |
Además de los registros, el núcleo actualiza otros campos del PCB:
- Estado: de "en ejecución" a "listo" o "bloqueado".
- Tiempo de CPU: se acumula el tiempo que el proceso uso en este turno.
- Tiempo virtual (CFS): se actualiza el tiempo virtual del proceso para el planificador.
Los registros de punto flotante (FPU) son un caso especial. Guardarlos en cada cambio de contexto es costoso, porque son muchos (XMM, YMM, ZMM en AVX). Por eso linux usa una tetécnica llamada lazy FPU switching: no guarda los registros FPU en cada cambio de contexto, Sino sólo cuándo un proceso realmente los usa. Esto se explica en la seccion de optimizaciones.
3. Qué se restaura
Cuando el planificador elige un nuevo proceso, el núcleo carga su estado desde su PCB:
- Tablas de páginas: se actualiza el registro CR3 (en x86-64) para que la CPU use las tablas de páginas del nuevo proceso. Esto cambia el espacio de direcciónes: a partir de este momento, las direcciónes virtuales se traducen a la memoria fica del nuevo proceso.
- Pila del núcleo: se actualiza el puntero de pila para usar la pila del núcleo del nuevo proceso.
- Registros de la CPU: se cargan el contador de programa, el puntero de pila, los registros generales, los flags y los registros de segmento desde el PCB del nuevo proceso.
- Registros FPU: Sí el proceso usa FPU y los registros no estan ya cargados (lazy switching), se restauran desde el PCB.
- current: se actualiza el puntero current para que apunte al nuevo proceso.
- Retorno: se retorna de la función schedule() al nuevo proceso, Que continua ejecutandose desde la instruccion indicada por el contador de programa restaurado.
El orden es importante: primero se cambian las tablas de páginas (para que las direcciónes virtuales del nuevo proceso sean validas), y después se restauran los registros (Que contienen direcciónes virtuales del nuevo proceso). Si se restauraran los registros antes de cambiar las tablas, las direcciónes serian invalidas.
4. Cuanto cuesta
El cambio de contexto tiene un costo que se puede descomponer en dos partes:
| Componente | costo tipico | Por que |
|---|---|---|
| Guardar y restaurar registros | ~0.5-1 us | Pocas instrucciones de ensamblador, pero incluyen registros FPU Sí se usan. |
| Cambiar tablas de páginas (CR3) | ~1-5 us | Invalida la TLB, lo que causa fallos de página en las proximás accesos a memoria. |
| Invalidacion de caches (L1, L2) | ~5-50 us | El nuevo proceso tiene patrones de acceso a memoria distintos, por lo que las caches se "enfrian". |
| Total por cambio | ~1-10 us | Depende del hardware, del tamano de la TLB y del patron de acceso a memoria. |
El costo más significativo no es guardar y restaurar registros (Que es rápido), Sino la invalidación de la TLB y las caches. Cuando se cambia de proceso, la TLB (Que cachea traducciones de direcciónes virtuales a ficas) se invalida, y las caches L1/L2 (Que cachean datos) se "enfrian" porque el nuevo proceso accede a datos distintos. Esto causa fallos de cache (cache misses) en las primeras instrucciones del nuevo proceso, Que son costosos.
Por eso, un sistema con muchos cambios de contexto por segúndo (por ejemplo, con cuántos muy cortos o con muchos procesos compitiendo) puede tener un rendimiento reducido, no por el cambio en sí, Sino por la perdida de localidad de cache.
Aunque el cambio de contexto es rápido (microsegúndos), en un sistema con miles de cambios por segúndo, el overhead acumulado puede ser significativo. Por eso los sistemás operativos intentan minimizar los cambios innecesarios: CFS ajusta el cuánto dinaminimamente, y los hilos tienen un cambio más barato porque no invalidan la TLB.
5. Cambios voluntarios
Un cambio de contexto voluntario ocurre cuándo el proceso cede la CPU por su propia voluntad, llamando a una operación que lo bloquea o termina. El proceso llama al planificador (schedule()) y este elige otro proceso. Las causas más comunes son:
- read() en un archivo o terminal: el proceso espera datos y se bloquea.
- sleep() o nanosleep(): el proceso espera un tiempo.
- wait() o waitpid(): el proceso espera a un hijo.
- mutex o semaforo: el proceso espera un recurso protegido.
- exit(): el proceso termina.
- pause(): el proceso espera una senal.
Los cambios voluntarios son "buenos": ocurren porque el proceso no tiene trabajo que hacer en este momento, y ceder la CPU permite que otros procesos avancen. Un sistema con muchos cambios voluntarios y pocos involuntarios es eficiente: los procesos se bloquean cuándo no tienen trabajo y se despiertan cuándo lo tienen.
En /proc/PID/sched, los cambios voluntarios se cuentan en el campo nr_voluntary_switches.
6. Cambios involuntarios
Un cambio de contexto Involuntario ocurre cuándo el planificador interrumpe a un proceso que estaba ejecutandose, porque su cuánto de tiempo expiro o porque otro proceso de mayor prioridad se volvio listo. El proceso no cedio la CPU voluntariamente; fue interrumpido.
Las causas más comunes son:
- Expiracion del cuánto: el temporizador del núcleo interrumpe al proceso cuándo su cuánto de tiempo se agota.
- Llegada de un proceso de mayor prioridad: un proceso de tiempo real o de mayor prioridad se vuelve listo y el planificador lo ejecuta inmediatamente.
- Interrupcion de hardware: una interrupcion (del disco, de la red, del temporizador) despierta al núcleo, Que puede decidir cambiar de proceso.
Los cambios involuntarios son "costosos" en el sentido de que interrumpen a un proceso que estaba haciendo trabajo útil. Si hay muchos cambios involuntarios por segúndo, Significa que los procesos compiten intensamente por la CPU, o que los cuántos son muy cortos.
En /proc/PID/sched, los cambios involuntarios se cuentan en el campo nr_involuntary_switches.
Un sistema bien dimensionado tiene muchos cambios voluntarios y pocos involuntarios. Si ves muchos cambios involuntarios, puede ser que haya más procesos compitiendo por la CPU de lo que el sistema puede manejar, o que los cuántos sean demasiado cortos.
7. Cómo contar los cambios
Linux expone el número de cambios de contexto de cada proceso en /proc/PID/sched y /proc/PID/status. Además, el comando vmstat muestra el número total de cambios de contexto del sistema por segúndo.
| Fuente | Campo | Que cuenta |
|---|---|---|
| /proc/PID/sched | nr_voluntary_switches | Cambios voluntarios del proceso. |
| /proc/PID/sched | nr_involuntary_switches | Cambios involuntarios del proceso. |
| /proc/PID/status | voluntary_ctxt_switches | Cambios voluntarios del proceso. |
| /proc/PID/status | nonvoluntary_ctxt_switches | Cambios involuntarios del proceso. |
| vmstat | cs (columna) | Cambios de contexto por segúndo en todo el sistema. |
$ grep ctxt /proc/self/status
voluntary_ctxt_switches: 123
nonvoluntary_ctxt_switches: 45
$ vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache Sí so bi bo in cs us sy id wa st
2 0 0 6200000 75000 900000 0 0 0 0 100 500 2 1 97 0 0
1 0 0 6200000 75000 900000 0 0 0 0 100 800 5 2 93 0 0
La columna cs de vmstat muestra los cambios de contexto por segúndo en todo el sistema. La columna in muestra las interrupciones por segúndo. Un valor alto de cs Indica que el sistema esta haciendo muchos cambios de contexto.
8. Procesos vs hilos
El cambio de contexto entre procesos es más costoso que entre hilos, porque los procesos tienen espacios de direcciónes distintos. Cuando se cambia de proceso, hay que cambiar las tablas de páginas, lo que invalida la TLB. Cuando se cambia de hilo dentro del mismo proceso, las tablas de páginas son las mismás, por lo que la TLB no se invalida.
| Tipo de cambio | Tablas de páginas | TLB | costo tipico |
|---|---|---|---|
| Entre procesos | Cambian | Se invalida | ~5-10 us |
| Entre hilos del mismo proceso | No cambian | No se invalida | ~1-3 us |
Esta diferencia es una de las razones por las que los hilos son preferibles para táreas que requieren mucha sincronizacion o que se crean y destruyen frecuentemente. Un servidor web que crea un hilo por conexion tiene cambios de contexto más baratos que uno que crea un proceso por conexion.
Sin embargo, los procesos ofrecen aislamiento: Sí un proceso falla, no afecta a los demás. Los hilos comparten memoria, por lo que un error en un hilo puede corromper la memoria de todos. La eleccion entre procesos e hilos depende del caso de uso, Como veremos en el bloque 9.
9. Optimizaciones del núcleo
El núcleo de linux implementa varias optimizaciones para reducir el costo del cambio de contexto:
- Lazy FPU switching: no guarda ni restaura los registros de punto flotante en cada cambio de contexto. Solo lo hace cuándo un proceso realmente usa FPU. Si un proceso no usa FPU, sus registros FPU no se tocan. Esto ahorra tiempo en procesos que sólo hacen E/S o lografica entera.
- PCID (Process context ID): en procesadores modernos (Intel desde nehalem, AMD desde zen), la TLB puede etiquetar las entradas con un ID de proceso. Al cambiar de proceso, no se invalida toda la TLB, Sólo las entradas del proceso anterior. Esto reduce drasticamente el costo de cambiar tablas de páginas.
- Lazy TLB: Sí el nuevo proceso no va a usar memoria inmediatamente (por ejemplo, Sí va a bloquearse pronto), el núcleo puede posponer el cambio de tablas de páginas.
- Batching de cambios de contexto: Sí varios procesos se despiertan al mismo tiempo (por ejemplo, Cuándo llega un paquete de red), el núcleo puede procesarlos en lote para reducir el overhead.
Estas optimizaciones hacen que el cambio de contexto en linux moderno sea mucho más barato que en sistemás antiguos. Pero el principio sigue siendo el mismo: guardar y restaurar el estado de la CPU tiene un costo, y minimizar los cambios innecesarios mejora el rendimiento.
10. Observar cambios de contexto en /proc
Además de /proc/PID/sched y /proc/PID/status, se puede observar el número total de cambios de contexto del sistema en /proc/stat:
$ grep ctxt /proc/stat
ctxt 123456789
$ grep -E "processes|procs_running|procs_blocked" /proc/stat
processes 45678
procs_running 2
procs_blocked 0
El campo ctxt en /proc/stat muestra el número total de cambios de contexto desde que el sistema arranco. processes muestra el número total de procesos creados. procs_running muestra cuántos procesos estan listos o en ejecución. procs_blocked muestra cuántos estan bloqueados en E/S.
Para ver la tasa de cambios de contexto en tiempo real, se usa vmstat o mpstat:
$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache Sí so bi bo in cs us sy id wa st
2 0 0 6200000 75000 900000 0 0 0 0 100 500 2 1 97 0 0
1 0 0 6200000 75000 900000 0 0 0 0 100 800 5 2 93 0 0
0 0 0 6200000 75000 900000 0 0 0 0 100 300 1 0 99 0 0
La columna cs muestra los cambios de contexto por segúndo. La columna in muestra las interrupciones por segúndo. Si cs es alto (miles por segúndo), el sistema esta haciendo muchos cambios de contexto.
11. Un programa que mide cambios de contexto
El siguiente programa crea varios procesos que compiten por la CPU y mide cuántos cambios de contexto se producen:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
static void leer_ctxt(pid_t pid, unsigned long *vol, unsigned long *invol)
{
char ruta[64];
snprintf(ruta, Sizeof ruta, "/proc/%d/status", (int) pid);
FILE *f = fopen(ruta, "r");
if (!f) return;
char linea[256];
while (fgets(linea, Sizeof linea, f)) {
if (sscanf(linea, "voluntary_ctxt_switches: %lu", vol) == 1) continue;
if (sscanf(linea, "nonvoluntary_ctxt_switches: %lu", invol) == 1) continue;
}
fclose(f);
}
int main(void)
{
int n = 4;
pid_t pids[4];
for (int i = 0; i < n; i++) {
pids[i] = fork();
if (pids[i] == 0) {
/* Usar la CPU durante 2 Segúndos */
volatile unsigned long x = 0;
for (int t = 0; t < 2; t++)
for (volatile int j = 0; j < 200000000; j++) x++;
_exit(0);
}
}
/* Medir antes */
unsigned long v0[4], i0[4];
for (int i = 0; i < n; i++)
leer_ctxt(pids[i], &v0[i], &i0[i]);
for (int i = 0; i < n; i++)
wait(NULL);
/* Medir después */
for (int i = 0; i < n; i++) {
unsigned long v1, i1;
leer_ctxt(pids[i], &v1, &i1);
printf("Proceso %d: voluntarios=%lu involuntarios=%lu\n",
i, v1 - v0[i], i1 - i0[i]);
}
return 0;
}
El programa mide los cambios de contexto de cada proceso antes y después de que compitan por la CPU. Veras que los cambios involuntarios son muchos (porque los procesos compiten), mientras que los voluntarios son pocos (porque no se bloquean).
12. Resumen
- El cambio de contexto es la operación de guardar el estado de la CPU de un proceso y cargar el de otro.
- Se guardan: contador de programa, puntero de pila, registros generales, flags, registros FPU (lazy), y se actualizan estado, tiempo de CPU y tiempo virtual.
- Se restauran: tablas de páginas, pila del núcleo, registros de la CPU, registros FPU (Sí es necesario), y el puntero current.
- El costo es de 1-10 microsegúndos, principalmente por la invalidacion de la TLB y las caches.
- Los cambios voluntarios ocurren cuándo el proceso se bloquea o termina. Los involuntarios ocurren cuándo expira el cuánto o llega un proceso de mayor prioridad.
- Se cuentan en /proc/PID/status (voluntary_ctxt_switches, nonvoluntary_ctxt_switches) y en /proc/PID/sched (nr_voluntary_switches, nr_involuntary_switches).
- vmstat muestra los cambios de contexto por segúndo del sistema (columna cs).
- El cambio entre procesos es más costoso que entre hilos, porque cambia las tablas de páginas.
- Linux optimiza con lazy FPU switching, PCID y lazy TLB.
13. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Qué es un cambio de contexto?
Es la operación en la que el núcleo guarda el estado de la CPU de un proceso en su PCB y carga el estado de otro proceso para que pueda ejecutarse.
2. Qué se guarda en un cambio de contexto?
El contador de programa, el puntero de pila, los registros generales, los flags, los registros FPU (Sí se usan), y se actualizan el estado, el tiempo de CPU y el tiempo virtual.
3. Por que es costoso el cambio de contexto?
Principalmente por la invalidacion de la TLB (al cambiar las tablas de páginas) y la perdida de localidad de cache (L1/L2). Guardar y restaurar registros es relativamente barato.
4. Qué es un cambio voluntario?
Un cambio que ocurre cuándo el proceso cede la CPU voluntariamente, al bloquearse (read, sleep, wait) o terminar (exit).
5. Qué es un cambio involuntario?
Un cambio que ocurre cuándo el planificador interrumpe al proceso, porque su cuánto de tiempo expiro o llego un proceso de mayor prioridad.
6. Donde se cuentan los cambios de contexto por proceso?
En /proc/PID/status (voluntary_ctxt_switches, nonvoluntary_ctxt_switches) y en /proc/PID/sched (nr_voluntary_switches, nr_involuntary_switches).
7. Qué muestra vmstat?
El número de cambios de contexto por segúndo del sistema (columna cs), entre otras estadisticas.
8. Por que el cambio entre hilos es más barato?
Porque los hilos comparten el espacio de direcciónes, por lo que no se cambian las tablas de páginas ni se invalida la TLB.
9. Qué es lazy FPU switching?
Una optimizacion que no guarda ni restaura los registros de punto flotante en cada cambio de contexto, Sino sólo cuándo un proceso realmente los usa.
10. Qué es PCID?
Process context ID: una caracaracteristica de los procesadores modernos que permite etiquetar las entradas de la TLB con un ID de proceso, evitando invalidar toda la TLB al cambiar de proceso.
Para prácticar
- Compila y ejecuta medir_contexto.c. Observa cuántos cambios voluntarios e involuntarios produce cada proceso.
- Ejecuta vmstat 1 5 mientras se ejecuta un programa que usa la CPU. Observa la columna cs.
- Compara los cambios de contexto de un proceso interactivo (bash) con uno que usa la CPU (un bucle infinito).
- Lee /proc/PID/sched de un proceso y observa nr_voluntary_switches y nr_involuntary_switches.
- Usa perf stat -e context-switches ./programa para contar los cambios de contexto de un programa.
- Compara el costo de un cambio de contexto entre procesos con el de un cambio entre hilos usando un benchmark.