E/S bloqueante frente a no bloqueante
Por qué son importantes los bucles de eventos
E/S bloqueante frente a no bloqueante es una lección gratuita de C Academy en CoddyKit. Esta es la lección 1 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de C Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de C Academy incluye 4 lecciones en total.
Qué significa bloquearse
Una llamada al sistema bloqueante suspende el hilo que la realiza hasta que la operación puede continuar. Cuando llama a recv() en un socket sin datos, el kernel pone el hilo en espera hasta que llegan bytes.
Es fácil de razonar: una conexión, un hilo y código lineal. El coste aparece cuando necesita atender a miles de clientes simultáneamente.
ssize_t n = recv(fd, buf, sizeof buf, 0);
/* thread sleeps here until data or error */
if (n > 0) handle(buf, n);El problema de la escalabilidad
Con E/S bloqueante, un cliente atascado bloquea todo el hilo. La solución clásica es usar un hilo (o proceso) por conexión.
Funciona hasta cierto punto, pero 10.000 hilos implican 10.000 pilas, cambios de contexto costosos y sobrecarga del planificador. Este es el famoso problema C10k, que impulsó a los servidores hacia diseños basados en eventos.
Modo no bloqueante
Un socket no bloqueante nunca se queda dormido. Si una llamada no puede completarse de inmediato, devuelve -1 en el acto y establece errno en EAGAIN o EWOULDBLOCK.
Ahora su código es responsable de volver a intentarlo más tarde. Esto permite que un único hilo gestione muchos sockets sin quedarse atascado en ninguno.
ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
/* no data right now, try again later */
}Configuración de O_NONBLOCK con fcntl
Puede cambiar un descriptor al modo no bloqueante añadiendo la marca O_NONBLOCK con fcntl(). Lea siempre primero las marcas actuales y después aplique OR con el bit, para no sobrescribir otras configuraciones.
Este mismo auxiliar se utiliza tanto con sockets de escucha como con sockets de clientes aceptados y tuberías.
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}Gestión de lecturas parciales
La E/S no bloqueante convierte las operaciones parciales en algo habitual. Un recv() puede devolver menos bytes de los solicitados y send() puede aceptar solo una parte del búfer.
Debe llevar la cuenta de cuánto ha enviado o recibido y continuar desde ese punto. Nunca suponga que una sola llamada mueve todos los bytes.
size_t sent = 0;
while (sent < len) {
ssize_t w = send(fd, buf + sent, len - sent, 0);
if (w < 0) { if (errno == EAGAIN) break; else return -1; }
sent += w;
}La espera activa es incorrecta
La forma ingenua de usar sockets no bloqueantes consiste en recorrerlos todos y reintentarlo constantemente. Esta espera activa consume el 100 % de la CPU incluso cuando no sucede nada.
Lo que realmente queremos es preguntar al kernel: "dime qué descriptores están listos y déjame dormir hasta entonces". Eso es exactamente lo que proporciona la notificación de disponibilidad.
Notificación de disponibilidad
La multiplexación de E/S permite que un hilo espere simultáneamente a muchos descriptores y se despierte solo cuando al menos uno está listo. El kernel se encarga de vigilarlos.
Las interfaces clásicas son select() y poll(). Funcionan, pero vuelven a recorrer todos los descriptores en cada llamada, lo que resulta costoso a gran escala.
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
select(fd + 1, &rfds, NULL, NULL, NULL);Por qué select y poll no escalan
Tanto select() como poll() son O(n): cada llamada pasa al kernel el conjunto completo de descriptores, el kernel los recorre todos y después usted vuelve a recorrerlos para encontrar los que están listos.
Además, select() está limitado aproximadamente por FD_SETSIZE (a menudo 1024). Con miles de conexiones, esta sobrecarga se vuelve dominante.
Entra epoll
epoll es la solución escalable de Linux. Registra una vez el interés por un descriptor y el kernel mantiene una estructura de datos interna que realiza el seguimiento de su disponibilidad.
Cada espera devuelve únicamente los descriptores que están realmente listos, por lo que el coste depende de las conexiones activas y no del total de conexiones. Esto lo hace aproximadamente O(1) por evento disponible.
int epfd = epoll_create1(0);
/* register fds once, then wait for ready events */No bloqueante más epoll
epoll y los sockets no bloqueantes forman un equipo. epoll le indica que un descriptor está listo; las llamadas no bloqueantes permiten vaciarlo sin quedarse dormido.
Siempre debe establecer O_NONBLOCK en los sockets que entregue a epoll. De lo contrario, una activación espuria o una lectura parcial podría bloquear el único hilo del bucle de eventos.
set_nonblocking(conn_fd);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);El modelo mental
Imagine el servidor como un bucle: bloquearse en epoll_wait(), recibir una lista pequeña de descriptores listos, realizar trabajo no bloqueante en cada uno y repetir.
El hilo duerme cuando no hay actividad y se despierta únicamente cuando hay trabajo real. Ahora un solo hilo puede atender eficazmente a decenas de miles de conexiones.
Comprobación rápida
Compruebe su comprensión de los sockets no bloqueantes.
Repaso
La E/S bloqueante es sencilla, pero mantiene ocupado un hilo por conexión, lo que no escala. La E/S no bloqueante devuelve inmediatamente EAGAIN en lugar de quedarse dormida.
Consultar los sockets en un bucle estrecho desperdicia CPU, por lo que usamos la notificación de disponibilidad. select/poll son O(n); epoll escala a muchos miles de conexiones. A continuación configuraremos epoll.
Preguntas frecuentes
¿La lección «E/S bloqueante frente a no bloqueante» es gratis?
Sí — el texto completo de «E/S bloqueante frente a no bloqueante» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de C Academy, actualiza a CoddyKit PRO. El curso de C Academy incluye 4 lecciones en total.
¿Qué aprenderé en «E/S bloqueante frente a no bloqueante»?
Por qué son importantes los bucles de eventos Practicas C Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar C Academy?
No se requiere experiencia previa. C Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 1 de 4.
¿Cuánto tiempo toma la lección «E/S bloqueante frente a no bloqueante»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de C Academy?
Sí. Cada lección de C Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- E/S bloqueante frente a no bloqueante
- Configurar epoll
- El bucle de eventos
- Un servidor de eco sencillo