NO_HZ y delays: silenciar el tick y esperar bien
El kernel tickless que elimina el latido periódico cuando la CPU está ociosa (NO_HZ_IDLE, ahorro de energía) o dedicada a una sola tarea (NO_HZ_FULL, HPC y tiempo real), y la disciplina de los retardos: udelay como espera activa que quema CPU en contexto atómico frente a msleep, usleep_range y fsleep que ceden el procesador durmiendo.
Un tick que late mil veces por segundo sobre una CPU que no tiene nada que hacer es energía tirada, y sobre una CPU dedicada a una sola tarea crítica es una interrupción que rompe su latencia sin motivo. El kernel tickless —NO_HZ— responde a ambos males apagando el latido periódico cuando nadie lo necesita. En el otro extremo del nivel están los delays: la diferencia, tantas veces mal entendida, entre quemar CPU esperando en un busy-wait y ceder la CPU durmiendo. Cerramos el tiempo en el kernel con las dos formas de no hacer nada durante un rato: la que gasta y la que descansa.
- Entender NO_HZ_IDLE: por qué apagar el tick en reposo ahorra energía sin perder corrección.
- Entender NO_HZ_FULL: eliminar el tick en CPUs dedicadas para HPC y tiempo real.
- Distinguir el busy-wait de
udelay/ndelayde los sleeps reales. - Elegir entre
udelay,usleep_range,msleepyfsleepsegún contexto y duración.
NO_HZ_IDLE: callar el tick en reposo
En un kernel clásico el tick late sin descanso, incluso con la CPU en el estado idle: mil interrupciones por segundo que despiertan al procesador de su sueño profundo solo para comprobar que no hay nada que hacer y volver a dormir. Eso arruina los estados C de bajo consumo y quema batería. CONFIG_NO_HZ_IDLE —activo por defecto en casi todo kernel de 2026— lo arregla: cuando una CPU entra en idle y no hay ningún timer próximo, el kernel detiene el tick periódico de esa CPU y programa una única interrupción para el instante del siguiente timer pendiente.
El mecanismo se apoya en todo lo anterior. Como el tick es ya un hrtimer en modo oneshot (nivel 45.4), silenciarlo es no reprogramarlo: la CPU duerme hasta que un evento real —un timer que vence, una IRQ de dispositivo— la despierte.
/* kernel/time/tick-sched.c, conceptual: al entrar en idle */
void tick_nohz_idle_enter(void)
{
/* si no hay trabajo, no reprograma el tick periodico:
la CPU dormira hasta el proximo timer o interrupcion */
}
La corrección se preserva porque jiffies no se pierde: al despertar, el kernel calcula cuántos ticks “debió” haber contado durante el sueño y ajusta jiffies_64 de golpe. El tiempo no se detuvo; solo dejó de medirse latido a latido mientras nadie miraba.
NO_HZ_FULL: CPUs sin latido para HPC y tiempo real
CONFIG_NO_HZ_FULL va mucho más lejos. En cargas de cómputo puro (HPC) o de tiempo real, quieres una CPU entregada a una sola tarea, sin que nada la interrumpa. Pero el tick sigue latiendo aunque solo haya un proceso runnable, para contabilidad y reparto. NO_HZ_FULL detiene el tick incluso con una tarea corriendo, siempre que sea la única en esa cola de ejecución: baja de mil interrupciones por segundo a, típicamente, una por segundo.
Se activa por línea de arranque, reservando unas CPUs como housekeeping (que siguen atendiendo el trabajo del sistema) y dedicando el resto:
# CPUs 1-7 sin tick; la 0 hace de housekeeping
nohz_full=1-7 rcu_nocbs=1-7 isolcpus=1-7
El resultado es una latencia y un jitter drásticamente menores: sin tick, no hay interrupción periódica que expulse la tarea crítica ni contamine su caché. Es la base del tiempo real determinista y de los kernels de trading y telco de baja latencia. El precio es la complejidad: las CPUs dedicadas ceden a las de housekeeping tareas que antes hacían en su tick —RCU, temporización, estadísticas—, y necesitan una sola tarea runnable para que el truco funcione. Con dos tareas competiendo, el tick vuelve, porque hace falta para repartir la CPU entre ellas.
NO_HZ_IDLE
Apaga el tick cuando la CPU está ociosa. Objetivo: ahorro de energía y estados C profundos. Activo por defecto en portátiles, móviles y servidores. Transparente para el código.
NO_HZ_FULL
Apaga el tick cuando una CPU corre una única tarea. Objetivo: latencia mínima y cero jitter para HPC y tiempo real. Se activa con nohz_full= y exige CPUs de housekeeping.
Delays: quemar la CPU o cederla
La otra cara del tiempo es esperar a propósito. Y hay dos maneras radicalmente distintas, cuya confusión es una de las fuentes de bugs más comunes en drivers. Un busy-wait gira en un bucle quemando ciclos sin ceder la CPU; un sleep cede la CPU al planificador y se duerme hasta que un timer lo despierte.
Los busy-wait son ndelay, udelay y mdelay (nano, micro y milisegundos). No duermen: cuentan ciclos calibrados al arrancar contra el loops_per_jiffy que da el BogoMIPS. Por eso son la única opción válida en contexto atómico —manejador de IRQ, softirq, con un spinlock cogido— donde dormir está prohibido:
#include <linux/delay.h>
/* en contexto atomico: el hardware exige 2 us tras el reset */
writel(RESET, dev->regs + REG_CTRL);
udelay(2); /* busy-wait: quema ~2 us de CPU */
writel(START, dev->regs + REG_CTRL);
La regla de oro: mdelay(100) es casi siempre un error. Quemar cien milisegundos de CPU en un bucle es un desperdicio brutal; si estás en un contexto donde puedes dormir, debes dormir.
Dormir bien: usleep_range, msleep y fsleep
Cuando estás en contexto de proceso y puedes ceder la CPU, usas un sleep de verdad. Aquí importa la granularidad:
msleep(ms)— duerme milisegundos con granularidad dejiffies. Para esperas de decenas de milisegundos o más. Pero como redondea al tick, unmsleep(1)puede dormir bastante más de un milisegundo, y para plazos cortos es impreciso y derrochador de wakeups.usleep_range(min, max)— duerme microsegundos apoyándose en hrtimers, y recibe un rango. Ese rango es deliberado: le da al kernel holgura para agrupar tu despertar con otros eventos cercanos, reduciendo el número de interrupciones. Es la forma correcta para esperas de microsegundos a pocos milisegundos.fsleep(us)— el ayudante que elige por ti:udelaypor debajo de 10 us,usleep_rangehasta 20 ms,msleeppor encima. Cómodo cuando la duración es variable.
/* contexto de proceso: esperar ~1 ms cediendo la CPU */
usleep_range(1000, 1200); /* duerme; despierta entre 1,0 y 1,2 ms */
/* espera larga y tosca: 200 ms */
msleep(200); /* duerme con granularidad de tick */
El error inverso también existe: llamar a usleep_range o msleep desde un contexto atómico produce un scheduling while atomic, porque no hay a quién dormir. La decisión, entonces, es una tabla de dos entradas —contexto y duración— que conviene memorizar.
Bajo todos estos sleeps late la misma primitiva: schedule_timeout, que arma un timer con el plazo pedido, marca la tarea como dormida y llama a schedule() para ceder la CPU. msleep no es más que schedule_timeout_uninterruptible sobre msecs_to_jiffies(ms) + 1 —de ahí su granularidad de tick y ese +1 que garantiza dormir al menos lo pedido—, mientras que usleep_range arma un hrtimer para lograr precisión de microsegundos:
/* kernel/time/timer.c, simplificado: el corazon de msleep */
void msleep(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout)
timeout = schedule_timeout_uninterruptible(timeout);
}
La variante schedule_timeout_interruptible despierta también ante una señal, devolviendo el tiempo restante; es la base de las esperas cancelables. Entender que todo sleep termina en schedule() cierra el círculo: dormir en el kernel es siempre ceder al planificador con un timer que promete despertarte, y por eso es imposible allí donde el planificador está prohibido.
flowchart TD A[Necesito esperar un tiempo] --> B[Estoy en contexto atomico] B -->|si, no puedo dormir| C[udelay o ndelay: busy-wait] B -->|no, puedo dormir| D[Cuanto tiempo] D -->|microsegundos a pocos ms| E[usleep_range] D -->|decenas de ms o mas| F[msleep] D -->|duracion variable| G[fsleep elige por mi] style C fill:#f38ba8,color:#11111b style E fill:#a6e3a1,color:#11111b style F fill:#a6e3a1,color:#11111b
Un busy-wait no libera la CPU: durante mdelay(50) ese núcleo no hace absolutamente nada útil cincuenta milisegundos enteros. La única justificación para un delay largo es estar atrapado en contexto atómico, y eso casi siempre significa que el diseño está mal y ese trabajo debería haberse diferido a un hilo o workqueue donde puedas dormir. Si te descubres escribiendo mdelay con un número grande, la pregunta correcta no es cuántos milisegundos, sino por qué no puedes dormir aquí.
Detente en la simetría que cierra el nivel, porque unifica las cinco lecciones en una sola idea. Todo lo que has estudiado sobre el tiempo se reduce a dos gestos opuestos: marcar el paso del tiempo y esperar a que pase. NO_HZ es la sabiduría de no marcar cuando nadie escucha; los sleeps son la sabiduría de no quemar cuando puedes ceder. Ambos nacen del mismo principio y confluyen en el mismo mecanismo. Fíjate en la profunda unidad: apagar el tick idle es posible porque el tick se volvió un hrtimer oneshot, y dormir con usleep_range es posible porque hay hrtimers que despiertan al durmiente en el instante justo. La misma pieza que desligó el tiempo de jiffies (nivel 45.4) es la que permite tanto silenciar el latido como despertar con precisión. Y aquí está la lección de sistemas que trasciende el tiempo: en un kernel bien diseñado, no hacer nada es una operación activa y deliberada, no una ausencia de código. La CPU ociosa no “simplemente no ejecuta”; el kernel decide silenciar su tick y hundirla en un estado C profundo. El driver que espera no “simplemente aguarda”; elige entre quemar y ceder según el contexto exacto en que se encuentra. Interioriza que la calidad de un sistema de bajo nivel se mide tanto por lo que hace cuando trabaja como por lo que deja de hacer cuando no. El tick que calla, el núcleo que duerme, el hilo que cede: el tiempo en el kernel culmina no en un reloj más rápido, sino en la disciplina de gastarlo solo cuando de verdad hace falta.
- Comprueba si tu kernel tiene
CONFIG_NO_HZ_IDLEyCONFIG_NO_HZ_FULL, y busca en/proc/interruptscómo cambian los contadores de la línea del temporizador local con la máquina ociosa. - Escribe un módulo que en contexto de proceso mida con
ktime_getcuánto duerme realmente unmsleep(1)frente a unusleep_range(1000, 1000), y explica la diferencia. - Razona por qué
udelay(50)es aceptable dentro de un spinlock peromsleep(50)provocaría unscheduling while atomic. - Argumenta, en tres líneas, por qué el rango de
usleep_rangereduce el número de wakeups del sistema frente a un plazo exacto. - Investiga la línea de arranque
nohz_full=y describe qué gana una CPU dedicada a una sola tarea de tiempo real al perder su tick, y qué debe ceder a cambio a las CPUs de housekeeping.