gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 815Rust 56Shell 37Markdown 291229 Definiciones

tasas: los 4 experimentos que §9 dejó nombrados, medidos con root

SDD 25 §9 listó seis experimentos que no se pudieron correr y por qué. Cuatro
de ellos sólo pedían privilegios, y esta sesión los tuvo. Programas y salidas
crudas en docs/evidencia/tasas-kernel-2026-08-29/.

1. proc connector (bench_connector.c) — el sondeo de /proc no es lento, es
   CIEGO: 200 procesos de vida mínima lanzados uno cada 10 ms, sondeando cada
   100 ms se ven **0 de 200**; por connector, **200 de 200**. Y esperar el
   evento no cuesta nada frente a waitpid (116,0 vs 114,8 µs, dentro del ruido),
   con 0 eventos perdidos en 1400.
   Sorpresa: taskstats por genetlink **no es más rápido** que parsear
   /proc/self/stat (2 688 vs 2 697 ns). Su valor es el CONTENIDO (delay
   accounting), no la velocidad.

2. reflink (bench_reflink.c) — el «copiar en O(1)» existe y NO está en ext4:
   FICLONE no soportado, y copy_file_range ≈ read+write (13,7 vs 14,5 ms).
   En xfs y btrfs es PLANO: 64 MiB en 0,010 ms y 512 MiB —ocho veces más— en
   0,020 ms, contra 512,7 ms de read+write. **25 600× a 512 MiB.**

3. io_uring SQPOLL (bench_sqpoll.c) — no rescata el veredicto de T12: sobre
   datos cacheados pierde igual. ext4: pread 554 ns < io_uring 618 < SQPOLL 868.
   tmpfs: pread 749 < SQPOLL 1 409 < io_uring 2 030 (ahí sí mejora al io_uring
   normal, 1,4×, pero sigue perdiendo contra la syscall). Y se come un core.

4. syscalls por invocación de compilador — con el strace DEL PROPIO CORPUS
   (store/3c1fd8c0…-strace), que es la respuesta a «no hay strace en esta
   máquina». gcc: 1 803 syscalls por un `-c` trivial, **1 095 con error (61%),
   919 de ellas readlink** — la canonicalización de rutas, que T8 no medía.
   Pero al piso de 78,7 ns son 0,14 ms sobre 30,5 ms de compilado: **el cruce
   al kernel es el 0,5%**. La tormenta de rutas vale milisegundos, no minutos.

Condiciones distintas a la corrida original (load ~1,3 sobre 4 vCPU contra 7,3)
⇒ los absolutos NO se comparan entre corridas, sólo los cocientes dentro de una.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih

849c041446deb004de5e53f066eecb152930a4cd · por Sergio · (hace 1 mes) · padre 7433bcba8 · ver el árbol en este commit

Delta semántico · 77

docs/evidencia/tasas-kernel-2026-08-29/bench_connector.c

docs/evidencia/tasas-kernel-2026-08-29/bench_reflink.c

docs/evidencia/tasas-kernel-2026-08-29/bench_sqpoll.c

7 archivos · +723 −0

