SDD 25 §9.5: el iterador BPF, medido — y H7 pasa a tener precio

Cae el quinto de los seis experimentos nombrados. Un `iter/task` que emite
registros binarios saca la foto de procesos cerca de 7x mas barato que el
barrido de /proc (6,9-8,0x en cuatro corridas de 33 rondas y las dos
direcciones), pero el numero que decide es otro: cuesta 1,75x el piso de
`readdir` contra 12,1x de /proc. Leer los campos deja de tener precio; lo que
se paga es enumerar.

Dos barreras comprobadas, no supuestas: el programa se ata por id de tipo BTF
del vmlinux (attach_btf_id=80137, prog_type=26) — sin DEBUG_INFO_BTF no hay a
que atarse, no hay version de esto que esquive H7 —, y hace falta
CAP_BPF+CAP_PERFMON, o sea que el consumidor es el supervisor, no la Card.

Y los dos caminos NO dicen lo mismo: 24 de 245 comm difieren, todos kworkers,
porque /proc SINTETIZA el nombre pegandole la workqueue. Un reemplazo vendido
como «la misma informacion mas rapido» tenia una diferencia de contenido, y
solo aparecio cotejando registro contra registro.

H7 deja de decir «pahole en el lab y un re-hasheo». Son cuatro cosas:
DEBUG_INFO_NONE=y en los .config sellados (BTF exige DWARF antes), el kernel
no declara la receta dwarves aunque este sellada, ese pahole es DINAMICO pese
a link="static" (INTERP + 5 NEEDED) y corre dentro del sandbox del build, y
BTF mete la version de pahole en la bit-repro con el lab fuera de hash_inputs.
Ya hay dos consumidores nombrados, no cero: este iterador y sched-ext.

