From bfa3c6445af08169ae086640682bda60f91ab5b9 Mon Sep 17 00:00:00 2001 From: Sergio Date: Sat, 29 Aug 2026 19:34:16 +0000 Subject: [PATCH] =?UTF-8?q?tasas:=20SDD=2025=20al=20d=C3=ADa=20=E2=80=94?= =?UTF-8?q?=20=C2=A74=20confirmado=20sobre=20artefactos,=20=C2=A79=20con?= =?UTF-8?q?=20cuatro=20medidos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §0 y §4.bis: el «sin MEMCG» deja de ser deducción del defconfig. Los ONCE .config sellados traen `# CONFIG_MEMCG is not set` y `# CONFIG_PSI is not set`, y ninguno tiene CFS_BANDWIDTH (⇒ tampoco existe `cpu.max`; hoy nadie lo pide). Y se nombra por qué sobrevivió: el kernel de la máquina de desarrollo SÍ los trae — el fallo sólo existe del lado del artefacto. - §4.ter: el guardián `hammer kernel contract`, con las tres decisiones de diseño (por perfil, contra el .config y no la receta, apagado ≠ ausente). - §3: de tres sorpresas a cinco (taskstats no es más rápido que /proc; el sondeo es ciego, no lento). - §9: reescrita. Cuatro de los seis experimentos nombrados quedaron medidos con root; siguen abiertos el iterador BPF (detrás de H7) y el Intel con PTI. - §5, §7 y §10: filas medidas, H1 marcado como pendiente-pero-vigilado, H8 (el contrato es parte del release) y H9 (reflink alimenta SDD 18). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih --- docs/25-tasas-del-kernel.md | 214 ++++++++++++++++++++++++++++++++---- 1 file changed, 194 insertions(+), 20 deletions(-) diff --git a/docs/25-tasas-del-kernel.md b/docs/25-tasas-del-kernel.md index 86cd351b..18cf1c14 100644 --- a/docs/25-tasas-del-kernel.md +++ b/docs/25-tasas-del-kernel.md @@ -28,6 +28,12 @@ El espejo de este documento para el lado tawasuyu es 3. **La medición encontró un bug cruzado que no se buscaba**: los kernels de hammer se construyen **sin `CONFIG_MEMCG`**, y arje/sandokan escriben `memory.max` ignorando el error. Un límite de memoria pedido por una Card **no se aplica y nadie se entera**. §4. + Confirmado después sobre los **11 `.config` sellados**, no deducido del defconfig (§4.bis), y + ya vigilado por `hammer kernel contract` (§4.ter): 2 de 11 cumplen su perfil. +4. **Sondear `/proc` no 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 de `read+write` + (§9.2). ## 1. Método y condiciones — leer antes de citar un número @@ -41,6 +47,9 @@ El espejo de este documento para el lado tawasuyu es - 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**. @@ -221,7 +230,7 @@ ruido. Gratis, y ya está en mainline. `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 tres sorpresas +## 3. Las cinco sorpresas Las anoto aparte porque contradicen el consejo de manual: @@ -231,6 +240,13 @@ Las anoto aparte porque contradicen el consejo de manual: futuro; sobre datos cacheados el futuro pierde 2,8×. 3. **`stat` cuesta 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. +4. **`taskstats` binario 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 que `open`+`read`+parse. Su valor es el **contenido** (delay accounting, que `/proc` no + da) y la estabilidad bajo carga, no la velocidad. +5. **Sondear `/proc` no 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` @@ -263,6 +279,59 @@ match std::fs::write(&path, format!("{mem}\n")) { 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-`, 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: + +```sh +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: + +1. **Por perfil, no global** — misma lección que el gate de hardware. `linux.toml` es el kernel de + QEMU del selfhost-verify: no hospeda Cards y su hash es load-bearing del baseline `of_tree`. + Exigirle contabilidad de memoria sería rechazar una receta sana, así que ahí los límites por + unidad son `wants` y no `requires`. +2. **Contra el `.config`, no contra la receta** — entre el `scripts/config -e X` y el `.config` + está `olddefconfig`, que puede tragarse el símbolo sin decir nada (es justo lo que + `hammer kernel diff-back` ya vigila para los planes). +3. **Distingue apagado de ausente** — un símbolo que el `.config` ni 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. + +Estado medido hoy: **2 de 11 configs sellados cumplen su perfil** (los dos `linux`), los 9 de +perfil `anfitrion-cards` fallan por `MEMCG`. 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. @@ -276,11 +345,12 @@ v2 es `obj-y` en `kernel/cgroup/Makefile`, no el `CONFIG_CGROUP_FREEZER` de v1. | 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 | deja de sondear | +| 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 @@ -315,7 +385,10 @@ Su precio, concreto: - **H1 · `-e MEMCG -e PSI` en las cuatro recetas de kernel.** Cierra §4 y habilita `memory.current` (T4) y las señales de presión. **Precio declarado**: re-hashea todos los artefactos de kernel ⇒ hacerlo en la misma tanda que cualquier otro cambio de kernel, nunca - suelto. + suelto. **Sigue PENDIENTE a propósito** — pero ya no puede olvidarse: desde §4.ter + `hammer kernel contract --sealed` sale distinto de cero mientras falte. Cuando se pague, el + mismo comando es la prueba de que se pagó. Nota de alcance: en `linux.toml` (perfil + `qemu-serial`) NO corre — su hash es el baseline del selfhost-verify y ahí no hay Cards. - **H2 · `MODULES=n` se 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 · `harkaq` usa `openat2 RESOLVE_BENEATH|RESOLVE_NO_SYMLINKS`** donde hoy valida rutas @@ -329,6 +402,15 @@ Su precio, concreto: 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. +- **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 `.dmerge` sobre + 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 @@ -337,7 +419,9 @@ Detalle en el handoff espejo. En una línea cada una: - **W1 · `pidfd` para liveness y señales** en `sandokan-local` (hoy `access("/proc/")` y `waitpid(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**: si `memory.current` no existe (kernel sin MEMCG), decirlo, no callarlo. + capacidad**: si `memory.current` no existe (kernel sin MEMCG), decirlo, no callarlo. Del lado + hammer esa sonda ya tiene con qué contrastarse: `hammer kernel contract --list --json` publica + qué promete cada perfil de kernel (§4.ter), que es el W6 en formato de dato. - **W3 · `arje-incarnate` con `posix_spawn`** donde el `pre_exec` no sea imprescindible (T1), y `close_range` en el que quede. - **W4 · Los errores de cgroup dejan de descartarse.** `let _ = write(...)` y `warn!` sobre un @@ -348,27 +432,106 @@ 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. Lo que NO pude medir (experimentos nombrados) +## 9. Los experimentos nombrados — cuatro medidos, dos abiertos -Ninguno se omite en silencio: +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 +(load ~1,3 sobre 4 vCPU, 4,3 GiB libres) ⇒ los números de acá **no se cruzan** con los de §2. -1. **proc connector y `taskstats`**: suscribirse al netlink pide `CAP_NET_ADMIN`; el uid de - la sesión es 1001. Experimento: un binario chico corriendo como root en la granja, - midiendo latencia de evento `fork/exec/exit` contra el sondeo de `/proc`. -2. **Copia con reflink** (T11): exige `mkfs` sobre un loop (root). Experimento: xfs y btrfs - en loop, `copy_file_range` de 64 MiB, esperado O(1) contra los 2 837 MB/s de ext4. -3. **Iterador BPF contra `/proc`** (T3): exige BTF y privilegios. Va detrás de H7. -4. **io_uring con `SQPOLL`**: exige `CAP_SYS_NICE`. Cambiaría el veredicto de T12 para el - caso «muchas operaciones que bloquean». -5. **Coste real de una tanda de hammer en syscalls**: sin `strace`/`perf` en esta máquina no - se puede atribuir. Experimento: instalarlos en la granja y contar `stat`/`open` por - invocación de compilador, para saber si T8 vale segundos o minutos por receta. -6. **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. +### 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 de `stat`** — 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 cc` tiene otro perfil**: casi no falla rutas, pero coordina hilos. El tiempo que + `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 + +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 + (TigerLake) las mismas medidas darían peor, y ahí T3/T6 duelen más. ## 10. Evidencia -`docs/evidencia/tasas-kernel-2026-08-29/` — cuatro programas en C +`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 @@ -377,3 +540,14 @@ sin argumentos salvo `bench_io ` y `bench_pin ` **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.