wandres.dev
KPROBES Y PERF · sondas dinámicas, perfilado

perf: el perfilador todo-en-uno

El binario perf como navaja suiza de la observabilidad de Linux: perf stat para contar eventos e interpretar el IPC, perf record y perf report para descubrir dónde se va el tiempo, perf top para el perfil en vivo, y la llamada perf_event_open que unifica por debajo la PMU, los tracepoints y los kprobes en un solo objeto: el evento.

⏱ 17 min

Hay quien colecciona una herramienta para cada pregunta: una para contar fallos de caché, otra para ver la pila caliente, otra para rastrear llamadas al sistema. Linux resolvió esa dispersión con un solo binario que habla a la vez con la unidad de monitorización de rendimiento del procesador y con el subsistema de rastreo del kernel. perf cuenta, muestrea, rastrea y anota código máquina; es a la observabilidad lo que el bisturí a la cirugía. Y bajo sus decenas de subcomandos late una única abstracción —el evento— y una única llamada al sistema que la materializa.

🎯 Al terminar esta lección sabrás
  • Contar eventos de una carga con perf stat e interpretar el IPC.
  • Muestrear con perf record y navegar el perfil con perf report.
  • Ver el consumo en vivo por función con perf top.
  • Entender que por debajo todo es la llamada perf_event_open.

perf stat: el recuento exacto

perf stat no muestrea: cuenta. Envuelve la ejecución de un programa y, al terminar, te entrega el total exacto de cada evento durante esa ventana. Es el primer diagnóstico que debes correr, porque una sola línea —el IPC, instrucciones por ciclo— ya te dice si el problema es que la CPU trabaja mucho o que se pasa el día esperando memoria.

$ perf stat ./mi_programa

 Performance counter stats for './mi_programa':

          1234,56 msec task-clock                #    0,998 CPUs utilized
                12      context-switches          #    9,720 /sec
                 3      cpu-migrations            #    2,430 /sec
               142      page-faults               #  115,02 /sec
     4.512.334.900      cycles                    #    3,655 GHz
     8.901.223.145      instructions              #    1,97  insn per cycle
     1.678.442.001      branches                  #    1,359 G/sec
        12.334.567      branch-misses             #    0,73% of all branches

       1,236900000 seconds time elapsed

Lee ese 1,97 insn per cycle: cada ciclo retira casi dos instrucciones, un IPC sano para código con buena localidad. Si ese número cayera a 0,4, la CPU estaría parada tres de cada cuatro ciclos esperando —típicamente memoria— y sabrías dónde mirar. Puedes elegir eventos con -e, repetir la medición para obtener desviación con -r, o contar toda la máquina con -a.

# cuenta eventos concretos, repite 5 veces y da media con desviacion
perf stat -e cycles,instructions,cache-misses -r 5 ./mi_programa
# cuenta en todo el sistema durante 10 segundos
perf stat -a sleep 10

perf record y perf report: dónde se va el tiempo

Contar dice cuánto; muestrear dice dónde. perf record interrumpe periódicamente la CPU, anota el puntero de instrucción y, con -g, la pila de llamadas completa, y lo vuelca todo a un fichero perf.data. Luego perf report te abre ese perfil en una interfaz navegable ordenada por peso.

# muestrea a 99 Hz, con pila de llamadas, toda la maquina, 10 s
perf record -F 99 -a -g -- sleep 10
# navega el perfil interactivo, o vuelcalo a texto
perf report --stdio
# Overhead  Command  Shared Object      Symbol
# ........  .......  .................  ...............................
#
    38,21%  mi_prog  mi_prog            [.] hash_bucket
    17,04%  mi_prog  [kernel.kallsyms]  [k] copy_user_enhanced_fast_string
     9,88%  mi_prog  libc.so.6          [.] __memmove_avx_unaligned
     4,10%  mi_prog  mi_prog            [.] parse_line

La columna Overhead es el porcentaje de muestras que cayeron en cada símbolo, es decir, la fracción de tiempo de CPU que se gasta allí. La marca [.] es espacio de usuario y [k] es kernel: aquí el 17% del tiempo se va copiando datos entre usuario y núcleo, una pista de que quizá se lee de más. Con la interfaz interactiva puedes entrar en hash_bucket y perf annotate te muestra el ensamblador con el porcentaje pegado a cada instrucción, hasta señalar la línea exacta que arde.

perf top: el perfil en vivo

Cuando no quieres capturar y analizar después, sino ver qué quema la CPU ahora mismo, perf top es el top de las funciones: una lista que se refresca en tiempo real con los símbolos más calientes del sistema, ordenados por porcentaje de muestras.

# perfil global en vivo; -p limita a un proceso
sudo perf top
sudo perf top -p $(pgrep mi_prog) -g

Es la herramienta de la sala de máquinas: un servidor va lento, lanzas perf top, y en dos segundos ves si el 60% se lo come una función de compresión, el spinlock de un cerrojo contendido o el manejador de una interrupción desbocada. No sustituye a perf record para el análisis profundo, pero es insuperable para la primera hipótesis.