diff --git a/docs/evidencia/tasas-kernel-2026-08-29/bench_connector.c b/docs/evidencia/tasas-kernel-2026-08-29/bench_connector.c
new file mode 100644
index 00000000..cd57516e
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/bench_connector.c
@@ -0,0 +1,312 @@
+/* bench_connector.c — proc connector (netlink) y taskstats contra sondear /proc.
+ *
+ * Cierra el experimento 1 de SDD 25 §9, que quedo sin medir por falta de CAP_NET_ADMIN.
+ * Requiere root. Tres fases:
+ *   A. LATENCIA   fork+_exit+waitpid  vs  fork+_exit+esperar el evento del connector.
+ *   B. CEGUERA    procesos cortos vistos por el connector vs vistos sondeando /proc cada 100 ms.
+ *   C. TASKSTATS  una consulta binaria por genetlink vs parsear /proc/<pid>/stat.
+ *
+ * Se compila con: cc -O2 -o bench_connector bench_connector.c
+ */
+#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 <dirent.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <sys/resource.h>
+#include <poll.h>
+#include <linux/netlink.h>
+#include <linux/connector.h>
+#include <linux/cn_proc.h>
+#include <linux/genetlink.h>
+#include <linux/taskstats.h>
+
+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); }
+static void report(const char*n,double*r,int c,long it){ qsort(r,c,sizeof(double),cmpd);
+  printf("%-44s %10.1f %10.1f   (n=%ld)\n",n,r[0],r[c/2],it); fflush(stdout); }
+#define ROUNDS 7
+
+/* ---------------------------------------------------------------- proc connector */
+
+static int cn_open(void){
+  int s = socket(PF_NETLINK, SOCK_DGRAM|SOCK_CLOEXEC, NETLINK_CONNECTOR);
+  if(s<0) return -1;
+  struct sockaddr_nl a; memset(&a,0,sizeof a);
+  a.nl_family=AF_NETLINK; a.nl_groups=CN_IDX_PROC; a.nl_pid=getpid();
+  if(bind(s,(struct sockaddr*)&a,sizeof a)<0){ close(s); return -1; }
+
+  struct {
+    struct nlmsghdr nl;
+    struct cn_msg cn;
+    enum proc_cn_mcast_op op;
+  } __attribute__((packed)) m;
+  memset(&m,0,sizeof m);
+  m.nl.nlmsg_len=sizeof m; m.nl.nlmsg_pid=getpid(); m.nl.nlmsg_type=NLMSG_DONE;
+  m.cn.id.idx=CN_IDX_PROC; m.cn.id.val=CN_VAL_PROC; m.cn.len=sizeof(enum proc_cn_mcast_op);
+  m.op=PROC_CN_MCAST_LISTEN;
+  if(send(s,&m,sizeof m,0)<0){ close(s); return -1; }
+  /* El multicast del connector NO reintenta: si el buffer se llena, el evento se PIERDE (ENOBUFS)
+   * y el que espera ese pid se queda esperando para siempre. Se agranda el buffer y, aun asi, la
+   * espera lleva plazo (abajo). Es una propiedad del mecanismo, no un detalle de este programa. */
+  int rb=4*1024*1024; setsockopt(s,SOL_SOCKET,SO_RCVBUFFORCE,&rb,sizeof rb);
+  return s;
+}
+
+/* Lee un evento. Devuelve el proc_event, o NULL si el mensaje no lo era. */
+static struct proc_event *cn_read(int s, char *buf, size_t n){
+  ssize_t r = recv(s, buf, n, 0);
+  if(r<=0) return NULL;
+  struct nlmsghdr *nlh=(struct nlmsghdr*)buf;
+  if(!NLMSG_OK(nlh,(size_t)r)) return NULL;
+  struct cn_msg *cn=(struct cn_msg*)NLMSG_DATA(nlh);
+  return (struct proc_event*)cn->data;
+}
+
+/* Vacia lo que haya en el socket sin bloquear. */
+static void cn_drain(int s){
+  int fl=fcntl(s,F_GETFL,0); fcntl(s,F_SETFL,fl|O_NONBLOCK);
+  char b[4096]; while(recv(s,b,sizeof b,0)>0){}
+  fcntl(s,F_SETFL,fl);
+}
+
+static void fase_a(int s){
+  printf("\n### A. Latencia de notificacion: waitpid contra evento del connector\n");
+  printf("%-44s %10s %10s\n","BENCH (ns por proceso)","min","mediana");
+  const long N=200;
+
+  double r[ROUNDS];
+  for(int q=0;q<ROUNDS;q++){
+    double t0=now_ns();
+    for(long i=0;i<N;i++){ pid_t p=fork(); if(p==0) _exit(0); int st; waitpid(p,&st,0); }
+    r[q]=(now_ns()-t0)/(double)N;
+  }
+  report("fork+_exit+waitpid (SIGCHLD, la base)",r,ROUNDS,N);
+
+  char buf[8192];
+  long perdidos=0;
+  for(int q=0;q<ROUNDS;q++){
+    cn_drain(s);
+    double t0=now_ns();
+    for(long i=0;i<N;i++){
+      pid_t p=fork(); if(p==0) _exit(0);
+      double lim=now_ns()+2e8;              /* 200 ms de plazo: si no llega, se PERDIO */
+      for(;;){
+        if(now_ns()>lim){ perdidos++; break; }
+        struct pollfd pf={.fd=s,.events=POLLIN};
+        if(poll(&pf,1,50)<=0) continue;
+        struct proc_event *e=cn_read(s,buf,sizeof buf);
+        if(!e) continue;
+        if(e->what==PROC_EVENT_EXIT && e->event_data.exit.process_pid==p) break;
+      }
+      int st; waitpid(p,&st,0);
+    }
+    r[q]=(now_ns()-t0)/(double)N;
+  }
+  report("fork+_exit+esperar EXIT del connector",r,ROUNDS,N);
+  printf("eventos EXIT que no llegaron en 200 ms: %ld de %ld\n",perdidos,(long)N*ROUNDS);
+}
+
+/* ---------------------------------------------------------------- ceguera del sondeo */
+
+#define BURST 200        /* procesos cortos */
+#define SPACING_US 10000 /* uno cada 10 ms => 2 s de rafaga */
+#define POLL_MS 100      /* periodo del sondeo, generoso: un supervisor real no baja de aca */
+
+/* Lanza BURST hijos que viven lo minimo, y escribe sus pids por el pipe. */
+static void spawner(int wfd){
+  for(int i=0;i<BURST;i++){
+    pid_t p=fork();
+    if(p==0) _exit(0);
+    (void)!write(wfd,&p,sizeof p);
+    int st; waitpid(p,&st,0);
+    usleep(SPACING_US);
+  }
+  close(wfd);
+  _exit(0);
+}
+
+static int en_lista(pid_t *v,int n,pid_t p){ for(int i=0;i<n;i++) if(v[i]==p) return 1; return 0; }
+
+static double cpu_ms(void){
+  struct rusage u; getrusage(RUSAGE_SELF,&u);
+  return (u.ru_utime.tv_sec+u.ru_stime.tv_sec)*1e3
+       + (u.ru_utime.tv_usec+u.ru_stime.tv_usec)/1e3;
+}
+
+static void fase_b(int s){
+  printf("\n### B. Ceguera: %d procesos de vida minima, uno cada %d ms\n",BURST,SPACING_US/1000);
+
+  /* --- observador 1: sondeo de /proc cada POLL_MS --- */
+  int fd[2]; if(pipe(fd)) { perror("pipe"); return; }
+  pid_t sp=fork();
+  if(sp==0){ close(fd[0]); spawner(fd[1]); }
+  close(fd[1]);
+  static pid_t lanzados[BURST]; int nl=0;
+  static pid_t vistos[BURST*8]; int nv=0;
+  double c0=cpu_ms(), t0=now_ns(); int barridos=0;
+  while(now_ns()-t0 < (double)BURST*SPACING_US*1000.0 + 3e8){
+    DIR *d=opendir("/proc"); struct dirent *e;
+    while(d && (e=readdir(d))){
+      if(e->d_name[0]<'0'||e->d_name[0]>'9') continue;
+      pid_t p=(pid_t)atol(e->d_name);
+      if(nv<BURST*8 && !en_lista(vistos,nv,p)) vistos[nv++]=p;
+    }
+    if(d) closedir(d);
+    barridos++;
+    usleep(POLL_MS*1000);
+  }
+  double cpu_sondeo=cpu_ms()-c0;
+  { int st; waitpid(sp,&st,0); }
+  { pid_t p; while(read(fd[0],&p,sizeof p)==sizeof p) if(nl<BURST) lanzados[nl++]=p; }
+  close(fd[0]);
+  int cazados=0; for(int i=0;i<nl;i++) if(en_lista(vistos,nv,lanzados[i])) cazados++;
+  printf("sondeo /proc cada %d ms : %d de %d procesos vistos  (%d barridos, %.1f ms de CPU)\n",
+         POLL_MS,cazados,nl,barridos,cpu_sondeo);
+
+  /* --- observador 2: connector --- */
+  if(pipe(fd)){ perror("pipe"); return; }
+  cn_drain(s);
+  sp=fork();
+  if(sp==0){ close(fd[0]); spawner(fd[1]); }
+  close(fd[1]);
+  nl=0; nv=0;
+  c0=cpu_ms(); t0=now_ns();
+  int flags=fcntl(s,F_GETFL,0); fcntl(s,F_SETFL,flags|O_NONBLOCK);
+  char buf[8192];
+  while(now_ns()-t0 < (double)BURST*SPACING_US*1000.0 + 3e8){
+    struct proc_event *e=cn_read(s,buf,sizeof buf);
+    if(!e){ usleep(200); continue; }
+    if(e->what==PROC_EVENT_EXIT){
+      pid_t p=e->event_data.exit.process_pid;
+      if(nv<BURST*8 && !en_lista(vistos,nv,p)) vistos[nv++]=p;
+    }
+  }
+  fcntl(s,F_SETFL,flags);
+  double cpu_conn=cpu_ms()-c0;
+  { int st; waitpid(sp,&st,0); }
+  { pid_t p; while(read(fd[0],&p,sizeof p)==sizeof p) if(nl<BURST) lanzados[nl++]=p; }
+  close(fd[0]);
+  cazados=0; for(int i=0;i<nl;i++) if(en_lista(vistos,nv,lanzados[i])) cazados++;
+  printf("proc connector (eventos)  : %d de %d procesos vistos  (%.1f ms de CPU)\n",
+         cazados,nl,cpu_conn);
+  printf("NOTA: el connector se lee aca con espera activa de 200 us, que NO es el modo de un\n"
+         "      supervisor real (seria epoll) => su CPU es cota superior. Lo que cuenta: COBERTURA.\n");
+}
+
+/* ---------------------------------------------------------------- taskstats */
+
+static int genl_family(int s,const char *name){
+  struct { struct nlmsghdr n; struct genlmsghdr g; char buf[256]; } req;
+  memset(&req,0,sizeof req);
+  req.n.nlmsg_type=GENL_ID_CTRL; req.n.nlmsg_flags=NLM_F_REQUEST; req.n.nlmsg_seq=1; req.n.nlmsg_pid=getpid();
+  req.g.cmd=CTRL_CMD_GETFAMILY; req.g.version=1;
+  struct nlattr *a=(struct nlattr*)req.buf;
+  a->nla_type=CTRL_ATTR_FAMILY_NAME; a->nla_len=NLA_HDRLEN+strlen(name)+1;
+  strcpy((char*)a+NLA_HDRLEN,name);
+  req.n.nlmsg_len=NLMSG_LENGTH(GENL_HDRLEN)+NLA_ALIGN(a->nla_len);
+  if(send(s,&req,req.n.nlmsg_len,0)<0) return -1;
+
+  char rb[4096]; ssize_t r=recv(s,rb,sizeof rb,0);
+  if(r<0) return -1;
+  struct nlmsghdr *nh=(struct nlmsghdr*)rb;
+  if(nh->nlmsg_type==NLMSG_ERROR) return -1;
+  struct nlattr *na=(struct nlattr*)((char*)NLMSG_DATA(nh)+GENL_HDRLEN);
+  int len=NLMSG_PAYLOAD(nh,0)-GENL_HDRLEN;
+  while(len>0){
+    if(na->nla_type==CTRL_ATTR_FAMILY_ID) return *(__u16*)((char*)na+NLA_HDRLEN);
+    len-=NLA_ALIGN(na->nla_len);
+    na=(struct nlattr*)((char*)na+NLA_ALIGN(na->nla_len));
+  }
+  return -1;
+}
+
+static int ts_query(int s,int fam,pid_t pid,unsigned long long *cpu_ns){
+  struct { struct nlmsghdr n; struct genlmsghdr g; char buf[128]; } req;
+  memset(&req,0,sizeof req);
+  req.n.nlmsg_type=fam; req.n.nlmsg_flags=NLM_F_REQUEST; req.n.nlmsg_seq=2; req.n.nlmsg_pid=getpid();
+  req.g.cmd=TASKSTATS_CMD_GET; req.g.version=TASKSTATS_VERSION;
+  struct nlattr *a=(struct nlattr*)req.buf;
+  a->nla_type=TASKSTATS_CMD_ATTR_PID; a->nla_len=NLA_HDRLEN+sizeof(__u32);
+  *(__u32*)((char*)a+NLA_HDRLEN)=pid;
+  req.n.nlmsg_len=NLMSG_LENGTH(GENL_HDRLEN)+NLA_ALIGN(a->nla_len);
+  if(send(s,&req,req.n.nlmsg_len,0)<0) return -1;
+
+  char rb[8192]; ssize_t r=recv(s,rb,sizeof rb,0);
+  if(r<0) return -1;
+  struct nlmsghdr *nh=(struct nlmsghdr*)rb;
+  if(nh->nlmsg_type==NLMSG_ERROR) return -1;
+  struct nlattr *na=(struct nlattr*)((char*)NLMSG_DATA(nh)+GENL_HDRLEN);
+  int len=NLMSG_PAYLOAD(nh,0)-GENL_HDRLEN;
+  while(len>0){
+    if(na->nla_type==TASKSTATS_TYPE_AGGR_PID || na->nla_type==TASKSTATS_TYPE_AGGR_TGID){
+      struct nlattr *in=(struct nlattr*)((char*)na+NLA_HDRLEN);
+      int il=na->nla_len-NLA_HDRLEN;
+      while(il>0){
+        if(in->nla_type==TASKSTATS_TYPE_STATS){
+          struct taskstats *ts=(struct taskstats*)((char*)in+NLA_HDRLEN);
+          *cpu_ns=ts->ac_utime*1000ULL+ts->ac_stime*1000ULL;
+          return 0;
+        }
+        il-=NLA_ALIGN(in->nla_len);
+        in=(struct nlattr*)((char*)in+NLA_ALIGN(in->nla_len));
+      }
+    }
+    len-=NLA_ALIGN(na->nla_len);
+    na=(struct nlattr*)((char*)na+NLA_ALIGN(na->nla_len));
+  }
+  return -1;
+}
+
+static void fase_c(void){
+  printf("\n### C. Estadisticas de UNA tarea: taskstats binario contra /proc de texto\n");
+  int s=socket(PF_NETLINK,SOCK_DGRAM|SOCK_CLOEXEC,NETLINK_GENERIC);
+  if(s<0){ printf("taskstats: sin socket genetlink (%s)\n",strerror(errno)); return; }
+  struct sockaddr_nl a; memset(&a,0,sizeof a); a.nl_family=AF_NETLINK; a.nl_pid=getpid();
+  if(bind(s,(struct sockaddr*)&a,sizeof a)<0){ printf("taskstats: bind (%s)\n",strerror(errno)); close(s); return; }
+  int fam=genl_family(s,TASKSTATS_GENL_NAME);
+  if(fam<0){ printf("taskstats: la familia %s no esta (kernel sin TASKSTATS)\n",TASKSTATS_GENL_NAME); close(s); return; }
+  unsigned long long ns=0;
+  if(ts_query(s,fam,getpid(),&ns)<0){ printf("taskstats: la consulta fallo (%s)\n",strerror(errno)); close(s); return; }
+  printf("familia TASKSTATS = %d, cpu de este proceso = %llu ns\n",fam,ns);
+
+  printf("%-44s %10s %10s\n","BENCH (ns por consulta)","min","mediana");
+  const long N=2000;
+  double r[ROUNDS];
+  for(int q=0;q<ROUNDS;q++){
+    double t0=now_ns();
+    for(long i=0;i<N;i++){ unsigned long long v; sink=ts_query(s,fam,getpid(),&v); }
+    r[q]=(now_ns()-t0)/(double)N;
+  }
+  report("taskstats por genetlink (binario)",r,ROUNDS,N);
+
+  for(int q=0;q<ROUNDS;q++){
+    double t0=now_ns();
+    for(long i=0;i<N;i++){
+      int fd=open("/proc/self/stat",O_RDONLY); char b[2048];
+      ssize_t n=read(fd,b,sizeof b-1); close(fd);
+      if(n>0){ b[n]=0; char *p=strrchr(b,')'); for(int f=0;p&&f<12;f++) p=strchr(p+1,' ');
+               sink=p?atol(p+1):0; }
+    }
+    r[q]=(now_ns()-t0)/(double)N;
+  }
+  report("open+read+parse /proc/self/stat",r,ROUNDS,N);
+  close(s);
+}
+
+int main(void){
+  if(geteuid()!=0) fprintf(stderr,"aviso: sin root el connector no se suscribe (CAP_NET_ADMIN)\n");
+  printf("# bench_connector — SDD 25 seccion 9, experimento 1\n");
+  int s=cn_open();
+  if(s<0){ printf("proc connector: no disponible (%s)\n",strerror(errno)); }
+  else { fase_a(s); fase_b(s); close(s); }
+  fase_c();
+  return 0;
+}
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/bench_reflink.c b/docs/evidencia/tasas-kernel-2026-08-29/bench_reflink.c
new file mode 100644
index 00000000..8e961ea5
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/bench_reflink.c
@@ -0,0 +1,106 @@
+/* bench_reflink.c — copiar un fichero: reflink (FICLONE), copy_file_range y read+write.
+ *
+ * Cierra el experimento 2 de SDD 25 seccion 9 (T11), que quedo sin medir porque exige mkfs sobre
+ * un loop, o sea root. La pregunta: el "copiar en O(1)" que la gente espera de copy_file_range,
+ * cuanto vale de verdad, y en que filesystem.
+ *
+ * Uso: bench_reflink <dir> <etiqueta>   (el dir debe estar en el FS que se quiere medir)
+ * Se compila con: cc -O2 -o bench_reflink bench_reflink.c
+ */
+#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 <sys/ioctl.h>
+#include <sys/stat.h>
+#include <linux/fs.h>
+
+#define MB (1024*1024)
+static long SIZE = 64*MB;   /* se puede fijar por argv[3] en MiB: la copia O(1) hay que verla PLANA */
+#define ROUNDS 7
+
+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); }
+static void report(const char*n,double*r,int c){
+  qsort(r,c,sizeof(double),cmpd);
+  double min=r[0],med=r[c/2];
+  /* El MB/s se imprime solo cuando la copia MUEVE datos. Con reflink el tiempo no depende del
+   * tamano, asi que dividir por el ni significa nada ni se puede citar. */
+  if(min > 1e6) printf("%-40s %9.3f ms %9.3f ms   %8.0f MB/s\n",n,min/1e6,med/1e6,(double)SIZE/(min/1e9)/1e6);
+  else          printf("%-40s %9.3f ms %9.3f ms   (plano: no mueve datos)\n",n,min/1e6,med/1e6);
+  fflush(stdout);
+}
+
+static int src_fd(const char *dir,char *path,size_t n){
+  snprintf(path,n,"%s/origen.bin",dir);
+  int fd=open(path,O_CREAT|O_RDWR|O_TRUNC,0644);
+  if(fd<0) return -1;
+  char *b=malloc(MB); memset(b,0xA5,MB);
+  for(long i=0;i<SIZE/MB;i++) if(write(fd,b,MB)!=MB){ free(b); close(fd); return -1; }
+  free(b);
+  fsync(fd);
+  /* Se deja en cache a proposito: se mide el camino de COPIA, no el de lectura de disco. */
+  return fd;
+}
+
+int main(int argc,char**argv){
+  if(argc<3){ fprintf(stderr,"uso: %s <dir> <etiqueta> [MiB]\n",argv[0]); return 2; }
+  const char *dir=argv[1], *etiqueta=argv[2];
+  if(argc>3) SIZE=atol(argv[3])*MB;
+  char src[512],dst[512];
+  int in=src_fd(dir,src,sizeof src);
+  if(in<0){ fprintf(stderr,"no pude crear el origen en %s: %s\n",dir,strerror(errno)); return 1; }
+  snprintf(dst,sizeof dst,"%s/copia.bin",dir);
+
+  printf("\n### copia de %ld MiB sobre %s\n",SIZE/MB,etiqueta);
+  printf("%-40s %12s %12s\n","BENCH","min","mediana");
+
+  double r[ROUNDS];
+  int ok=0;
+
+  /* 1. reflink de fichero entero: la copia O(1) de verdad (comparte extents COW). */
+  for(int q=0;q<ROUNDS;q++){
+    unlink(dst);
+    int out=open(dst,O_CREAT|O_WRONLY|O_TRUNC,0644);
+    double t0=now_ns();
+    int rc=ioctl(out,FICLONE,in);
+    r[q]=now_ns()-t0;
+    close(out);
+    if(rc<0){ printf("%-40s no soportado (%s)\n","FICLONE (reflink entero)",strerror(errno)); ok=-1; break; }
+  }
+  if(ok==0) report("FICLONE (reflink entero)",r,ROUNDS);
+
+  /* 2. copy_file_range: la API portable. En un FS con reflink cae en el mismo camino; en ext4
+   *    cae a una copia DENTRO del kernel. */
+  for(int q=0;q<ROUNDS;q++){
+    unlink(dst);
+    int out=open(dst,O_CREAT|O_WRONLY|O_TRUNC,0644);
+    loff_t off_in=0,off_out=0; size_t left=(size_t)SIZE;
+    double t0=now_ns();
+    while(left){ ssize_t c=copy_file_range(in,&off_in,out,&off_out,left,0); if(c<=0) break; left-=c; }
+    r[q]=now_ns()-t0;
+    close(out);
+  }
+  report("copy_file_range",r,ROUNDS);
+
+  /* 3. read+write en trozos de 1 MiB: lo que hace un cp cualquiera. */
+  char *buf=malloc(MB);
+  for(int q=0;q<ROUNDS;q++){
+    unlink(dst);
+    int out=open(dst,O_CREAT|O_WRONLY|O_TRUNC,0644);
+    lseek(in,0,SEEK_SET);
+    double t0=now_ns();
+    for(long i=0;i<SIZE/MB;i++){ ssize_t n=read(in,buf,MB); if(n>0) (void)!write(out,buf,n); }
+    r[q]=now_ns()-t0;
+    close(out);
+  }
+  report("read+write (1 MiB)",r,ROUNDS);
+  free(buf);
+
+  unlink(dst); unlink(src); close(in);
+  return 0;
+}
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/bench_sqpoll.c b/docs/evidencia/tasas-kernel-2026-08-29/bench_sqpoll.c
new file mode 100644
index 00000000..0d97e3cc
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/bench_sqpoll.c
@@ -0,0 +1,112 @@
+/* bench_sqpoll.c — io_uring con SQPOLL contra io_uring normal y contra pread.
+ *
+ * Cierra el experimento 4 de SDD 25 seccion 9: SQPOLL pide CAP_SYS_NICE, asi que la corrida
+ * original no pudo mirarlo. La pregunta que dejo abierta T12: io_uring pierde sobre datos ya
+ * cacheados porque el cruce al kernel no se amortiza — con SQPOLL NO HAY cruce (un hilo del
+ * kernel consume la cola), asi que este es el caso donde deberia ganar.
+ *
+ * Uso: bench_sqpoll <dir> <etiqueta>
+ * Se compila con: cc -O2 -o bench_sqpoll bench_sqpoll.c -luring
+ */
+#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 <liburing.h>
+
+#define MB (1024*1024)
+#define SIZE (64*MB)
+#define BS 4096
+#define ROUNDS 7
+#define DEPTH 32
+#define OPS 50000
+
+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); }
+static void report(const char*n,double*r,int c,long it){ qsort(r,c,sizeof(double),cmpd);
+  printf("%-44s %10.1f %10.1f   (n=%ld)\n",n,r[0],r[c/2],it); fflush(stdout); }
+
+/* Un pase de OPS lecturas de 4K en profundidad DEPTH. Devuelve ns/op. */
+static double pase(struct io_uring *ring,int fd,char *buf,long nblk){
+  double t0=now_ns();
+  for(long done=0;done<OPS;){
+    int batch = (OPS-done<DEPTH)?(int)(OPS-done):DEPTH;
+    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;
+  }
+  return (now_ns()-t0)/(double)OPS;
+}
+
+int main(int argc,char**argv){
+  if(argc<3){ fprintf(stderr,"uso: %s <dir> <etiqueta>\n",argv[0]); return 2; }
+  char path[512]; snprintf(path,sizeof path,"%s/sqpoll.bin",argv[1]);
+  int fd=open(path,O_CREAT|O_RDWR|O_TRUNC,0644);
+  if(fd<0){ fprintf(stderr,"no pude crear %s: %s\n",path,strerror(errno)); return 1; }
+  char *b=malloc(MB); memset(b,0x5A,MB);
+  for(int i=0;i<SIZE/MB;i++) if(write(fd,b,MB)!=MB){ perror("write"); return 1; }
+  free(b);
+  /* Se lee entero una vez para dejarlo en cache: se mide el camino de ENTREGA, no el disco. */
+  char *buf=aligned_alloc(4096,DEPTH*BS);
+  for(long o=0;o<SIZE;o+=BS) sink=pread(fd,buf,BS,o);
+  long nblk=SIZE/BS;
+
+  printf("\n### 4K aleatorios en cache sobre %s (profundidad %d)\n",argv[2],DEPTH);
+  printf("%-44s %10s %10s\n","BENCH (ns/op)","min","mediana");
+
+  double r[ROUNDS];
+  for(int q=0;q<ROUNDS;q++){
+    double t0=now_ns();
+    for(long i=0;i<OPS;i++) sink=pread(fd,buf,BS,(long)((i*7919)%nblk)*BS);
+    r[q]=(now_ns()-t0)/(double)OPS;
+  }
+  report("pread 4K (una syscall por op)",r,ROUNDS,OPS);
+
+  struct io_uring ring;
+  if(io_uring_queue_init(256,&ring,0)==0){
+    for(int q=0;q<ROUNDS;q++) r[q]=pase(&ring,fd,buf,nblk);
+    report("io_uring sin SQPOLL",r,ROUNDS,OPS);
+    io_uring_queue_exit(&ring);
+  } else printf("io_uring: no disponible (%s)\n",strerror(errno));
+
+  struct io_uring_params p; memset(&p,0,sizeof p);
+  p.flags=IORING_SETUP_SQPOLL; p.sq_thread_idle=2000;
+  if(io_uring_queue_init_params(256,&ring,&p)==0){
+    /* Con SQPOLL el fd hay que registrarlo: el hilo del kernel no tiene la tabla de fds. */
+    int rc=io_uring_register_files(&ring,&fd,1);
+    int usar_fijo = (rc==0);
+    for(int q=0;q<ROUNDS;q++){
+      double t0=now_ns();
+      for(long done=0;done<OPS;){
+        int batch=(OPS-done<DEPTH)?(int)(OPS-done):DEPTH;
+        for(int k=0;k<batch;k++){
+          struct io_uring_sqe *s=io_uring_get_sqe(&ring);
+          io_uring_prep_read(s,usar_fijo?0:fd,buf+k*BS,BS,(long)(((done+k)*7919)%nblk)*BS);
+          if(usar_fijo) s->flags|=IOSQE_FIXED_FILE;
+        }
+        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){ if(c->res<0) sink=c->res; seen++; }
+        io_uring_cq_advance(&ring,seen);
+        done+=batch;
+      }
+      r[q]=(now_ns()-t0)/(double)OPS;
+    }
+    report(usar_fijo?"io_uring SQPOLL (fd registrado)":"io_uring SQPOLL (fd normal)",r,ROUNDS,OPS);
+    io_uring_queue_exit(&ring);
+  } else printf("SQPOLL: no disponible (%s) — pide CAP_SYS_NICE\n",strerror(errno));
+
+  unlink(path); close(fd);
+  return 0;
+}
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/connector.txt b/docs/evidencia/tasas-kernel-2026-08-29/connector.txt
new file mode 100644
index 00000000..46d80e9b
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/connector.txt
@@ -0,0 +1,20 @@
+# condiciones: 2026-08-29T19:27:32Z · 7.1.4-artix1-1 · load 1.25 1.30 1.93 · 4285 MiB libres · root
+# bench_connector — SDD 25 seccion 9, experimento 1
+
+### A. Latencia de notificacion: waitpid contra evento del connector
+BENCH (ns por proceso)                              min    mediana
+fork+_exit+waitpid (SIGCHLD, la base)          114825.2   123130.6   (n=200)
+fork+_exit+esperar EXIT del connector          116041.0   133773.5   (n=200)
+eventos EXIT que no llegaron en 200 ms: 0 de 1400
+
+### B. Ceguera: 200 procesos de vida minima, uno cada 10 ms
+sondeo /proc cada 100 ms : 0 de 200 procesos vistos  (23 barridos, 10.5 ms de CPU)
+proc connector (eventos)  : 200 de 200 procesos vistos  (95.4 ms de CPU)
+NOTA: el connector se lee aca con espera activa de 200 us, que NO es el modo de un
+      supervisor real (seria epoll) => su CPU es cota superior. Lo que cuenta: COBERTURA.
+
+### C. Estadisticas de UNA tarea: taskstats binario contra /proc de texto
+familia TASKSTATS = 41, cpu de este proceso = 198921000 ns
+BENCH (ns por consulta)                             min    mediana
+taskstats por genetlink (binario)                2688.1     2726.3   (n=2000)
+open+read+parse /proc/self/stat                  2696.7     2716.5   (n=2000)
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/reflink.txt b/docs/evidencia/tasas-kernel-2026-08-29/reflink.txt
new file mode 100644
index 00000000..ce993f7f
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/reflink.txt
@@ -0,0 +1,34 @@
+# condiciones: 2026-08-29T19:29:16Z · 7.1.4-artix1-1 · load 1.30 1.30 1.87
+# xfsprogs mkfs.xfs version 7.1.1 · btrfs-progs mkfs.btrfs, part of btrfs-progs v7.1
+
+### copia de 64 MiB sobre ext4
+BENCH                                             min      mediana
+FICLONE (reflink entero)                 no soportado (Operation not supported)
+copy_file_range                             13.655 ms    15.013 ms       4914 MB/s
+read+write (1 MiB)                          14.455 ms    14.911 ms       4643 MB/s
+# xfs montado: xfs rw,relatime,inode64,logbufs=8,logbsize=32k,noquota
+
+### copia de 64 MiB sobre xfs (loop)
+BENCH                                             min      mediana
+FICLONE (reflink entero)                     0.010 ms     0.013 ms   (plano: no mueve datos)
+copy_file_range                              0.010 ms     0.010 ms   (plano: no mueve datos)
+read+write (1 MiB)                          10.964 ms    11.290 ms       6121 MB/s
+
+### copia de 512 MiB sobre xfs (loop), fichero 8x mas grande
+BENCH                                             min      mediana
+FICLONE (reflink entero)                     0.020 ms     0.021 ms   (plano: no mueve datos)
+copy_file_range                              0.019 ms     0.020 ms   (plano: no mueve datos)
+read+write (1 MiB)                         512.712 ms   600.930 ms       1047 MB/s
+# btrfs montado: btrfs rw,relatime,ssd,discard=async,space_cache=v2,subvolid=5,subvol=/
+
+### copia de 64 MiB sobre btrfs (loop)
+BENCH                                             min      mediana
+FICLONE (reflink entero)                     0.006 ms     0.014 ms   (plano: no mueve datos)
+copy_file_range                              0.006 ms     0.008 ms   (plano: no mueve datos)
+read+write (1 MiB)                          31.323 ms    32.181 ms       2143 MB/s
+
+### copia de 512 MiB sobre btrfs (loop), fichero 8x mas grande
+BENCH                                             min      mediana
+FICLONE (reflink entero)                     0.015 ms     0.020 ms   (plano: no mueve datos)
+copy_file_range                              0.015 ms     0.020 ms   (plano: no mueve datos)
+read+write (1 MiB)                         236.891 ms   533.540 ms       2266 MB/s
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/sqpoll.txt b/docs/evidencia/tasas-kernel-2026-08-29/sqpoll.txt
new file mode 100644
index 00000000..cbbf17e5
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/sqpoll.txt
@@ -0,0 +1,13 @@
+# condiciones: 2026-08-29T19:30:07Z · 7.1.4-artix1-1 · load 2.23 1.61 1.95 · root
+
+### 4K aleatorios en cache sobre ext4 (profundidad 32)
+BENCH (ns/op)                                       min    mediana
+pread 4K (una syscall por op)                     554.4      590.2   (n=50000)
+io_uring sin SQPOLL                               617.7      633.8   (n=50000)
+io_uring SQPOLL (fd registrado)                   867.7     1068.6   (n=50000)
+
+### 4K aleatorios en cache sobre tmpfs (profundidad 32)
+BENCH (ns/op)                                       min    mediana
+pread 4K (una syscall por op)                     749.3      764.2   (n=50000)
+io_uring sin SQPOLL                              2029.6     2237.6   (n=50000)
+io_uring SQPOLL (fd registrado)                  1409.3     2296.8   (n=50000)
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/syscalls-compilador.txt b/docs/evidencia/tasas-kernel-2026-08-29/syscalls-compilador.txt
new file mode 100644
index 00000000..c92d1aa9
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/syscalls-compilador.txt
@@ -0,0 +1,126 @@
+# condiciones: 2026-08-29T19:30:50Z · 7.1.4-artix1-1 · strace 6.19 DEL PROPIO CORPUS (store/3c1fd8c0…-strace)
+# fuente: hola.c — un main() con stdio/stdlib/string, o sea el caso MAS BARATO posible
+
+=== cc -O2 -c (gcc 16) ===
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 17960 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 17971 attached
+% time     seconds  usecs/call     calls    errors syscall
+------ ----------- ----------- --------- --------- ----------------
+ 80.09    0.048227       24113         2           wait4
+  7.44    0.004479           4       923       919 readlink
+  3.00    0.001808          17       105           read
+  2.30    0.001387           6       228       127 openat
+  1.28    0.000772           8        92           mmap
+  1.02    0.000614          87         7         4 execve
+  0.87    0.000522           5       101           close
+  0.78    0.000470           4       100           fstat
+  0.77    0.000466         232         2           clone3
+  0.77    0.000463           6        67        24 newfstatat
+  0.37    0.000220           7        31           brk
+  0.27    0.000165           8        20           mprotect
+  0.17    0.000100           4        23        15 access
+  0.11    0.000065           3        19           rt_sigaction
+  0.10    0.000060          30         2           unlink
+  0.09    0.000056          18         3           getcwd
+  0.09    0.000056           5        11           prlimit64
+  0.09    0.000055           6         8           write
+  0.09    0.000053           3        14           rt_sigprocmask
+  0.07    0.000043           3        12           lseek
+  0.04    0.000026           3         7           getrandom
+  0.04    0.000022          11         2           munmap
+  0.04    0.000022           3         6         6 ioctl
+  0.02    0.000013           4         3           sysinfo
+  0.02    0.000011           3         3           rseq
+  0.02    0.000010           3         3           arch_prctl
+  0.02    0.000010           3         3           set_tid_address
+  0.02    0.000010           3         3           set_robust_list
+  0.01    0.000007           3         2           fcntl
+  0.01    0.000004           3         1           getrusage
+------ ----------- ----------- --------- --------- ----------------
+100.00    0.060214          33      1803      1095 total
+
+=== zig cc -O2 -c (zig 0.16.0) ===
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 17979 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 17980 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 17984 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 17988 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 17989 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 17990 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 17993 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 18047 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 18053 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 18054 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 18055 attached
+/mnt/vvv/hammer/store/3c1fd8c0fbb20c272c2935d5b5ca63bc51be257c271e96e312ee86ed6886f89d-strace/usr/bin/strace: Process 18056 attached
+% time     seconds  usecs/call     calls    errors syscall
+------ ----------- ----------- --------- --------- ------------------
+ 69.79    1.378703        1248      1104        14 futex
+ 25.60    0.505698      101139         5           wait4
+  1.26    0.024957        2495        10           poll
+  0.69    0.013629          42       318        80 openat
+  0.33    0.006426          18       353           mmap
+  0.31    0.006187          38       162        43 newfstatat
+  0.18    0.003615          47        76           read
+  0.17    0.003425          54        63           pread64
+  0.17    0.003361          17       195           rt_sigprocmask
+  0.15    0.003003          12       245           close
+  0.13    0.002634          25       105        29 readlink
+  0.13    0.002511         179        14         8 execve
+  0.12    0.002410          15       159           fstat
+  0.12    0.002312          28        82           mprotect
+  0.09    0.001870         103        18           munmap
+  0.09    0.001743         435         4           clone
+  0.08    0.001520          27        56           brk
+  0.08    0.001497         748         2           mkdirat
+  0.07    0.001355          43        31        19 access
+  0.06    0.001176           4       254           preadv
+  0.06    0.001096          16        67           rt_sigaction
+  0.05    0.000936         117         8           clone3
+  0.03    0.000545          45        12           readv
+  0.02    0.000492          28        17           set_robust_list
+  0.02    0.000483          53         9         7 faccessat2
+  0.02    0.000430           6        69           statx
+  0.02    0.000353          25        14           write
+  0.02    0.000342          15        22           prlimit64
+  0.02    0.000297          22        13           rseq
+  0.01    0.000277          25        11           madvise
+  0.01    0.000256          32         8           pipe2
+  0.01    0.000247          19        13        13 ioctl
+  0.01    0.000242          16        15           getrandom
+  0.01    0.000210          30         7           sched_getaffinity
+  0.01    0.000155          25         6           dup2
+  0.01    0.000149          18         8           sigaltstack
+  0.01    0.000147          13        11           lseek
+  0.01    0.000134          22         6           getdents64
+  0.00    0.000088          29         3         1 unlinkat
+  0.00    0.000078          12         6           arch_prctl
+  0.00    0.000075          18         4           gettid
+  0.00    0.000073          12         6           set_tid_address
+  0.00    0.000060          29         2           rename
+  0.00    0.000048          16         3           ftruncate
+  0.00    0.000045          22         2           pwritev
+  0.00    0.000036          17         2           flock
+  0.00    0.000028          28         1           readlinkat
+  0.00    0.000022          22         1           renameat
+  0.00    0.000020          20         1           unlink
+  0.00    0.000016           5         3           sysinfo
+  0.00    0.000008           7         1           uname
+  0.00    0.000005           4         1           getcwd
+------ ----------- ----------- --------- --------- ------------------
+100.00    1.975424         547      3608       214 total
+
+=== reparto de fallos del gcc (ENOENT por buscar cabeceras) ===
+166
+
+=== tiempo de pared del MISMO compilado, sin strace (mínimo/mediana de 7) ===
+cc -O2 -c hola.c        min  30.5 ms   mediana  33.3 ms
+zig cc -O2 -c hola.c    min  53.6 ms   mediana  58.9 ms
+
+=== lectura ===
+gcc:    1803 syscalls, 1095 con error (61%) — 919 de 923 readlink FALLAN.
+        Al piso medido de 78,7 ns por syscall, 1803 cruces = 0,14 ms sobre 30,5 ms de compilado
+        => el cruce al kernel es el 0,5% del compilado. La tormenta de rutas de T8 vale
+        MILISEGUNDOS por invocación, no minutos.
+zig cc: 3608 syscalls, sólo 214 con error, pero 1104 futex (70% del tiempo EN syscall) y 5 wait4.
+        Su perfil no es de búsqueda de rutas sino de coordinación entre hilos; el tiempo de
+        syscall que reporta strace -c es espera BLOQUEADA, no CPU.