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
This commit is contained in:
Sergio
2026-08-29 19:31:48 +00:00
co-authored by Claude Opus 5
parent 7433bcba83
commit 849c041446
7 changed files with 723 additions and 0 deletions
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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)
@@ -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
@@ -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)
@@ -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.