1. Dos llamadas para transferir bytes
En el tema 5 obtuvimos descriptores mediante open y organizamos su cierre. Ahora vamos a usarlos. Las interfaces básicas de transferencia tienen tres argumentos: una referencia al objeto, una región de memoria y una cantidad máxima de bytes.
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
Read intenta transferir hasta count bytes desde el objeto referenciado por fd hacia la memoria indicada por buf. Write intenta transferir hasta count bytes desde esa memoria hacia el objeto. En write, const expresa que la llamada no necesita modificar los bytes de entrada del programa.
El tercer argumento es una cantidad solicitada. El retorno no negativo comunica la cantidad realmente transferida. Si hay un error comunicado como tal, el retorno es -1 y errno describe la causa. La lógica del programa debe avanzar según el retorno, no según lo solicitado.
Las llamadas trabajan con bytes. No buscan un salto de línea, no reconocen números decimales ni añaden un terminador de cadena. Pueden usarse con texto, imágenes, estructuras serializadas y otros datos binarios, siempre que el programa sepa qué significan y cuántos son válidos.
El descriptor debe permitir la operación
Un read sobre una apertura O_WRONLY normalmente falla con EBADF. Un write sobre O_RDONLY también. Tener el entero no autoriza cualquier operación. Además, el tipo de objeto importa: un archivo regular, una terminal, una tubería y un socket tienen reglas de disponibilidad diferentes.
Por ahora nos centraremos en archivos regulares y flujos de bytes como tuberías. Una lectura de dos bytes en una tubería no significa necesariamente leer un «mensaje de dos bytes». Otros objetos, como sockets de datagramas o dispositivos especiales, tienen contratos adicionales que no deben deducirse de un ejemplo con archivos regulares.
2. size_t, ssize_t y el valor de retorno
La cantidad solicitada usa size_t, un tipo entero sin signo adecuado para tamaños de memoria. El resultado usa ssize_t, que tiene signo para poder representar el valor -1 además de cantidades de bytes. Los dos tipos tienen propósitos distintos.
unsigned char buf[4096];
ssize_t n = read(fd, buf, sizeof buf);
if (n == -1) {
/* Error: examinar errno. */
} else if (n == 0) {
/* Con una peticion positiva y el objeto apropiado: EOF. */
} else {
size_t validos = (size_t)n;
/* Solo buf[0] hasta buf[validos - 1] son datos de esta lectura. */
}
Si guardas directamente el resultado en size_t, el -1 se convierte en una cantidad sin signo enorme. Una condición como n > 0 dejaría de distinguir el error correctamente. Conserva el retorno en ssize_t y conviértelo a size_t después de comprobar que es positivo.
También importa cómo se imprime
Para printf, %zu corresponde a size_t y %zd a su tipo con signo compatible usado para ssize_t en este entorno. No supongas que cualquier tamaño o retorno cabe en int y lo imprimas con %d. En los programas del tema usaremos los formatos adecuados y límites pequeños para que la práctica sea fácil de seguir.
No pedir más memoria de la que existe
Si buf tiene 100 bytes, no se pueden solicitar 1000 para llenarlo. El núcleo no conoce la intención de tu arreglo de C: una dirección puede apuntar a memoria accesible y aun así sobrepasar el objeto que reservaste. Es responsabilidad del programa mantener count dentro de la región válida.
Los tamaños enormes también tienen límites de interfaz y plataforma. Una petición superior a SSIZE_MAX no es una receta portable; Linux limita además la transferencia por llamada. No necesitas acercarte a esos máximos para transferir un archivo grande: se usan bloques de tamaño moderado en un bucle.
3. read: qué parte del búfer es válida
Supongamos un arreglo de ocho bytes y una lectura que devuelve 3. Los tres primeros bytes se han obtenido del objeto. Los cinco restantes no se convierten automáticamente en ceros y no forman parte de los datos recién leídos. Pueden conservar datos de una vuelta anterior o estar sin inicializar.
capacidad del arreglo: 8 bytes
peticion: read(fd, buf, 8)
retorno: 3
indice: 0 1 2 | 3 4 5 6 7
contenido ilustrativo: A B C | ? ? ? ? ?
validos | no obtenidos en esta llamada
Si después escribes ocho bytes de ese arreglo en otra salida, no transfieres solo ABC: también envías cinco bytes que no pertenecen a esa lectura. Es un error de corrección y puede revelar datos previos de memoria. La cantidad que debe acompañar al búfer es el retorno n.
Read no construye una cadena de C
Si vas a tratar una lectura como texto terminado en nulo, debes reservar una posición y añadirla tú después de una lectura positiva:
char texto[101];
ssize_t n = read(fd, texto, sizeof texto - 1);
if (n > 0) {
texto[n] = '\0';
/* Ahora hay terminacion, pero el contenido podria incluir otros NUL. */
}
Esto no convierte datos binarios en una cadena íntegra. Si dentro de los bytes leídos ya había un NUL, strlen o printf con %s se detendrán allí. Para conservar todo el contenido, mantén la longitud explícita y usa operaciones que la acepten. El lector hexadecimal del laboratorio nunca necesita tratar sus datos como cadenas.
Bytes, caracteres y codificación
En UTF-8 un carácter puede ocupar varios bytes. Read puede terminar entre esos bytes: no promete que cada retorno contenga texto completo en términos de caracteres. Si procesas la codificación, conserva las secuencias incompletas entre llamadas; si solo transportas bytes, conserva su orden y cantidad.
Por ejemplo, los bytes C3 B1 representan ñ en UTF-8. Una lectura de un byte puede recibir C3 y la siguiente B1. Ninguna de las dos llamadas está equivocada. Tampoco hay una regla de que un salto de línea o un terminador NUL provoquen EOF en un archivo regular.
4. EOF, lecturas cortas y longitud cero
En un archivo regular, read con una cantidad positiva devuelve cero cuando la posición ya no encuentra bytes disponibles en ese momento: es el fin de archivo, EOF. No se trata de un byte especial almacenado al final. Un archivo vacío puede devolver cero en la primera lectura.
llamada solicitado retorno datos posicion
read 1 4 4 ABCD 4
read 2 4 4 EFGH 8
read 3 4 2 IJ 10
read 4 4 0 ninguno 10
La tercera lectura es corta y correcta. Todavía debes procesar IJ. Si el bucle solo acepta retornos iguales a cuatro, perderá los dos últimos bytes. La cuarta confirma EOF. El manual de read(2) describe los retornos cortos y las condiciones de las interfaces.
Una lectura corta no prueba EOF
En una tubería o una terminal puede haber una cantidad de datos disponible menor que tu petición aunque después lleguen más. Una señal también puede hacer que una operación comunique la parte que ya transfirió. No uses n < pedido como criterio general para terminar un flujo.
En una tubería, mientras haya escritores abiertos y no haya datos, una lectura bloqueante puede esperar; en modo no bloqueante puede informar EAGAIN. Cuando no quedan escritores y el contenido pendiente se consumió, read devuelve cero. Una tubería temporalmente vacía y una sin escritores no son el mismo estado.
Pedir cero no es una prueba de fin
Si count es cero, una llamada puede devolver cero sin transferir datos y también puede detectar determinados errores. Ese cero no demuestra EOF. La interpretación del bucle de lectura presupone una petición positiva y el contrato del objeto observado.
Tampoco generalices «cero siempre es EOF» a cualquier interfaz que use un descriptor: por ejemplo, algunos objetos orientados a mensajes pueden tener mensajes de longitud cero. Aquí el criterio se aplica a archivos regulares y a los flujos de bytes estudiados.
EOF no cierra el descriptor
El proceso puede seguir teniendo una apertura válida después de una lectura cero. Si se vuelve a posicionar antes del final, puede leer datos anteriores. Si un archivo regular crece después, otra lectura desde la posición conservada puede obtener los bytes nuevos. EOF describe un resultado de lectura, no una orden automática de close.
En una terminal de Linux con entrada canónica, Ctrl+D puede hacer que se entregue una línea pendiente o que una lectura sin datos devuelva cero; no introduce por ello un «carácter EOF» en un documento. En el laboratorio usaremos archivos y tuberías que terminan por sí solos para no depender de la interacción de la terminal.
5. write: contar solo lo aceptado
Write devuelve cuántos bytes aceptó desde el principio de la región indicada. Si solicitas diez y devuelve tres, los bytes de índices 0, 1 y 2 se transfirieron; quedan siete. La siguiente llamada debe comenzar en buf + 3 y pedir siete, no volver a enviar los diez.
buffer: ABCDEFGHIJ
primera: write(fd, buf, 10) devuelve 3 → ABC
pendiente: DEFGHIJ
segunda: write(fd, buf + 3, 7) devuelve 4 → DEFG
pendiente: HIJ
tercera: write(fd, buf + 7, 3) devuelve 3 → HIJ
total: 10 bytes, sin duplicados
Una escritura parcial no es necesariamente un error comunicado con -1. Es un retorno positivo menor que la petición. Puede aparecer en tuberías, sockets, por límites de recursos, interrupciones tras progreso y otras condiciones. Una llamada posterior puede seguir avanzando o fallar: aceptar los primeros tres no reserva de antemano la capacidad de los siete restantes.
El conteo del contenido debe ser explícito
write(fd, "hola", 4) pide enviar los cuatro bytes del texto, sin el terminador de la cadena. Usar sizeof "hola" pediría cinco e incluiría el NUL. Para un arreglo binario, en cambio, sizeof puede ser exactamente la longitud deseada. Lo que importa es la región que se pretende transferir.
No calcules strlen sobre datos que acabas de leer arbitrariamente. Pueden contener NUL y no tienen por qué estar terminados. Para reenviar una lectura válida usa su cantidad n, y comprueba las posibles escrituras parciales de esos n bytes.
Write cero y ausencia de progreso
Una solicitud de longitud cero no envía datos y sus resultados dependen del objeto y de las comprobaciones realizadas. No sirve para demostrar que una escritura futura de longitud positiva funcionará. En el patrón de «escribir todo» de este tema, si se pidió una cantidad positiva y se observa un retorno cero, se comunica ausencia de progreso como error defensivo en vez de repetir indefinidamente la misma operación.
Ese error defensivo no es una afirmación de que toda escritura normal a un archivo regular vaya a devolver cero inesperadamente. Es una decisión explícita del ayudante: no entrar en un bucle infinito si la operación no avanza.
Aceptado no significa persistido
Un retorno positivo puede significar que los bytes llegaron a una caché o a un búfer del núcleo. No demuestra que sobrevivirán a un corte de energía ni que el proceso receptor ya los procesó. El manual de write(2) explica las transferencias parciales y sus límites; la persistencia se estudia con otras operaciones y garantías del sistema.
6. EINTR y EAGAIN requieren decisiones distintas
Ambas situaciones pueden devolver -1, pero no indican el mismo problema. EINTR comunica una interrupción por señal antes de transferir datos en esa llamada, en el comportamiento de Linux que estudiamos. EAGAIN o EWOULDBLOCK indican que la operación no puede avanzar ahora sin bloquear en un descriptor no bloqueante, según su tipo.
| Resultado | Qué se sabe | Acción habitual |
|---|---|---|
n > 0 | Hubo progreso de n bytes. | Contabilizarlo y continuar desde lo pendiente. |
-1, EINTR | Esa llamada se interrumpió sin comunicar bytes transferidos. | Reintentar si la política del programa es continuar. |
-1, EAGAIN/EWOULDBLOCK | No hay disponibilidad inmediata. | Conservar estado y esperar una condición de disponibilidad. |
Otro -1. | Un error que no se resuelve con esas dos reglas. | Informarlo y aplicar la política correspondiente. |
Señales y reinicios automáticos
No todas las señales visibles para un programa se convierten en EINTR. La instalación del manejador y las reglas del sistema pueden reiniciar ciertas llamadas automáticamente. Si una transferencia ya progresó antes de una señal, Linux puede devolver la cantidad positiva transferida en vez de EINTR: hay que conservar ese progreso.
Reintentar EINTR suele ser apropiado en una herramienta sencilla que quiere terminar su trabajo. No es una política universal: si la señal marca una solicitud de cancelación, el programa puede necesitar salir. Escribe esa decisión en lugar de ocultarla en una condición que trate cualquier error como «volver a intentar».
Por qué no girar esperando EAGAIN
Un bucle inmediato que repite write sobre una tubería llena puede consumir CPU sin que nada cambie. La solución es esperar a que el receptor libere capacidad o integrar el descriptor en un mecanismo como poll o epoll. Esas interfaces se estudiarán más adelante. Aquí necesitamos conservar el índice pendiente hasta que sea razonable volver a intentar.
En software que deba contemplar interfaces con ambas constantes, comprueba EAGAIN y EWOULDBLOCK sin suponer que siempre son números distintos o siempre iguales. Si son iguales, una expresión con || funciona; dos etiquetas case iguales en un switch no compilarían.
Lo pendiente no se reinicia al esperar
Si una escritura aceptó tres bytes y la siguiente devuelve EAGAIN, esos tres no se deshacen. Al reanudar quedan siete. Tampoco se debe avanzar siete por haberlos pedido: solo avanzas cuando el retorno positivo confirma esa cantidad. El simulador hará visibles las dos equivocaciones.
7. Los patrones de lectura y escritura
Leer un flujo hasta EOF y reunir una cantidad exacta son dos tareas distintas. Un lector general procesa cada retorno positivo y vuelve a pedir más. Un protocolo que necesita una cabecera de doce bytes puede acumular varias lecturas hasta reunir doce o detectar EOF prematuro. No cambies una política por otra sin considerar si la espera es aceptable.
for (;;) {
ssize_t n = read(fd, buf, capacidad); /* capacidad positiva */
if (n > 0) {
/* Procesar exactamente (size_t)n bytes. */
continue;
}
if (n == 0)
break;
if (errno == EINTR)
continue;
/* Informar error: EAGAIN no significa EOF. */
break;
}
En el esquema, «procesar» también debe comprobar sus propios errores si realiza salidas. No necesitas poner a cero todo el búfer antes de cada vuelta si solo procesas el rango válido. Borrar memoria no sustituye usar correctamente la longitud recibida.
Un ayudante que informa el progreso incluso al fallar
Esta función intenta enviar todo el bloque. Recibe un puntero válido a la región y un puntero de salida para comunicar cuántos bytes se aceptaron. La salida parcial importa: un error posterior no implica que el destino siga intacto. El llamador puede conservar el progreso para decidir qué hacer.
#include <unistd.h>
#include <errno.h>
int escribir_todo(int fd, const void *datos, size_t longitud,
size_t *completados)
{
const unsigned char *p = datos;
*completados = 0;
while (*completados < longitud) {
size_t pedido = longitud - *completados;
if (pedido > 65536)
pedido = 65536; /* peticiones moderadas, lejos de SSIZE_MAX */
ssize_t n = write(fd, p + *completados, pedido);
if (n > 0) {
*completados += (size_t)n;
continue;
}
if (n == -1 && errno == EINTR)
continue;
if (n == 0)
errno = EIO; /* politica defensiva: no hay progreso */
return -1; /* conserva el progreso; no gira con EAGAIN */
}
return 0;
}
Si devuelve 0, se aceptó toda la longitud. Si devuelve -1, errno explica el último fallo y completados conserva lo que sí se envió. En particular, EAGAIN no se transforma en espera activa. Para reanudar después de disponibilidad, el llamador debe conservar el acumulado y entregar la región pendiente; no debe reiniciar el envío del bloque original.
La función inicializa su contador de salida en cada invocación. Si realizas varias invocaciones para una transferencia mayor, suma ese progreso en el estado exterior. No prometemos rollback, entrega exclusiva de un registro ni persistencia; la función solo organiza las cantidades aceptadas por write.
No mezclar un bloque nuevo con una salida vieja pendiente
Para retransmitir un flujo usando un único búfer, primero se lee una porción, luego se envían todos sus bytes pendientes y solo después se vuelve a llenar ese búfer. Si haces otra lectura mientras parte de la anterior sigue sin enviarse, puedes sobrescribir precisamente los bytes que faltaban. El tema 8 utilizará estas reglas para construir herramientas de copia completas.
8. Un lector hexadecimal en C
El programa leer-bloques muestra cada petición de read y los bytes que realmente obtuvo. Acepta una ruta o - para usar la entrada estándar; el tamaño de petición opcional va de 1 a 4096, con valor inicial 4. Su salida es una inspección hexadecimal, no una copia del contenido.
Guárdalo como leer-bloques.c. El arreglo tiene capacidad fija de 4096 y solo se muestran n bytes. El conteo acumulado indica cuántos se procesaron, no pretende consultar la posición de un objeto que podría ser una tubería.
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3) {
fprintf(stderr, "uso: %s RUTA|- [TAMANO:1..4096]\n", argv[0]);
return 2;
}
size_t pedido = 4;
if (argc == 3) {
char *fin;
errno = 0;
unsigned long valor = strtoul(argv[2], &fin, 10);
if (errno != 0 || fin == argv[2] || *fin != '\0' ||
valor < 1 || valor > 4096 || argv[2][0] == '-') {
fprintf(stderr, "tamano invalido: %s\n", argv[2]);
return 2;
}
pedido = (size_t)valor;
}
int propio = strcmp(argv[1], "-") != 0;
int fd = propio ? open(argv[1], O_RDONLY | O_CLOEXEC) : STDIN_FILENO;
if (fd == -1) {
perror("open");
return 1;
}
unsigned char buf[4096];
uintmax_t total = 0;
uintmax_t llamada = 0;
int estado = 0;
for (;;) {
ssize_t n = read(fd, buf, pedido);
llamada++;
if (n == -1) {
if (errno == EINTR)
continue;
perror("read");
estado = 1;
break;
}
printf("read %ju: pedido=%zu retorno=%zd", llamada, pedido, n);
if (n == 0) {
printf(" EOF; total=%ju\n", total);
break;
}
printf(" bytes:");
for (size_t i = 0; i < (size_t)n; i++)
printf(" %02x", (unsigned int)buf[i]);
putchar('\n');
total += (uintmax_t)n;
if (ferror(stdout)) {
fprintf(stderr, "error al producir el informe\n");
estado = 1;
break;
}
}
if (propio && close(fd) == -1) {
perror("close entrada");
estado = 1;
}
if (fflush(stdout) == EOF) {
perror("fflush informe");
estado = 1;
}
return estado;
}
Qué decisiones hay detrás del código
- La cantidad se valida antes de abrir. Cero, números fuera de rango o texto no numérico producen un error de uso.
- El búfer es unsigned char. Los valores se imprimen como bytes de 0 a 255, sin depender del signo de char.
- No usa strlen sobre la entrada. Un NUL dentro del archivo sigue apareciendo como 00 y cuenta como byte.
- El retorno gobierna el procesamiento. Una lectura de dos bytes produce dos valores hexadecimales, aunque el arreglo tenga capacidad para muchos más.
- EINTR es reintentable en esta herramienta. Los demás errores, incluido EAGAIN si la entrada heredada fuera no bloqueante, se informan y terminan la inspección.
- La propiedad del descriptor es explícita. Se cierra la referencia que el programa abrió; la entrada estándar heredada no se cierra expresamente aquí.
Los mensajes usan stdio para presentar el informe. Por eso se comprueba su estado y el vaciado final de stdout. Es otra capa diferente de las lecturas realizadas con read: observar bytes de entrada no elimina la posibilidad de que falle la salida del propio informe.
9. Laboratorio con texto, ceros y entrada estándar
Usaremos Bash en Linux o WSL, sin permisos de administrador. Crea una carpeta nueva, guarda allí leer-bloques.c y compílalo. Los archivos de prueba son pequeños y no requieren dispositivos de almacenamiento adicionales.
$ LAB6=$(mktemp -d "$HOME/tema6.XXXXXX")
$ cd "$LAB6"
$ printf 'ABCDEFGHIJ' > letras.txt
$ printf 'A\0B\n' > binario.dat
$ printf '\303\261\n' > utf8.txt
$ touch vacio.dat
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 leer-bloques.c -o leer-bloques
La variable LAB6 conserva la ruta durante la sesión. La construcción de utf8.txt mediante escapes octales crea exactamente los bytes C3 B1 0A con independencia de cómo se haya introducido la letra en el editor. binario.dat contiene cuatro bytes, uno de ellos NUL.
Leer diez bytes en peticiones de cuatro
$ ./leer-bloques letras.txt 4
read 1: pedido=4 retorno=4 bytes: 41 42 43 44
read 2: pedido=4 retorno=4 bytes: 45 46 47 48
read 3: pedido=4 retorno=2 bytes: 49 4a
read 4: pedido=4 retorno=0 EOF; total=10
$ ./leer-bloques letras.txt 6
read 1: pedido=6 retorno=6 bytes: 41 42 43 44 45 46
read 2: pedido=6 retorno=4 bytes: 47 48 49 4a
read 3: pedido=6 retorno=0 EOF; total=10
Estas salidas representan un archivo regular estable sin interrupciones. El programa no depende de que el sistema siempre complete cada petición anterior al final: utiliza cualquier retorno positivo válido. La numeración puede saltar si una llamada devuelve EINTR y se reintenta sin imprimirla.
Un NUL no es el final de un archivo
$ wc -c < binario.dat
4
$ ./leer-bloques binario.dat 4
read 1: pedido=4 retorno=4 bytes: 41 00 42 0a
read 2: pedido=4 retorno=0 EOF; total=4
$ od -An -tx1 binario.dat
41 00 42 0a
El byte cero se transfiere igual que los demás. No es EOF ni implica que la longitud recibida sea uno. Si el programa hubiera aplicado strlen al bloque, habría contado solo la A e ignorado datos válidos posteriores.
Un carácter de varios bytes puede repartirse
$ ./leer-bloques utf8.txt 1
read 1: pedido=1 retorno=1 bytes: c3
read 2: pedido=1 retorno=1 bytes: b1
read 3: pedido=1 retorno=1 bytes: 0a
read 4: pedido=1 retorno=0 EOF; total=3
$ ./leer-bloques vacio.dat
read 1: pedido=4 retorno=0 EOF; total=0
El mismo lector sobre la entrada estándar
$ ./leer-bloques - 4 < letras.txt
$ printf 'ABC' | ./leer-bloques - 8
read 1: pedido=8 retorno=3 bytes: 41 42 43
read 2: pedido=8 retorno=0 EOF; total=3
$ ./leer-bloques /dev/null
read 1: pedido=4 retorno=0 EOF; total=0
La salida de la tubería es ilustrativa: su fragmentación puede variar según la producción y la planificación. El productor escribe tres bytes y termina; después de consumirlos, la tubería ya no tiene escritores. No se deduce que todo read de una tubería devuelva siempre exactamente lo escrito en un write del otro extremo.
No ejecutes el lector sin límite sobre /dev/zero esperando EOF: ese dispositivo produce ceros indefinidamente. Para generar una entrada finita puedes usar head -c 10 /dev/zero | ./leer-bloques - 4. A diferencia de /dev/null, leer ceros como datos no significa recibir un retorno cero.
10. Experimento: escritura parcial real y EAGAIN
En archivos pequeños y regulares muchas pruebas completan write de una vez, lo que puede ocultar un supuesto incorrecto. Para observar una escritura parcial de forma controlada usaremos una tubería de Linux sin lector concurrente y con el extremo de escritura en modo no bloqueante.
El programa consulta la capacidad de la tubería con F_GETPIPE_SZ y pide escribir más bytes que esa capacidad. En una tubería vacía de Linux, con una petición mayor que PIPE_BUF, puede aceptarse solo la parte que cabe. La siguiente escritura encuentra el conducto lleno y devuelve EAGAIN. Después el mismo proceso drena los bytes aceptados y completa el sufijo pendiente.
La capacidad indica cuánto puede almacenar la tubería. PIPE_BUF se relaciona con la atomicidad de escrituras pequeñas frente a otros escritores. Pedimos capacidad + PIPE_BUF para que la operación exceda ambos límites relevantes del experimento. Este no es un formato de mensajes ni una regla para los archivos regulares.
Guarda el programa como parcial-pipe.c. Usa una interfaz específica de Linux y no necesita cambiar la capacidad del conducto ni elevar permisos. La lectura de drenaje está acotada por la cantidad que se sabe que entró; no espera datos imaginarios.
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static ssize_t escribir(int fd, const void *p, size_t n)
{
ssize_t resultado;
do {
resultado = write(fd, p, n);
} while (resultado == -1 && errno == EINTR);
return resultado;
}
static int drenar(int fd, size_t cantidad)
{
unsigned char buf[4096];
size_t total = 0;
while (total < cantidad) {
size_t pedido = cantidad - total;
if (pedido > sizeof buf)
pedido = sizeof buf;
ssize_t n = read(fd, buf, pedido);
if (n == -1) {
if (errno == EINTR)
continue;
return -1;
}
if (n == 0) {
errno = EIO;
return -1;
}
for (size_t i = 0; i < (size_t)n; i++) {
if (buf[i] != 'X') {
errno = EIO;
return -1;
}
}
total += (size_t)n;
}
return 0;
}
int main(void)
{
int p[2];
if (pipe(p) == -1) {
perror("pipe");
return 1;
}
int estado = 1;
char *datos = NULL;
int capacidad = fcntl(p[1], F_GETPIPE_SZ);
if (capacidad == -1) {
perror("F_GETPIPE_SZ");
goto limpiar;
}
int flags = fcntl(p[1], F_GETFL);
if (flags == -1 || fcntl(p[1], F_SETFL, flags | O_NONBLOCK) == -1) {
perror("O_NONBLOCK");
goto limpiar;
}
size_t longitud = (size_t)capacidad + (size_t)PIPE_BUF;
datos = malloc(longitud);
if (datos == NULL) {
perror("malloc");
goto limpiar;
}
memset(datos, 'X', longitud);
ssize_t n = escribir(p[1], datos, longitud);
if (n == -1) {
perror("primer write");
goto limpiar;
}
printf("capacidad=%d PIPE_BUF=%d pedido=%zu retorno=%zd\n",
capacidad, PIPE_BUF, longitud, n);
if (n <= 0 || (size_t)n >= longitud) {
fprintf(stderr, "no se obtuvo la escritura parcial esperada\n");
goto limpiar;
}
size_t enviados = (size_t)n;
n = escribir(p[1], datos + enviados, longitud - enviados);
if (n != -1 || (errno != EAGAIN && errno != EWOULDBLOCK)) {
fprintf(stderr, "se esperaba EAGAIN con la tuberia llena\n");
goto limpiar;
}
printf("siguiente write: EAGAIN; enviados=%zu pendientes=%zu\n",
enviados, longitud - enviados);
if (drenar(p[0], enviados) == -1) {
perror("drenar primera parte");
goto limpiar;
}
while (enviados < longitud) {
n = escribir(p[1], datos + enviados, longitud - enviados);
if (n <= 0) {
if (n == 0)
errno = EIO;
perror("write pendiente");
goto limpiar;
}
enviados += (size_t)n;
if (drenar(p[0], (size_t)n) == -1) {
perror("drenar pendiente");
goto limpiar;
}
}
printf("transferencia completada y drenada: %zu bytes\n", enviados);
estado = 0;
limpiar:
free(datos);
if (close(p[1]) == -1) {
perror("close escritor");
estado = 1;
}
if (close(p[0]) == -1) {
perror("close lector");
estado = 1;
}
if (fflush(stdout) == EOF) {
perror("fflush informe");
estado = 1;
}
return estado;
}
$ cd "$LAB6"
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 parcial-pipe.c -o parcial-pipe
$ ./parcial-pipe
capacidad=8192 PIPE_BUF=4096 pedido=12288 retorno=8192
siguiente write: EAGAIN; enviados=8192 pendientes=4096
transferencia completada y drenada: 12288 bytes
La capacidad puede ser distinta en tu equipo. No dependemos de que sea 8192: se consulta y se construye la petición a partir del resultado. El primer write pide más de lo que cabe y el retorno positivo comunica el progreso real. El segundo no borra ese progreso al fallar: mantiene los pendientes. Solo después de drenar se vuelve a intentar desde datos + enviados.
Qué papel cumple el drenaje
Este experimento puede usar un único proceso porque organiza las fases: llenar sin bloquear, observar EAGAIN, consumir lo aceptado y completar. En un programa real, el lector podría estar en otro proceso y un mecanismo de espera coordinaría la disponibilidad. Aquí no hacemos un bucle rápido hasta que «mágicamente aparezca espacio».
Después del primer drenaje, el resto cabe en el conducto vacío del escenario. Aun así, el código vuelve a usar el retorno positivo como cantidad real y drena cada parte aceptada. Si apareciera un error no esperado, se informa y se liberan los recursos. No se supone que read o write tengan éxito solo porque la fase anterior lo tuvo.
Todos los datos son X para que el lector compruebe que recibió bytes válidos; la cantidad total comprueba la contabilidad. Para investigar el orden exacto de secuencias diferentes puedes adaptar la generación de datos. Las reglas de tuberías y la atomicidad se profundizarán cuando tratemos otros mecanismos de E/S.
11. Errores y supuestos que rompen una transferencia
| Supuesto incorrecto | Qué puede ocurrir | Corrección |
|---|---|---|
| «Una lectura llena siempre el búfer». | Se procesa memoria que no llegó en esa llamada. | Usar el retorno positivo como longitud válida. |
| «Un retorno corto es EOF». | Se termina antes de recibir datos posteriores. | Continuar según el contrato; en estos flujos, cero con petición positiva indica EOF. |
| «Escribí porque llamé a write». | Solo se aceptó una parte o hubo un error. | Comprobar y acumular el retorno. |
| «Al reintentar envío el mismo bloque». | Se duplican los bytes ya aceptados. | Avanzar puntero y reducir pendientes. |
| «Cualquier error se arregla repitiendo». | CPU ocupada sin progreso o repetición de un fallo permanente. | Separar EINTR, EAGAIN y otros errores. |
| «Un byte NUL termina la lectura». | Se pierde contenido binario. | Separar longitud de datos y terminación de cadenas. |
| «Puedo leer otro bloque antes de terminar el actual». | Se sobrescribe la porción aún no enviada. | Conservar el bloque hasta consumir sus pendientes. |
Errores habituales que conviene reconocer
- EBADF: el número no es válido o la apertura no permite la operación solicitada.
- EFAULT: la región de memoria indicada no es accesible para la operación. No sustituye los límites correctos del arreglo de C.
- EIO: un error de E/S comunicado por el objeto o el sistema. En nuestro ayudante también se usa como política defensiva ante un write sin progreso.
- ENOSPC o EDQUOT: una escritura necesita espacio o cuota que no puede obtener.
- EPIPE: puede aparecer al escribir en una tubería sin lectores, junto con SIGPIPE. Por defecto esa señal puede terminar el proceso antes de que observe el retorno.
El ejemplo de tubería mantiene su extremo lector abierto hasta terminar, por lo que no provoca EPIPE deliberadamente. Si diseñas un programa que deba gestionar ese caso, tendrás que definir también su política de señales; no basta con añadir un if sobre errno y suponer que siempre llegará a ejecutarse.
El sizeof correcto depende de qué objeto tienes
unsigned char datos[1024];
/* Aqui sizeof datos es la capacidad del arreglo. */
void usar(unsigned char *buf, size_t capacidad)
{
/* Aqui sizeof buf es el tamano del puntero, no la capacidad recibida. */
/* La cantidad disponible la comunica el argumento capacidad. */
}
Escribir read(fd, buf, sizeof buf) dentro de una función donde buf es un puntero no recupera la longitud del arreglo del llamador. La solución es pasar y respetar una capacidad explícita. Tampoco se puede usar sizeof para medir cuántos bytes de una lectura anterior son válidos: esa longitud sigue siendo el retorno.
Conservar el primer resultado que explica el fallo
Si read devuelve -1, guarda errno antes de llamar a otras funciones si necesitas ese código después. No inspecciones un errno antiguo tras un retorno positivo para decidir que la operación falló. Una interrupción previa no invalida los bytes que una llamada posterior sí entregó.
Si una secuencia envió una parte y terminó con error, comunícalo como transferencia incompleta. Una política de reintento de toda la operación desde cero puede duplicar efectos en el destino. La decisión de continuar, reconstruir o publicar un resultado nuevo depende del problema; el contador de progreso permite tomarla con información.
12. Búferes, posición y persistencia
Los arreglos de los ejemplos viven en la memoria del proceso. No son la caché de páginas del núcleo ni un bloque físico del dispositivo. Elegir un búfer de 4096 bytes no obliga a que cada read produzca una lectura física exacta de 4096 desde el disco.
La posición avanza por los bytes transferidos
En un archivo regular con operaciones ordinarias, una lectura positiva avanza la posición por n, no por count. Una escritura también avanza según los bytes aceptados; con O_APPEND primero selecciona el final correspondiente. Los descriptores duplicados y los heredados que comparten apertura observan ese estado, como vimos en el tema 4.
Si necesitas operaciones en posiciones explícitas, veremos pread y pwrite en el tema 7. Aquí el lector cuenta bytes procesados sin asumir que cualquier descriptor sea posicionable. Una tubería no tiene un cursor de archivo consultable con lseek.
Lectura completa y estructura de una aplicación
Un read no tiene por qué coincidir con un registro, una línea o un paquete lógico de tu aplicación. Si un formato indica una cabecera y luego una carga útil, debes conservar el estado del análisis entre bloques. Acumular hasta una cantidad exacta puede ser correcto para esa cabecera, pero inconveniente para un visor interactivo que deba mostrar cuanto vaya llegando.
También hay que decidir si es aceptable leer hasta EOF. Un archivo estable suele tener un final accesible; un flujo de una conexión que permanece abierta puede no terminar nunca. La política de espera pertenece al programa, no a una creencia de que todo descriptor contiene un documento finito.
Rendimiento sin sacrificar el contrato
Leer byte a byte facilita algunas demostraciones, pero añade llamadas y suele ser ineficiente para archivos grandes. Un búfer moderado reduce ese coste. Aun así, la corrección no debe depender de que todas las llamadas se completen de una vez. En el tema 11 mediremos tamaños de búfer y rendimiento después de tener un bucle correcto.
Finalmente, comprobar todos los bytes aceptados es distinto de garantizar persistencia. El cierre y las operaciones de sincronización tienen errores y condiciones propios. Al construir el cat y el cp del tema 8 integraremos apertura, transferencia y cierre sin confundir una capa con otra.
13. Simulador de transferencia paso a paso
Este modelo transporta los diez bytes ABCDEFGHIJ desde una fuente finita hacia un destino. Tiene un único búfer y alterna dos fases: leer una porción y completar todas sus escrituras pendientes. Solo vuelve a leer cuando el bloque anterior se envió por completo.
Pedir, recibir, conservar y avanzar
| Paso | Operación | Pedido | Retorno | Consecuencia |
|---|
El límite de read permite observar lecturas positivas menores que la petición sin declararlas EOF. El de write permite fraccionar la salida. Los errores se inyectan como situaciones del modelo; no modifican descriptores reales del equipo. Los controles de límite se pueden cambiar entre pasos y solo afectan a futuras llamadas.
Con EINTR se conserva el estado para reintentar. Con EAGAIN el modelo se detiene hasta que pulses «Simular disponibilidad», sin girar sobre la misma llamada. EIO termina con fallo y conserva visible lo que ya llegó al destino. Un write de cero con petición positiva activa la política defensiva de ausencia de progreso. El evento elegido vuelve a Normal después de usarse.
Secuencias para comprobar
- Con los valores iniciales, lee ABCD y escribe AB. Observa que quedan CD pendientes. Inyecta EINTR y verifica que el siguiente paso sigue trabajando sobre CD.
- Inyecta EAGAIN cuando quede un sufijo pendiente. Pulsa otra vez «Ejecutar un paso»: no se avanza. Simula disponibilidad y comprueba que no se repite el prefijo enviado.
- Reinicia, limita cada lectura a un byte y mantén una petición de cuatro. Aunque todas las lecturas positivas son cortas, el flujo continúa hasta entregar ABCDEFGHIJ.
- Inyecta EIO después de aceptar una parte. El destino conserva ese prefijo: el error no deshace las escrituras anteriores.
- Completa la transferencia. El último bloque puede ser corto, pero la terminación correcta se reconoce al hacer una lectura positiva solicitada que devuelve cero.
El modelo representa un flujo de bytes con EOF definitivo y una única transferencia secuencial, sin escritores adicionales, cambios de contenido, señales reales, búferes de stdio ni persistencia. Aísla la contabilidad que también necesitarás en programas más complejos.
14. Resumen y guía de decisiones
- Read y write transfieren bytes; no interpretan líneas, cadenas ni formatos.
- Count usa size_t y expresa una petición. El retorno usa ssize_t y puede ser -1, cero o una cantidad positiva.
- Después de read solo los n bytes recibidos son válidos. No se añade un NUL automáticamente.
- Una lectura corta puede ser correcta y no demuestra EOF. Con una petición positiva, el retorno cero indica EOF en los objetos y flujos estudiados.
- Una escritura parcial exige avanzar puntero y reducir pendientes por la cantidad aceptada.
- EINTR puede permitir un reintento según la política del programa. EAGAIN exige conservar estado y esperar disponibilidad.
- Un error posterior no revierte los bytes que se transfirieron antes.
- No debe sobrescribirse un búfer mientras aún contenga salida pendiente.
- Los bytes NUL, los caracteres UTF-8 y el EOF pertenecen a conceptos distintos.
- Transferencia aceptada, cierre de referencias y persistencia son garantías separadas.
| Al observar… | La siguiente decisión es… |
|---|---|
| Read positivo. | Procesar exactamente n bytes, aunque n sea menor que la petición. |
| Read cero con petición positiva en estos flujos. | Tratar EOF conforme a la política de la aplicación. |
| Write positivo. | Sumar n al progreso y continuar con el sufijo pendiente. |
| -1 con EINTR. | Reintentar o cancelar de forma explícita; no inventar progreso. |
| -1 con EAGAIN/EWOULDBLOCK. | Guardar estado y esperar disponibilidad. |
| Otro error. | Conservar errno, comunicar fallo y limpiar los recursos adquiridos. |
| Write sin progreso ante petición positiva. | Evitar un bucle infinito; aplicar una política explícita. |
15. Preguntas de repaso y práctica
Razona sobre la cantidad válida y la pendiente antes de desplegar cada respuesta.
1. ¿Pedir 4096 bytes obliga a read a devolver 4096?
No. Es una cantidad máxima solicitada. Puede devolver una cantidad positiva menor, cero en las condiciones de EOF estudiadas o -1 con un error. El programa debe usar el retorno real.
2. ¿Por qué se guarda el retorno en ssize_t y no directamente en size_t?
Porque ssize_t puede representar -1. Convertir ese error a un tipo sin signo produce una cantidad enorme y puede romper las comprobaciones y los límites posteriores.
3. Read devuelve 3 al pedir 8. ¿Qué parte del búfer puedes enviar como datos recibidos?
Solo los índices 0, 1 y 2. Los demás no fueron obtenidos en esa llamada. La capacidad del arreglo no indica la longitud válida.
4. ¿Read añade un byte NUL al final?
No. Si necesitas una cadena, reserva espacio y añádelo tras comprobar el retorno. Para datos binarios conserva la longitud explícita, porque puede haber NUL dentro del contenido.
5. En un archivo de diez bytes, ¿qué retornos esperas con peticiones de cuatro?
En el caso estable ordinario: 4, 4, 2 y 0. Los dos bytes de la tercera llamada deben procesarse; el retorno cero posterior señala EOF.
6. ¿Una lectura corta de una tubería significa que ya no llegará nada?
No. Puede haber menos bytes disponibles que los pedidos y seguir habiendo escritores. El flujo puede continuar. Una tubería vacía con escritores no equivale a una sin escritores.
7. ¿Un retorno cero al pedir cero demuestra EOF?
No. No se pidió transferir ningún dato. El criterio de EOF del bucle presupone una petición positiva y el contrato del objeto.
8. Si write acepta tres de diez bytes, ¿qué argumentos corresponden al siguiente intento?
El puntero inicial más tres y una longitud pendiente de siete. Reenviar los diez duplicaría el prefijo, y avanzar diez perdería el sufijo no aceptado.
9. Después de aceptar tres bytes aparece EAGAIN. ¿Se reinicia el contador?
No. Los tres siguen transferidos. Se conserva el estado, se espera disponibilidad y después se continúa por el sufijo que quedaba pendiente.
10. ¿Por qué un bucle inmediato ante EAGAIN puede ser un problema?
Puede ocupar CPU sin que cambie la disponibilidad del objeto. Hay que esperar una condición de progreso o integrar el descriptor en el mecanismo de espera del programa.
11. ¿Una señal obliga siempre a recibir EINTR?
No. Algunas llamadas pueden reiniciarse automáticamente según la configuración y las reglas del sistema. Si ya hubo transferencia, Linux puede comunicar un retorno positivo que debe contabilizarse.
12. ¿Por qué un NUL dentro de binario.dat no es EOF?
Porque es un byte de contenido, con valor cero. EOF es un resultado de la operación, no un valor de uno de los bytes transferidos. Read puede entregar datos antes y después del NUL.
13. ¿Puede read partir un carácter UTF-8?
Sí. Trabaja con bytes y puede devolver solo parte de la secuencia de un carácter. El intérprete de la codificación debe conservar y completar esa secuencia cuando corresponda.
14. ¿Por qué no debe leerse otro bloque si aún quedan bytes del anterior por escribir?
Si se reutiliza el mismo búfer, la nueva lectura puede sobrescribir la región pendiente. Primero se completa la salida o se guarda el estado en otra región adecuada.
15. ¿Capacidad de tubería y PIPE_BUF representan la misma cantidad?
No. La primera describe almacenamiento del conducto; PIPE_BUF se relaciona con garantías de atomicidad de escrituras pequeñas. El experimento consulta la capacidad y pide más que ella.
16. ¿Por qué escribir en una tubería sin lectores puede terminar el proceso?
Porque se genera SIGPIPE y su acción por defecto puede terminarlo. Si se gestiona esa señal de otro modo, la llamada puede comunicar EPIPE. El manejo del error y la política de señales están relacionados.
17. ¿EOF cierra automáticamente el descriptor?
No. La apertura puede seguir válida. Un archivo regular reposicionado o que crezca puede ofrecer datos en otra lectura; el cierre es una operación aparte.
18. ¿Write positivo garantiza que los bytes sobrevivirán a un corte de luz?
No. Confirma aceptación según la interfaz, no durabilidad completa. Las cachés, sincronización y garantías del sistema de archivos pertenecen a otras capas.
Para practicar
- Ejecuta leer-bloques sobre letras.txt con tamaños 1, 3, 4, 6 y 16. Predice el número de llamadas positivas y la llamada final de EOF. Comprueba que el total siempre es diez.
- Crea un archivo con
printf 'A\0B\0C' > ceros.dat. Compara wc -c, od y el informe del lector. Explica por qué strlen no sería un contador válido de todo el contenido. - Lee utf8.txt de uno y de dos bytes por llamada. Identifica en qué bloques queda repartida la secuencia C3 B1. Distingue caracteres, bytes y saltos de línea.
- Prueba los tamaños 0, 4097 y abc y una ruta inexistente. Comprueba los estados de salida con echo $?. Explica qué errores se detectaron antes de open.
- Genera diez bytes de /dev/zero con head y entrégalos por una tubería al lector. Comprueba que aparecen valores 00 y que EOF llega solo al terminar la fuente finita.
- Compila parcial-pipe.c y anota la capacidad observada, lo pedido, lo aceptado y el resto. Verifica las sumas sin suponer que tu capacidad es igual a la salida ilustrativa.
- Modifica la generación de datos del experimento para que cada posición tenga un patrón verificable, y adapta el drenaje para comprobar orden además de cantidad.
- Integra escribir_todo en un programa pequeño que envíe un bloque a /dev/null. Si existe /dev/full, prueba allí y muestra tanto errno como el progreso comunicado por completados.
- En el simulador provoca EINTR después de una escritura parcial y EAGAIN en el siguiente intento. Comprueba que al terminar el destino es ABCDEFGHIJ, sin prefijos repetidos.
- En el simulador limita todas las lecturas a uno y todas las escrituras a uno. Después cambia los límites entre pasos. Explica por qué el resultado final sigue siendo idéntico.
- Provoca EIO después de enviar un prefijo y observa el destino conservado. Explica por qué una política de reintentar la transferencia completa desde cero necesita tomar en cuenta ese prefijo.
En el tema 7 estudiaremos lseek, los orígenes de desplazamiento, los huecos y las operaciones pread y pwrite. Con las reglas de transferencia de este tema podrás separar cuánto se movió realmente de la posición en la que querías operar.