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:
@@ -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/<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)
|
||||
Reference in New Issue
Block a user