jiffies, HZ y el tick: el latido del kernel
El contador global jiffies que avanza en cada tick del temporizador, HZ como la frecuencia de ese latido, el compromiso entre resolución y sobrecarga que fija su valor, la lectura segura de los 64 bits con get_jiffies_64 y la aritmética a prueba de desbordamiento con time_after.
El kernel se pregunta millones de veces por segundo “¿cuánto tiempo ha pasado?”: para expirar un timer, para caducar un socket, para decidir a quién planificar. Necesita una noción de tiempo barata, siempre disponible y que no dependa de leer un reloj de hardware en cada consulta. Esa noción es jiffies, un simple contador que un latido periódico —el tick del temporizador— hace avanzar una unidad cada vez. Todo el tiempo grueso del kernel, desde los timers clásicos hasta la contabilidad de CPU, se apoya en este entero que crece solo.
- Entender el tick del temporizador como el latido periódico que hace avanzar el tiempo del kernel.
- Leer el contador
jiffiesy convertir entre milisegundos y ticks conmsecs_to_jiffies. - Conocer
HZy el compromiso entre resolución y sobrecarga que fija su valor. - Comparar instantes con
time_aftery leer los 64 bits completos conget_jiffies_64.
El tick: el latido que mueve el tiempo
Desde el arranque, el kernel programa una interrupción de temporizador que llega a intervalos regulares. Cada una de esas interrupciones es un tick, y su manejador hace, entre otras cosas, una operación conceptualmente trivial: sumar uno a un contador global. Ese contador es jiffies. No mide segundos ni nanosegundos: mide ticks transcurridos desde el arranque. Si sabes cuántos ticks caben en un segundo —eso es HZ— puedes traducir entre ambos mundos.
El corazón del asunto vive en kernel/time/timekeeping.c, y es de una sencillez casi cómica para lo que sostiene:
/* kernel/time/timekeeping.c */
void do_timer(unsigned long ticks)
{
jiffies_64 += ticks;
calc_global_load();
}
Cada tick, la ruta del temporizador llama a do_timer(1), jiffies_64 avanza una unidad y el mundo del kernel envejece un latido. Pero incrementar el contador es solo el principio: el mismo tick arrastra un cortejo de trabajo periódico a través de update_process_times, la función que convierte el latido en gobierno del sistema.
/* kernel/time/timer.c -- lo que cada tick desencadena, mas alla de jiffies */
void update_process_times(int user_tick)
{
struct task_struct *p = current;
account_process_tick(p, user_tick); /* imputa el tick a user o sys */
run_local_timers(); /* dispara TIMER_SOFTIRQ si toca */
rcu_sched_clock_irq(user_tick); /* avanza los periodos de gracia RCU */
sched_tick(); /* reevalua al planificador */
}
Ahí se ve por qué el tick es el latido y no un mero contador: en cada uno se imputa tiempo de CPU a la tarea que corría, se revisa si algún timer_list ha vencido, se hace avanzar RCU y se le da al planificador la oportunidad de expulsar a la tarea actual. El tiempo del kernel no fluye de forma continua: salta, cuanto en cuanto, y cada salto es también el pulso que mueve la contabilidad, los timers y la planificación.
jiffies: el contador de ticks
jiffies es una variable global volatile unsigned long que cualquier código del kernel puede leer sin coste: es solo un acceso a memoria, no una consulta al hardware. El patrón canónico para “haz algo durante N segundos” es capturar el valor inicial y comparar contra él:
#include <linux/jiffies.h>
unsigned long deadline = jiffies + 2 * HZ; /* dentro de dos segundos */
while (dispositivo_ocupado(dev)) {
if (time_after(jiffies, deadline)) {
pr_warn("timeout esperando al dispositivo\n");
return -ETIMEDOUT;
}
cpu_relax();
}
Nunca deberías escribir la aritmética de tiempo con constantes crudas de ticks, porque HZ cambia según la configuración. Para eso existe una familia de conversores en linux/jiffies.h:
unsigned long t = msecs_to_jiffies(250); /* 250 ms -> ticks */
unsigned long u = usecs_to_jiffies(500); /* 500 us -> ticks */
unsigned int ms = jiffies_to_msecs(t); /* ticks -> ms */
Estas funciones redondean con cuidado y saturan en los extremos, de modo que msecs_to_jiffies de una constante conocida se resuelve incluso en tiempo de compilación. Escribir 2 * HZ para dos segundos es idiomático; escribir 500 esperando medio segundo es un bug latente que estalla en cuanto alguien recompila con otro HZ.
HZ: cuántos latidos por segundo
HZ es la frecuencia del tick, fijada en la configuración con CONFIG_HZ. Los valores habituales en x86 son 100, 250, 300 y 1000; muchos servidores y portátiles de 2026 usan 250 o 1000. La elección es un compromiso puro:
HZalto (1000) da un tiempo de reacción más fino: los timers de baja resolución tienen granularidad de un milisegundo y el planificador reevalúa más a menudo. El precio es más interrupciones por segundo, más consumo y más contaminación de caché.HZbajo (100) reduce la sobrecarga del tick y mejora el rendimiento en cargas de cómputo puro, a costa de una granularidad temporal más gruesa.
# ver el HZ con el que se compiló este kernel
zcat /proc/config.gz | grep CONFIG_HZ=
# CONFIG_HZ=1000
Conviene interiorizar que este debate ha perdido buena parte de su filo. Los hrtimers (nivel 45.4) proporcionan precisión de nanosegundos con independencia de HZ, y NO_HZ (nivel 45.5) elimina el tick cuando no hace falta. Hoy HZ gobierna la granularidad de los timers clásicos y el ritmo máximo de contabilidad, no la precisión temporal del sistema entero.
El desbordamiento y los 64 bits
Aquí acecha la trampa. En arquitecturas de 32 bits jiffies es un unsigned long de 32 bits, y con HZ de 1000 se desborda en apenas 49,7 días. Por eso jamás debes comparar tiempos con < o > directos: usa las macros que manejan el envoltorio con aritmética con signo:
/* include/linux/jiffies.h -- comparaciones a prueba de wraparound */
#define time_after(a, b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)((b) - (a)) < 0))
#define time_before(a, b) time_after(b, a)
Al restar dos instantes e interpretarlos como long con signo, la diferencia sigue siendo correcta aunque uno haya cruzado el límite del contador. Para forzar la aparición temprana de estos bugs, el kernel arranca jiffies deliberadamente cerca del desbordamiento, en INITIAL_JIFFIES = (unsigned long)(unsigned int)(-300 * HZ): si tu código sobrevive a los primeros cinco minutos, probablemente maneja bien el envoltorio.
Cuando de verdad necesitas los 64 bits completos —para plazos larguísimos o estadísticas acumuladas— existe jiffies_64, del que jiffies es el alias de 32 bits bajos. Leerlo en una máquina de 32 bits no es atómico, así que hay un ayudante:
u64 base = get_jiffies_64(); /* atómico en 32 bits; trivial en 64 bits */
En arquitecturas de 64 bits jiffies ya es de 64 bits y get_jiffies_64 se compila a una simple lectura: el desbordamiento tardaría cientos de millones de años. Aun así, escribir con las macros y con get_jiffies_64 es la disciplina portable.
sequenceDiagram participant HW as Temporizador participant IRQ as Manejador de tick participant J as jiffies_64 participant K as Resto del kernel HW->>IRQ: interrupcion periodica cada 1 sobre HZ IRQ->>J: do_timer suma un tick IRQ->>K: expira timers y contabiliza CPU K-->>IRQ: fin del tick
No confundas jiffies con la hora del sistema. jiffies cuenta ticks desde el arranque y sirve para intervalos relativos. La hora de pared —lo que devuelve clock_gettime— la mantiene el subsistema de timekeeping sobre una clocksource monótona (nivel 45.2), no sobre este contador. Usar jiffies para timeouts está bien; usarlo para fechar eventos con precisión no.
Detente en la naturaleza de lo que acabas de ver, porque reordena la intuición de todo el nivel. Fuera del kernel pensamos el tiempo como un continuo que fluye; dentro, el tiempo grueso es un entero que salta. jiffies no aproxima un reloj continuo: es la unidad, y entre dos ticks no existe ningún instante que el kernel de baja resolución pueda nombrar. De esa única decisión —cuantizar el tiempo en latidos— se deduce en cascada casi todo lo demás. Se deduce por qué un msleep(1) puede dormir bastante más de un milisegundo: no hay dónde despertarte antes del siguiente tick. Se deduce por qué HZ es un compromiso y no un lujo: cada latido cuesta una interrupción, y multiplicar latidos es multiplicar sobrecarga. Se deduce por qué hicieron falta los hrtimers: para nombrar instantes entre los ticks hubo que abandonar el contador y volver al hardware. Y se deduce por qué NO_HZ es posible: si el tiempo solo avanza cuando lo necesitas, un latido que nadie escucha es energía desperdiciada que puede callarse. Interioriza jiffies no como “una forma barata de medir el tiempo”, sino como la prueba de que el kernel eligió discretizar el tiempo para poder razonar sobre él con un entero. Todo el resto del nivel es la historia de cómo el kernel escapa de esa rejilla cuando la precisión lo exige, sin renunciar a su comodidad cuando no.
- Escribe un módulo que en la carga guarde
jiffiesyget_jiffies_64, duerma dos segundos conmsleep(2000)y al terminar imprima cuántos ticks transcurrieron. - Compara ese número con
2 * HZy explica la pequeña diferencia por el redondeo demsecs_to_jiffies. - Consulta
CONFIG_HZde tu kernel y razona qué granularidad tendría un timer clásico con ese valor. - Argumenta, en tres líneas, por qué
if (jiffies > deadline)es incorrecto ytime_after(jiffies, deadline)no lo es. - Investiga en
include/linux/jiffies.hqué haceINITIAL_JIFFIESy por qué arrancar cerca del desbordamiento es una decisión defensiva.