From aeeaa6d07fcd98d955e98e3b6779693a80f07cf5 Mon Sep 17 00:00:00 2001 From: Sergio Date: Sat, 29 Aug 2026 13:35:55 +0000 Subject: [PATCH] SDD 25: las tasas del kernel medidas, y el MEMCG que falta en las recetas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 16 tasas de Linux medidas en momento (KVM, 4 vCPU, bajo carga): piso de syscall, /proc como API de texto, fork vs espacio de direcciones del padre, enlazado dinámico, VFS, io_uring, THP, namespaces. Programas en C y salidas crudas en docs/evidencia/tasas-kernel-2026-08-29/. Lo que sale de medir y después verificar contra las recetas: los kernels de hammer se construyen sin CONFIG_MEMCG (no está en x86_64_defconfig y no tiene default y), y arje/sandokan escriben memory.max descartando el error. Un tope de memoria pedido por una Card no se aplica y sólo deja un warn. Tres resultados que contradicen el consejo de manual y quedan documentados: THP 3x más lento que 4K con defrag=madvise, io_uring 2,8x más lento que pread sobre tmpfs, y stat cuesta lo mismo en tmpfs que en ext4 (es tasa del VFS). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih --- docs/25-tasas-del-kernel.md | 379 ++++++++++++++++++ .../tasas-kernel-2026-08-29/bench_extra.c | 92 +++++ .../tasas-kernel-2026-08-29/bench_io.c | 199 +++++++++ .../tasas-kernel-2026-08-29/bench_pin.c | 53 +++ .../tasas-kernel-2026-08-29/bench_proc.c | 147 +++++++ .../tasas-kernel-2026-08-29/extra.txt | 23 ++ .../tasas-kernel-2026-08-29/io-ext4.txt | 40 ++ .../tasas-kernel-2026-08-29/io-tmpfs.txt | 27 ++ .../evidencia/tasas-kernel-2026-08-29/pin.txt | 18 + .../tasas-kernel-2026-08-29/proc.txt | 35 ++ 10 files changed, 1013 insertions(+) create mode 100644 docs/25-tasas-del-kernel.md create mode 100644 docs/evidencia/tasas-kernel-2026-08-29/bench_extra.c create mode 100644 docs/evidencia/tasas-kernel-2026-08-29/bench_io.c create mode 100644 docs/evidencia/tasas-kernel-2026-08-29/bench_pin.c create mode 100644 docs/evidencia/tasas-kernel-2026-08-29/bench_proc.c create mode 100644 docs/evidencia/tasas-kernel-2026-08-29/extra.txt create mode 100644 docs/evidencia/tasas-kernel-2026-08-29/io-ext4.txt create mode 100644 docs/evidencia/tasas-kernel-2026-08-29/io-tmpfs.txt create mode 100644 docs/evidencia/tasas-kernel-2026-08-29/pin.txt create mode 100644 docs/evidencia/tasas-kernel-2026-08-29/proc.txt diff --git a/docs/25-tasas-del-kernel.md b/docs/25-tasas-del-kernel.md new file mode 100644 index 00000000..86cd351b --- /dev/null +++ b/docs/25-tasas-del-kernel.md @@ -0,0 +1,379 @@ +# 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 + +1. **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.toml` no lo apaga pero sólo compila + `bzImage`, nunca `make 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 fase `configure` **es** la identidad del artefacto (SDD 22) y así queda sellado + y atestado junto al kernel. +2. **Casi todo lo que duele ya tiene salida en mainline y nadie la está usando.** `pidfd`, + `cgroup` v2 agregado, `posix_spawn`, `getdents64`, `close_range`, `openat2`. Cero código + de kernel, ganancias de 3× a 17× medidas abajo. +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. + +## 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), `/tmp` tmpfs. +- **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. +- 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**. + +### 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/`: la carrera, no la lentitud + +| | ns | +|---|---| +| `access("/proc/")` — 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 tres sorpresas + +Las anoto aparte porque contradicen el consejo de manual: + +1. **THP más lento que 4K** (T13) — el manual dice que THP acelera; con `defrag=madvise` y + memoria apretada, triplica el coste del fallo. +2. **io_uring más lento que `pread`** sobre tmpfs (T12) — el manual dice que io_uring es el + 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. 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í: + +```rust +// 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. + +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 | deja de sondear | +| 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 | +| 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: + +1. **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». +2. **Eventos de estado con más contexto que el proc connector.** +3. **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 en `hash_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.md` promete *«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 `/proc` como respaldo que no se borra.** + +## 7. Propuestas — lado hammer + +- **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. +- **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 + a mano (T9, ~gratis). +- **H4 · Auditar los `Command` de hammer que ponen `pre_exec`**: Rust apaga el camino + `posix_spawn` en cuanto hay `pre_exec`, y cae a `fork+exec` (T1). `hammer-build/sandbox.rs` + lanza `bwrap` sin `pre_exec` ⇒ está bien hoy; el punto es que sea una regla, no una suerte. +- **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** 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. + +## 8. Propuestas — lado tawasuyu + +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. +- **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 + límite no aplicado son el bug de §4, no un detalle de estilo. +- **W5 · `/proc` no 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 con `hammer kernel probe`); tawasuyu elige + backend en runtime. + +## 9. Lo que NO pude medir (experimentos nombrados) + +Ninguno se omite en silencio: + +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. + +## 10. Evidencia + +`docs/evidencia/tasas-kernel-2026-08-29/` — 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 ` 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. diff --git a/docs/evidencia/tasas-kernel-2026-08-29/bench_extra.c b/docs/evidencia/tasas-kernel-2026-08-29/bench_extra.c new file mode 100644 index 00000000..02063073 --- /dev/null +++ b/docs/evidencia/tasas-kernel-2026-08-29/bench_extra.c @@ -0,0 +1,92 @@ +/* bench_extra.c — lo que las dos sorpresas piden aislar: + * a) coste de fork EN FUNCION del espacio de direcciones del padre + * b) contabilidad por cgroup v2 vs sumar /proc + * c) tasa del enlazado dinamico en el exec + * d) cierre de descriptores en el spawn */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; +static volatile long sink; +static inline double now_ns(void){ struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec*1e9+t.tv_nsec; } +static int cmpd(const void*a,const void*b){ double x=*(const double*)a,y=*(const double*)b; return (x>y)-(x1?argv[1]:"/bin/true"; + const char *dyn = argc>2?argv[2]:"/bin/true"; + printf("%-44s %10s %10s\n","BENCH (us/op)","min","mediana"); + printf("--- a) fork segun el espacio de direcciones del padre ---\n"); + long mbs[]={0,16,64,256}; + for(unsigned k=0;k +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; +static volatile long sink; +static inline double now_ns(void){ struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec*1e9+t.tv_nsec; } +static int cmpd(const void*a,const void*b){ double x=*(const double*)a,y=*(const double*)b; return (x>y)-(x=0){ (void)!write(fd,"x",1); close(fd);} } + /* ruta profunda: base/d/d/d/... (12 niveles) con un fichero al fondo */ + char deep[600]; snprintf(deep,sizeof deep,"%s/deep",base); mkdir(deep,0755); + for(int i=0;i<12;i++){ strcat(deep,"/d"); mkdir(deep,0755); } + char df[700]; snprintf(df,sizeof df,"%s/x",deep); + int fd=open(df,O_CREAT|O_WRONLY,0644); if(fd>=0){(void)!write(fd,"x",1);close(fd);} +} +static void rm_tree(const char *base){ char c[700]; snprintf(c,sizeof c,"rm -rf '%s'",base); (void)!system(c); } + +static void metadata(const char *base,const char *etiqueta){ + char flat[600]; snprintf(flat,sizeof flat,"%s/flat",base); + int dfd=open(flat,O_RDONLY|O_DIRECTORY); + printf("\n### metadatos sobre %s (%d ficheros en un directorio)\n",etiqueta,NFILES); + printf("%-40s %10s %10s\n","BENCH (ns/fichero)","min","mediana"); + BENCH("stat() por ruta completa", NFILES, {struct stat s;char f[700];snprintf(f,sizeof f,"%s/f%05d",flat,(int)(i%NFILES));sink=stat(f,&s);}); + BENCH("fstatat(dirfd, nombre)", NFILES, {struct stat s;char f[32];snprintf(f,sizeof f,"f%05d",(int)(i%NFILES));sink=fstatat(dfd,f,&s,0);}); + BENCH("statx(dirfd,nombre,DONT_SYNC)", NFILES, {struct statx s;char f[32];snprintf(f,sizeof f,"f%05d",(int)(i%NFILES));sink=syscall(SYS_statx,dfd,f,AT_STATX_DONT_SYNC,STATX_BASIC_STATS,&s);}); + BENCH("openat(dirfd,nombre)+close", NFILES, {char f[32];snprintf(f,sizeof f,"f%05d",(int)(i%NFILES));int fd=openat(dfd,f,O_RDONLY);if(fd>=0)close(fd);}); + { struct open_how how; memset(&how,0,sizeof how); how.flags=O_RDONLY; how.resolve=RESOLVE_BENEATH|RESOLVE_NO_SYMLINKS; + int probe=syscall(SYS_openat2,dfd,"f00000",&how,sizeof how); + if(probe>=0){ close(probe); + BENCH("openat2 RESOLVE_BENEATH+NO_SYMLINKS",NFILES,{char f[32];snprintf(f,sizeof f,"f%05d",(int)(i%NFILES));int fd=syscall(SYS_openat2,dfd,f,&how,sizeof how);if(fd>=0)close(fd);}); + } else printf("openat2: no disponible (%s)\n",strerror(errno)); } + /* getdents64: barrido completo, coste por entrada */ + { double r[ROUNDS]; long cnt=0; + for(int q=0;q0){ long off=0; while(offd_reclen; } } + close(fd); r[q]=(now_ns()-t0)/(double)c; cnt=c; } + report("getdents64 (barrido, sin stat)",r,ROUNDS,cnt); } + close(dfd); + /* profundidad de ruta */ + { char shallow[700],deep[700]; snprintf(shallow,sizeof shallow,"%s/flat/f00000",base); + snprintf(deep,sizeof deep,"%s/deep",base); for(int i=0;i<12;i++) strcat(deep,"/d"); strcat(deep,"/x"); + BENCH("open ruta corta (2 componentes bajo base)",20000,{int fd=open(shallow,O_RDONLY);if(fd>=0)close(fd);}); + BENCH("open ruta 13 componentes mas profunda", 20000,{int fd=open(deep,O_RDONLY);if(fd>=0)close(fd);}); } +} + +static void copia(const char *base){ + const long SZ = 64L*1024*1024; + char src[700],dst[700]; snprintf(src,sizeof src,"%s/src.bin",base); snprintf(dst,sizeof dst,"%s/dst.bin",base); + int fd=open(src,O_CREAT|O_WRONLY|O_TRUNC,0644); + char *buf=malloc(1<<20); memset(buf,0xA5,1<<20); + for(long w=0;w>20); + printf("%-40s %10s %10s\n","BENCH (MB/s)","max","mediana"); + double r[ROUNDS]; + for(int q=0;q0) (void)!write(d,b,n); + r[q]=SZ/((now_ns()-t0)/1e9)/1e6; close(s);close(d);} + qsort(r,ROUNDS,sizeof(double),cmpd); printf("%-40s %10.1f %10.1f\n","read()+write() 64K",r[ROUNDS-1],r[ROUNDS/2]); + for(int q=0;q0){ ssize_t n=syscall(SYS_copy_file_range,s,&io,d,&oo,left,0); if(n<=0)break; left-=n; } + r[q]=SZ/((now_ns()-t0)/1e9)/1e6; close(s);close(d);} + qsort(r,ROUNDS,sizeof(double),cmpd); printf("%-40s %10.1f %10.1f\n","copy_file_range()",r[ROUNDS-1],r[ROUNDS/2]); + /* materializacion de arbol: hardlink vs copia, 2000 ficheros pequeños */ + char lk[700]; snprintf(lk,sizeof lk,"%s/mat",base); mkdir(lk,0755); + printf("\n### materializar 2000 ficheros de 1 byte (el problema del store)\n"); + BENCH("link() [hardlink]",2000,{char a[700],b[700];snprintf(a,sizeof a,"%s/flat/f%05d",base,(int)i);snprintf(b,sizeof b,"%s/mat/l%05d",base,(int)i);unlink(b);sink=link(a,b);}); + BENCH("open+read+write+close [copia]",2000,{char a[700],b[700];char c[64];snprintf(a,sizeof a,"%s/flat/f%05d",base,(int)i);snprintf(b,sizeof b,"%s/mat/c%05d",base,(int)i);int s=open(a,O_RDONLY),d=open(b,O_CREAT|O_WRONLY|O_TRUNC,0644);ssize_t n=read(s,c,sizeof c);(void)!write(d,c,n);close(s);close(d);}); + unlink(src); unlink(dst); +} + +static void uring(const char *base){ + const long SZ=64L*1024*1024; const int BS=4096; const int DEPTH=32; + char src[700]; snprintf(src,sizeof src,"%s/u.bin",base); + int fd=open(src,O_CREAT|O_RDWR|O_TRUNC,0644); + char *b=malloc(1<<20); memset(b,7,1<<20); for(long w=0;w1?argv[1]:"/tmp/bench-linux"; + snprintf(root,sizeof root,"%s",base); + rm_tree(root); mk_tree(root); + metadata(root, base); + copia(root); + uring(root); + if(argc<=2){ memoria(); namespaces(); } + rm_tree(root); + return 0; +} diff --git a/docs/evidencia/tasas-kernel-2026-08-29/bench_pin.c b/docs/evidencia/tasas-kernel-2026-08-29/bench_pin.c new file mode 100644 index 00000000..847be262 --- /dev/null +++ b/docs/evidencia/tasas-kernel-2026-08-29/bench_pin.c @@ -0,0 +1,53 @@ +/* bench_pin.c — mismas medidas, PINNEADAS a un cpu, para sacar la cola del scheduler + * de en medio y dejar ver el efecto puro. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern char **environ; +static inline double now_ns(void){ struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec*1e9+t.tv_nsec; } +static int cmpd(const void*a,const void*b){ double x=*(const double*)a,y=*(const double*)b; return (x>y)-(x1?argv[1]:"/bin/true", *dyn=argc>2?argv[2]:"/bin/true", *mus=argc>3?argv[3]:sta; + printf("%-46s %9s %9s\n","BENCH (us/op), PINNEADO a 1 cpu","min","mediana"); + printf("--- fork vs espacio de direcciones del padre ---\n"); + long mbs[]={0,64,256}; + for(unsigned k=0;k<3;k++){ grow(mbs[k]); double a[R],b[R]; char n[90]; + for(int q=0;q +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; +static volatile long sink; + +static inline double now_ns(void){ struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec*1e9+t.tv_nsec; } +static int cmpd(const void*a,const void*b){ double x=*(const double*)a,y=*(const double*)b; return (x>y)-(xiters;i++){ rd1(r->a[0],r->kind); wr1(r->b[1],r->kind); } return 0; } + +static double rtt_bench(int kind,long iters){ + struct rtt r; r.kind=kind; r.iters=iters; + if(kind==0){ if(pipe(r.a)||pipe(r.b)) return -1; } + else if(kind==1){ if(socketpair(AF_UNIX,SOCK_STREAM,0,r.a)||socketpair(AF_UNIX,SOCK_STREAM,0,r.b)) return -1; } + else { r.a[0]=r.a[1]=eventfd(0,0); r.b[0]=r.b[1]=eventfd(0,0); } + pthread_t t; pthread_create(&t,0,echo,&r); pin(3); + double best=1e18; + for(int q=0;q<3;q++){ long n=iters/3; double t0=now_ns(); + for(long i=0;i) [lo que se usa hoy]",200000, sink=access(p,F_OK)); + BENCH("kill(pid,0)", 300000, sink=kill(me,0)); + if(pfd>=0){ BENCH("pidfd_send_signal(fd,0) [sin carrera]",300000, sink=syscall(SYS_pidfd_send_signal,pfd,0,NULL,0)); } + else printf("pidfd_send_signal: no disponible (%s)\n",strerror(errno)); + BENCH("pidfd_open+close", 100000, {int f=syscall(SYS_pidfd_open,me,0); if(f>=0)close(f);}); + if(pfd>=0) close(pfd); } + + printf("--- D. barrido de /proc (enumeracion) ---\n"); + { int nproc=0; + double r1[ROUNDS],r2[ROUNDS]; + for(int q=0;qd_name[0]<'0'||e->d_name[0]>'9')continue; c++; } + closedir(d); r1[q]=now_ns()-t0; nproc=c; + t0=now_ns(); d=opendir("/proc"); + while((e=readdir(d))){ if(e->d_name[0]<'0'||e->d_name[0]>'9')continue; + char p[512]; snprintf(p,sizeof p,"/proc/%s/stat",e->d_name); + int fd=open(p,O_RDONLY); if(fd<0)continue; sink=read_stat_utime(fd); close(fd); } + closedir(d); r2[q]=now_ns()-t0; } + qsort(r1,ROUNDS,sizeof(double),cmpd); qsort(r2,ROUNDS,sizeof(double),cmpd); + printf("procesos vivos = %d\n",nproc); + printf("%-38s %10.1f %10.1f (us/barrido)\n","solo readdir(/proc)",r1[0]/1000,r1[ROUNDS/2]/1000); + printf("%-38s %10.1f %10.1f (us/barrido)\n","readdir + stat de cada pid",r2[0]/1000,r2[ROUNDS/2]/1000); + printf("%-38s %10.1f %10.1f (ns/proceso)\n"," => coste por proceso",r2[0]/nproc,r2[ROUNDS/2]/nproc); } + + printf("--- E. creacion de procesos ---\n"); + BENCH("fork + _exit + waitpid", 400, {pid_t p=fork(); if(p==0)_exit(0); int st;waitpid(p,&st,0);}); + BENCH("vfork + execve(/bin/true) + wait", 300, {pid_t p=vfork(); if(p==0){char*a[]={"/bin/true",0};execve(a[0],a,environ);_exit(127);} int st;waitpid(p,&st,0);}); + { BENCH("posix_spawn(/bin/true) + wait", 300, {pid_t p;char*a[]={"/bin/true",0};if(posix_spawn(&p,a[0],0,0,a,environ)==0){int st;waitpid(p,&st,0);} }); } + + printf("--- F. IPC: round-trip entre 2 hilos (mismo cpu) ---\n"); + { pthread_mutex_t m=PTHREAD_MUTEX_INITIALIZER; + BENCH("pthread_mutex lock+unlock (sin disputa)",1000000,{pthread_mutex_lock(&m);pthread_mutex_unlock(&m);}); } + printf("%-38s %10.1f (RTT)\n","pipe read/write", rtt_bench(0,60000)); + printf("%-38s %10.1f (RTT)\n","socketpair AF_UNIX", rtt_bench(1,60000)); + printf("%-38s %10.1f (RTT)\n","eventfd", rtt_bench(2,60000)); + { signal(SIGUSR1,h); + BENCH("kill(self,SIGUSR1) + handler", 200000, {got=0;kill(getpid(),SIGUSR1);sink=got;}); } + + printf("--- G. temporizadores ---\n"); + { double r[ROUNDS]; + for(int q=0;q) [lo que se usa hoy] 344.9 352.4 (n=200000) +kill(pid,0) 119.0 126.0 (n=300000) +pidfd_send_signal(fd,0) [sin carrera] 119.0 129.1 (n=300000) +pidfd_open+close 413.7 423.4 (n=100000) +--- D. barrido de /proc (enumeracion) --- +procesos vivos = 291 +solo readdir(/proc) 106.0 121.2 (us/barrido) +readdir + stat de cada pid 990.7 1061.7 (us/barrido) + => coste por proceso 3404.3 3648.5 (ns/proceso) +--- E. creacion de procesos --- +fork + _exit + waitpid 83874.0 87347.9 (n=400) +vfork + execve(/bin/true) + wait 324621.2 353117.9 (n=300) +posix_spawn(/bin/true) + wait 323507.4 335470.5 (n=300) +--- F. IPC: round-trip entre 2 hilos (mismo cpu) --- +pthread_mutex lock+unlock (sin disputa) 8.6 8.7 (n=1000000) +pipe read/write 2955.8 (RTT) +socketpair AF_UNIX 3638.5 (RTT) +eventfd 2507.1 (RTT) +kill(self,SIGUSR1) + handler 1165.1 1188.5 (n=200000) +--- G. temporizadores --- +nanosleep(1ms) overshoot 65179.4 70381.1 (sobrecoste ns sobre 1ms)