Tracepoints: puntos de traza estáticos en el kernel
Los ganchos con nombre que los desarrolladores del kernel colocan en sitios críticos —planificador, interrupciones, bloque, red— con la macro TRACE_EVENT: cómo se definen, por qué su coste apagado es un nop, dónde viven bajo events, y por qué son estables de facto y por eso la base de perf y eBPF.
El function tracer ve todas las funciones, pero no sabe nada de su significado: para él __schedule es un nombre más. Los tracepoints son lo contrario: puntos de instrumentación que un desarrollador del kernel colocó a mano en un sitio semánticamente cargado —justo cuando el planificador cambia de tarea, cuando una interrupción entra, cuando una petición de bloque se emite— y a los que dotó de campos con nombre y significado. No son un accidente de la compilación: son una interfaz de observación diseñada, estable de facto y tan barata apagada que el kernel de tu portátil lleva miles esperando a que alguien los mire.
- Entender qué es un tracepoint y en qué se diferencia de trazar una función cualquiera.
- Leer la anatomía de una definición
TRACE_EVENT: prototipo, campos y formato de impresión. - Localizar los tracepoints por subsistema bajo
events/y activarlos uno a uno. - Comprender su estabilidad de facto y por qué son el cimiento de
perfy eBPF.
Qué es un tracepoint
Un tracepoint es una llamada a función inline, colocada en el código fuente del kernel, que no hace nada hasta que alguien la activa. En el sitio de la traza, el autor escribe una sola línea:
/* En kernel/sched/core.c, dentro de __schedule(): */
trace_sched_switch(preempt, prev, next, prev_state);
Esa función trace_sched_switch no la escribió nadie a mano: la generó la macro TRACE_EVENT. Y su cuerpo, como vimos en el nivel anterior, está gobernado por una static key: apagado es un nop, sin comparación ni salto. La diferencia con el function tracer es de naturaleza, no de grado. Trazar __schedule te dice que se ejecutó esa función; el tracepoint sched_switch te entrega los datos del evento —qué tarea salía, cuál entraba, con qué prioridad y en qué estado— porque el desarrollador que lo colocó eligió exactamente esos campos por su relevancia. Es la diferencia entre observar el mecanismo y observar el hecho.
Trazar una función
Explota un efecto colateral de la compilación: el gancho __fentry__ está en todas partes. Ves que algo se ejecutó, pero sin campos ni significado, y un refactor puede romper el nombre.
Disparar un tracepoint
Un desarrollador marcó ese punto a propósito y eligió sus campos. Ves el hecho —qué tarea, cuántos bytes— y sobrevive a los refactores porque va atado al significado, no a la forma.
Anatomía de un TRACE_EVENT
La definición vive en una cabecera bajo include/trace/events/. Esta es, condensada, la de sched_switch en include/trace/events/sched.h:
TRACE_EVENT(sched_switch,
TP_PROTO(bool preempt,
struct task_struct *prev,
struct task_struct *next,
unsigned int prev_state),
TP_ARGS(preempt, prev, next, prev_state),
TP_STRUCT__entry(
__array( char, prev_comm, TASK_COMM_LEN )
__field( pid_t, prev_pid )
__field( int, prev_prio )
__field( long, prev_state )
__array( char, next_comm, TASK_COMM_LEN )
__field( pid_t, next_pid )
__field( int, next_prio )
),
TP_fast_assign(
memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
__entry->prev_pid = prev->pid;
__entry->prev_prio = prev->prio;
__entry->prev_state = __trace_sched_switch_state(preempt, prev_state, prev);
memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
__entry->next_pid = next->pid;
__entry->next_prio = next->prio;
),
TP_printk("prev_comm=%s prev_pid=%d prev_prio=%d ==> next_comm=%s next_pid=%d next_prio=%d",
__entry->prev_comm, __entry->prev_pid, __entry->prev_prio,
__entry->next_comm, __entry->next_pid, __entry->next_prio)
);
Cada bloque cumple un papel. TP_PROTO y TP_ARGS declaran la firma de trace_sched_switch. TP_STRUCT__entry define el registro binario que se escribe en el buffer —compacto, de anchura fija—. TP_fast_assign es el código que rellena ese registro copiando del contexto vivo, y corre solo si el tracepoint está activo. Y TP_printk es la plantilla que ftrace usa para renderizar el registro binario como texto legible cuando lo lees. Esa separación entre almacenar en binario y formatear al leer es la que mantiene el coste bajo: en el camino caliente solo se copian enteros, nunca se formatea una cadena.
Dónde viven: sched, irq, block
Cada tracepoint aparece como un directorio bajo events/<subsistema>/<evento>/. Explorar ese árbol es descubrir qué instrumentó el kernel por ti.
cd /sys/kernel/tracing
ls events/ # sched irq block net kmem timer workqueue ...
ls events/sched/ # sched_switch sched_wakeup sched_migrate_task ...
cat events/sched/sched_switch/format
name: sched_switch
ID: 328
format:
field:char prev_comm[16]; offset:8; size:16; signed:0;
field:pid_t prev_pid; offset:24; size:4; signed:1;
field:int prev_prio; offset:28; size:4; signed:1;
...
print fmt: "prev_comm=%s prev_pid=%d ...", REC->prev_comm, REC->prev_pid, ...
El archivo format no es decorativo: es el contrato binario que perf, trace-cmd y eBPF leen para saber en qué offset vive cada campo y así decodificar el registro sin conocer la versión exacta del kernel. Activar un evento es escribir un 1 en su archivo enable; los subsistemas más útiles son sched (cambios de contexto y despertares), irq (irq_handler_entry, softirq_entry), block (block_rq_issue, block_rq_complete) y net.
echo 1 > events/sched/sched_switch/enable
cat trace | head
# bash-1934 [001] d..2. 5123.11: sched_switch: prev_comm=bash prev_pid=1934
# ... ==> next_comm=swapper/1 next_pid=0 next_prio=120
echo 0 > events/sched/sched_switch/enable
Estabilidad y coste
Aquí hay un matiz que separa al aficionado del profesional. Los tracepoints no forman parte del ABI garantizado del kernel como sí lo son las llamadas al sistema: en teoría, un desarrollador podría cambiar los campos de sched_switch entre versiones. En la práctica son estables de facto, porque una industria entera de herramientas —perf, bpftrace, observabilidad de producción— depende de ellos, y romperlos genera una regresión visible. Esa estabilidad pragmática, unida a su coste nulo apagado, es lo que los convierte en el cimiento común: el mismo sched_switch alimenta a ftrace, a perf record -e sched:sched_switch y a un programa eBPF, sin que ninguno sepa cuál de los otros está mirando.
Muchos eventos comparten estructura. El kernel usa DECLARE_EVENT_CLASS para definir una vez los campos y el formato, y luego DEFINE_EVENT para crear varios tracepoints concretos sobre esa clase. Por eso sched_wakeup y sched_wakeup_new comparten formato: son dos instancias de la misma clase de evento, y verlo explica por qué familias enteras de tracepoints tienen los mismos campos.
Interioriza la diferencia ontológica entre trazar una función y disparar un tracepoint, porque reordena cómo lees el kernel entero. Cuando el function tracer engancha __schedule, está explotando un efecto colateral de la compilación: el compilador puso un __fentry__ ahí porque lo pone en todas partes, sin intención semántica. El nombre de la función es lo único que tienes, y mañana un refactor podría partirla en dos o fundirla con otra, y tu traza se rompería sin que nadie lo notara. Un tracepoint es lo contrario: es una afirmación deliberada de un desarrollador que dice aqui ocurre algo que merece la pena observar, y estos son exactamente los datos que importan de ese algo. Por eso sched_switch sobrevive a los refactores de __schedule: no está atado a la forma del código sino al significado del evento. Esa intencionalidad tiene una consecuencia epistemológica enorme. Los tracepoints trazan un mapa de lo que la comunidad del kernel considera los momentos observables del sistema —los puntos donde el estado cambia de un modo que a alguien le importó lo suficiente como para nombrarlo—. Cuando recorres events/ no estás viendo una lista de ganchos: estás leyendo la teoría implícita que los desarrolladores tienen de su propio sistema, qué transiciones consideran fundamentales. Aprender a observar el kernel es, en el fondo, heredar ese mapa de significado y saber qué evento preguntar cuando tienes una hipótesis. El function tracer te dice cómo se mueve la maquinaria; los tracepoints te dicen qué le está pasando al sistema.
- Cuenta cuántos subsistemas hay bajo
events/en tu kernel y elige tres cuyos nombres reconozcas de niveles anteriores. - Abre
events/block/block_rq_issue/formate identifica en qué offset y con qué tamaño se guarda el campobytes. - Localiza en
include/trace/events/sched.hla definición real desched_switchy explica qué haceTP_fast_assignqueTP_printkno. - Activa
sched_wakeupysched_switcha la vez, reproduce actividad y razona qué te dice su intercalado sobre la latencia de planificación. - Argumenta por qué el archivo
formates imprescindible para queperfdecodifique un evento sin haberse compilado contra tu kernel exacto.