Lo que sigue SIN medir es el precio en bytes del bzImage, que es como se pago
H1 — se mide construyendo, y esa es la primera tarea el dia que se decida.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
This commit is contained in:
Sergio
2026-08-31 14:08:40 +00:00
co-authored by Claude Opus 5
parent 8fef37dd19
commit 09168bf1e3
4 changed files with 511 additions and 9 deletions
+103 -9
View File
@@ -36,6 +36,10 @@ El espejo de este documento para el lado tawasuyu es
cada 100 ms vio **cero**; el proc connector, los 200. Y **el «copiar en O(1)» existe pero no en
ext4**: en xfs/btrfs un reflink de 512 MiB tarda 0,020 ms contra 512,7 ms de `read+write`
(§9.2).
5. **Cuando `/proc` sí se sondea entero, hay algo cerca de 7× más barato** (§9.5, medido
2026-08-31): un iterador BPF `task` saca la misma foto **al precio de listar el directorio**
—1,75× el piso de `readdir` contra 12,1× de `/proc`—. Su portón es `DEBUG_INFO_BTF`, que sigue
apagado, y H7 ahora dice **cuánto cuesta encenderlo** en vez de sólo que está apagado.
## 1. Método y condiciones — leer antes de citar un número
@@ -101,7 +105,9 @@ Leer el CPU propio por `/proc` cuesta **11× más** que `getrusage`. Y la mitad
abrir la ruta, no el texto (1 271 vs 2 692 con el fd cacheado).
**Barrido completo** (291 procesos vivos): `readdir` solo **106 µs**; `readdir` + `stat` de
cada pid **991 µs**, o sea **3,4 µs por proceso**.
cada pid **991 µs**, o sea **3,4 µs por proceso**. Ese barrido tiene alternativa medida: un
iterador BPF hace el mismo trabajo por **0,80 µs por proceso** contra 5,54 µs del mismo barrido
medido en esa corrida (§9.5), pero exige BTF (H7).
### T4 · Contabilidad por unidad: cgroup v2 es O(1) donde `/proc` es O(procesos)
@@ -466,8 +472,39 @@ Su precio, concreto:
reflink (§9) alimenta SDD 18 (wawafs), donde el store como FS vivo cambiaría el cociente.
- **H6 · No `madvise(MADV_HUGEPAGE)` en el camino de build** mientras `defrag=madvise` y la
granja corra apretada de RAM (T13).
- **H7 · `DEBUG_INFO_BTF` sigue apagado** hasta que haya un consumidor real de BPF; cuando lo
haya, la deuda es pahole en el lab y un re-hasheo, no un rediseño.
- **H7 · `DEBUG_INFO_BTF` sigue apagado — pero ya no por falta de número.** §9.5 midió qué
compra: un iterador `task` saca la foto de procesos **cerca de 7× más barato** que el barrido de
`/proc` (6,9-8,0× en cuatro corridas y dos órdenes), y —lo que de verdad decide— **al precio de
listar el directorio**: 1,75× el piso de `readdir`, contra 12,1× de `/proc`. Leer los campos deja de costar; sólo se paga enumerar.
El precio, en cambio, es más grande de lo que decía esta línea («pahole en el lab y un
re-hasheo»). Son **cuatro** cosas, y ninguna es un rediseño:
1. **Los kernels no compilan con información de depuración**: `CONFIG_DEBUG_INFO_NONE=y` en los
`.config` sellados. BTF exige DWARF, así que primero hay que encender `DEBUG_INFO_DWARF5`.
Eso encarece el build, **no el `bzImage`**: el DWARF vive en `vmlinux`, que la receta no
empaqueta — lo único que entra a la imagen es la sección `.BTF`.
2. **pahole está en el catálogo, pero el kernel no lo declara.** `recipes/dwarves.toml` (1.30)
se selló al cerrarse el frente libdw/musl, y su propia cabecera lo dice: «El kernel NO la
declara todavía».
3. **Ese pahole es DINÁMICO pese a `link = "static"`.** El binario del artefacto vigente
(`8057bcd…`) trae `INTERP` y cinco `NEEDED`: `libelf`, `libz`, `liblzma`, `libbz2`, `libc`.
Corre **dentro del sandbox del build del kernel**, así que esas librerías tienen que estar
ahí o no arranca — y el fallo saldría como «pahole no está» estando.
4. **BTF mete a pahole en el juego de la bit-repro.** El `.BTF` lo emite pahole ⇒ su versión
pasa a ser parte de la identidad del artefacto, y el lab **no está en `hash_inputs`**: dos
labs con pahole distinto sellarían bytes distintos en la misma dirección sin que el store lo
note. Es el agujero conocido, con una herramienta más adentro.
**Y ya hay dos consumidores nombrados, no cero.** El iterador de §9.5, que H8 aceptaría porque
trae ruta y símbolo (`bpf_iter_create`, `attach_btf_id` resuelto contra el BTF del vmlinux); y
**sched-ext**, que es por lo que existe la receta de dwarves — `SCHED_CLASS_EXT` depende de
`DEBUG_INFO_BTF`. El primero está medido; el segundo todavía no tiene su símbolo.
Lo que **no** está medido es el precio en bytes del `bzImage`, que es como se pagó H1. No se
puede estimar del kernel de Artix: sus 6,4 MB de BTF son de un kernel modular enorme y no dicen
nada de uno monolítico y pelado. **Se mide construyendo, y esa medición es el primer paso el día
que se decida encenderlo** — no antes, porque enciende el re-hasheo de los cuatro kernels.
- **H8 · El contrato de capacidades es parte del release del kernel** (§4.ter). Toda receta de
kernel nueva declara su `[[target]]` con perfil; un kernel sin perfil sale **SIN COMPROBAR**, no
aprobado. Y toda capacidad nueva entra con consumidor —ruta y símbolo— o no entra: «estaría bueno
@@ -498,10 +535,10 @@ Detalle en el handoff espejo. En una línea cada una:
la lista de símbolos garantizados (verificable con `hammer kernel probe`); tawasuyu elige
backend en runtime.
## 9. Los experimentos nombrados — cuatro medidos, dos abiertos
## 9. Los experimentos nombrados — cinco medidos, uno abierto
La primera corrida los dejó nombrados y sin medir, todos por privilegios o por herramienta
ausente. **Cuatro cayeron el mismo día, con root.** Condiciones distintas a la corrida de arriba
ausente. **Cuatro cayeron el mismo día, con root**, y el quinto —el iterador BPF— el 2026-08-31. Condiciones distintas a la corrida de arriba
(load ~1,3 sobre 4 vCPU, 4,3 GiB libres) ⇒ los números de acá **no se cruzan** con los de §2.
### 9.1 · proc connector y `taskstats` — MEDIDO (`bench_connector.c`, `connector.txt`)
@@ -588,11 +625,60 @@ Dos lecturas:
`strace -c` le atribuye es espera **bloqueada**, no CPU — y por eso `strace -c` no sirve para
comparar compiladores, sólo para contar llamadas.
### 9.5 · Lo que sigue abierto
### 9.5 · Iterador BPF contra `/proc` — MEDIDO (`bench_bpf_iter.c` + `bpf_iter_task.bpf.c` → `bpf-iter.txt`)
1. **Iterador BPF contra `/proc`** (T3): exige BTF y privilegios. Va detrás de H7
`DEBUG_INFO_BTF` está apagado en las cuatro recetas y su deuda (pahole) ya está medio pagada.
2. **Un Intel con PTI**: todos los pisos de syscall de acá son de un AMD sin PTI. En el laptop
Medido el **2026-08-31**, sobre el kernel de desarrollo (Artix 7.1.4, que sí trae BTF)
**no sobre un kernel de hammer, que es justamente lo que está en discusión**. Tercera corrida,
condiciones propias: **sus números no se cruzan con los de §2 ni con los de §9.1-9.4.**
Los dos caminos hacen **el mismo trabajo**: sacar `{pid, ppid, estado, utime, stime, comm}` de
todos los procesos vivos, que es la foto que sandokan necesita de la máquina. Uno lo hace con
`readdir` + `open`+`read`+parse de `/proc/<pid>/stat`; el otro con un `iter/task` que escribe
**registros binarios de tamaño fijo** con `bpf_seq_write`, leídos de un solo fd.
| 245 procesos vivos, 33 rondas, mínimo | total | por proceso | × sobre el piso |
|---|---|---|---|
| A) `/proc` readdir+`stat`+parse | 1 356,1 µs | 5,54 µs | 12,1× |
| B) iterador BPF (binario) | **196,1 µs** | **0,80 µs** | **1,75×** |
| C) piso: `readdir(/proc)` a secas | 112,1 µs | 0,46 µs | 1× |
**6,9× a favor del iterador** — pero el número que decide no es ése: **el iterador cuesta casi lo
mismo que listar el directorio y no abrir nada** (1,75× el piso, contra 12,1× de `/proc`). Leer
los campos deja de tener precio; lo que se paga es enumerar.
Siete corridas, dos órdenes (A→B y B→A, para descartar que el primero de cada ronda pague el
enfriamiento de las dentries de `/proc`): **6,9-8,0× en las cuatro de 33 rondas**, en las dos
direcciones. La de 15 rondas dio 4,50× y es de la que hay que desconfiar, no de la mayoría: con
pocas rondas el mínimo no converge y la máquina estaba cargada.
**Amortización**: cargar y verificar el programa cuesta **13-17 ms una vez**; cada barrido ahorra
más de 1 ms ⇒ **se paga solo en 12-40 barridos** según la corrida, 15 en la citada. Para un
sandokan que mire la máquina una vez por segundo, medio minuto.
**Lo que la ganancia cuesta, comprobado y no supuesto** — son dos barreras, no una:
1. **BTF es un portón, no una optimización.** El programa cargado informa
`prog_type=26 (BPF_PROG_TYPE_TRACING)`, `attach_btf_id=80137`, `attach_btf_obj_id=1`: el
iterador **se ata por id de tipo BTF del vmlinux**. Un kernel sin `DEBUG_INFO_BTF` no publica
`/sys/kernel/btf/vmlinux` y no hay a qué atarse. **No hay versión de esto que esquive H7.**
2. **Privilegio**: `CAP_BPF`+`CAP_PERFMON` o root. Sin él, `bpf_object__load` sale
`Operation not permitted`. Una Card no lo tiene ni debe tenerlo ⇒ **el consumidor es el
supervisor, no la Card** — que es donde vive sandokan, así que encaja.
**Y los dos caminos NO dicen exactamente lo mismo.** De 245 procesos, **24 `comm` difieren**, y
son **todos kworkers**: `/proc` **sintetiza** el nombre pegándole la workqueue que el hilo está
sirviendo (`kworker/0:0H-kblockd`), mientras `task->comm` trae el nombre pelado
(`kworker/0:0H`). Ningún proceso de usuario difiere. Para las Cards da igual; para cualquiera que
muestre nombres de kworker, no. **Un reemplazo que se vende como "la misma información más
rápido" tenía una diferencia de contenido, y sólo apareció cotejando registro contra registro** —
no medir es lo que la habría dejado pasar.
**Nota de método**: el bench aborta con exit 2 si el iterador devuelve **cero registros**. Un
barrido vacío es infinitamente rápido y no mide nada: es CLAUDE.md §3 aplicado a un benchmark.
### 9.6 · Lo que sigue abierto
1. **Un Intel con PTI**: todos los pisos de syscall de acá son de un AMD sin PTI. En el laptop
(TigerLake) las mismas medidas darían peor, y ahí T3/T6 duelen más.
## 10. Evidencia
@@ -618,6 +704,14 @@ Se compilan igual que los otros (`bench_sqpoll` pide `-luring`) y los tres prime
`hammer kernel contract --list` (la vista). Los ficheros de esta carpeta no cambian; el contrato sí
—cada vez que un consumidor nuevo usa el kernel por nombre.
**Tercera corrida (§9.5, iterador BPF, 2026-08-31)**: `bpf_iter_task.bpf.c` (el programa BPF) +
`bench_bpf_iter.c` (el de usuario) → `bpf-iter.txt`. Se compilan con
`clang -O2 -g -target bpf -D__TARGET_ARCH_x86 -c bpf_iter_task.bpf.c -o bpf_iter_task.bpf.o` y
`cc -O2 -o bench_bpf_iter bench_bpf_iter.c -lbpf`; se corre `./bench_bpf_iter [rondas] [orden]`
como root, con el `.bpf.o` al lado. **No usa `vmlinux.h`** —haría falta bpftool, que no está en la
máquina ni en el store— sino los tipos mínimos declarados a mano con `preserve_access_index`, que
es lo que deja a CO-RE reubicar los offsets contra el BTF del kernel en carga.
**El pago de H1** (2026-08-30): `contrato-h1-pagado.txt` — el barrido entero con los cuatro kernels
reconstruidos (5 de 5 vigentes en verde, 10 superados listados) y el `diff-back` del plan de gioser
contra su `.config` nuevo. Es la única salida de esta carpeta que sí cambia si se vuelve a correr:
@@ -0,0 +1,256 @@
// SDD 25 §9.5.1 — Iterador BPF contra el barrido de /proc.
//
// Mide el MISMO trabajo por los dos caminos: sacar {pid, ppid, estado, utime,
// stime, comm} de todos los procesos vivos, que es lo que sandokan necesita para
// una foto de la máquina.
//
// A) /proc completo : readdir(/proc) + open+read+parse de /proc/<pid>/stat
// B) iterador BPF : bpf_iter_create + read() de registros BINARIOS de tamaño fijo
// C) piso : readdir(/proc) a secas, sin abrir nada
//
// Compilar: cc -O2 -o bench_bpf_iter bench_bpf_iter.c -lbpf
// Correr: ./bench_bpf_iter [rondas] (pide root o CAP_BPF+CAP_PERFMON)
// Necesita: bpf_iter_task.bpf.o al lado, y BTF del kernel en /sys/kernel/btf/vmlinux.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <time.h>
#include <errno.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#define MAX_RONDAS 33
struct registro {
int pid;
int ppid;
unsigned int estado;
unsigned int _pad;
unsigned long long utime;
unsigned long long stime;
char comm[16];
};
static long long ahora_ns(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (long long)ts.tv_sec * 1000000000LL + ts.tv_nsec;
}
static int cmp_ll(const void *a, const void *b)
{
long long x = *(const long long *)a, y = *(const long long *)b;
return (x > y) - (x < y);
}
// ---------------------------------------------------------------- A) /proc
static int barrido_proc(struct registro *out, int max)
{
DIR *d = opendir("/proc");
struct dirent *e;
char ruta[64], buf[2048];
int n = 0;
if (!d)
return -1;
while ((e = readdir(d)) && n < max) {
if (e->d_name[0] < '0' || e->d_name[0] > '9')
continue;
snprintf(ruta, sizeof ruta, "/proc/%s/stat", e->d_name);
int fd = open(ruta, O_RDONLY);
if (fd < 0)
continue; // murió entre el readdir y el open
ssize_t k = read(fd, buf, sizeof buf - 1);
close(fd);
if (k <= 0)
continue;
buf[k] = 0;
// comm puede traer espacios y paréntesis ⇒ cortar por el ÚLTIMO ')'.
char *cierre = strrchr(buf, ')');
char *abre = strchr(buf, '(');
if (!cierre || !abre)
continue;
struct registro *r = &out[n];
r->pid = atoi(buf);
size_t largo = (size_t)(cierre - abre - 1);
if (largo > 15)
largo = 15;
memcpy(r->comm, abre + 1, largo);
r->comm[largo] = 0;
char estado;
int ppid, pgrp, sid, tty, tpgid;
unsigned int flags;
unsigned long minflt, cminflt, majflt, cmajflt, utime, stime;
if (sscanf(cierre + 2, "%c %d %d %d %d %d %u %lu %lu %lu %lu %lu %lu",
&estado, &ppid, &pgrp, &sid, &tty, &tpgid, &flags,
&minflt, &cminflt, &majflt, &cmajflt, &utime, &stime) != 13)
continue;
r->ppid = ppid;
r->estado = (unsigned char)estado;
r->utime = utime;
r->stime = stime;
n++;
}
closedir(d);
return n;
}
// -------------------------------------------------------------- C) el piso
static int solo_readdir(void)
{
DIR *d = opendir("/proc");
struct dirent *e;
int n = 0;
if (!d)
return -1;
while ((e = readdir(d)))
if (e->d_name[0] >= '0' && e->d_name[0] <= '9')
n++;
closedir(d);
return n;
}
// ------------------------------------------------------------------ B) BPF
static int barrido_bpf(int link_fd, struct registro *out, int max)
{
char buf[65536];
int n = 0, resto = 0;
int fd = bpf_iter_create(link_fd);
if (fd < 0)
return -1;
for (;;) {
ssize_t k = read(fd, buf + resto, sizeof buf - resto);
if (k < 0) {
if (errno == EAGAIN)
continue;
close(fd);
return -1;
}
if (k == 0)
break;
int total = resto + (int)k, i = 0;
while (total - i >= (int)sizeof(struct registro) && n < max) {
memcpy(&out[n++], buf + i, sizeof(struct registro));
i += sizeof(struct registro);
}
resto = total - i;
if (resto > 0)
memmove(buf, buf + i, resto);
}
close(fd);
return n;
}
static void reporte(const char *nombre, long long *v, int r, int procesos)
{
qsort(v, r, sizeof *v, cmp_ll);
double min = v[0] / 1000.0, med = v[r / 2] / 1000.0;
printf(" %-34s min %9.1f us mediana %9.1f us %6.2f us/proceso (min)\n",
nombre, min, med, procesos ? v[0] / 1000.0 / procesos : 0.0);
}
int main(int argc, char **argv)
{
int rondas = argc > 1 ? atoi(argv[1]) : 9;
// 1 = medir B antes que A. Control de sesgo de orden: el primero de cada
// ronda paga el enfriamiento de las dentries de /proc, y hay que ver cuánto.
int invertido = argc > 2 ? atoi(argv[2]) : 0;
if (rondas < 3 || rondas > MAX_RONDAS)
rondas = 9;
static struct registro a[16384], b[16384];
long long t_proc[MAX_RONDAS], t_bpf[MAX_RONDAS], t_dir[MAX_RONDAS];
long long t0;
libbpf_set_print(NULL);
long long t_carga0 = ahora_ns();
struct bpf_object *obj = bpf_object__open_file("bpf_iter_task.bpf.o", NULL);
if (!obj) {
fprintf(stderr, "open_file: %s\n", strerror(errno));
return 1;
}
if (bpf_object__load(obj)) {
fprintf(stderr, "load (¿root? ¿BTF?): %s\n", strerror(errno));
return 1;
}
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "barrido");
if (!prog) {
fprintf(stderr, "no está el programa 'barrido'\n");
return 1;
}
struct bpf_link *link = bpf_program__attach_iter(prog, NULL);
if (!link) {
fprintf(stderr, "attach_iter: %s\n", strerror(errno));
return 1;
}
int link_fd = bpf_link__fd(link);
long long carga = ahora_ns() - t_carga0;
int n_proc = 0, n_bpf = 0, n_dir = 0;
for (int i = 0; i < rondas; i++) {
t0 = ahora_ns(); n_dir = solo_readdir(); t_dir[i] = ahora_ns() - t0;
if (invertido) {
t0 = ahora_ns(); n_bpf = barrido_bpf(link_fd, b, 16384); t_bpf[i] = ahora_ns() - t0;
t0 = ahora_ns(); n_proc = barrido_proc(a, 16384); t_proc[i] = ahora_ns() - t0;
} else {
t0 = ahora_ns(); n_proc = barrido_proc(a, 16384); t_proc[i] = ahora_ns() - t0;
t0 = ahora_ns(); n_bpf = barrido_bpf(link_fd, b, 16384); t_bpf[i] = ahora_ns() - t0;
}
if (n_bpf < 0) {
fprintf(stderr, "bpf_iter_create/read falló: %s\n", strerror(errno));
return 1;
}
}
printf("procesos vistos: /proc %d · BPF %d · readdir %d (%d rondas, orden %s)\n",
n_proc, n_bpf, n_dir, rondas, invertido ? "B->A" : "A->B");
if (n_bpf == 0) {
fprintf(stderr, "\nEL ITERADOR DEVOLVIÓ CERO REGISTROS: eso no es 'rápido', "
"es que no midió nada. Abortando.\n");
return 2;
}
printf("carga+verificación+attach del programa BPF (una vez): %.1f ms\n\n", carga / 1e6);
reporte("A) /proc readdir+stat+parse", t_proc, rondas, n_proc);
reporte("B) iterador BPF (binario)", t_bpf, rondas, n_bpf);
reporte("C) piso: readdir(/proc) solo", t_dir, rondas, n_dir);
qsort(t_proc, rondas, sizeof *t_proc, cmp_ll);
qsort(t_bpf, rondas, sizeof *t_bpf, cmp_ll);
printf("\ncociente A/B (min, misma corrida): %.2fx\n",
(double)t_proc[0] / (double)t_bpf[0]);
// El programa BPF se carga y verifica UNA vez; a partir de ahí cada barrido
// ahorra (A-B). El punto de equilibrio es lo que decide si conviene.
long long ahorro = t_proc[0] - t_bpf[0];
if (ahorro > 0)
printf("equilibrio: la carga (%.1f ms) se amortiza en %lld barridos\n",
carga / 1e6, carga / ahorro + 1);
// Cotejo de contenido: dos caminos que dicen lo mismo, o el bench no sirve.
int coinciden = 0, solo_a = 0;
for (int i = 0; i < n_proc; i++) {
int hallado = 0;
for (int j = 0; j < n_bpf; j++)
if (a[i].pid == b[j].pid) {
hallado = 1;
if (strncmp(a[i].comm, b[j].comm, 15) == 0)
coinciden++;
break;
}
if (!hallado)
solo_a++;
}
printf("cotejo: %d pid+comm idénticos, %d sólo en /proc, %d sólo en BPF\n",
coinciden, solo_a, n_bpf - (n_proc - solo_a));
return 0;
}
@@ -0,0 +1,80 @@
SDD 25 §9.5.1 — Iterador BPF contra el barrido de /proc
Medido 2026-08-31 en 'momento'. TERCERA corrida: condiciones propias, NO se cruza
con los números de §2 ni con los de §9.
== condiciones ==
kernel de desarrollo : 7.1.4-artix1-1 (Artix; NO es un kernel de hammer)
cpu : 4 vCPU load: 3.85 5.27 4.91
memoria libre : 4483 MiB disponibles
BTF del kernel : /sys/kernel/btf/vmlinux, 6455713 bytes
libbpf : 1.7.0
clang : clang version 22.1.8
ruido conocido : un 'yes' huérfano (pid 1750, reparentado a arje-zero) llevaba
3,5 dias quemando un core entero de los 4. Los ABSOLUTOS de
abajo son cotas superiores; la señal está en los cocientes.
== qué compara ==
El MISMO trabajo por los dos caminos: {pid, ppid, estado, utime, stime, comm} de todos
los procesos vivos — la foto que sandokan necesita de la máquina.
A) /proc : readdir(/proc) + open+read+parse de /proc/<pid>/stat, uno por proceso
B) BPF : bpf_iter_create + read() de registros BINARIOS de tamaño fijo
C) piso : readdir(/proc) a secas, sin abrir nada
== las DOS barreras, comprobadas, no supuestas ==
1) El iterador se ata por ID DE TIPO BTF del vmlinux:
prog_type=26 (BPF_PROG_TYPE_TRACING=26)
attach_btf_id=80137 attach_btf_obj_id=1 (0 en obj_id = BTF del vmlinux)
=> el iterador se ata por ID DE TIPO BTF: sin /sys/kernel/btf/vmlinux no hay a qué atarse.
=> un kernel sin CONFIG_DEBUG_INFO_BTF no expone /sys/kernel/btf/vmlinux y no hay
a qué atarse. La ganancia de abajo NO se compra sin pagar BTF.
2) Hace falta privilegio (CAP_BPF+CAP_PERFMON, o root). Sin él:
$ ./bench_bpf_iter 3
load (¿root? ¿BTF?): Operation not permitted
== 33 rondas, orden A->B ==
procesos vistos: /proc 245 · BPF 245 · readdir 245 (33 rondas, orden A->B)
carga+verificación+attach del programa BPF (una vez): 16.7 ms
A) /proc readdir+stat+parse min 1356.1 us mediana 1693.9 us 5.54 us/proceso (min)
B) iterador BPF (binario) min 196.1 us mediana 273.6 us 0.80 us/proceso (min)
C) piso: readdir(/proc) solo min 112.1 us mediana 143.8 us 0.46 us/proceso (min)
cociente A/B (min, misma corrida): 6.91x
equilibrio: la carga (16.7 ms) se amortiza en 15 barridos
cotejo: 221 pid+comm idénticos, 0 sólo en /proc, 0 sólo en BPF
== 33 rondas, orden B->A (control de sesgo de orden) ==
procesos vistos: /proc 245 · BPF 245 · readdir 245 (33 rondas, orden B->A)
carga+verificación+attach del programa BPF (una vez): 16.5 ms
A) /proc readdir+stat+parse min 1676.6 us mediana 2115.2 us 6.84 us/proceso (min)
B) iterador BPF (binario) min 219.6 us mediana 338.7 us 0.90 us/proceso (min)
C) piso: readdir(/proc) solo min 141.1 us mediana 218.1 us 0.58 us/proceso (min)
cociente A/B (min, misma corrida): 7.64x
equilibrio: la carga (16.5 ms) se amortiza en 12 barridos
cotejo: 221 pid+comm idénticos, 0 sólo en /proc, 0 sólo en BPF
== las tres corridas cortas previas, mismo día ==
9 rondas A->B : A 1018,7 us · B 135,4 us · piso 101,4 us -> 7,52x
15 rondas A->B : A 2120,7 us · B 471,1 us · piso 193,2 us -> 4,50x
15 rondas B->A : A 1534,1 us · B 220,8 us · piso 134,0 us -> 6,95x
33 rondas A->B : A 1184,0 us · B 151,3 us · piso 108,1 us -> 7,82x
33 rondas B->A : A 1266,4 us · B 158,7 us · piso 110,4 us -> 7,98x
Con pocas rondas el mínimo no converge y la máquina cargada lo ensucia (el 4,50x).
Con 33 rondas el cociente se planta en 7,2-8,0x en las CINCO corridas y en las DOS
direcciones: el orden no lo explica.
== los 24 comm que DIFIEREN entre los dos caminos ==
No es un fallo del programa BPF: /proc SINTETIZA el nombre de los kworkers y le pega
la workqueue que están sirviendo (wq_worker_comm). task->comm trae el nombre pelado.
DIFIERE pid 11: /proc=[kworker/0:0H-kb] bpf=[kworker/0:0H]
DIFIERE pid 29: /proc=[kworker/1:0H-kb] bpf=[kworker/1:0H]
DIFIERE pid 35: /proc=[kworker/2:0H-kb] bpf=[kworker/2:0H]
DIFIERE pid 41: /proc=[kworker/3:0H-kb] bpf=[kworker/3:0H]
DIFIERE pid 112: /proc=[kworker/3:1H-kb] bpf=[kworker/3:1H]
DIFIERE pid 181: /proc=[kworker/1:1H-kb] bpf=[kworker/1:1H]
DIFIERE pid 279: /proc=[kworker/2:1H-kb] bpf=[kworker/2:1H]
DIFIERE pid 5361: /proc=[kworker/3:0-eve] bpf=[kworker/3:0]
[...] los 24 son kworkers; NINGÚN proceso de usuario difiere. Quien cambie /proc por
el iterador y muestre nombres de kworker verá otra cosa — para las Cards, da igual.
@@ -0,0 +1,72 @@
// Programa BPF del experimento §9.5.1 de SDD 25: un iterador `task` que emite
// un registro BINARIO por proceso, para comparar contra el barrido de texto de /proc.
//
// No usa vmlinux.h a propósito (haría falta bpftool, que no está en la máquina ni
// en el store): declara a mano el mínimo de tipos con `preserve_access_index`, que
// es lo que hace CO-RE reubicar los offsets contra el BTF del kernel en carga.
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
char LICENSE[] SEC("license") = "GPL";
typedef unsigned int u32;
typedef unsigned long long u64;
struct bpf_iter_meta {
struct seq_file *seq;
u64 session_id;
u64 seq_num;
} __attribute__((preserve_access_index));
struct task_struct {
unsigned int __state;
int pid;
int tgid;
struct task_struct *real_parent;
char comm[16];
u64 utime;
u64 stime;
} __attribute__((preserve_access_index));
struct bpf_iter__task {
struct bpf_iter_meta *meta;
struct task_struct *task;
} __attribute__((preserve_access_index));
// El mismo puñado de campos que sandokan saca hoy de /proc/<pid>/stat.
struct registro {
int pid;
int ppid;
u32 estado;
u32 _pad;
u64 utime;
u64 stime;
char comm[16];
};
SEC("iter/task")
int barrido(struct bpf_iter__task *ctx)
{
struct task_struct *t = ctx->task;
struct registro r = {};
if (!t)
return 0;
// El iterador `task` recorre HILOS. sandokan cuenta procesos ⇒ sólo los
// líderes de grupo, que es exactamente lo que hay bajo /proc/<pid>.
r.pid = BPF_CORE_READ(t, pid);
r.ppid = BPF_CORE_READ(t, tgid);
if (r.pid != r.ppid)
return 0;
r.ppid = BPF_CORE_READ(t, real_parent, tgid);
r.estado = BPF_CORE_READ(t, __state);
r.utime = BPF_CORE_READ(t, utime);
r.stime = BPF_CORE_READ(t, stime);
BPF_CORE_READ_INTO(&r.comm, t, comm);
bpf_seq_write(ctx->meta->seq, &r, sizeof(r));
return 0;
}