1. Tres formas de relacionarse con la espera
En el tema 46, pread entregaba bytes o un error cuando retornaba. Ahora queremos administrar varias fuentes de trabajo sin quedar esperando una sola de ellas. Para razonar sobre ello conviene separar tres contratos.
| Modelo | Qué hace la aplicación | Qué recibe |
|---|---|---|
| Llamada bloqueante | Solicita una operación que puede esperar dentro de la llamada. | Su resultado al retornar. |
| No bloqueante y disponibilidad | Intenta operar, y espera eventos cuando todavía no puede avanzar. | Una indicación para volver a intentar read o write. |
| Envío y finalización | Describe una operación y conserva sus recursos hasta su resultado. | Una finalización asociada a esa solicitud. |
Disponibilidad: condición que permite intentar una operación sin la espera ordinaria de esa fuente. Finalización: resultado de una operación ya solicitada. Un evento de lectura de epoll no es una lectura terminada; un CQE de io_uring para una lectura sí informa su resultado.
Una interfaz asíncrona no elimina todas las esperas ni garantiza ejecución simultánea en hardware. La aplicación puede esperar finalizaciones, y el envío y procesamiento tienen costes. El objetivo es administrar trabajo pendiente y dependencias con un contrato explícito.
2. O_NONBLOCK y las banderas del descriptor
Para los objetos que lo soportan, O_NONBLOCK modifica qué ocurre cuando una operación no puede avanzar de inmediato. En lugar de esperar por disponibilidad puede retornar -1 con EAGAIN o EWOULDBLOCK. El comportamiento depende del tipo de objeto y del controlador.
int banderas = fcntl(fd, F_GETFL);
if (banderas == -1) {
/* Tratar el error; no usar banderas como si fueran validas. */
} else if (fcntl(fd, F_SETFL, banderas | O_NONBLOCK) == -1) {
/* Tratar el error de configuracion. */
}Estas son banderas de estado de la descripción de archivo abierta: descriptores duplicados pueden compartirlas. FD_CLOEXEC pertenece a otro conjunto de banderas y no se configura con F_SETFL. En el ejemplo de temporizadores elegiremos el modo no bloqueante al crearlos.
O_NONBLOCK no convierte automáticamente un acceso a disco en una operación asíncrona. En Linux no evita la espera de E/S de archivos regulares y dispositivos de bloque. La referencia open(2) delimita su efecto.
3. Datos, EOF, EAGAIN y EINTR
| Resultado de read con cantidad positiva | Interpretación | Próximo paso habitual |
|---|---|---|
| n > 0 | Se recibieron n bytes; pueden ser menos que los pedidos. | Procesar solo ese rango y actualizar el progreso. |
| n = 0 | EOF según el contrato de la fuente. | Finalizar o aplicar la regla correspondiente al objeto. |
| n = -1, EAGAIN/EWOULDBLOCK | No hay progreso disponible ahora en modo no bloqueante. | Esperar disponibilidad; no confundirlo con EOF. |
| n = -1, EINTR | La llamada fue interrumpida. | Reintentar o atender la política de señales y cancelación. |
| Otro error | La operación falló. | Registrar y resolver el estado; no usar el búfer como resultado. |
Una notificación no reserva datos para un consumidor determinado. El estado puede cambiar antes de nuestra lectura; por eso el intento sigue tratando EAGAIN. El ejemplo usa fuentes propias sin otros lectores, pero conserva esa comprobación.
Una escritura no bloqueante también puede ser parcial. Se conserva el tramo pendiente, y se solicita interés de escritura solo mientras haya datos por enviar; vigilar permanentemente una salida que siempre está lista puede despertar el bucle sin trabajo útil.
4. poll: consultar varios descriptores a la vez
poll recibe un arreglo de pollfd con fd, events y revents. events expresa el interés; revents contiene lo observado. El retorno positivo cuenta elementos con algún evento, no bytes ni necesariamente solo los eventos que solicitamos.
preparar descriptores e intereses
mientras quede trabajo:
esperar eventos con poll
si EINTR: aplicar la politica y recalcular el plazo si corresponde
si timeout: no inventar una lectura completada
por cada descriptor con revents:
tratar disponibilidad, cierre y errores
intentar la operacion y revisar su retornoPOLLIN indica disponibilidad para lectura. POLLOUT se refiere a escritura. POLLERR, POLLHUP y POLLNVAL requieren atención y pueden aparecer sin solicitarlos. Un fd negativo se ignora, lo que permite retirar entradas del arreglo.
En fuentes de flujo pueden coexistir POLLIN y POLLHUP: aún puede quedar contenido antes de EOF. Cerrar por ver HUP sin consumir lo que corresponde puede perder datos. La referencia poll(2) describe eventos y retorno.
5. Plazos, espera activa y reparto del trabajo
timeout=0 consulta sin esperar. Repetirlo continuamente consume CPU mientras no ocurre nada útil. Un timeout positivo permite esperar, y -1 expresa espera sin límite de esa llamada.
Si una aplicación tiene un plazo total, conviene expresarlo como instante límite del reloj monotónico y recalcular lo restante tras cada interrupción. Repetir el timeout completo después de cada EINTR extiende el presupuesto original.
limite = monotonic_now + presupuesto
antes de cada espera:
restante = limite - monotonic_now
si restante ≤ 0: terminar por plazo
esperar hasta restante
El timeout de poll es milisegundos; convertir y redondear con criterio.Un bucle también necesita repartir trabajo: una fuente muy activa no debe monopolizar la CPU. Si se limita su procesamiento por turno, hay que conservar el estado pendiente y volver a atenderlo. Con notificación por flancos esto será especialmente importante.
6. epoll y la notificación por nivel
epoll mantiene una lista de interés en el kernel. Se crea una instancia con epoll_create1, se agregan o modifican fuentes con epoll_ctl y se esperan notificaciones mediante epoll_wait. La aplicación no vuelve a pasar todo el arreglo de interés en cada espera.
El modo por defecto es por nivel, level-triggered: mientras persista una condición de disponibilidad, puede volver a notificarse. Si una fuente tiene 12 bytes y solo se consumen 4, aún quedan 8 que justifican disponibilidad en una espera posterior.
El campo data permite asociar una identidad de aplicación. El número de descriptor puede reutilizarse después de close, por lo que la identidad de una operación no debería depender de un entero viejo sin controlar su ciclo de vida.
La documentación de epoll(7) explica las listas y modos. epoll ayuda con muchos descriptores, pero no elimina trabajo de registro, procesamiento ni sincronización y no ofrece un multiplicador universal de rendimiento frente a poll.
7. Flancos, drenaje y EAGAIN
Con EPOLLET se elige notificación por flancos, edge-triggered. No se debe esperar que una condición que sigue lista se notifique de nuevo simplemente porque quedó trabajo sin consumir.
La regla de un consumidor sencillo es utilizar el descriptor no bloqueante y continuar intentando la operación hasta EAGAIN, EOF o error. Si se impone un presupuesto por turno, la aplicación conserva el descriptor en su propia lista de trabajo listo hasta terminar de drenarlo.
al recibir el evento:
repetir:
n = read(fd, buffer, capacidad)
n > 0: procesar esos n bytes y continuar
n == 0: resolver EOF y retirar la fuente cuando corresponda
EINTR: reintentar segun la politica
EAGAIN/EWOULDBLOCK: esperar una nueva notificacion
otro error: resolver falloEPOLLONESHOT es otra opción: desactiva el interés tras una notificación hasta rearmarlo con EPOLL_CTL_MOD. No es sinónimo de EPOLLET. En el programa usaremos temporizadores de un único vencimiento; eso describe al temporizador, no el uso de EPOLLONESHOT.
8. Qué cambia con archivos regulares
En Linux, poll suele considerar los archivos regulares disponibles para lectura y escritura, aunque una operación posterior pueda esperar por el almacenamiento. Esa disponibilidad no significa que sus datos ya estén residentes en RAM.
Registrar un archivo regular en epoll normalmente falla con EPERM por no admitir ese tipo de notificación. No se resuelve ese fallo agregando O_NONBLOCK. La referencia epoll_ctl(2) describe el error para objetivos sin soporte.
| Necesidad | Herramienta del tema | Qué no supone |
|---|---|---|
| Esperar vencimientos de varios temporizadores. | poll o epoll sobre timerfd. | No se están leyendo bloques de disco. |
| Recibir resultados de lecturas de un archivo. | Solicitudes READ de io_uring. | No dependen de registrar el archivo en epoll. |
| Consultar sin esperar un evento de disponibilidad. | poll con timeout cero. | No transforma una lectura regular en asíncrona. |
9. Una fuente local de eventos: timerfd
timerfd permite representar vencimientos de un temporizador mediante un descriptor. Se crea con un reloj, se arma y luego se lee un contador de expiraciones como uint64_t. El tamaño de esa lectura es parte de su contrato: no es una fuente arbitraria de bytes.
Usaremos dos temporizadores locales con un solo vencimiento y sin intervalo periódico. Antes de armarlos, una lectura no bloqueante devuelve EAGAIN; después del vencimiento, entrega el contador. Esta práctica no requiere comunicación entre procesos.
Un temporizador periódico podría acumular varias expiraciones antes de leerlo. Por eso en general una notificación no equivale a «un único vencimiento». En nuestro contrato de una sola expiración por temporizador, el contador esperado es uno. Véase timerfd_create(2).
10. Programa en C: poll, epoll y EPOLLET
Guarda esperar-eventos.c. Recibe modo poll|epoll|epollet y dos demoras de 1 a 1000 ms. Las identidades 0 y 1 son estables durante la ejecución; no se presupone orden de entrega. Devuelve 0 al observar ambos vencimientos, 1 ante error y 2 por uso incorrecto.
#define _POSIX_C_SOURCE 200809L
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <poll.h>
#include <unistd.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
static int demora(const char *s)
{
unsigned n = 0;
if (!*s || strlen(s) > 4) return -1;
for (; *s; ++s) {
if (*s < '0' || *s > '9') return -1;
n = n * 10 + (unsigned)(*s - '0');
}
return n >= 1 && n <= 1000 ? (int)n : -1;
}
int main(int argc, char **argv)
{
int ms[2];
if (argc != 4 || (strcmp(argv[1], "poll") && strcmp(argv[1], "epoll") &&
strcmp(argv[1], "epollet")) || (ms[0] = demora(argv[2])) < 0 ||
(ms[1] = demora(argv[3])) < 0) {
fprintf(stderr, "Uso: %s poll|epoll|epollet ms0 ms1 (1..1000)\n", argv[0]);
return 2;
}
int usar_epoll = strcmp(argv[1], "poll") != 0;
int flancos = !strcmp(argv[1], "epollet");
int fd[2] = {-1, -1}, ep = -1, completos = 0, estado = 1;
if (usar_epoll && (ep = epoll_create1(EPOLL_CLOEXEC)) == -1) goto fallo;
for (unsigned i = 0; i < 2; ++i) {
fd[i] = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if (fd[i] == -1) goto fallo;
uint64_t contador;
ssize_t n;
do { n = read(fd[i], &contador, sizeof contador); } while (n == -1 && errno == EINTR);
if (n != -1 || (errno != EAGAIN && errno != EWOULDBLOCK)) { errno = EIO; goto fallo; }
printf("INICIAL_%u=EAGAIN\n", i);
if (usar_epoll) {
struct epoll_event e = {0};
e.events = EPOLLIN | (flancos ? EPOLLET : 0);
e.data.u32 = i;
if (epoll_ctl(ep, EPOLL_CTL_ADD, fd[i], &e)) goto fallo;
}
}
for (unsigned i = 0; i < 2; ++i) {
struct itimerspec t = {0};
t.it_value.tv_sec = ms[i] / 1000;
t.it_value.tv_nsec = (long)(ms[i] % 1000) * 1000000L;
if (timerfd_settime(fd[i], 0, &t, NULL)) goto fallo;
}
while (completos < 2) {
unsigned ids[2];
int nids = 0, n;
if (usar_epoll) {
struct epoll_event eventos[2];
n = epoll_wait(ep, eventos, 2, 2000);
if (n == -1 && errno == EINTR) continue;
if (n < 0) goto fallo;
for (int j = 0; j < n; ++j) {
if (eventos[j].events & (EPOLLERR | EPOLLHUP)) { errno = EIO; goto fallo; }
if (eventos[j].events & EPOLLIN) ids[nids++] = eventos[j].data.u32;
}
} else {
struct pollfd p[2] = {{fd[0], POLLIN, 0}, {fd[1], POLLIN, 0}};
n = poll(p, 2, 2000);
if (n == -1 && errno == EINTR) continue;
if (n < 0) goto fallo;
for (unsigned j = 0; j < 2; ++j) {
if (p[j].revents & (POLLERR | POLLHUP | POLLNVAL)) { errno = EIO; goto fallo; }
if (p[j].revents & POLLIN) ids[nids++] = j;
}
}
if (n == 0) { fprintf(stderr, "Timeout sin ambos vencimientos\n"); goto limpiar; }
for (int j = 0; j < nids; ++j) {
unsigned id = ids[j];
if (id >= 2 || fd[id] == -1) { errno = EIO; goto fallo; }
uint64_t contador;
ssize_t r;
do { r = read(fd[id], &contador, sizeof contador); } while (r == -1 && errno == EINTR);
if (r == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) continue;
if (r == -1) goto fallo;
if (r != (ssize_t)sizeof contador || contador != 1) { errno = EIO; goto fallo; }
printf("EVENTO=%u EXPIRACIONES=%" PRIu64 "\n", id, contador);
if (usar_epoll && epoll_ctl(ep, EPOLL_CTL_DEL, fd[id], NULL)) goto fallo;
int viejo = fd[id]; fd[id] = -1;
if (close(viejo)) goto fallo;
++completos;
}
}
puts("COMPLETOS=2");
estado = 0;
goto limpiar;
fallo:
perror("eventos");
limpiar:
for (unsigned i = 0; i < 2; ++i) if (fd[i] != -1 && close(fd[i])) estado = 1;
if (ep != -1 && close(ep)) estado = 1;
if (fflush(stdout) == EOF) estado = 1;
return estado;
}El timeout es de cada llamada, no un plazo global: el ejemplo permite reintentar tras EINTR. Cada fuente es de un solo vencimiento y se retira después de consumir su contador completo; no queda contenido pendiente que exija otra notificación por flanco. Un consumidor de un flujo reutilizable necesitaría el bucle de drenaje de la sección 7.
ERR/HUP se tratan aquí como fallos del contrato de estos temporizadores propios. Ese camino no se copia como regla de cierre inmediato para fuentes de flujo que puedan conservar datos después de HUP.
11. io_uring: SQE, CQE y propiedad del búfer
io_uring utiliza una cola de envío, SQ, y una de finalización, CQ. La aplicación prepara una SQE describiendo la operación; un CQE posterior identifica la solicitud y su resultado. La biblioteca liburing encapsula el manejo de los anillos.
En una lectura, cqe->res contiene bytes si es positivo, cero para EOF o un código -errno si la operación falla. El retorno de esperar el CQE describe la espera, no el resultado de la lectura. Se consume la entrada con io_uring_cqe_seen una vez copiados y procesados sus datos relevantes.
Los búferes y el estado referenciado permanecen vivos hasta que termina la operación correspondiente. Enviar una solicitud no los libera. Tampoco se supone copia cero universal ni que todas las operaciones se ejecuten fuera del hilo que envía. La referencia io_uring(7) describe el modelo.
Se usarán offsets explícitos para evitar competir por una posición de archivo compartida. io_uring_prep_read(3) explica la diferencia respecto de offset=-1.
12. Programa en C: dos lecturas con liburing
Guarda dos-lecturas-uring.c. Requiere liburing y un archivo regular estable de al menos 128 bytes. Envía lecturas de 64 bytes en offsets 0 y 64. Conserva búferes estáticos, identifica CQE mediante user_data y reenvía el tramo faltante si una lectura es corta y positiva.
Devuelve 0 al completar ambos rangos, 1 ante fallo, 2 por uso incorrecto y 3 si no logra inicializar el anillo. Estado 3 puede deberse a política del entorno o recursos, no solo a ausencia de soporte del kernel.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <liburing.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
struct trabajo {
unsigned char buffer[64];
unsigned usados;
int pendiente, terminado;
};
static struct trabajo trabajos[2];
static _Noreturn void fatal(const char *mensaje, int codigo)
{
fprintf(stderr, "%s: %s\n", mensaje, strerror(codigo));
fflush(stderr);
/* No retornar ni reutilizar memoria con operaciones aun pendientes. */
_Exit(1);
}
static void preparar(struct io_uring *ring, int fd, unsigned id)
{
struct trabajo *t = &trabajos[id];
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
if (!sqe) fatal("Sin espacio SQ para el lote acotado", EIO);
io_uring_prep_read(sqe, fd, t->buffer + t->usados, 64 - t->usados,
(uint64_t)id * 64 + t->usados);
io_uring_sqe_set_data64(sqe, id);
t->pendiente = 1;
}
static void enviar(struct io_uring *ring, int esperado)
{
int r = io_uring_submit(ring);
if (r < 0) fatal("io_uring_submit", -r);
if (r != esperado) {
fprintf(stderr, "Envio incompleto: %d de %d SQE\n", r, esperado);
fatal("El ejemplo no continua tras envio parcial", EIO);
}
}
int main(int argc, char **argv)
{
if (argc != 2) { fprintf(stderr, "Uso: %s archivo-regular\n", argv[0]); return 2; }
int fd = open(argv[1], O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
if (fd == -1) { perror("open"); return 1; }
struct stat st;
if (fstat(fd, &st) == -1) { perror("fstat"); close(fd); return 1; }
if (!S_ISREG(st.st_mode) || st.st_size < 128) {
fprintf(stderr, "Se requiere archivo regular estable de al menos 128 bytes\n");
close(fd); return 1;
}
struct io_uring ring;
int r = io_uring_queue_init(4, &ring, 0);
if (r < 0) {
fprintf(stderr, "INIT_ERROR=%d %s\n", -r, strerror(-r)); close(fd); return 3;
}
preparar(&ring, fd, 0);
preparar(&ring, fd, 1);
enviar(&ring, 2);
unsigned completos = 0;
while (completos < 2) {
struct io_uring_cqe *cqe;
do { r = io_uring_wait_cqe(&ring, &cqe); } while (r == -EINTR);
if (r < 0) fatal("io_uring_wait_cqe", -r);
uint64_t identidad = io_uring_cqe_get_data64(cqe);
int resultado = cqe->res;
io_uring_cqe_seen(&ring, cqe);
if (identidad >= 2) fatal("Identidad CQE invalida", EIO);
unsigned id = (unsigned)identidad;
struct trabajo *t = &trabajos[id];
if (!t->pendiente || t->terminado) fatal("CQE duplicado o fuera de estado", EIO);
t->pendiente = 0;
if (resultado < 0) fatal("Resultado READ", -resultado);
if (resultado == 0) fatal("EOF antes de completar el rango", EIO);
if ((unsigned)resultado > 64 - t->usados) fatal("Resultado demasiado grande", EIO);
t->usados += (unsigned)resultado;
if (t->usados == 64) {
t->terminado = 1; ++completos;
printf("ID=%u OFFSET=%u BYTES=64 HEX=", id, id * 64);
for (unsigned i = 0; i < 64; ++i) printf("%02X", (unsigned)t->buffer[i]);
putchar('\n');
} else {
preparar(&ring, fd, id);
enviar(&ring, 1);
}
}
/* Ambos rangos terminaron: ya no queda memoria de lectura en uso. */
io_uring_queue_exit(&ring);
int estado = close(fd) == -1 ? 1 : 0;
if (fflush(stdout) == EOF) estado = 1;
return estado;
}Los helpers de liburing retornan errores negativos según su contrato: se usa strerror(-r), no un errno posiblemente ajeno. El programa no activa SQPOLL ni opciones avanzadas. Exige el envío completo del pequeño lote; ante envío parcial termina con error en lugar de asumir dos operaciones aceptadas.
Este ejecutable adopta un camino fatal explícito: si falla después de enviar trabajo, _Exit termina el proceso y deja al kernel gestionar su terminación y recursos; no retorna a código que pueda reutilizar los búferes estáticos. No es una función reutilizable de biblioteca ni un ejemplo de cancelación ordenada. En el camino normal sí consume todas las finalizaciones antes de destruir el anillo y cerrar.
13. Errores, cancelación y cierre del ciclo
Preparar, enviar y completar son estados distintos. io_uring_submit(3) informa cuántas SQE se enviaron con las opciones utilizadas. Un programa general conserva las no enviadas y sigue su progreso; no espera CQE por operaciones que nunca quedaron enviadas.
La espera de CQE también puede fallar independientemente de una lectura. io_uring_wait_cqe(3) distingue su retorno de cqe->res. Después de copiar los datos pertinentes se marca el consumo con io_uring_cqe_seen(3); no se conserva el puntero CQE como si siguiera siendo propio indefinidamente.
Solicitar cancelación es una operación adicional y puede competir con una finalización ya producida. Un programa de larga vida debe identificar tanto la solicitud original como la cancelación, interpretar sus resultados y no liberar el búfer original antes de resolver su operación. «Pedí cancelar» no significa «ya puedo reutilizar memoria».
Cerrar un descriptor o destruir un anillo no se usa en este tema como prueba de que la aplicación consumió todas las finalizaciones. io_uring_queue_exit(3) desmonta los recursos del anillo; nuestro camino normal llega allí cuando el contador de trabajo pendiente ya es cero.
La disponibilidad real depende de kernel, biblioteca y política de ejecución. Un entorno puede rechazar la inicialización con EPERM o limitar operaciones. Se registra ese resultado sin presentarlo como una lectura completada ni reemplazar silenciosamente el ensayo por otro.
14. Laboratorio con eventos y archivo propio
En Bash de Linux crea un directorio nuevo y guarda los dos fuentes. El programa de eventos solo necesita el compilador y las interfaces Linux utilizadas. Para el segundo se requieren cabeceras y biblioteca liburing; en entornos Debian/Ubuntu suelen estar en liburing-dev. Comprueba su disponibilidad en tu entorno.
mkdir laboratorio47
cd laboratorio47
# Guardar aqui esperar-eventos.c y dos-lecturas-uring.c.
cc -std=c11 -O2 -Wall -Wextra -Wpedantic esperar-eventos.c -o esperar-eventos
./esperar-eventos poll 40 90
./esperar-eventos epoll 40 90
./esperar-eventos epollet 40 90
./esperar-eventos epoll 50 50Cada ejecución debe mostrar INICIAL_0=EAGAIN, INICIAL_1=EAGAIN, un EVENTO por identidad con EXPIRACIONES=1 y COMPLETOS=2. No se exige el orden entre identidades, especialmente si ambas están listas cuando se despierta el proceso. Los tiempos pedidos no son una promesa de planificación en tiempo real.
python3 - <<'PY'
from pathlib import Path
with Path('datos47.bin').open('xb') as f:
f.write(bytes(range(256)) * 4)
PY
cc -std=c11 -O2 -Wall -Wextra -Wpedantic dos-lecturas-uring.c -luring -o dos-lecturas-uring
./dos-lecturas-uring datos47.binLa referencia io_uring_queue_init(3) explica inicialización y errores. Si falta la biblioteca no se compila este segundo ejecutable; si el anillo no se puede inicializar, el programa informa estado 3. Ninguno de esos casos convierte el ensayo en un éxito de E/S.
La salida puede comenzar por ID=0 o por ID=1. Para ID=0 se esperan los bytes 00 a 3F y para ID=1, 40 a 7F. El archivo solo se abre en lectura; no se exige una mejora de velocidad con 128 bytes que probablemente ya estén en caché.
Como ampliación, repite el experimento bajo carga y registra comportamiento sin usar sus demoras como prueba de que una interfaz sea siempre más rápida. Los dos programas comparan modelos de eventos y finalización, no cargas de trabajo equivalentes para un benchmark.
15. Verificador de identidades y contenido
Desde laboratorio47 ejecuta este bloque con Python 3. Comprueba los tres modos con demoras distintas y coincidentes, rechazos de argumentos y contenido por identidad. El ensayo io_uring se ejecuta si el binario fue compilado; una inicialización rechazada se informa como caso no ejecutado, separado del éxito.
python3 - <<'PY'
from pathlib import Path
from tempfile import TemporaryDirectory
import hashlib
import os
import re
import subprocess
eventos = str(Path('esperar-eventos').resolve())
uring = Path('dos-lecturas-uring').resolve()
def ejecutar(args, estado=0):
r = subprocess.run(args, text=True, capture_output=True, timeout=10)
assert r.returncode == estado, (args, r.returncode, r.stderr)
return r.stdout
for modo in ('poll', 'epoll', 'epollet'):
for a, b in ((10, 30), (30, 10), (15, 15)):
texto = ejecutar([eventos, modo, str(a), str(b)])
assert 'INICIAL_0=EAGAIN' in texto and 'INICIAL_1=EAGAIN' in texto
vistos = re.findall(r'^EVENTO=(\d+) EXPIRACIONES=(\d+)$', texto, re.M)
assert sorted(vistos) == [('0', '1'), ('1', '1')]
assert texto.splitlines()[-1] == 'COMPLETOS=2'
for args in ([], ['otro', '10', '20'], ['poll', '0', '20'],
['epoll', '-1', '20'], ['epollet', '10', '1001'],
['poll', '1.5', '20']):
ejecutar([eventos, *args], 2)
print('OK eventos: identidades, EAGAIN, vencimientos y rechazos')
if not uring.is_file():
print('NO EJECUTADO io_uring: falta el binario compilado con liburing')
else:
with TemporaryDirectory(prefix='uring47-') as d:
archivo = Path(d) / 'propio.bin'
datos = bytes(range(256)) * 4
archivo.write_bytes(datos)
antes = hashlib.sha256(datos).digest()
r = subprocess.run([str(uring), str(archivo)], capture_output=True, text=True, timeout=10)
if r.returncode == 3:
assert 'INIT_ERROR=' in r.stderr
print('NO EJECUTADO io_uring: inicializacion rechazada:', r.stderr.strip())
else:
assert r.returncode == 0, (r.returncode, r.stderr)
vistos = {}
for linea in r.stdout.splitlines():
m = re.fullmatch(r'ID=(\d+) OFFSET=(\d+) BYTES=64 HEX=([0-9A-F]+)', linea)
assert m, linea
i, offset = int(m[1]), int(m[2])
assert i in (0, 1) and i not in vistos and offset == i * 64
vistos[i] = bytes.fromhex(m[3])
assert set(vistos) == {0, 1}
for i in vistos: assert vistos[i] == datos[i * 64:(i + 1) * 64]
print('OK io_uring: ambos rangos por identidad y contenido')
assert hashlib.sha256(archivo.read_bytes()).digest() == antes
ejecutar([str(uring)], 2)
corta = Path(d) / 'corta.bin'; corta.write_bytes(b'ABC')
ejecutar([str(uring), str(corta)], 1)
enlace = Path(d) / 'enlace'; enlace.symlink_to(archivo)
ejecutar([str(uring), str(enlace)], 1)
fifo = Path(d) / 'fifo'; os.mkfifo(fifo)
ejecutar([str(uring), str(fifo)], 1)
PYUn archivo corto se rechaza antes de enviar solicitudes, y un FIFO no provoca una apertura bloqueante gracias a O_NONBLOCK y la comprobación de tipo. El verificador no fuerza ni presupone el orden de las finalizaciones y no mide un rendimiento mínimo.
16. Simuladores de disponibilidad y finalización
¿Qué ocurre si una notificación por flanco deja datos pendientes?
Hay una única llegada inicial de bytes a una fuente modelada que sigue abierta; después no hay nuevas llegadas ni EOF. Cada lectura entrega hasta el tramo elegido. El modelo compara una lectura por notificación con drenar hasta EAGAIN. Los turnos sin evento representan espera, no llamadas repetidas con timeout cero.
Nivel, flanco y datos sin consumir
| Turno | Evento | Llamadas read | Bytes recibidos | Pendientes | EAGAIN observado |
|---|
Seguir resultados y propiedad de dos búferes
Dos lecturas independientes piden 64 bytes en offsets 0 y 64. El orden es elegido para observar que no determina la identidad. En el caso corto, ID=0 entrega primero 20 y se reenvían sus 44 bytes restantes. El caso de error conserva su resultado como fallo y permite observar la otra finalización; el ejecutable C elige terminar el proceso ante un error operativo.
Preparar, enviar, completar y liberar
| ID | Offset base | Bytes acumulados | Estado | Búfer |
|---|
| CQE | user_data | res | Acción de la aplicación |
|---|
17. Diagnóstico y resumen
| Observación | Qué revisar |
|---|---|
| Se cierra una fuente cuando read da EAGAIN. | No es EOF: falta disponibilidad en ese momento. |
| El proceso consume CPU sin eventos. | Buscar consultas repetidas con timeout cero e intereses de escritura sin trabajo. |
| EPOLLET deja datos que ya no vuelven a notificarse. | Drenar hasta EAGAIN o mantener una lista propia de trabajo listo. |
| epoll rechaza un archivo regular con EPERM. | Ese tipo no admite la notificación requerida; no se arregla con O_NONBLOCK. |
| Una lectura se atribuye al offset equivocado. | Usar user_data para identificar la solicitud; no inferir por orden del CQE. |
| El búfer se libera tras submit. | Conservarlo hasta resolver la finalización de esa operación. |
| Se pidió cancelación y se reutilizó memoria inmediatamente. | Resolver la carrera y los resultados de la operación original y la cancelación. |
- No bloqueante, disponibilidad y finalización son contratos diferentes.
- EAGAIN expresa falta de progreso inmediato; EOF y error son otros estados.
- poll y epoll notifican condiciones para intentar operar, no bytes ya transferidos.
- En EPOLLET, dejar trabajo sin gestionar puede impedir el avance sin nuevas notificaciones.
- O_NONBLOCK y epoll no convierten una lectura de archivo regular en E/S de disco asíncrona.
- io_uring asocia resultados a solicitudes mediante CQE y permite conservar varias pendientes.
- Una lectura parcial avanza su búfer y offset; una finalización no se atribuye por su orden.
- Los recursos se conservan hasta resolver su operación y los errores se distinguen de una inicialización no disponible.
18. Preguntas de repaso y práctica
1. ¿EAGAIN es fin de archivo?
No. Indica que no hay progreso inmediato disponible en esa operación no bloqueante.
2. ¿Qué se conserva al activar O_NONBLOCK con fcntl?
Las banderas existentes, obtenidas con F_GETFL antes de combinar O_NONBLOCK.
3. ¿poll realiza la lectura de los datos?
No. Devuelve eventos; la aplicación sigue intentando read y revisando su retorno.
4. ¿Qué cuenta el retorno positivo de poll?
Entradas del arreglo con algún revents, no bytes recibidos.
5. ¿POLLHUP obliga siempre a cerrar sin leer?
No. En algunas fuentes puede coexistir con datos pendientes y POLLIN.
6. ¿Qué riesgo tiene repetir timeout cero?
Convertir el bucle en espera activa sin trabajo útil y consumir CPU.
7. ¿Por qué recalcular un plazo tras EINTR?
Porque repetir el timeout original puede extender el presupuesto total.
8. ¿Qué significa notificación por nivel?
Que una condición de disponibilidad persistente puede volver a notificarse.
9. ¿Cuál es la regla sencilla para EPOLLET?
Usar modo no bloqueante y consumir hasta EAGAIN, EOF o error, conservando trabajo propio si se limita el turno.
10. ¿EPOLLONESHOT es EPOLLET?
No. EPOLLONESHOT desactiva el interés después de notificar hasta rearmarlo.
11. ¿O_NONBLOCK evita toda espera de disco regular?
No en Linux. No transforma ese acceso en una lectura asíncrona.
12. ¿Por qué se usan timerfd en el ejemplo?
Para disponer de dos fuentes locales de eventos sin introducir comunicación entre procesos.
13. ¿Qué devuelve una lectura válida de timerfd?
Un contador uint64_t de expiraciones; su tamaño e interpretación son específicos de esa interfaz.
14. ¿Una notificación de timerfd siempre equivale a una expiración?
No en general. Los periódicos pueden acumular varias; el ejemplo usa temporizadores de un único vencimiento.
15. ¿Qué identifica user_data?
La solicitud o estado que la aplicación asoció a la SQE y reconoce en el CQE.
16. ¿Un retorno cero de wait_cqe significa cero bytes leídos?
No. Describe éxito de la espera; los bytes o error de la operación están en cqe->res.
17. ¿Qué significa cqe->res = -EIO?
La operación terminó con ese error; no se consulta un errno ajeno para interpretar el resultado.
18. ¿Qué hacer con una lectura corta positiva?
Avanzar el progreso y, si el contrato exige completar, enviar o ejecutar el resto con búfer y offset ajustados.
19. ¿Cuándo puede reutilizarse un búfer de lectura pendiente?
Cuando la operación que lo utiliza ya fue resuelta; enviar o solicitar cancelar no basta por sí solo.
20. ¿Este ejemplo prueba que io_uring siempre supera a pread?
No. Verifica identidad y contenido; un benchmark necesita cargas equivalentes y mediciones.
Para practicar
- Clasifica read positivo, cero, EAGAIN, EINTR y error en estados distintos.
- Ejecuta los tres modos de eventos y verifica identidades sin exigir orden.
- Cambia las demoras de los temporizadores y explica por qué no se exige una duración exacta del proceso.
- En el simulador, compara 12 bytes en tramos de 4 con LT y ET usando una sola lectura por evento.
- Activa drenaje en ET y distingue el último read con EAGAIN de una lectura con EOF.
- Propón cómo conservar una lista propia de trabajo listo al limitar el presupuesto de un turno.
- Compila el ejemplo liburing si está disponible y contrasta ambos rangos del archivo conocido.
- En el simulador de CQE, invierte el orden y comprueba que no cambia la identidad de los datos.
- Observa la lectura corta de 20 bytes y calcula offset, dirección de búfer y cantidad para los 44 restantes.
- Ejecuta el verificador y distingue éxito, fallo y ensayo io_uring no ejecutado.
- Describe qué estados adicionales necesitaría una biblioteca reutilizable que cancela y drena sin terminar el proceso.
- Diseña una comparación de rendimiento con la misma carga, concurrencia y condiciones de caché en ambas interfaces.
En el tema 48 integraremos lo aprendido en un mini sistema de archivos sobre una imagen: estructuras propias, asignación y operaciones verificables.