SDD 25: las tasas del kernel medidas, y el MEMCG que falta en las recetas

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
This commit is contained in:
Sergio
2026-08-29 13:35:55 +00:00
co-authored by Claude Opus 5
parent 2176f26c05
commit aeeaa6d07f
10 changed files with 1013 additions and 0 deletions
+379
View File
@@ -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 79 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/<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 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/<pid>")` 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 <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.
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>
#include <spawn.h>
#include <dirent.h>
#include <sched.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/stat.h>
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<y); }
#define ROUNDS 7
static void rep(const char*n,double*r,int c){ qsort(r,c,sizeof(double),cmpd);
printf("%-44s %10.1f %10.1f\n",n,r[0],r[c/2]); fflush(stdout); }
static double *area=NULL; static long area_sz=0;
static void grow(long mb){
if(area) munmap(area,area_sz);
area_sz=mb*1024L*1024; if(mb==0){area=NULL;return;}
area=mmap(0,area_sz,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
madvise(area,area_sz,MADV_NOHUGEPAGE); /* tablas de paginas reales, no THP */
for(long o=0;o<area_sz;o+=4096) ((char*)area)[o]=1;
}
static double t_fork(int n){ double t0=now_ns();
for(int i=0;i<n;i++){ pid_t p=fork(); if(p==0)_exit(0); int st; waitpid(p,&st,0);} return (now_ns()-t0)/n/1000; }
static double t_vfork_exec(int n,const char*prog){ double t0=now_ns();
for(int i=0;i<n;i++){ pid_t p=vfork(); if(p==0){char*a[]={(char*)prog,0};execve(prog,a,environ);_exit(127);} int st; waitpid(p,&st,0);} return (now_ns()-t0)/n/1000; }
static double t_fork_exec(int n,const char*prog){ double t0=now_ns();
for(int i=0;i<n;i++){ pid_t p=fork(); if(p==0){char*a[]={(char*)prog,0};execve(prog,a,environ);_exit(127);} int st; waitpid(p,&st,0);} return (now_ns()-t0)/n/1000; }
static double t_spawn(int n,const char*prog){ double t0=now_ns();
for(int i=0;i<n;i++){ pid_t p; char*a[]={(char*)prog,0}; if(posix_spawn(&p,prog,0,0,a,environ)==0){int st;waitpid(p,&st,0);} } return (now_ns()-t0)/n/1000; }
int main(int argc,char**argv){
const char *sta = argc>1?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<sizeof mbs/sizeof*mbs;k++){
grow(mbs[k]); double r[ROUNDS],r2[ROUNDS],r3[ROUNDS];
for(int q=0;q<ROUNDS;q++) r[q]=t_fork(150);
for(int q=0;q<ROUNDS;q++) r2[q]=t_fork_exec(120,sta);
for(int q=0;q<ROUNDS;q++) r3[q]=t_spawn(120,sta);
char n1[80],n2[80],n3[80];
snprintf(n1,sizeof n1," padre %3ld MB tocados: fork+_exit",mbs[k]);
snprintf(n2,sizeof n2," padre %3ld MB tocados: fork+exec",mbs[k]);
snprintf(n3,sizeof n3," padre %3ld MB tocados: posix_spawn(CLONE_VFORK)",mbs[k]);
rep(n1,r,ROUNDS); rep(n2,r2,ROUNDS); rep(n3,r3,ROUNDS); }
grow(0);
printf("--- c) tasa del enlazado dinamico en exec ---\n");
{ double r[ROUNDS]; for(int q=0;q<ROUNDS;q++) r[q]=t_vfork_exec(150,sta); rep(" vfork+exec binario ESTATICO",r,ROUNDS);
for(int q=0;q<ROUNDS;q++) r[q]=t_vfork_exec(150,dyn); rep(" vfork+exec binario DINAMICO",r,ROUNDS); }
printf("--- d) cierre de descriptores antes del exec ---\n");
{ int base=open("/dev/null",O_RDONLY); int fds[512]; for(int i=0;i<512;i++) fds[i]=dup(base);
double r[ROUNDS];
for(int q=0;q<ROUNDS;q++){ double t0=now_ns(); for(int i=0;i<512;i++){int f=dup(base); close(f);} r[q]=(now_ns()-t0)/512/1000; }
rep(" dup+close x512, uno a uno (us/fd)",r,ROUNDS);
for(int q=0;q<ROUNDS;q++){ int a[512]; for(int i=0;i<512;i++) a[i]=dup(base);
double t0=now_ns(); sink=syscall(SYS_close_range,a[0],a[511],0); r[q]=(now_ns()-t0)/512/1000; }
rep(" close_range(lo,hi) (us/fd)",r,ROUNDS);
for(int i=0;i<512;i++) close(fds[i]); close(base); }
printf("--- b) contabilidad: cgroup v2 vs sumar /proc ---\n");
{ char cg[512]="", path[600]; FILE*f=fopen("/proc/self/cgroup","r");
if(f){ char line[512]; while(fgets(line,sizeof line,f)){ char*p=strstr(line,"0::"); if(p){ p+=3; p[strcspn(p,"\n")]=0; snprintf(cg,sizeof cg,"/sys/fs/cgroup%s",p);} } fclose(f);}
if(cg[0]){ printf(" cgroup: %s\n",cg);
const char*campos[]={"cpu.stat","memory.current","pids.current"};
double r[ROUNDS];
for(int q=0;q<ROUNDS;q++){ int N=2000; double t0=now_ns();
for(int i=0;i<N;i++){ for(int c=0;c<3;c++){ snprintf(path,sizeof path,"%s/%s",cg,campos[c]);
int fd=open(path,O_RDONLY); if(fd<0)continue; char b[512]; sink=read(fd,b,sizeof b); close(fd);} }
r[q]=(now_ns()-t0)/N/1000; }
rep(" leer cpu.stat+memory.current+pids.current",r,ROUNDS);
} else printf(" sin cgroup v2 legible\n"); }
return 0;
}
@@ -0,0 +1,199 @@
/* bench_io.c — metadatos de FS, resolucion de rutas, copia, io_uring, fallos de pagina, namespaces. */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>
#include <sched.h>
#include <sys/syscall.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <linux/openat2.h>
#include <liburing.h>
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<y); }
#define ROUNDS 7
static void report(const char*n,double*r,int c,long it){ qsort(r,c,sizeof(double),cmpd);
printf("%-40s %10.1f %10.1f (n=%ld)\n",n,r[0],r[c/2],it); fflush(stdout); }
#define BENCH(name,iters,...) do{ double _r[ROUNDS]; long _N=(iters); \
for(int _q=0;_q<ROUNDS;_q++){ double _t0=now_ns(); for(long i=0;i<_N;i++){ __VA_ARGS__; } \
_r[_q]=(now_ns()-_t0)/(double)_N; } report(name,_r,ROUNDS,_N);}while(0)
#define NFILES 4000
static char root[512];
static void mk_tree(const char *base){
mkdir(base,0755);
char p[600]; snprintf(p,sizeof p,"%s/flat",base); mkdir(p,0755);
for(int i=0;i<NFILES;i++){ char f[700]; snprintf(f,sizeof f,"%s/flat/f%05d",base,i);
int fd=open(f,O_CREAT|O_WRONLY|O_TRUNC,0644); if(fd>=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;q<ROUNDS;q++){ char buf[65536]; long c=0; double t0=now_ns();
int fd=open(flat,O_RDONLY|O_DIRECTORY); long n;
while((n=syscall(SYS_getdents64,fd,buf,sizeof buf))>0){ long off=0; while(off<n){ struct dirent64 *e=(void*)(buf+off); c++; off+=e->d_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<SZ;w+=1<<20) (void)!write(fd,buf,1<<20);
close(fd); free(buf);
printf("\n### copiar %ld MB (fichero en cache de pagina)\n",SZ>>20);
printf("%-40s %10s %10s\n","BENCH (MB/s)","max","mediana");
double r[ROUNDS];
for(int q=0;q<ROUNDS;q++){ int s=open(src,O_RDONLY),d=open(dst,O_CREAT|O_WRONLY|O_TRUNC,0644);
char b[65536]; ssize_t n; double t0=now_ns(); while((n=read(s,b,sizeof b))>0) (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;q<ROUNDS;q++){ int s=open(src,O_RDONLY),d=open(dst,O_CREAT|O_WRONLY|O_TRUNC,0644);
off_t off=0; double t0=now_ns(); while(off<SZ){ ssize_t n=sendfile(d,s,&off,SZ-off); if(n<=0)break; }
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","sendfile()",r[ROUNDS-1],r[ROUNDS/2]);
for(int q=0;q<ROUNDS;q++){ int s=open(src,O_RDONLY),d=open(dst,O_CREAT|O_WRONLY|O_TRUNC,0644);
loff_t io=0,oo=0; double t0=now_ns(); long left=SZ;
while(left>0){ 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;w<SZ;w+=1<<20)(void)!write(fd,b,1<<20); free(b);
fsync(fd);
printf("\n### 4K aleatorios sobre 64MB en cache: syscall vs io_uring\n");
printf("%-40s %10s %10s\n","BENCH (ns/lectura)","min","mediana");
char *buf=aligned_alloc(4096,BS*DEPTH);
long nblk=SZ/BS;
BENCH("pread() 4K",50000,{sink=pread(fd,buf,BS,(long)((i*7919)%nblk)*BS);});
struct io_uring ring;
if(io_uring_queue_init(256,&ring,0)==0){
double r[ROUNDS];
for(int q=0;q<ROUNDS;q++){ long done=0,total=50000; double t0=now_ns();
while(done<total){ int batch=DEPTH; if(total-done<batch)batch=total-done;
for(int k=0;k<batch;k++){ struct io_uring_sqe*s=io_uring_get_sqe(&ring);
io_uring_prep_read(s,fd,buf+k*BS,BS,(long)(((done+k)*7919)%nblk)*BS); }
io_uring_submit_and_wait(&ring,batch);
struct io_uring_cqe*c; unsigned h; int seen=0;
io_uring_for_each_cqe(&ring,h,c){ seen++; } io_uring_cq_advance(&ring,seen);
done+=batch; }
r[q]=(now_ns()-t0)/total; }
report("io_uring read 4K (profundidad 32)",r,ROUNDS,50000);
/* piso de submission: NOP en lote */
for(int q=0;q<ROUNDS;q++){ long done=0,total=200000; double t0=now_ns();
while(done<total){ int batch=64; if(total-done<batch)batch=total-done;
for(int k=0;k<batch;k++){ struct io_uring_sqe*s=io_uring_get_sqe(&ring); io_uring_prep_nop(s); }
io_uring_submit_and_wait(&ring,batch);
struct io_uring_cqe*c; unsigned h; int seen=0;
io_uring_for_each_cqe(&ring,h,c){ seen++; } io_uring_cq_advance(&ring,seen);
done+=batch; }
r[q]=(now_ns()-t0)/total; }
report("io_uring NOP (piso de operacion)",r,ROUNDS,200000);
io_uring_queue_exit(&ring);
} else printf("io_uring: no disponible (%s)\n",strerror(errno));
free(buf); close(fd); unlink(src);
}
static void memoria(void){
const long SZ=128L*1024*1024;
printf("\n### fallos de pagina: tocar 128MB anonimos (ns por pagina de 4K)\n");
printf("%-40s %10s %10s\n","BENCH","min","mediana");
double r[ROUNDS]; long pages=SZ/4096;
for(int q=0;q<ROUNDS;q++){ char*m=mmap(0,SZ,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
madvise(m,SZ,MADV_HUGEPAGE); double t0=now_ns();
for(long o=0;o<SZ;o+=4096) m[o]=1; r[q]=(now_ns()-t0)/pages; munmap(m,SZ);}
report("MADV_HUGEPAGE (THP)",r,ROUNDS,pages);
for(int q=0;q<ROUNDS;q++){ char*m=mmap(0,SZ,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
madvise(m,SZ,MADV_NOHUGEPAGE); double t0=now_ns();
for(long o=0;o<SZ;o+=4096) m[o]=1; r[q]=(now_ns()-t0)/pages; munmap(m,SZ);}
report("MADV_NOHUGEPAGE (4K de a uno)",r,ROUNDS,pages);
for(int q=0;q<ROUNDS;q++){ double t0=now_ns();
char*m=mmap(0,SZ,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE,-1,0);
for(long o=0;o<SZ;o+=4096) m[o]=1; r[q]=(now_ns()-t0)/pages; munmap(m,SZ);}
report("MAP_POPULATE (prefault)",r,ROUNDS,pages);
BENCH("mmap+munmap 4K (churn)",100000,{void*m=mmap(0,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);munmap(m,4096);});
}
static void namespaces(void){
printf("\n### coste del sandbox: crear namespaces (us por proceso)\n");
printf("%-40s %10s %10s\n","BENCH","min","mediana");
double r[ROUNDS];
for(int q=0;q<ROUNDS;q++){ int N=100; double t0=now_ns();
for(int i=0;i<N;i++){ pid_t p=fork(); if(p==0){ char*a[]={"/bin/true",0}; execve(a[0],a,environ); _exit(127);} int st; waitpid(p,&st,0);}
r[q]=(now_ns()-t0)/N/1000; } report("fork+exec /bin/true (base)",r,ROUNDS,100);
int fallo=0;
for(int q=0;q<ROUNDS;q++){ int N=100; double t0=now_ns();
for(int i=0;i<N;i++){ pid_t p=fork();
if(p==0){ if(unshare(CLONE_NEWUSER|CLONE_NEWNS|CLONE_NEWPID|CLONE_NEWIPC|CLONE_NEWUTS|CLONE_NEWNET)<0) _exit(9);
char*a[]={"/bin/true",0}; execve(a[0],a,environ); _exit(127);}
int st; waitpid(p,&st,0); if(WEXITSTATUS(st)==9) fallo=1; }
r[q]=(now_ns()-t0)/N/1000; }
if(fallo) printf("unshare completo: no permitido para este uid\n");
else report("fork+unshare(6 ns)+exec",r,ROUNDS,100);
for(int q=0;q<ROUNDS;q++){ int N=100; double t0=now_ns();
for(int i=0;i<N;i++){ pid_t p=fork();
if(p==0){ unshare(CLONE_NEWUSER|CLONE_NEWNS); char*a[]={"/bin/true",0}; execve(a[0],a,environ); _exit(127);}
int st; waitpid(p,&st,0); }
r[q]=(now_ns()-t0)/N/1000; } report("fork+unshare(USER|MNT)+exec",r,ROUNDS,100);
}
int main(int argc,char**argv){
const char *base = argc>1?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;
}
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sched.h>
#include <pthread.h>
#include <spawn.h>
#include <sys/wait.h>
#include <sys/mman.h>
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)-(x<y); }
#define R 9
static void rep(const char*n,double*r){ qsort(r,R,sizeof(double),cmpd); printf("%-46s %9.1f %9.1f\n",n,r[0],r[R/2]); fflush(stdout); }
static void pin(int c){ cpu_set_t s; CPU_ZERO(&s); CPU_SET(c,&s); sched_setaffinity(0,sizeof s,&s); }
static char *area; static long asz;
static void grow(long mb){ if(area)munmap(area,asz); asz=mb*1024L*1024; if(!mb){area=0;return;}
area=mmap(0,asz,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
madvise(area,asz,MADV_NOHUGEPAGE); for(long o=0;o<asz;o+=4096) area[o]=1; }
static void* nada(void*p){ (void)p; return 0; }
int main(int argc,char**argv){
pin(2);
const char *sta=argc>1?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<R;q++){ double t0=now_ns(); for(int i=0;i<120;i++){pid_t p=fork(); if(!p)_exit(0); int st;waitpid(p,&st,0);} a[q]=(now_ns()-t0)/120/1000; }
for(int q=0;q<R;q++){ double t0=now_ns(); for(int i=0;i<120;i++){pid_t p;char*ar[]={(char*)sta,0}; if(!posix_spawn(&p,sta,0,0,ar,environ)){int st;waitpid(p,&st,0);}} b[q]=(now_ns()-t0)/120/1000; }
snprintf(n,sizeof n," padre %3ld MB: fork+_exit+wait",mbs[k]); rep(n,a);
snprintf(n,sizeof n," padre %3ld MB: posix_spawn+wait (estatico)",mbs[k]); rep(n,b); }
grow(0);
printf("--- tasa del enlazador dinamico (misma libc: glibc) ---\n");
{ double r[R];
for(int q=0;q<R;q++){ double t0=now_ns(); for(int i=0;i<150;i++){pid_t p;char*a[]={(char*)sta,0};if(!posix_spawn(&p,sta,0,0,a,environ)){int st;waitpid(p,&st,0);}} r[q]=(now_ns()-t0)/150/1000; } rep(" exec glibc ESTATICO",r);
for(int q=0;q<R;q++){ double t0=now_ns(); for(int i=0;i<150;i++){pid_t p;char*a[]={(char*)dyn,0};if(!posix_spawn(&p,dyn,0,0,a,environ)){int st;waitpid(p,&st,0);}} r[q]=(now_ns()-t0)/150/1000; } rep(" exec glibc DINAMICO",r);
for(int q=0;q<R;q++){ double t0=now_ns(); for(int i=0;i<150;i++){pid_t p;char*a[]={(char*)mus,0};if(!posix_spawn(&p,mus,0,0,a,environ)){int st;waitpid(p,&st,0);}} r[q]=(now_ns()-t0)/150/1000; } rep(" exec musl ESTATICO",r); }
printf("--- hilo vs proceso ---\n");
{ double r[R]; for(int q=0;q<R;q++){ double t0=now_ns(); for(int i=0;i<2000;i++){pthread_t t;pthread_create(&t,0,nada,0);pthread_join(t,0);} r[q]=(now_ns()-t0)/2000/1000; } rep(" pthread_create+join",r); }
printf("--- fallos de pagina, 64MB (defrag=madvise) ---\n");
{ const long SZ=64L<<20; double r[R]; long pg=SZ/4096;
for(int q=0;q<R;q++){ char*m=mmap(0,SZ,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0); madvise(m,SZ,MADV_HUGEPAGE);
double t0=now_ns(); for(long o=0;o<SZ;o+=4096)m[o]=1; r[q]=(now_ns()-t0)/pg*1000; munmap(m,SZ);} rep(" MADV_HUGEPAGE (ns/pagina 4K)",r);
for(int q=0;q<R;q++){ char*m=mmap(0,SZ,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0); madvise(m,SZ,MADV_NOHUGEPAGE);
double t0=now_ns(); for(long o=0;o<SZ;o+=4096)m[o]=1; r[q]=(now_ns()-t0)/pg*1000; munmap(m,SZ);} rep(" MADV_NOHUGEPAGE (ns/pagina 4K)",r);
for(int q=0;q<R;q++){ double t0=now_ns(); char*m=mmap(0,SZ,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE,-1,0);
for(long o=0;o<SZ;o+=4096)m[o]=1; r[q]=(now_ns()-t0)/pg*1000; munmap(m,SZ);} rep(" MAP_POPULATE (ns/pagina 4K)",r); }
return 0;
}
@@ -0,0 +1,147 @@
/* bench_proc.c — piso de syscall, control de procesos, /proc, IPC, scheduler.
* Método: R rondas; se reporta MIN (mejor caso, robusto al ruido) y MEDIANA. */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>
#include <signal.h>
#include <sched.h>
#include <pthread.h>
#include <spawn.h>
#include <dirent.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <sys/eventfd.h>
#include <sys/socket.h>
#include <sys/stat.h>
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<y); }
#define ROUNDS 9
static void report(const char*name,double*r,int n,long it){
qsort(r,n,sizeof(double),cmpd);
printf("%-38s %10.1f %10.1f (n=%ld)\n",name,r[0],r[n/2],it); fflush(stdout);
}
#define BENCH(name,iters,...) do{ double _r[ROUNDS]; long _N=(iters); \
for(int _q=0;_q<ROUNDS;_q++){ double _t0=now_ns(); for(long i=0;i<_N;i++){ __VA_ARGS__; } \
_r[_q]=(now_ns()-_t0)/(double)_N; } report(name,_r,ROUNDS,_N);}while(0)
/* ---------- helpers ---------- */
static void pin(int cpu){ cpu_set_t s; CPU_ZERO(&s); CPU_SET(cpu,&s); sched_setaffinity(0,sizeof s,&s); }
static long read_stat_utime(int fd){
char buf[1024]; ssize_t n=pread(fd,buf,sizeof buf-1,0); if(n<=0) return -1; buf[n]=0;
char *p=strrchr(buf,')'); if(!p) return -1; p+=2;
int f=1; long v=0; while(*p&&f<12){ if(*p==' ')f++; p++; }
v=strtol(p,NULL,10); return v;
}
static long parse_vmrss(void){
int fd=open("/proc/self/status",O_RDONLY); if(fd<0) return -1;
char buf[4096]; ssize_t n=read(fd,buf,sizeof buf-1); close(fd); if(n<=0)return -1; buf[n]=0;
char*p=strstr(buf,"VmRSS:"); return p?strtol(p+6,NULL,10):-1;
}
static volatile sig_atomic_t got;
static void h(int s){ (void)s; got=1; }
/* RTT entre dos hilos pinneados al MISMO cpu */
struct rtt { int a[2], b[2]; int kind; long iters; };
static void rd1(int fd,int kind){ if(kind==2){uint64_t v;(void)!read(fd,&v,8);} else {char c;(void)!read(fd,&c,1);} }
static void wr1(int fd,int kind){ if(kind==2){uint64_t v=1;(void)!write(fd,&v,8);} else {char c=1;(void)!write(fd,&c,1);} }
static void*echo(void*p){ struct rtt*r=p; pin(3);
for(long i=0;i<r->iters;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<n;i++){ wr1(r.a[1],kind); rd1(r.b[0],kind); }
double v=(now_ns()-t0)/n; if(v<best)best=v; }
pthread_join(t,0);
return best;
}
int main(void){
pin(3);
printf("%-38s %10s %10s\n","BENCH (ns/op)","min","mediana");
printf("--- A. piso de syscall ---\n");
BENCH("getppid (syscall nulo)", 300000, sink=syscall(SYS_getppid));
BENCH("clock_gettime MONOTONIC (vDSO)", 300000, {struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);sink=t.tv_nsec;});
BENCH("clock_gettime MONOTONIC (syscall)", 200000, {struct timespec t;syscall(SYS_clock_gettime,CLOCK_MONOTONIC,&t);sink=t.tv_nsec;});
BENCH("getpid (vDSO no; glibc syscall)", 300000, sink=getpid());
BENCH("sched_yield", 200000, sched_yield());
printf("--- B. contabilidad de proceso propio ---\n");
BENCH("getrusage(RUSAGE_SELF)", 200000, {struct rusage u;getrusage(RUSAGE_SELF,&u);sink=u.ru_utime.tv_usec;});
BENCH("clock_gettime(PROCESS_CPUTIME)", 200000, {struct timespec t;clock_gettime(CLOCK_PROCESS_CPUTIME_ID,&t);sink=t.tv_nsec;});
{ int fd=open("/proc/self/stat",O_RDONLY);
BENCH("pread+parse /proc/self/stat (fd abierto)", 100000, sink=read_stat_utime(fd));
close(fd); }
BENCH("open+read+parse /proc/self/stat", 50000, {int fd=open("/proc/self/stat",O_RDONLY);sink=read_stat_utime(fd);close(fd);});
BENCH("open+read+parse /proc/self/status(RSS)",30000, sink=parse_vmrss());
printf("--- C. liveness de otro proceso ---\n");
{ pid_t me=getpid(); char p[64]; snprintf(p,sizeof p,"/proc/%d",me);
int pfd=syscall(SYS_pidfd_open,me,0);
BENCH("access(/proc/<pid>) [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;q<ROUNDS;q++){
double t0=now_ns(); int c=0;
DIR*d=opendir("/proc"); struct dirent*e;
while((e=readdir(d))){ if(e->d_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<ROUNDS;q++){ struct timespec req={0,1000000},rem; double t0=now_ns();
for(int i=0;i<20;i++) nanosleep(&req,&rem);
r[q]=(now_ns()-t0)/20 - 1000000.0; }
qsort(r,ROUNDS,sizeof(double),cmpd);
printf("%-38s %10.1f %10.1f (sobrecoste ns sobre 1ms)\n","nanosleep(1ms) overshoot",r[0],r[ROUNDS/2]); }
return 0;
}
@@ -0,0 +1,23 @@
BENCH (us/op) min mediana
--- a) fork segun el espacio de direcciones del padre ---
padre 0 MB tocados: fork+_exit 992.4 1430.4
padre 0 MB tocados: fork+exec 806.8 1985.0
padre 0 MB tocados: posix_spawn(CLONE_VFORK) 77.9 81.0
padre 16 MB tocados: fork+_exit 1804.9 2090.5
padre 16 MB tocados: fork+exec 1381.1 2181.3
padre 16 MB tocados: posix_spawn(CLONE_VFORK) 383.8 887.2
padre 64 MB tocados: fork+_exit 2324.1 5504.8
padre 64 MB tocados: fork+exec 1392.4 4248.0
padre 64 MB tocados: posix_spawn(CLONE_VFORK) 87.3 91.1
padre 256 MB tocados: fork+_exit 4021.7 5060.5
padre 256 MB tocados: fork+exec 4324.5 4523.2
padre 256 MB tocados: posix_spawn(CLONE_VFORK) 80.7 89.2
--- c) tasa del enlazado dinamico en exec ---
vfork+exec binario ESTATICO 64.1 65.7
vfork+exec binario DINAMICO 308.5 334.2
--- d) cierre de descriptores antes del exec ---
dup+close x512, uno a uno (us/fd) 0.2 0.2
close_range(lo,hi) (us/fd) 0.0 0.0
--- b) contabilidad: cgroup v2 vs sumar /proc ---
cgroup: /sys/fs/cgroup/openrc.shuma-daemon
leer cpu.stat+memory.current+pids.current 4.6 4.7
@@ -0,0 +1,40 @@
### metadatos sobre /mnt/vvv/hammer/work/.bench-linux (4000 ficheros en un directorio)
BENCH (ns/fichero) min mediana
stat() por ruta completa 647.9 814.4 (n=4000)
fstatat(dirfd, nombre) 439.0 444.9 (n=4000)
statx(dirfd,nombre,DONT_SYNC) 467.2 476.0 (n=4000)
openat(dirfd,nombre)+close 898.8 994.1 (n=4000)
openat2 RESOLVE_BENEATH+NO_SYMLINKS 1025.2 1449.8 (n=4000)
getdents64 (barrido, sin stat) 168.2 172.8 (n=4002)
open ruta corta (2 componentes bajo base) 759.1 908.2 (n=20000)
open ruta 13 componentes mas profunda 964.6 1002.5 (n=20000)
### copiar 64 MB (fichero en cache de pagina)
BENCH (MB/s) max mediana
read()+write() 64K 2837.0 2722.6
sendfile() 3345.1 2958.8
copy_file_range() 3520.7 2859.6
### materializar 2000 ficheros de 1 byte (el problema del store)
link() [hardlink] 9223.4 10895.0 (n=2000)
open+read+write+close [copia] 142151.6 154711.4 (n=2000)
### 4K aleatorios sobre 64MB en cache: syscall vs io_uring
BENCH (ns/lectura) min mediana
pread() 4K 539.9 674.2 (n=50000)
io_uring read 4K (profundidad 32) 593.6 654.2 (n=50000)
io_uring NOP (piso de operacion) 47.7 50.5 (n=200000)
### fallos de pagina: tocar 128MB anonimos (ns por pagina de 4K)
BENCH min mediana
MADV_HUGEPAGE (THP) 2730.4 3205.5 (n=32768)
MADV_NOHUGEPAGE (4K de a uno) 1055.7 1073.5 (n=32768)
MAP_POPULATE (prefault) 796.4 846.9 (n=32768)
mmap+munmap 4K (churn) 1663.9 1784.1 (n=100000)
### coste del sandbox: crear namespaces (us por proceso)
BENCH min mediana
fork+exec /bin/true (base) 1648.6 2092.8 (n=100)
fork+unshare(6 ns)+exec 2742.0 2922.3 (n=100)
fork+unshare(USER|MNT)+exec 2050.4 2512.9 (n=100)
@@ -0,0 +1,27 @@
### metadatos sobre /tmp/bench-linux-tmpfs (4000 ficheros en un directorio)
BENCH (ns/fichero) min mediana
stat() por ruta completa 713.6 1196.5 (n=4000)
fstatat(dirfd, nombre) 489.1 761.1 (n=4000)
statx(dirfd,nombre,DONT_SYNC) 593.2 1059.3 (n=4000)
openat(dirfd,nombre)+close 960.8 1272.4 (n=4000)
openat2 RESOLVE_BENEATH+NO_SYMLINKS 762.8 996.5 (n=4000)
getdents64 (barrido, sin stat) 59.4 63.8 (n=4002)
open ruta corta (2 componentes bajo base) 657.8 672.4 (n=20000)
open ruta 13 componentes mas profunda 860.3 902.3 (n=20000)
### copiar 64 MB (fichero en cache de pagina)
BENCH (MB/s) max mediana
read()+write() 64K 3084.8 2970.9
sendfile() 3325.8 3115.7
copy_file_range() 3221.2 3077.9
### materializar 2000 ficheros de 1 byte (el problema del store)
link() [hardlink] 2851.8 3274.8 (n=2000)
open+read+write+close [copia] 4964.9 5339.4 (n=2000)
### 4K aleatorios sobre 64MB en cache: syscall vs io_uring
BENCH (ns/lectura) min mediana
pread() 4K 601.9 611.9 (n=50000)
io_uring read 4K (profundidad 32) 1672.3 2132.6 (n=50000)
io_uring NOP (piso de operacion) 50.3 54.4 (n=200000)
@@ -0,0 +1,18 @@
BENCH (us/op), PINNEADO a 1 cpu min mediana
--- fork vs espacio de direcciones del padre ---
padre 0 MB: fork+_exit+wait 109.1 201.9
padre 0 MB: posix_spawn+wait (estatico) 230.8 1029.8
padre 64 MB: fork+_exit+wait 937.8 1016.7
padre 64 MB: posix_spawn+wait (estatico) 234.0 240.4
padre 256 MB: fork+_exit+wait 3949.4 4383.5
padre 256 MB: posix_spawn+wait (estatico) 230.8 253.6
--- tasa del enlazador dinamico (misma libc: glibc) ---
exec glibc ESTATICO 232.7 245.6
exec glibc DINAMICO 324.3 335.6
exec musl ESTATICO 77.4 86.4
--- hilo vs proceso ---
pthread_create+join 11.9 12.6
--- fallos de pagina, 64MB (defrag=madvise) ---
MADV_HUGEPAGE (ns/pagina 4K) 3046278.5 5007046.6
MADV_NOHUGEPAGE (ns/pagina 4K) 1013225.8 1088726.2
MAP_POPULATE (ns/pagina 4K) 752505.7 802625.3
@@ -0,0 +1,35 @@
BENCH (ns/op) min mediana
--- A. piso de syscall ---
getppid (syscall nulo) 78.7 97.2 (n=300000)
clock_gettime MONOTONIC (vDSO) 31.2 31.7 (n=300000)
clock_gettime MONOTONIC (syscall) 143.8 147.0 (n=200000)
getpid (vDSO no; glibc syscall) 71.1 74.3 (n=300000)
sched_yield 174.2 180.9 (n=200000)
--- B. contabilidad de proceso propio ---
getrusage(RUSAGE_SELF) 238.1 244.6 (n=200000)
clock_gettime(PROCESS_CPUTIME) 214.0 245.8 (n=200000)
pread+parse /proc/self/stat (fd abierto) 1271.4 1334.6 (n=100000)
open+read+parse /proc/self/stat 2691.7 2801.0 (n=50000)
open+read+parse /proc/self/status(RSS) 4642.6 4857.3 (n=30000)
--- C. liveness de otro proceso ---
access(/proc/<pid>) [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)