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:
+103
-9
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user