Era el ultimo hueco medible de SDD 25 en esta maquina: H7 tenia el precio en cuatro partes pero no el numero en bytes, que es como se pago H1. Se construyo una copia derivada de linux-generic con UNA sola diferencia en el .config (DEBUG_INFO_DWARF5 + DEBUG_INFO_BTF) y todo lo demas byte a byte igual: bzImage sin BTF : 16 937 984 B bzImage con BTF : 19 473 408 B (+2 535 424 B = +2,42 MiB = +14,97%) seccion .BTF : 7 888 166 B sin comprimir, entran comprimidos 3,11x Contra la unica vara comparable: H1 (MEMCG+PSI) costo +80 KiB / +0,49% del mismo bzImage => BTF cuesta 31 veces eso. No lo decide, pero lo saca de "un re-hasheo y ya". Sobre linux-generic y no sobre linux, que es mas barato: BTF tiene depends on BPF_SYSCALL y el .config sellado de linux lo trae APAGADO, asi que medir ahi habria mezclado dos precios. TRES cosas que salieron construyendo y no leyendo: 1. Una QUINTA parte del precio que nadie habia nombrado: hace falta python3. BTF hace que kbuild descienda a tools/bpf/resolve_btfids, que compila un libbpf vendorizado cuyo Makefile genera bpf_helper_defs.h con un script de Python (Error 127). 2. El numero de Artix fallaba en la direccion CONTRARIA a la esperable. El doc decia que sus 6,4 MB "no dicen nada de uno monolitico y pelado"; el nuestro sale MAS GRANDE, 7,5 MiB. Artix es modular y su .BTF cubre solo el core built-in; el nuestro es monolitico y los tipos de i915+nouveau+radeon+iwlwifi entran todos. 3. Una trampa de kconfig que es CLAUDE.md §3 dentro de make: scripts/pahole-version.sh imprime 0 si pahole no esta en el PATH, el depends on PAHOLE_VERSION >= 122 deja de cumplirse, y olddefconfig BORRA la linea en silencio. El build sale OK y sella un kernel SIN BTF diciendo que todo fue bien. Por eso la receta comprueba el .config PRODUCIDO y sale 1 si no esta. Salio OK, lo que ademas paga la parte (3) del precio: el pahole estatico de ayer se encuentra y se ejecuta DENTRO del sandbox. El artefacto de medicion se borro a proposito: `hammer kernel contract --sealed` lo listaba como SIN COMPROBAR (no declara [[target]], que es lo que exige H8), o sea un aviso permanente en un fichero que el cron commitea cada 30 min. Un aviso fijo que no corresponde a ningun problema es como se deja de leer un vigia. La receta derivada queda en la evidencia. Nada del corpus se re-hasheo: la derivada vive fuera de recipes/ y el catalogo se le presta por symlinks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
47 KiB
SDD 25 — Las tasas del kernel: qué cobra Linux, cuánto, y por dónde se esquiva
Escrito 2026-08-29. Nace de una pregunta del operador: «¿hay cómo crear módulos para
kernel para optimizar cosas que son estándares y por eso no se cambian — por ejemplo una API
más cercana del control de procesos sin que sandokan use /proc? Con tawasuyu ponerse de
acuerdo para pasar por debajo de mecanismos pesados POSIX.»
/proc y sandokan eran el ejemplo, no el alcance. Lo que sigue es la lista entera de tasas
que Linux cobra por compatibilidad histórica, medida en esta máquina, y para cada una: si
se esquiva, con qué, y quién paga hoy.
El espejo de este documento para el lado tawasuyu es
tawasuyu/HANDOFF-TASAS-KERNEL-DESDE-HAMMER.md (mismo patrón que ADR 0010 ↔
HANDOFF-arranque-grafo.md).
0. La respuesta corta
- Un módulo cargable no es una opción en hammer y no hace falta. Las tres recetas de
kernel (
linux-generic.toml:43,linux-metal.toml:104,linux-metal-dual.toml:51) configuran-d MODULES: kernel monolítico.linux.tomlno lo apaga pero sólo compilabzImage, nuncamake modules. Cualquier código de kernel propio en hammer es built-in, o sea un parche a la receta — que además es lo correcto para el proyecto, porque la faseconfigurees la identidad del artefacto (SDD 22) y así queda sellado y atestado junto al kernel. - Casi todo lo que duele ya tiene salida en mainline y nadie la está usando.
pidfd,cgroupv2 agregado,posix_spawn,getdents64,close_range,openat2. Cero código de kernel, ganancias de 3× a 17× medidas abajo. - La medición encontró un bug cruzado que no se buscaba: los kernels de hammer se
construían sin
CONFIG_MEMCG, y arje/sandokan escribenmemory.maxignorando el error. Un límite de memoria pedido por una Card no se aplicaba y nadie se enteraba. §4. Confirmado sobre los 11.configsellados, no deducido del defconfig (§4.bis), vigilado porhammer kernel contract(§4.ter) y PAGADO el 2026-08-30: los cuatro kernels que hospedan Cards se reconstruyeron conMEMCGyPSIy el barrido sale 5 de 5 vigentes en verde (§7-H1). - Sondear
/procno es lento, es ciego (§9.1): de 200 procesos de vida mínima, un sondeo 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 deread+write(§9.2). - Cuando
/procsí se sondea entero, hay algo cerca de 7× más barato (§9.5, medido 2026-08-31): un iterador BPFtasksaca la misma foto al precio de listar el directorio —1,75× el piso dereaddircontra 12,1× de/proc—. Su portón esDEBUG_INFO_BTF, que sigue apagado, y H7 ya no dice sólo que está apagado: cuesta +14,97% de bzImage (+2,42 MiB, medido construyendo el 2026-08-31 — 31 veces lo que costó MEMCG+PSI) máspaholeypython3en el lab. Los dos lados del balance están sobre la mesa; la decisión no.
1. Método y condiciones — leer antes de citar un número
- Máquina:
momento, KVM sobre AMD EPYC-Rome, 4 vCPU, 7,7 GiB RAM, Linux 7.1.4, glibc/Artix. Store en/dev/sdb(ext4), árbol en/dev/sdc(ext4),/tmptmpfs. - Mitigaciones activas: retpolines (
spectre_v2), retbleed, SSBD por prctl. Sin PTI (AMD,meltdown: Not affected). En un Intel con PTI el piso de syscall sería peor. - La máquina estaba CARGADA: load 7,3 sobre 4 vCPU, un build de LLVM en vuelo, 773 MiB libres. ⇒ los absolutos son cotas superiores; la señal está en los cocientes dentro de una misma corrida, que es como está escrito todo lo de abajo.
- Cada medida son 7–9 rondas; se reporta mínimo (mejor caso, el estimador robusto al ruido del scheduler) y mediana. Las corridas marcadas «pinneado» fijan el proceso a un cpu para sacar de en medio la cola del planificador.
- SEGUNDA CORRIDA, mismo día, con root (§9): la máquina estaba mucho más suelta —load ~1,3 sobre 4 vCPU y 4,3 GiB libres— y por eso los absolutos de las dos corridas NO se comparan entre sí. Cada cociente que se cita abajo sale de una sola corrida, y se dice de cuál.
- Fuentes y salidas crudas:
docs/evidencia/tasas-kernel-2026-08-29/. - Piso de referencia: una syscall nula (
getppid) cuesta 78,7 ns. El vDSO (clock_gettime) cuesta 31,2 ns y por syscall 143,8 ns ⇒ cruzar al kernel son ~110 ns. Todo lo de abajo se lee en «syscalls equivalentes».
2. La lista
T1 · fork() copia el espacio de direcciones del padre
| padre con … | fork+_exit+wait |
posix_spawn+wait |
|---|---|---|
| 0 MB tocados | 109 µs | 231 µs |
| 64 MB | 938 µs | 234 µs |
| 256 MB | 3 949 µs | 231 µs |
≈ 59 ns por página de 4 KiB del padre, ida y vuelta. posix_spawn (que usa
CLONE_VFORK|CLONE_VM) es plano: no mira el tamaño del padre. A 256 MB, 17× más
barato — haciendo estrictamente más trabajo (además ejecuta un binario).
Es la tasa POSIX por excelencia: fork no se puede arreglar sin romper la semántica, y la
salida está dentro del propio POSIX.
T2 · exec + enlazado dinámico
| µs/exec | |
|---|---|
| musl ESTÁTICO | 77,4 |
| glibc ESTÁTICO | 232,7 |
| glibc DINÁMICO | 324,3 |
ld.so cuesta +92 µs (+39%). Pero el salto grande es glibc→musl: 4,2× entre el peor y
el mejor, con el mismo main(){return 0;}. El corpus de hammer ya está del lado bueno
(verificado: los binarios del store son statically linked); esto cuantifica lo que la
de-Alpinización compró: un build que lanza 20 000 procesos se ahorra ~5 s sólo en arranque.
T3 · /proc es una API de TEXTO, y se paga como tal
| ns | = syscalls | |
|---|---|---|
getrusage(RUSAGE_SELF) |
238 | 3 |
clock_gettime(PROCESS_CPUTIME) |
214 | 3 |
pread+parse /proc/self/stat (fd ya abierto) |
1 271 | 16 |
open+read+parse /proc/self/stat |
2 692 | 34 |
open+read+parse /proc/self/status (VmRSS) |
4 643 | 59 |
Leer el CPU propio por /proc cuesta 11× más que getrusage. Y la mitad del coste es
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. 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)
Leer cpu.stat + memory.current + pids.current de un cgroup: 4,6 µs, y no depende
de cuántos procesos tenga la unidad. Sumar /proc para esa misma unidad: 3,4 µs × N.
El cruce está en N = 2. Para una unidad de 20 procesos, 15× a favor del cgroup.
T5 · Liveness por /proc/<pid>: la carrera, no la lentitud
| ns | |
|---|---|
access("/proc/<pid>") — lo que se usa hoy |
345 |
kill(pid, 0) |
119 |
pidfd_send_signal(fd, 0) |
119 |
pidfd no es más rápido que kill(0); es 2,9× más rápido que la ruta /proc. Su valor
real es otro: no hay reuso de PID, y el fd se vuelve legible al morir el proceso ⇒ entra
en el epoll y el supervisor deja de sondear.
T6 · Señales: la notificación más cara de POSIX
kill(self,SIGUSR1) + handler = 1 165 ns = 15 syscalls. Un eventfd en el bucle de
eventos hace el mismo trabajo por una fracción.
T7 · Despertar cuesta µs; sincronizar en usuario cuesta ns
| ns | |
|---|---|
pthread_mutex lock+unlock sin disputa |
8,6 |
RTT eventfd (2 hilos, mismo cpu) |
2 507 |
RTT pipe |
2 956 |
RTT socketpair AF_UNIX |
3 639 |
291× entre sincronizar en espacio de usuario y despertar por el kernel. El bus de arje (postcard sobre socket Unix) paga ~3,6 µs por ida y vuelta: aceptable por mensaje, ruinoso por campo.
Adyacente: sched_yield 174 ns; nanosleep(1 ms) se pasa 65 µs bajo carga ⇒ los lazos
de control no se construyen sobre sleep.
T8 · stat() es tasa del VFS, no del filesystem
| ns/fichero | ext4 | tmpfs |
|---|---|---|
stat() por ruta completa |
648 | 714 |
fstatat(dirfd, nombre) |
439 | 489 |
statx(…, DONT_SYNC) |
467 | 593 |
openat+close |
899 | 961 |
getdents64 (barrido, sin stat) |
168 | 59 |
ext4 ≈ tmpfs en stat. ⇒ una tormenta de stat no se arregla cambiando de
filesystem; se arregla no haciéndola: getdents64 da el barrido 3,9× más barato en ext4
y 12× en tmpfs, y fstatat relativo a un dirfd ahorra 32% sobre la ruta completa.
Profundidad de ruta: 12 componentes extra cuestan +205 ns en ext4 y +203 en tmpfs ⇒ ~17 ns por componente, idéntico en ambos. Otra vez: el VFS, no el disco.
T9 · openat2 con RESOLVE_BENEATH es casi gratis
ext4: 1 025 vs 899 ns (+14%). tmpfs: 763 vs 961 ns (midió más rápido, o sea dentro del ruido). ⇒ la resolución hermética de rutas no tiene precio apreciable. Es material directo para harkaq (SDD 16): la garantía «no se sale del árbol» sin demonio auxiliar y sin una syscall extra.
T10 · Materializar un árbol: hardlink vs copia
| 2 000 ficheros de 1 byte | ext4 | tmpfs |
|---|---|---|
link() |
9,2 µs | 2,9 µs |
open+read+write+close |
142,2 µs | 5,0 µs |
| cociente | 15,4× | 1,7× |
Lo que hace cara la copia es el journal de ext4, no el VFS. La estrategia de hardlinks de
.dmerge está bien elegida — y su contrapartida ya está documentada (la caché retiene lo que
se borra del store).
T11 · copy_file_range/sendfile no ganan sobre ext4
64 MiB en caché: read+write 2 837 MB/s · sendfile 3 345 · copy_file_range 3 521. ~20%
en el mejor caso. ext4 no tiene reflink, así que copy_file_range cae a una copia dentro
del kernel. El «copiar en O(1)» que la gente espera exige un FS con reflink (xfs/btrfs) —
sin medir acá, §9.
T12 · io_uring no es una ganancia universal
| ns/op | |
|---|---|
| syscall nula | 78,7 |
| io_uring NOP en lote (piso de operación) | 47,7 |
pread 4K en caché (ext4) |
540 |
| io_uring read 4K profundidad 32 (ext4) | 594 |
pread 4K (tmpfs) |
602 |
| io_uring read 4K (tmpfs) | 1 672 |
El piso por operación es 0,61× una syscall: en lote, io_uring amortiza el cruce. Pero sobre datos ya cacheados pierde — 10% en ext4 y 2,8× en tmpfs. io_uring paga cuando las operaciones son muchas, agrupables y de verdad bloquean. Sobre caché es una pesimización.
T13 · THP puede ser 3× PEOR (y acá lo es)
64 MiB anónimos, ns por página de 4 KiB: MADV_HUGEPAGE 3 046 · MADV_NOHUGEPAGE
1 013 · MAP_POPULATE 752.
La causa está en la máquina, no en el kernel:
/sys/kernel/mm/transparent_hugepage/defrag = madvise ⇒ una región con MADV_HUGEPAGE
dispara compactación síncrona en el fallo de página, y con 773 MiB libres eso es caro.
THP bajo presión de memoria es un pasivo de latencia, no una optimización. Reproducido en
las dos corridas independientes (128 MiB y 64 MiB).
mmap+munmap de 4K: 1 664 ns.
T14 · El sandbox por namespaces cuesta ~1 ms por proceso
fork+exec base 1 649 µs · con unshare de 6 namespaces 2 742 µs · con USER|MNT 2 050 µs.
⇒ +1,09 ms por el juego completo, +0,40 ms por USER|MNT.
Para hammer es ruido por fase de build (segundos a minutos). Sería caro sólo si se enjaulara por-exec: 20 000 execs × 1,09 ms = 22 s.
T15 · Cerrar descriptores antes del exec
dup+close uno a uno: 0,2 µs/fd ⇒ 512 fds = 102 µs. close_range(lo,hi): por debajo del
ruido. Gratis, y ya está en mainline.
T16 · Hilo vs proceso
pthread_create+join = 11,9 µs, contra 109 µs de fork+_exit con el padre vacío:
9×, y la distancia crece con el tamaño del padre (T1).
3. Las cinco sorpresas
Las anoto aparte porque contradicen el consejo de manual:
- THP más lento que 4K (T13) — el manual dice que THP acelera; con
defrag=madvisey memoria apretada, triplica el coste del fallo. - io_uring más lento que
preadsobre tmpfs (T12) — el manual dice que io_uring es el futuro; sobre datos cacheados el futuro pierde 2,8×. statcuesta lo mismo en tmpfs que en ext4 (T8) — invalida la reacción natural («movamos el árbol a tmpfs y vuela»): el coste está en el VFS.taskstatsbinario NO es más rápido que parsear/proc(§9.1) — 2 688 vs 2 697 ns por consulta. La intuición «binario le gana a texto» falla porque el ida y vuelta de netlink cuesta lo mismo queopen+read+parse. Su valor es el contenido (delay accounting, que/procno da) y la estabilidad bajo carga, no la velocidad.- Sondear
/procno es lento: es CIEGO (§9.1) — de 200 procesos de vida mínima lanzados uno cada 10 ms, un sondeo cada 100 ms vio cero. Toda la discusión de «cuánto cuesta el barrido» (T3) presupone que el barrido VE lo que hay, y para procesos cortos no lo ve.
4. El hallazgo cruzado: los kernels de hammer no tienen MEMCG
No lo buscaba; salió de verificar la recomendación antes de escribirla.
Lado hammer. Las recetas hacen make ARCH=x86_64 defconfig y después
scripts/config. Lo que arch/x86/configs/x86_64_defconfig trae de contabilidad:
CONFIG_TASKSTATS=y, TASK_DELAY_ACCT=y, CGROUPS=y, CGROUP_SCHED=y, CGROUP_PIDS=y,
CGROUP_FREEZER=y, CONNECTOR=y ⇒ y PROC_EVENTS es default y con depends on CONNECTOR=y, así que también entra.
CONFIG_MEMCG no está en el defconfig y no tiene default y ⇒ queda en n en todos los
kernels de hammer. CONFIG_PSI, igual.
Lado tawasuyu. sandokan-core::Engine expone set_memory_max / set_memory_high
(shared/sandokan/sandokan-core/src/engine.rs:60-72), sandokan-local los delega a
arje_incarnate::cgroup (03_ukupacha/sandokan/sandokan-local/src/lib.rs:675-681), y ahí:
// arje/init/arje-incarnate/src/cgroup.rs:60
let _ = std::fs::write(abs.join("cpu.weight"), format!("{w}\n")); // error DESCARTADO
// :79
match std::fs::write(&path, format!("{mem}\n")) {
Err(e) => tracing::warn!(..., "memory.max write failed"), // sólo un warn
Consecuencia. Sobre un sistema hammer, una Card que pide un tope de memoria arranca
sin tope, y la única huella es una línea de log. Es exactamente la forma de fallo que
CLAUDE.md §3 nombra: un ausente falla ruidosamente; un vacío llega hasta el final diciendo
que todo fue bien. La jaula que no está se comporta igual que la jaula que sí está, hasta
que hace falta.
4.bis · Confirmado sobre los artefactos, no deducido del defconfig
Lo de arriba se dedujo leyendo x86_64_defconfig. Se comprobó después contra los .config que
las recetas instalan en boot/config-<versión>, que es la prueba dura porque incluye lo que hizo
olddefconfig. Los 11 kernels sellados del store —cinco nombres de artefacto: linux,
linux-generic, linux-metal, linux-metal-dual, linux-gioser— traen literalmente:
# CONFIG_MEMCG is not set
# CONFIG_PSI is not set
y ninguno tiene CFS_BANDWIDTH (⇒ cpu.max, la cuota dura de CPU, tampoco existe; hoy nadie la
pide, pero cuando pacha la quiera será otro re-hasheo). Lo que sí está en los once:
CGROUP_SCHED+FAIR_GROUP_SCHED (cpu.weight), CGROUP_PIDS (pids.max), CPUSETS
(cpuset.cpus), BLK_CGROUP+BLK_CGROUP_IOCOST (io.weight, con la sintaxis «default N» que
arje ya usa), TASKSTATS+CONNECTOR+PROC_EVENTS, SECCOMP_FILTER, SECURITY_LANDLOCK,
FANOTIFY, OVERLAY_FS con metacopy e IO_URING.
Y por qué el bug sobrevivió meses: el kernel de la máquina de desarrollo (Artix 7.1.4) sí
trae MEMCG, PSI y CFS_BANDWIDTH. El fallo sólo existe del lado del artefacto sellado, que es
el lado que nadie mira con un depurador.
4.ter · El guardián: hammer kernel contract
Un hallazgo escrito en un documento no impide que vuelva a pasar. El contrato vive en
docs/state/kernel-contract.toml y declara, por capacidad: los símbolos que la construyen, la
interfaz que aparece o no aparece (memory.max, pids.max, landlock_restrict_self…), el
consumidor con ruta y símbolo, y cómo falla hoy si no está. Se comprueba contra un .config
ya producido:
hammer --store ./store kernel contract --sealed # barre los kernels sellados
hammer kernel contract --profile anfitrion-cards # el kernel vivo de esta máquina
hammer kernel contract --list # qué promete hammer (esto es el W6)
Tres decisiones que valen la pena nombrar:
- Por perfil, no global — misma lección que el gate de hardware.
linux.tomles el kernel de QEMU del selfhost-verify: no hospeda Cards y su hash es load-bearing del baselineof_tree. Exigirle contabilidad de memoria sería rechazar una receta sana, así que ahí los límites por unidad sonwantsy norequires. - Contra el
.config, no contra la receta — entre elscripts/config -e Xy el.configestáolddefconfig, que puede tragarse el símbolo sin decir nada (es justo lo quehammer kernel diff-backya vigila para los planes). - Distingue apagado de ausente — un símbolo que el
.configni nombra puede ser un renombrado entre versiones. Cuenta igual como capacidad ausente, pero se informa aparte; y un kernel sin perfil declarado queda SIN COMPROBAR en vez de contar como aprobado. - Distingue vigente de superado (agregado al pagar H1) — el store guarda todos los
sellados, no el último. Mirándolos a todos por igual, arreglar la receta dejaba el gate rojo
para siempre por artefactos que nadie va a volver a construir, y un portón que no puede
ponerse verde deja de leerse. Ahora cada sellado se compara con el
ArtifactHashque la receta de hoy produce —la misma vigencia dehammer hash --check, lab incluido—: el vigente bloquea; el superado sale en su propia sección con lo que le falta, porque sigue siendo cierto que una máquina que arranque ese kernel corre sus Cards con lo que ese kernel traiga. Y dos negativas explícitas: si la vigencia no se puede resolver se comprueba todo y se dice por qué, y cero vigentes con superados a la vista sale ≠0, no verde — el vacío leído como presencia (CLAUDE.md §3) es justo el fallo que este guardián vino a arreglar.
Estado al escribirlo: 2 de 11 configs sellados cumplían su perfil (los dos linux), los 9 de
perfil anfitrion-cards fallaban por MEMCG. Estado tras pagar H1 (2026-08-30): 5 de 5 vigentes
cumplen, y los 10 sellados que quedaron atrás salen listados como superados. El kernel vivo de
esta máquina pasa las 11 exigidas.
Lo que sí funciona en un kernel hammer: cpu.weight (CGROUP_SCHED), pids.max
(CGROUP_PIDS), cpu.stat (rstat del core) y cgroup.freeze — este último porque el freezer
v2 es obj-y en kernel/cgroup/Makefile, no el CONFIG_CGROUP_FREEZER de v1.
5. Qué se esquiva SIN escribir kernel
| Tasa | Reemplazo mainline | Ganancia medida |
|---|---|---|
T1 fork de padre grande |
posix_spawn / CLONE_VFORK |
17× a 256 MB, y plano |
T2 ld.so |
binario estático musl | 4,2× |
T3 /proc texto |
getrusage, taskstats |
11× |
T4 sumar /proc por unidad |
agregado de cgroup v2 | O(1) vs O(N), cruce en N=2 |
T5 liveness por /proc |
pidfd (+ sin carrera de PID) |
2,9× y correctitud |
| T3 enumerar procesos | proc connector (netlink): eventos push | 0 vs 200 procesos vistos (§9.1) |
| T6 señales | eventfd/signalfd/pidfd |
15 syscalls → 1 |
T8 tormenta de stat |
getdents64, fstatat(dirfd,…) |
3,9×–12× |
| T9 hermetismo de rutas | openat2 RESOLVE_BENEATH |
~gratis |
| T10 materializar árbol | link() |
15,4× en ext4 |
| T11 copiar un fichero entero | FICLONE/reflink en xfs o btrfs |
25 600× a 512 MiB (§9.2) |
| T15 cerrar fds | close_range |
102 µs → ~0 |
Ninguna de estas exige un módulo, un parche ni un kernel propio. Es la primera conclusión del documento y la que ordena todo lo demás.
6. Qué exigiría tocar el kernel — y su precio en hammer
Después de agotar §5 quedan tres deseos legítimos que mainline no da:
- Una syscall que devuelva un lote de estadísticas por proceso en binario. Lo más
cercano es
taskstats(por tarea, netlink) y el agregado de cgroup. No existe el «dame los 291 en una llamada». - Eventos de estado con más contexto que el proc connector.
- Un plano de control de unidades que no pase por el filesystem.
Su precio, concreto:
- Es built-in, no
.ko(-d MODULES). ⇒ parche a la receta ⇒ entra enhash_inputs⇒ kernel nuevo, artefacto nuevo, y rebase manual en cada versión. - Nunca un número de syscall nuevo: choca con upstream para siempre. Lo compatible es un char device, un fichero propio en sysfs, o una kfunc BPF.
- Con eBPF (que es la forma sin código out-of-tree) el precio es
DEBUG_INFO_BTF, hoy apagado en las cuatro recetas con motivo escrito (linux.toml:32: «pahole (BTF), ausente del toolchain»). Deuda ya medio pagada: dwarves construye (frente libdw/musl cerrado). - El bloqueo de diseño, que no es técnico:
SDD-SERVIDOR-AJENO.mdpromete «cero invasión — la máquina sigue siendo su Ubuntu de siempre». Un sandokan que dependa de un kernel hammer deja de correr ahí. ⇒ el núcleo se queda portable siempre; lo rápido es un backend opcional detectado en runtime, con/proccomo respaldo que no se borra.
7. Propuestas — lado hammer
-
H1 ·
-e MEMCG -e PSIen los kernels que hospedan Cards. PAGADO 2026-08-30. Cierra §4 y habilitamemory.current(T4) y las señales de presión. Se hizo en una sola tanda —el precio era el re-hasheo de todos los artefactos de kernel— sobrelinux-generic,linux-metal,linux-metal-dualy la derivadalinux-gioser, que hubo que regenerar: una receta derivada inlinea la faseconfigurede su base y sin regenerarla se queda con la vieja. Enlinux.toml(perfilqemu-serial) NO corre: su hash es el baseline del selfhost-verify y ahí no hay Cards.Tres cosas que valen para la próxima:
- El
x86_64_defconfigde 7.1 ya no traeMEMCG=y— de ahí que hiciera falta pedirlo. YMEMCGno tiene un solodepends on(sóloselects), así que elolddefconfigno se lo podía tragar; se comprobó igual contra el.configproducido, que es la regla. - Precio en bytes, medido: bzImage
linux-generic16 856 064 → 16 937 984 (+80 KiB, +0,49%),linux-metal+84 KiB (+0,57%),linux-metal-dual+88 KiB (+0,53%),linux-gioser+80 KiB (+0,71%). Contabilidad por unidad y presión, por medio punto porcentual de kernel. - La prueba de que se pagó es el mismo comando:
hammer kernel contract --sealedsale 0 y dice5 de 5 config(s) VIGENTES cumplen su perfil. La salida cruda quedó endocs/evidencia/tasas-kernel-2026-08-29/contrato-h1-pagado.txt, junto con eldiff-backdel plan de gioser sobre el config nuevo (35 cumplidos, 0 incumplidos).
- El
-
H2 ·
MODULES=nse queda. Si algún día hace falta código de kernel, es built-in y parcheado en la receta. Queda escrito para no rediscutirlo. -
H3 · Contención de rutas: el sitio no era harkaq, era la hidratación. La propuesta original decía «que harkaq use
openat2 RESOLVE_BENEATHdonde hoy valida rutas a mano (T9, ~gratis)». Buscando ese sitio (2026-08-30) resultó que harkaq no valida rutas a mano: delega la contención en Landlock, que es más fuerte que cualquier chequeo de ruta. Donde no se validaba nada era enhammer-build/hydrate.rs.El fallo, reproducido: hidratar es
target_fhs.join(rel)y escribir por ruta ⇒ un symlink de directorio ya presente en el FHS se sigue. Un artefacto que traeusr/share/pkg → /algún/ladodeja ese symlink puesto —los symlinks se replican literales, y así debe ser— y la hidratación siguiente escribíausr/share/pkg/archivofuera del root, devolviendoOk(1). Conhammer hydrate --intosobre una imagen, eso es escribir en el sistema anfitrión diciendo que todo fue bien: el fallo de CLAUDE.md §3, otra vez.Cuánto se estaba disparando: cero. Del store: 160 symlinks absolutos, ninguno a un directorio; ~42 000 relativos, ninguno que salga de su artefacto. Era una mina desactivada, no un incendio — y por eso nadie lo iba a encontrar mirando logs.
HECHO:
no_escapa()comprueba la contención por directorio (no por fichero: uncanonicalizepor entrada costaría un realpath por fichero, T8/§9.4) y falla nombrando el symlink y adónde lleva. SemánticaRESOLVE_IN_ROOT, noRESOLVE_NO_SYMLINKS: un symlink que se queda dentro tiene que seguir andando (usr-merge/lib → usr/lib, los22@2x → 22de los temas de iconos), y hay un test para cada dirección encrates/hammer-build/tests/hidratacion_no_escapa.rs.Y el mismo patrón estaba en el OTRO proyector:
hammer-upgrade::project_planestarget_root.join(rel)+ escribir por ruta, igual que hydrate. Con--root /no cambia nada (todo empieza por/), perohammer upgrade apply --root /mnt/imagenes el caso real de armar una imagen: reproducido —la generación 2 escribía fuera del root conApplyReporten verde— y tapado con la misma regla (se mide el directorio padre, porque unReplacedsobre un symlink que apunta afuera es legítimo:renamepisa el symlink, no escribe a través de él). Dos tests, uno por dirección, encrates/hammer-upgrade/src/lib.rs.Lo que queda de la propuesta original:
canonicalize+escribir es comprobar-y-después-usar, o sea que un adversario concurrente podría cambiar un componente entre las dos. Para el fallo real —un symlink persistente puesto por otro artefacto— alcanza y es determinista. La versión sin carrera es mover las escrituras aopenat2(RESOLVE_IN_ROOT)+linkat/renameat, y arrastra el camino depatchelf(que necesita una ruta, no un fd). Queda escrito acá con su precio, que es más de lo que se sabía cuando H3 se llamaba «~gratis». -
H4 · Ningún
Commandde hammer ponepre_exec— Rust apaga el caminoposix_spawnen cuanto hay uno, y cae afork+exec(T1: 17× a 256 MB tocados).hammer-build/sandbox.rslanzabwrapsinpre_exec, o sea que estaba bien por suerte: nadie lo comprobaba. HECHO: el testcrates/hammer-build/tests/sin_pre_exec.rsbarre las fuentes del workspace y falla si aparece uno (comprobado en los dos sentidos: inyectando unpre_execel test se pone rojo, y un barrido que no encuentra fuentes también, para que no apruebe por vacío). Si algún día hace falta de verdad, la salida es escribir en el test por qué ese caso paga los 17×, no borrarlo. -
H5 · La materialización de dependencias se queda en hardlink (T10) y el experimento de 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 mientrasdefrag=madvisey la granja corra apretada de RAM (T13). -
H7 ·
DEBUG_INFO_BTFsigue apagado — pero ya no por falta de número. §9.5 midió qué compra: un iteradortasksaca 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 dereaddir, 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 cinco cosas —la quinta salió construyendo, el 2026-08-31— y ninguna es un rediseño:
- Los kernels no compilan con información de depuración:
CONFIG_DEBUG_INFO_NONE=yen los.configsellados. BTF exige DWARF, así que primero hay que encenderDEBUG_INFO_DWARF5. Eso encarece el build, no elbzImage: el DWARF vive envmlinux, que la receta no empaqueta — lo único que entra a la imagen es la sección.BTF. - 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». - Ese pahole era DINÁMICO pese a
link = "static"— PAGADO el 2026-08-31. El artefacto de entonces (8057bcd…) traíaINTERPy cincoNEEDED(libelf,libz,liblzma,libbz2,libc), y corre dentro del sandbox del build del kernel: sin esas librerías ahí no arranca, y el fallo habría salido como «pahole no está» estando. Arreglado al reabrirse el frente delink = static(ver elgit logderecipes/dwarves.toml); el artefacto vigente esv1.30y estáticamente enlazado. Y quedó probado de la única manera que vale: el build de medición de abajo lo encontró y lo ejecutó dentro del sandbox, no en el host. - BTF mete a pahole en el juego de la bit-repro. El
.BTFlo emite pahole ⇒ su versión pasa a ser parte de la identidad del artefacto, y el lab no está enhash_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 una quinta, que no está en ninguna documentación de BTF: hace falta
python3. Encender BTF hace que kbuild descienda atools/bpf/resolve_btfids, que compila un libbpf vendorizado cuyo Makefile generabpf_helper_defs.hcon un script de Python ⇒env: 'python3': No such file or directory,Error 127. Salió construyendo, no leyendo.
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_idresuelto contra el BTF del vmlinux); y sched-ext, que es por lo que existe la receta de dwarves —SCHED_CLASS_EXTdepende deDEBUG_INFO_BTF. El primero está medido; el segundo todavía no tiene su símbolo.El precio en bytes, MEDIDO el 2026-08-31 (
docs/evidencia/tasas-kernel-2026-08-29/btf-precio.txt). Se construyó una copia derivada delinux-genericcon una sola diferencia en el.config—DEBUG_INFO_DWARF5+DEBUG_INFO_BTF— y todo lo demás byte a byte igual:bzImage delta linux-genericsellado (sin BTF)16 937 984 B — el mismo con DWARF5 + BTF 19 473 408 B +2 535 424 B = +2,42 MiB = +14,97% La sección
.BTFdelvmlinuxson 7,52 MiB sin comprimir (.BTF_ids, 2 344 B más) y entran albzImagecomprimidos 3,11×. Contra la única vara comparable que tenemos: H1 costó +80 KiB (+0,49%) del mismo bzImage ⇒ BTF cuesta 31 veces lo que costó H1. Eso no lo decide, pero lo saca del terreno de «un re-hasheo y ya»: un +15% de imagen es una discusión de producto, no de toolchain, y hay que tenerla contra los dos consumidores nombrados.Sobre
linux-genericy no sobrelinux, que es más barato de construir: BTF tienedepends on BPF_SYSCALLy el.configsellado delinuxlo trae apagado, así que medir ahí habría obligado a encender las dos cosas a la vez y el delta mezclaría dos precios.El número de Artix no servía — y falla en la dirección contraria a la esperable. Esta sección decía que sus 6,4 MB «no dicen nada de uno monolítico y pelado». Cierto, pero el nuestro sale más grande: 7,5 MiB. Artix es modular y el
.BTFde suvmlinuxcubre sólo el core built-in; el nuestro es monolítico (i915 + nouveau + radeon + iwlwifi + todo lo demás son=y) y los tipos de todos esos drivers entran. Un kernel con menos drivers cargables tiene MÁS BTF que uno enorme lleno de módulos.Lo que cuesta construirlo no es lo que se paga al arrancar: el
vmlinuxcon DWARF pesa 561 MiB —396 de ellos.debug_info— y el árbol de build llegó a 7,9 G; el build tardó 48 min con-j2. Nada de eso entra en la imagen: la receta empaquetabzImage, novmlinux. Es precio de build (tiempo, disco y la RAM de pahole), no de arranque.Y una trampa del kconfig que hay que dejar escrita, porque es CLAUDE.md §3 dentro de
make:scripts/pahole-version.shimprime0si pahole no está en el PATH,depends on PAHOLE_VERSION >= 122deja de cumplirse, yolddefconfigborra la línea en silencio. El build sale OK y sella un kernel sin BTF diciendo que todo fue bien. Por eso la faseconfigurede la receta de medición comprueba el.configproducido y sale 1 si no está — la misma regla de §4.bis, aplicada un escalón antes. SalióOK: CONFIG_DEBUG_INFO_BTF=y, que es lo que paga la parte (3): ese pahole estático se encuentra y se ejecuta dentro del sandbox.El artefacto de medición se borró a propósito tras anotar los números:
hammer kernel contract --sealedlo listaba como SIN COMPROBAR —no declara[[target]], que es justo lo que exige H8— o sea un ⚠ permanente endocs/state/kernel-contract.txt, que el cron commitea cada 30 min. Un aviso fijo que no corresponde a ningún problema es exactamente cómo se deja de leer un vigía. La receta derivada queda junto a la evidencia, con las instrucciones para reconstruirla. - Los kernels no compilan con información de depuración:
-
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 tener» no es un contrato. -
H9 · El experimento de reflink cambia el cálculo de SDD 18, no el de hoy. Con
.dmergesobre ext4 el hardlink sigue siendo la elección correcta (T10). Lo que §9.2 agrega es que un store sobre xfs o btrfs compraría semántica de copia —mutación privada, sin la contrapartida de retener lo borrado— al mismo precio. Es un argumento para wawafs, medido; no un cambio de filesystem decidido acá.
8. Propuestas — lado tawasuyu
Detalle en el handoff espejo. En una línea cada una:
- W1 ·
pidfdpara liveness y señales ensandokan-local(hoyaccess("/proc/<pid>")ywaitpid(WNOHANG)en sondeo). Se gana correctitud (carrera de PID) antes que velocidad. - W2 · Telemetría por unidad desde el cgroup, no sumando
/proc(T4). Con sonda de capacidad: simemory.currentno existe (kernel sin MEMCG), decirlo, no callarlo. Del lado hammer esa sonda ya tiene con qué contrastarse:hammer kernel contract --list --jsonpublica qué promete cada perfil de kernel (§4.ter), que es el W6 en formato de dato. - W3 ·
arje-incarnateconposix_spawndonde elpre_execno sea imprescindible (T1), yclose_rangeen el que quede. - W4 · Los errores de cgroup dejan de descartarse.
let _ = write(...)ywarn!sobre un límite no aplicado son el bug de §4, no un detalle de estilo. - W5 ·
/procno se borra nunca: es el respaldo portable que hace posible SDD-SERVIDOR-AJENO. - W6 · El contrato entre repos es el trait
Engine, no una ABI de kernel. hammer publica la lista de símbolos garantizados (verificable conhammer kernel probe); tawasuyu elige backend en runtime.
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, 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)
a) Latencia. Esperar el evento PROC_EVENT_EXIT del connector no cuesta más que el
waitpid que el padre ya hace: 116,0 µs contra 114,8 µs por fork+_exit (mínimo de 7 rondas de
200), es decir dentro del ruido, con 0 eventos perdidos de 1 400. Con una advertencia dura que
salió del propio experimento: el multicast del connector no reintenta. Si el buffer del socket
se llena, el evento se pierde y quien lo espera se cuelga para siempre — la primera versión del
programa se colgó exactamente así. Un supervisor sobre connector necesita SO_RCVBUF grande y
un plazo, no una espera infinita.
b) Cobertura — el resultado que importa. 200 procesos de vida mínima, uno cada 10 ms:
| observador | vistos | CPU |
|---|---|---|
sondeo de /proc cada 100 ms (23 barridos) |
0 de 200 | 10,5 ms |
| proc connector (eventos) | 200 de 200 | 95,4 ms |
El sondeo no es caro: es ciego. Y la CPU del connector de esa tabla es cota superior — el
programa lo lee con espera activa de 200 µs, que no es el modo de un supervisor real (sería
epoll, donde el coste es despertar por evento).
c) taskstats. La familia genetlink existe y responde. Por consulta: 2 688 ns contra
2 697 ns de open+read+parse de /proc/self/stat. No gana en velocidad (sorpresa §3.4).
Lo que da y /proc no: contabilidad de retrasos (delay accounting) y una estructura binaria
estable en vez de un texto que hay que parsear posicionalmente.
9.2 · Copia con reflink — MEDIDO (bench_reflink.c, reflink.txt)
mkfs sobre un loop, xfs con reflink=1 y btrfs por defecto. Copiar un fichero entero:
| ext4 | xfs (loop) | btrfs (loop) | |
|---|---|---|---|
FICLONE (reflink) |
no soportado | 0,010 ms | 0,006 ms |
copy_file_range |
13,66 ms | 0,010 ms | 0,006 ms |
read+write (1 MiB) |
14,46 ms | 10,96 ms | 31,32 ms |
Y la prueba de que es O(1) y no «rápido»: con el fichero ocho veces más grande (512 MiB), el
reflink pasa de 0,010 a 0,020 ms —el doble, no ocho veces— mientras read+write pasa de 10,96 a
512,7 ms. 25 600× a 512 MiB en xfs. En btrfs, 0,006 → 0,015 ms.
Confirma T11 por el lado contrario: en ext4 copy_file_range no gana nada (13,66 contra 14,46 ms,
un 6%) porque ext4 no tiene reflink; el «copiar en O(1)» que la gente espera de esa syscall es
una propiedad del filesystem, no de la syscall. Material directo para SDD 18 (wawafs): con reflink
se obtiene semántica de copia al precio del hardlink, y sin la contrapartida del hardlink —que
.dmerge ya paga: retener en la caché lo que se borró del store.
9.3 · io_uring con SQPOLL — MEDIDO (bench_sqpoll.c, sqpoll.txt)
4K aleatorios en caché, profundidad 32, fd registrado:
| ns/op | ext4 | tmpfs |
|---|---|---|
pread |
554 | 749 |
| io_uring sin SQPOLL | 618 | 2 030 |
| io_uring con SQPOLL | 868 | 1 409 |
No cambia el veredicto de T12. Sobre datos cacheados SQPOLL sigue perdiendo contra la
syscall (1,6× peor en ext4), y aunque en tmpfs mejora al io_uring normal (1,4×), sigue estando
1,9× por detrás de pread. Encima se come un core con el hilo del kernel, que en una máquina
de 4 vCPU es el 25% de la granja. SQPOLL es para operaciones que de verdad bloquean, no para
amortizar cruces sobre caché.
9.4 · Coste en syscalls de un compilado — MEDIDO (syscalls-compilador.txt)
La corrida original decía «sin strace/perf en esta máquina no se puede atribuir». El
strace estaba en el propio corpus (store/3c1fd8c0…-strace, 6.19): la distro se paga a sí
misma la herramienta de diagnóstico. Un -c de un main() trivial con stdio/stdlib/string:
| syscalls | con error | el grueso | |
|---|---|---|---|
cc -O2 -c (gcc 16) |
1 803 | 1 095 (61%) | 919 de 923 readlink FALLAN |
zig cc -O2 -c (zig 0.16) |
3 608 | 214 (6%) | 1 104 futex (70% del tiempo en syscall) |
Dos lecturas:
- La tormenta de rutas de T8 existe y es de
readlink, no destat— canonicalización de rutas de gcc, que T8 no medía. Pero al piso de 78,7 ns, 1 803 cruces son 0,14 ms sobre 30,5 ms de compilado: el 0,5%. La respuesta a la pregunta original («¿vale segundos o minutos por receta?») es milisegundos por invocación: segundos por millar de compilados. No es ahí donde está el tiempo de una tanda. zig cctiene otro perfil: casi no falla rutas, pero coordina hilos. El tiempo questrace -cle atribuye es espera bloqueada, no CPU — y por esostrace -cno sirve para comparar compiladores, sólo para contar llamadas.
9.5 · Iterador BPF contra /proc — MEDIDO (bench_bpf_iter.c + bpf_iter_task.bpf.c → bpf-iter.txt)
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:
- 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 sinDEBUG_INFO_BTFno publica/sys/kernel/btf/vmlinuxy no hay a qué atarse. No hay versión de esto que esquive H7. - Privilegio:
CAP_BPF+CAP_PERFMONo root. Sin él,bpf_object__loadsaleOperation not permitted. Una Card no lo tiene ni debe tenerlo ⇒ el consumidor tiene que ser el proceso privilegiado. Y del lado tawasuyu eso no es un cambio en el sitio: el barrido de/procde hoy vive ensandokan-local, que corre sin privilegio (se mete en un user namespace a propósito), mientras que el único proceso privilegiado de la máquina essandokan-daemon—lo dice su propiocortafuegos.rs—. O sea que W2 por esta vía no es un reemplazo en el lugar: mueve la telemetría al otro lado de la frontera de privilegio, y ése es un coste de diseño que no aparece en el cociente de 7×.
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
- 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
docs/evidencia/tasas-kernel-2026-08-29/ — primera corrida: cuatro programas en C
(bench_proc.c, bench_io.c, bench_extra.c, bench_pin.c) y sus cinco salidas crudas
(proc.txt, io-ext4.txt, io-tmpfs.txt, extra.txt, pin.txt). Se compilan con
cc -O2 (bench_io pide -luring, bench_proc/bench_pin piden -lpthread) y se corren
sin argumentos salvo bench_io <dir> y bench_pin <estatico> <dinamico> <musl>.
Nota de unidad: en pin.txt la sección de fallos de página imprime ns/página × 1000 por
un factor de escala equivocado en el programa; los valores citados en T13 son los de esa
salida divididos por 1000, y coinciden con io-ext4.txt, que sí está en la unidad correcta.
Segunda corrida (§9, con root): bench_connector.c → connector.txt; bench_reflink.c →
reflink.txt (xfs y btrfs sobre loop, montados y desmontados por el propio guion);
bench_sqpoll.c → sqpoll.txt; y syscalls-compilador.txt, hecho con el strace del store.
Se compilan igual que los otros (bench_sqpoll pide -luring) y los tres primeros piden root:
bench_connector por CAP_NET_ADMIN, bench_reflink por mkfs+mount, bench_sqpoll por
CAP_SYS_NICE.
Contrato de capacidades: docs/state/kernel-contract.toml (el dato) y
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.
Cuarta corrida (§7-H7, precio de BTF): btf-precio.txt —el delta de bzImage, la sección .BTF,
las condiciones y las dos deps que hubo que añadir— junto con linux-generic-btf.toml (la receta
derivada, con las instrucciones para reconstruirla) y config-7.1.2-generic-btf (el .config
producido, que es donde se comprueba que BTF sobrevivió al olddefconfig). No es un
microbenchmark: es un kernel construido, así que sus números no se cruzan con los de §2 ni §9.
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:
las demás son mediciones de un momento, ésta es un veredicto de hoy.