Todo es perf_event_open

Cada subcomando anterior es azúcar sobre una única llamada al sistema: perf_event_open. Le pasas un struct perf_event_attr que describe el evento, y recibes un descriptor de fichero. Contar es leer ese descriptor; muestrear es mapear un búfer en anillo donde el núcleo deposita las muestras. Este es el perf stat mínimo, escrito a mano:

#include <linux/perf_event.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <unistd.h>

struct perf_event_attr attr = {
	.type           = PERF_TYPE_HARDWARE,
	.config         = PERF_COUNT_HW_INSTRUCTIONS,
	.size           = sizeof(attr),
	.disabled       = 1,
	.exclude_kernel = 1,     /* solo cuenta en espacio de usuario */
	.exclude_hv     = 1,
};

int fd = syscall(SYS_perf_event_open, &attr, 0, -1, -1, 0);
ioctl(fd, PERF_EVENT_IOC_RESET, 0);
ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);

/* ... aqui corre la carga de trabajo a medir ... */

ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
long long instrucciones;
read(fd, &instrucciones, sizeof(instrucciones));

Esa .type y .config seleccionan el evento; para un tracepoint pondrías PERF_TYPE_TRACEPOINT y el identificador del punto; para un kprobe, el tipo dinámico que registró tracefs. El primer argumento es el atributo, luego el pid, la CPU y un descriptor de grupo. Para muestrear en vez de contar, rellenas .sample_freq y .sample_type, y en lugar de read mapeas con mmap un búfer en anillo del que drenas las muestras. Todo perf no es más que esta llamada, repetida y orquestada.

mindmap
root((perf))
  contar
    perf stat
    IPC y eventos
  muestrear
    perf record
    perf report
    perf annotate
  en vivo
    perf top
  por debajo
    perf_event_open
    buffer en anillo
ℹ️
perf_event_paranoid: el guardián del acceso

No todo usuario puede leer la PMU ni muestrear el kernel. El sysctl kernel.perf_event_paranoid gobierna el permiso: con 2 solo mides tus propios procesos en espacio de usuario, con 1 añades datos del kernel, con 0 los contadores por CPU, y con -1 todo. Si perf record -a se queja de permisos, ese sysctl o la capacidad CAP_PERFMON es la puerta que hay que abrir. Además, perf vive en tools/perf del árbol y conviene que su versión case con la del kernel en ejecución.

El evento como átomo universal de la observabilidad

Reflexiona sobre la unificación que acabas de presenciar, porque es una de las abstracciones más logradas del kernel. Antes de perf_event, cada fuente de información sobre el rendimiento vivía en su propio mundo con su propia interfaz: los contadores de hardware se leían por registros del modelo específico del procesador, el rastreo estático tenía su tubería, los breakpoints la suya. perf_event disolvió esa balcanización bajo un solo concepto: el evento. Un ciclo de reloj de la CPU, un fallo de caché de última línea, una conmutación de proceso, el retorno de vfs_read interceptado por un kprobe —fenómenos de naturalezas radicalmente distintas, unos medidos por silicio dedicado y otros por software del núcleo— se modelan todos como lo mismo: algo que ocurre, que se puede contar y sobre lo que se puede muestrear. Y una vez que son la misma clase de cosa, una sola llamada al sistema los abre, un solo descriptor de fichero los representa, un solo búfer en anillo transporta sus muestras, y una sola herramienta los combina. Ahí está la lección de diseño que trasciende a perf: el poder no viene de acumular funciones, sino de encontrar la abstracción correcta bajo la cual fenómenos aparentemente inconexos resultan ser instancias de una misma idea. Cuando puedes decir “un fallo de caché y una llamada al sistema son ambos eventos”, has ganado la capacidad de correlacionarlos, de contarlos juntos, de preguntarte qué pila de código estaba corriendo cuando el silicio falló una lectura de memoria. La navaja suiza no es potente por tener muchas hojas; es potente porque todas se pliegan sobre el mismo eje.

⚔️ Recorre las cuatro caras de perf
  1. Corre perf stat sobre un programa tuyo y sobre openssl speed, compara sus IPC y explica qué te dice la diferencia sobre cuán limitada por memoria está cada carga.
  2. Captura un perfil con perf record -F 99 -g de una carga real y usa perf report para bajar hasta la función más caliente; luego perf annotate sobre ella y localiza la instrucción exacta que concentra las muestras.
  3. Lanza perf top mientras generas carga con stress-ng y describe qué símbolos del kernel emergen y por qué.
  4. Escribe el programa en C con perf_event_open que cuenta instrucciones de un bucle, y verifica que el total casa con el que reporta perf stat -e instructions sobre el mismo bucle.
  5. Baja kernel.perf_event_paranoid y documenta qué mediciones nuevas se te habilitan y qué riesgo de seguridad justifica que por defecto esté restringido.