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 · 2026-08-29T19:31:48Z (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
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.