> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
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
diff --git a/docs/25-tasas-del-kernel.md b/docs/25-tasas-del-kernel.md
index 141faf24..b6a93b2e 100644
--- a/docs/25-tasas-del-kernel.md
+++ b/docs/25-tasas-del-kernel.md
@@ -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:
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/bench_bpf_iter.c b/docs/evidencia/tasas-kernel-2026-08-29/bench_bpf_iter.c
new file mode 100644
index 00000000..23978a6a
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/bench_bpf_iter.c
@@ -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;
+}
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/bpf-iter.txt b/docs/evidencia/tasas-kernel-2026-08-29/bpf-iter.txt
new file mode 100644
index 00000000..fe34023b
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/bpf-iter.txt
@@ -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.
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/bpf_iter_task.bpf.c b/docs/evidencia/tasas-kernel-2026-08-29/bpf_iter_task.bpf.c
new file mode 100644
index 00000000..8210ce93
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/bpf_iter_task.bpf.c
@@ -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;
+}