1. Del recorrido secuencial al acceso por posición
En el tema 6 avanzamos según los bytes que read o write transferían realmente. Si una apertura comienza en 0 y una lectura recibe cuatro bytes, su posición pasa a 4. Repetir la operación permite recorrer el contenido secuencialmente. Es apropiado para copiar, calcular una suma de comprobación o procesar un flujo de principio a fin.
Hay otras tareas donde interesa llegar directamente a una región. Un visor necesita mostrar el final de un registro grande; una base de datos consulta una página; una herramienta corrige un campo de longitud fija; un lector de imágenes busca una tabla interna indicada por una cabecera. Leer todo el prefijo cada vez sería innecesario si el objeto admite posiciones.
El acceso directo o aleatorio permite seleccionar una posición lógica del archivo para operar allí. Esa posición se expresa en bytes y no requiere que el programa transfiera previamente todos los bytes anteriores.
«Aleatorio» no significa que el programa elija al azar. Significa que no está obligado a seguir un orden secuencial. Tampoco equivale a O_DIRECT ni a saltarse la caché del núcleo: esa bandera pertenece a otra cuestión, el modo de realizar E/S y sus restricciones. Aquí estudiaremos cómo se selecciona una ubicación lógica.
Recordar dónde vive la posición
La posición actual pertenece a la descripción de archivo abierta. Dos descriptores obtenidos mediante dup comparten ese estado; dos open independientes tienen posiciones propias aunque lleguen al mismo inodo. lseek modifica ese estado intermedio, no una posición universal guardada dentro del archivo.
Un cambio de posición no desplaza los bytes del documento ni modifica sus nombres. Indica desde dónde actuarán las operaciones ordinarias que usan esa apertura. Mantener separadas estas piezas evita confundir «mover el cursor al byte 1000» con «mover el contenido hasta el byte 1000».
2. La interfaz de lseek y sus tres orígenes
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t desplazamiento, int origen);
El descriptor debe referirse a un objeto que admita la operación. El origen selecciona una base; el desplazamiento se suma a esa base. Si tiene éxito, lseek devuelve la nueva posición absoluta desde el comienzo. Si falla, devuelve (off_t)-1 y establece errno.
| Origen | Base | Nueva posición |
|---|---|---|
SEEK_SET | Comienzo del archivo, posición 0. | desplazamiento |
SEEK_CUR | Posición actual de la apertura. | actual + desplazamiento |
SEEK_END | Tamaño lógico del archivo en ese momento. | tamaño + desplazamiento |
En un archivo regular de diez bytes, los índices de los datos van de 0 a 9. La posición 10 señala el límite justo después del último byte: es el final, no el índice del último dato. Por eso SEEK_END con -1 selecciona el último byte y con 0 selecciona el final.
off_t p;
p = lseek(fd, 4, SEEK_SET); /* ir a la posicion absoluta 4 */
p = lseek(fd, -2, SEEK_CUR); /* retroceder 2 desde la posicion actual */
p = lseek(fd, -1, SEEK_END); /* ultimo byte de un archivo no vacio */
p = lseek(fd, 0, SEEK_CUR); /* consultar la posicion sin moverla */
p = lseek(fd, 0, SEEK_SET); /* volver al comienzo */
El fragmento enumera llamadas y omite las comprobaciones para concentrarse en las fórmulas; los programas completos comprobarán cada resultado. No necesitas usar los valores numéricos de las constantes: utiliza sus nombres.
Un desplazamiento negativo puede ser correcto
Desde la posición 8, SEEK_CUR con -3 produce 5 y es válido en el caso ordinario del archivo regular. Con SEEK_END y -2 sobre un archivo de diez bytes se obtiene 8. El problema es que la posición resultante sea negativa, no que el argumento lleve un signo menos.
Desde 0, SEEK_CUR con -1 pediría -1 y falla para un archivo regular. Sobre un archivo vacío, SEEK_END con -1 también. Comprueba el retorno antes de usarlo como una ubicación válida.
Consultar el final también mueve el cursor
lseek(fd, 0, SEEK_END) devuelve el tamaño en el caso de un archivo regular, pero además cambia la posición de esa apertura. Si solo necesitas sus atributos, fstat permite consultar st_size sin introducir ese desplazamiento compartido. Tampoco conviertas lseek al final en una promesa de que el tamaño no cambiará después.
3. off_t, límites y errores de cálculo
Los desplazamientos y posiciones utilizan off_t, un tipo entero con signo definido por la plataforma. No uses int por costumbre: su rango puede ser insuficiente para archivos grandes. Para imprimirlo en nuestros programas lo convertiremos a intmax_t y utilizaremos %jd.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <sys/types.h>
#include <unistd.h>
#include <inttypes.h>
#include <stdio.h>
/* ... fd es un descriptor valido ... */
off_t posicion = lseek(fd, 0, SEEK_CUR);
if (posicion == (off_t)-1) {
perror("lseek");
} else {
printf("posicion=%jd\n", (intmax_t)posicion);
}
En entornos Linux con glibc, _FILE_OFFSET_BITS=64 permite seleccionar interfaces de desplazamientos grandes donde es necesario, especialmente en compilaciones de 32 bits. En plataformas que ya usan off_t de 64 bits puede no cambiar el tamaño efectivo. La macro debe definirse antes de incluir las cabeceras; no basta con colocarla después de las declaraciones.
Convertir tarde no repara un desbordamiento anterior
La expresión (off_t)(indice * tamano) puede desbordarse antes de la conversión si ambos operandos eran de un tipo pequeño. Hay que escoger tipos adecuados y comprobar los límites de la multiplicación y de las sumas antes de realizarlas. Para un registro de longitud R y un índice i no negativo, se comprueba que i no supere el máximo representable dividido por R.
Los programas del tema utilizan constantes pequeñas o validan explícitamente un intervalo limitado. Así evitamos aparentar que cualquier número decimal recibido por la línea de comandos cabe automáticamente en off_t. Las interfaces pueden informar EOVERFLOW si un resultado no se representa en el tipo correspondiente.
Unidades explícitas
En un archivo regular los offsets representan bytes. Un desplazamiento 4 no significa «cuatro registros» ni «cuatro bloques del dispositivo». Si un formato organiza registros de 32 bytes, el registro de índice 4 comienza en 128; la transformación la calcula tu programa.
El offset lógico tampoco es una dirección física de disco. El sistema de archivos traduce las regiones lógicas a su representación y al almacenamiento que utilice. En un archivo fragmentado, dos regiones consecutivas pueden no estar almacenadas en bloques físicos consecutivos.
4. Posición, tamaño y EOF son datos distintos
lseek puede situar una apertura después del final de un archivo regular. Ese salto por sí solo no amplía su tamaño. No escribe ceros, no reserva todos los bloques intermedios y no convierte la posición en contenido.
datos existentes: A B C D E F G H I J
indices: 0 1 2 3 4 5 6 7 8 9
final: 10
posicion elegida: 15
st_size = 10
read desde 15, con peticion positiva: devuelve 0
la posicion puede seguir siendo 15
Si posteriormente escribes un byte en 15, el tamaño pasa a 16: el dato escrito ocupa el índice 15 y las posiciones 10 a 14 forman un intervalo que se leerá como ceros. Si el sistema admite archivos dispersos, puede representar ese intervalo sin asignar bloques de datos para toda su longitud.
Buscar no es leer ni escribir
lseek no devuelve bytes del contenido: devuelve una posición. Read en EOF no crea datos para alcanzar el cursor. Un write positivo sí puede ampliar el objeto hasta la nueva región escrita. Una operación como ftruncate cambia la longitud por otro mecanismo; no es equivalente a mover una apertura.
El manual de lseek(2) describe los orígenes y el posicionamiento más allá del final. El principio práctico es separar tres preguntas: dónde apunta la apertura, hasta dónde llega el archivo y qué almacenamiento se ha asignado.
EOF no es una barrera irreversible
Después de leer cero al final, el descriptor puede reposicionarse y leer contenido anterior. No hay que cerrar y abrir de nuevo para «quitar el EOF» de una operación read. No confundas esto con el indicador de fin de flujo de FILE* en stdio, que estudiaremos en el tema 10.
Un truncado efectuado por otra apertura puede reducir el tamaño por debajo de tu posición. Esa posición no se rebobina automáticamente. La lectura posterior puede devolver cero, y una escritura ordinaria desde allí podría volver a extender el archivo con un hueco.
5. Experimento: moverse por un archivo
El programa posiciones.c abre un archivo de lectura y realiza una secuencia de desplazamientos. Cada lectura pide dos bytes y muestra sus valores hexadecimales; así no necesita suponer que el contenido sea una cadena terminada en NUL. El laboratorio utilizará ABCDEFGHIJ sin salto de línea.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
static int mover(int fd, off_t salto, int origen, const char *nombre)
{
off_t p = lseek(fd, salto, origen);
if (p == (off_t)-1) {
perror(nombre);
return -1;
}
printf("%s: posicion=%jd\n", nombre, (intmax_t)p);
return 0;
}
static int leer_dos(int fd)
{
unsigned char datos[2];
ssize_t n;
do {
n = read(fd, datos, sizeof datos);
} while (n == -1 && errno == EINTR);
if (n == -1) {
perror("read");
return -1;
}
printf("read: %zd bytes", n);
for (size_t i = 0; i < (size_t)n; i++)
printf(" %02x", (unsigned int)datos[i]);
putchar('\n');
return 0;
}
static int longitud(int fd)
{
struct stat s;
if (fstat(fd, &s) == -1) {
perror("fstat");
return -1;
}
printf("longitud=%jd\n", (intmax_t)s.st_size);
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "uso: %s ARCHIVO\n", argv[0]);
return 2;
}
int fd = open(argv[1], O_RDONLY | O_CLOEXEC);
if (fd == -1) {
perror("open");
return 1;
}
int estado = 1;
if (mover(fd, 4, SEEK_SET, "SET +4") == -1 || leer_dos(fd) == -1)
goto limpiar;
if (mover(fd, -3, SEEK_CUR, "CUR -3") == -1 || leer_dos(fd) == -1)
goto limpiar;
if (mover(fd, -2, SEEK_END, "END -2") == -1 || leer_dos(fd) == -1)
goto limpiar;
if (mover(fd, 5, SEEK_END, "END +5") == -1 || longitud(fd) == -1)
goto limpiar;
if (leer_dos(fd) == -1 || mover(fd, 0, SEEK_CUR, "CUR 0") == -1)
goto limpiar;
if (mover(fd, 0, SEEK_SET, "SET 0") == -1 || leer_dos(fd) == -1)
goto limpiar;
estado = 0;
limpiar:
if (close(fd) == -1) {
perror("close");
estado = 1;
}
if (fflush(stdout) == EOF) {
perror("fflush informe");
estado = 1;
}
return estado;
}
El programa procesa el retorno real de cada read y no promete completar dos bytes mediante varias llamadas: para este archivo regular estable suelen recibirse de una vez. Si se le da un archivo muy corto, los desplazamientos relativos al final pueden fallar o las lecturas pueden ser cortas. Los errores se comunican, no se convierten en posiciones válidas.
6. Laboratorio de desplazamientos
Trabaja en Bash sobre Linux o WSL. Crea una carpeta nueva, guarda posiciones.c dentro y compila. No se requieren permisos de administrador. Mantén la terminal para conservar la variable LAB7 durante los experimentos.
$ LAB7=$(mktemp -d "$HOME/tema7.XXXXXX")
$ cd "$LAB7"
$ printf 'ABCDEFGHIJ' > letras.txt
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 posiciones.c -o posiciones
$ ./posiciones letras.txt
SET +4: posicion=4
read: 2 bytes 45 46
CUR -3: posicion=3
read: 2 bytes 44 45
END -2: posicion=8
read: 2 bytes 49 4a
END +5: posicion=15
longitud=10
read: 0 bytes
CUR 0: posicion=15
SET 0: posicion=0
read: 2 bytes 41 42
La primera lectura obtiene EF y mueve la apertura a 6. El salto CUR -3 llega a 3, no a 1, porque se calcula desde la posición que dejó esa lectura. Después se leen DE y la posición queda en 5. SEEK_END no utiliza esa posición: toma el tamaño diez como base y llega a ocho.
Una consulta que confirma que no hubo escritura
$ stat -c 'longitud=%s' letras.txt
longitud=10
$ od -An -tx1 letras.txt
41 42 43 44 45 46 47 48 49 4a
El lector llegó a la posición 15 y después volvió a cero, pero nunca añadió datos. El archivo mantiene diez bytes. El éxito de un salto no debe interpretarse como una escritura oculta de todo el tramo.
Qué cambia si se comparte la apertura
Si fd2 = dup(fd), ejecutar lseek mediante fd2 cambia la posición que verá read mediante fd. Puedes adaptar el programa del tema 4 para comprobarlo. Si fd2 procede de otro open, su posición no se mueve. El hecho de usar el mismo nombre no decide si los cursores son compartidos.
Este modelo también explica por qué «consultar el tamaño con lseek al final y volver después» puede interferir con otro usuario de la misma apertura. No se trata de que consultar sea siempre incorrecto, sino de conocer la modificación de estado que implica.
7. Experimento: crear un hueco desde C
Vamos a crear un archivo nuevo, mover la posición a 1 MiB y escribir un único byte X. Mostraremos el tamaño antes y después. O_CREAT | O_EXCL exige un nombre nuevo para no alterar por accidente un documento existente.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
static int mostrar(int fd, const char *etapa)
{
struct stat s;
if (fstat(fd, &s) == -1)
return -1;
off_t p = lseek(fd, 0, SEEK_CUR);
if (p == (off_t)-1)
return -1;
printf("%s: posicion=%jd longitud=%jd bloques512=%jd\n",
etapa, (intmax_t)p, (intmax_t)s.st_size,
(intmax_t)s.st_blocks);
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "uso: %s ARCHIVO_NUEVO\n", argv[0]);
return 2;
}
int fd = open(argv[1], O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0666);
if (fd == -1) {
perror("open exclusivo");
return 1;
}
int estado = 1;
if (lseek(fd, (off_t)1048576, SEEK_SET) == (off_t)-1) {
perror("lseek a 1 MiB");
goto limpiar;
}
if (mostrar(fd, "tras lseek") == -1) {
perror("mostrar antes");
goto limpiar;
}
const char dato = 'X';
ssize_t n;
do {
n = write(fd, &dato, 1);
} while (n == -1 && errno == EINTR);
if (n != 1) {
if (n == 0)
errno = EIO;
perror("write X");
goto limpiar;
}
if (mostrar(fd, "tras write") == -1) {
perror("mostrar despues");
goto limpiar;
}
estado = 0;
limpiar:
if (close(fd) == -1) {
perror("close");
estado = 1;
}
if (fflush(stdout) == EOF) {
perror("fflush informe");
estado = 1;
}
return estado;
}
La petición de escritura es de un byte, por lo que un retorno positivo correcto completa ese dato; se contempla EINTR y la ausencia de progreso. El programa no necesita escribir un millón de bytes cero antes de la X. Su sistema de archivos decide cómo representar el intervalo anterior.
$ cd "$LAB7"
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 crear-hueco.c -o crear-hueco
$ ./crear-hueco hueco.img
tras lseek: posicion=1048576 longitud=0 bloques512=0
tras write: posicion=1048577 longitud=1048577 bloques512=8
La longitud exacta final es 1.048.577 bytes: los índices empiezan en cero y se escribió en 1.048.576. Los ocho bloques de 512 son una salida ilustrativa de asignación de 4096 bytes, no una garantía universal. La asignación puede variar por formato, políticas de reserva y momento de observación.
Los ceros del hueco son datos al leer
Al leer el intervalo desde 0 hasta 1.048.575 se obtienen valores cero. Un byte cero recibido es contenido válido, no un retorno cero de read. Cuando la lectura atraviese el índice 1.048.576 aparecerá X y solo al llegar después del final se reconocerá EOF.
Si vuelves a ejecutar crear-hueco con el mismo nombre, la creación exclusiva falla y no debe borrar el resultado anterior. El cierre libera la apertura, pero el nombre y el objeto permanecen para inspeccionarlos.
8. Medir el tamaño y la asignación
Este laboratorio conecta los desplazamientos con las herramientas del tema 3. Observa longitud, bloques asignados y contenido en dos regiones concretas del archivo:
$ stat -c 'longitud=%s bloques=%b unidad=%B' hueco.img
longitud=1048577 bloques=8 unidad=512
$ du -B1 hueco.img
4096 hueco.img
$ du -b hueco.img
1048577 hueco.img
$ od -An -tx1 -N 8 hueco.img
00 00 00 00 00 00 00 00
$ od -An -tx1 -j 1048573 -N 4 hueco.img
00 00 00 58
La asignación mostrada representa un caso ordinario con bloques de 4 KiB. La longitud y los cuatro bytes de la segunda región sí proceden de lo escrito. La lectura empieza tres bytes antes de la X y termina justo en ella.
Un hueco no es cualquier secuencia de ceros
Un archivo puede contener ceros porque se escribieron explícitamente en bloques asignados, o porque una región no tiene datos físicos asignados y se representa como hueco. La lectura puede devolver exactamente los mismos valores. No se puede demostrar la representación solo buscando bytes 00 en el contenido.
Además, un bloque asignado para X puede incluir posiciones anteriores que se leen como cero dentro de ese mismo bloque. No confundas «byte que nunca escribió nuestro programa» con «bloque enteramente no asignado». La granularidad del almacenamiento puede ser mayor que un byte.
Copiar puede materializar los ceros
Una copia que lea todos los bytes y escriba cada bloque en el destino puede crear un archivo con la misma longitud y contenido, pero con más almacenamiento asignado. Una herramienta que preserve huecos puede representar el mismo resultado con menos espacio. Ambas pueden ser correctas como copia de contenido y distintas como representación física.
Los tamaños aparentes no garantizan disponibilidad futura para materializar los huecos. Tampoco cero bloques de datos implica coste cero: siguen existiendo inodo, nombre y otros metadatos. Las imágenes de disco, archivos de máquinas virtuales y bases de datos hacen que esta distinción tenga consecuencias prácticas.
9. pread y pwrite: una posición por operación
Cuando la posición actual se comparte, un par de llamadas lseek + read puede intercalarse con otra operación. Las interfaces posicionales evitan depender de ese estado para seleccionar dónde transferir:
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
El offset se interpreta desde el comienzo del archivo, como una posición absoluta. No hay argumento SEEK_CUR o SEEK_END. Pread lee allí y pwrite escribe allí, conservando la posición actual de la descripción de archivo abierta.
| Operación | Cómo elige la región | Qué pasa con el cursor compartido |
|---|---|---|
read | Posición actual. | Avanza por los bytes leídos. |
write | Posición actual; con O_APPEND, final al escribir. | Se actualiza según la escritura. |
pread | Offset absoluto de la llamada. | No lo modifica. |
pwrite | Offset absoluto, con la salvedad de O_APPEND en Linux. | No lo modifica. |
Se mantienen las reglas del tema 6: retornos parciales, EOF en lecturas, EINTR y otros errores. Si una pwrite acepta n bytes y necesitas completar el bloque, el siguiente intento usa buf + n y offset + n. Actualizar solo el puntero repetiría la escritura en la misma ubicación.
No son lseek más una operación aislada por casualidad
Un pread evita que otro usuario del cursor cambie la posición entre «buscar» y «leer». No es una promesa de que todo el contenido sea una instantánea inmutable: otro escritor puede modificar la región. Tampoco convierte varios pwrite en una transacción ni bloquea el archivo completo.
Estas interfaces requieren objetos que admitan acceso por posición. Una tubería no adquiere esa capacidad por usar pread. El modo de acceso también sigue aplicándose: pwrite sobre una apertura de solo lectura no se vuelve válido por aportar un offset.
POSIX espera que pwrite respete el offset incluso con O_APPEND, pero Linux documenta que, con esa bandera, pwrite anexa al final en lugar de usar la ubicación indicada. Para los ejemplos de modificación posicional abrimos sin O_APPEND. La posición de la apertura sigue sin modificarse.
La salvedad y el contrato están descritos en pread(2) y pwrite(2). No deduzcas una garantía para todas las combinaciones de banderas solo del nombre de la función.
10. Experimento: modificar sin mover el cursor
El programa parche-posicional.c recibe una ruta, un offset y un texto de hasta 64 bytes. Crea el archivo si falta, sin truncar si existe, sitúa su cursor en 2 y realiza un pwrite completo en el offset pedido. Después hace un pread de hasta ocho bytes en esa región y muestra que la posición continúa en 2.
Para que el ejemplo tenga límites explícitos, admite offsets y resultados hasta 1 GiB. Es una restricción del programa didáctico, no el tamaño máximo de un archivo Linux. El texto debe tener al menos un byte y no se añade un salto de línea. La validación ocurre antes de abrir.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
static int mostrar(int fd, const char *etapa)
{
struct stat s;
if (fstat(fd, &s) == -1)
return -1;
off_t p = lseek(fd, 0, SEEK_CUR);
if (p == (off_t)-1)
return -1;
printf("%s: posicion=%jd longitud=%jd\n",
etapa, (intmax_t)p, (intmax_t)s.st_size);
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 4) {
fprintf(stderr, "uso: %s ARCHIVO OFFSET TEXTO\n", argv[0]);
return 2;
}
char *fin;
errno = 0;
intmax_t valor = strtoimax(argv[2], &fin, 10);
size_t longitud = strlen(argv[3]);
if (errno != 0 || fin == argv[2] || *fin != '\0' || valor < 0 ||
valor > 1073741824 || longitud == 0 || longitud > 64 ||
valor > 1073741824 - (intmax_t)longitud) {
fprintf(stderr, "offset o texto fuera de los limites del ejemplo\n");
return 2;
}
off_t base = (off_t)valor;
int fd = open(argv[1], O_RDWR | O_CREAT | O_CLOEXEC, 0666);
if (fd == -1) {
perror("open");
return 1;
}
int estado = 1;
if (lseek(fd, 2, SEEK_SET) == (off_t)-1) {
perror("lseek inicial");
goto limpiar;
}
if (mostrar(fd, "antes") == -1) {
perror("mostrar antes");
goto limpiar;
}
size_t total = 0;
while (total < longitud) {
ssize_t n = pwrite(fd, argv[3] + total, longitud - total,
base + (off_t)total);
if (n == -1) {
if (errno == EINTR)
continue;
perror("pwrite");
goto limpiar;
}
if (n == 0) {
fprintf(stderr, "pwrite sin progreso\n");
goto limpiar;
}
total += (size_t)n;
}
unsigned char buf[8];
ssize_t n;
do {
n = pread(fd, buf, sizeof buf, base);
} while (n == -1 && errno == EINTR);
if (n == -1) {
perror("pread");
goto limpiar;
}
printf("pread en %jd: %zd bytes", (intmax_t)base, n);
for (size_t i = 0; i < (size_t)n; i++)
printf(" %02x", (unsigned int)buf[i]);
putchar('\n');
if (mostrar(fd, "despues") == -1) {
perror("mostrar despues");
goto limpiar;
}
estado = 0;
limpiar:
if (close(fd) == -1) {
perror("close");
estado = 1;
}
if (fflush(stdout) == EOF) {
perror("fflush informe");
estado = 1;
}
return estado;
}
Modificar el centro de un archivo existente
$ cd "$LAB7"
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 parche-posicional.c -o parche-posicional
$ ./parche-posicional letras.txt 7 xy
antes: posicion=2 longitud=10
pread en 7: 3 bytes 78 79 4a
despues: posicion=2 longitud=10
$ cat letras.txt; printf '\n'
ABCDEFGxyJ
El parche sustituye H e I y conserva J. La consulta pide hasta ocho bytes desde 7, pero solo quedan tres hasta el final. El programa utiliza ese retorno real. Ni pwrite ni pread han movido la posición de la apertura fuera de 2.
Escritura posicional después del final
$ ./parche-posicional otro-hueco.img 1048576 X
antes: posicion=2 longitud=0
pread en 1048576: 1 bytes 58
despues: posicion=2 longitud=1048577
$ stat -c 'longitud=%s bloques=%b unidad=%B' otro-hueco.img
La posición compartida nunca estuvo en 1 MiB: se mantuvo en 2. La escritura explícita sí cambió el objeto y su tamaño. Crear huecos no depende exclusivamente de llamar lseek; depende de escribir después del final sin materializar todo el intervalo, con el comportamiento que permita el sistema de archivos.
Si una escritura parcial ocurre en el bucle, se avanzan tanto el puntero como base + total. El límite validado evita desbordar esa suma en el intervalo del programa. El código no promete un parche transaccional: un fallo después de progreso puede dejar parte del texto aplicada.
11. Registros, concurrencia y acceso directo
Un formato con registros de longitud fija permite calcular posiciones sin recorrer todos los registros anteriores. Si hay una cabecera de H bytes y cada registro ocupa R, el de índice i comienza en H + i × R. Es una regla del formato, no algo que lseek descubra automáticamente.
| Formato de ejemplo | Valor | Consecuencia |
|---|---|---|
| Cabecera H. | 16 bytes. | El primer registro empieza en 16. |
| Tamaño R. | 32 bytes. | Los comienzos quedan separados por 32. |
| Registro i = 0. | 16 + 0 × 32 = 16. | Ocupa índices 16 a 47. |
| Registro i = 4. | 16 + 4 × 32 = 144. | Ocupa índices 144 a 175. |
Si los registros son variables, esa fórmula deja de bastar. Puedes necesitar un índice de offsets o leer una estructura que indique dónde empieza cada uno. Tampoco uses sizeof de una struct como formato durable sin decidir relleno, orden de bytes y representación de campos: el diseño de un formato no se resuelve solo eligiendo una llamada de E/S.
Una carrera con lseek seguido de read
hilo A: lseek(fd, 100, SEEK_SET)
hilo B: lseek(fd, 500, SEEK_SET)
hilo A: read(fd, bufA, 20) /* ya no empieza en 100 */
alternativa para A:
pread(fd, bufA, 20, 100) /* selecciona 100 sin usar el cursor compartido */
No basta con que ambas llamadas del hilo A sean correctas individualmente. La intención exige que no se cambie la posición entre ellas. Una interfaz posicional evita esta dependencia; otra opción de diseño sería sincronizar toda la secuencia, o utilizar aperturas independientes cuando corresponda.
Pread no bloquea la región frente a escrituras concurrentes y pwrite no evita que dos usuarios elijan la misma región. Si ambos escriben offsets solapados, el programa necesita reglas adicionales para coordinar datos. Eliminar la carrera del cursor no elimina todas las carreras de la aplicación.
Acceso directo no garantiza rendimiento superior
En un HDD, saltar entre muchas regiones pequeñas puede ser caro; en un SSD cambian las latencias, pero siguen existiendo costes y patrones de acceso. Leer secuencialmente puede aprovechar caché y lectura anticipada. La capacidad de seleccionar un offset sirve para expresar la tarea, no para prometer que cualquier orden de peticiones será más rápido.
12. Objetos no posicionables y límites del modelo
Una tubería entrega un flujo de bytes; no permite volver a los datos ya consumidos mediante un índice. Sockets y otros objetos tampoco se comportan como un archivo regular posicionable. Lseek suele fallar con ESPIPE en esos casos. Pread y pwrite tampoco convierten un flujo en almacenamiento de acceso aleatorio.
| Error | Interpretación típica | Qué revisar |
|---|---|---|
EBADF | Descriptor inválido; en operaciones de E/S, también acceso no permitido por la apertura. | Vida útil, modo de acceso y resultado de open. |
ESPIPE | El objeto no admite ese posicionamiento. | Si es tubería, socket u otro flujo. |
EINVAL | Origen no válido o resultado negativo para un archivo regular, entre otras causas. | Base y desplazamiento elegidos. |
EOVERFLOW | El resultado no se representa en el tipo de desplazamiento correspondiente. | Tipos, rangos y configuración de archivos grandes. |
| Errores de pread/pwrite. | Además de posicionamiento, siguen siendo operaciones de E/S. | Retornos parciales, señales, acceso y recursos del sistema. |
SEEK_DATA y SEEK_HOLE: ayuda del sistema de archivos
Linux ofrece también orígenes para buscar regiones que el sistema de archivos informa como datos o huecos. No son un análisis del contenido para encontrar el siguiente byte distinto de cero. Su soporte y precisión dependen del formato y de la implementación.
Un sistema puede ofrecer una visión simple en la que considera datos todo el intervalo hasta EOF y solo identifica el final como hueco. Por eso una herramienta que preserve regiones dispersas debe contemplar soporte limitado y errores como ENXIO cuando no existe una región adecuada desde el offset pedido. Estos recorridos también usan lseek y cambian la posición.
Los programas del tema usan los tres orígenes básicos para no depender de una cartografía particular de extents. La igualdad de contenido sigue siendo una pregunta distinta de si el formato puede describir exactamente sus regiones no asignadas.
Los offsets tampoco son persistencia ni publicación
Un pwrite correcto puede quedar en caché, y varios parches no forman automáticamente una actualización atómica del documento completo. La aplicación debe comprobar resultados y decidir cómo coordina lectores, sincroniza datos y publica versiones. Lo que hemos resuelto es la selección y el avance de las regiones, no todas las garantías de una base de datos.
13. Simulador de posiciones, datos y huecos
El modelo comienza con ABCDEFGHIJ. Los descriptores 3 y 4 comparten la apertura A1, mientras 5 referencia A2, independiente. Todos llegan al mismo objeto. Puedes mover una posición, leer dos bytes o escribir un texto, tanto mediante el cursor como con un offset explícito.
Mismo archivo, dos posiciones y tres referencias
| fd | Apertura | Posición actual | Objeto |
|---|
Historial de operaciones
Para visualizarlo usamos bloques de 8 bytes, un máximo de 128 bytes lógicos y posiciones entre 0 y 128. Son límites didácticos, no tamaños de bloques ni capacidades reales de Linux. Una escritura asigna los bloques del modelo que toca; el intervalo no escrito se lee como cero. Los bloques asignados pueden contener ceros y no equivalen necesariamente a datos escritos byte por byte.
Todas las aperturas son O_RDWR sin O_APPEND; se omiten permisos, concurrencia real, fallos parciales y persistencia. El simulador completa cada transferencia válida hasta dos bytes o el texto completo, salvo EOF. Pread y pwrite interpretan el número como offset absoluto e ignoran el selector de origen de lseek.
Secuencias para comprobar
- Con 3, busca 4 desde SEEK_SET y lee dos bytes. Cambia a 4: también observa A1 en 6. El descriptor 5 sigue con A2 en 0.
- Con 5, ejecuta SEEK_END +5. La posición llega a 15 y el archivo sigue midiendo diez. Un read devuelve cero y conserva esa posición.
- Escribe X en esa posición. El tamaño pasa a 16 y los índices 10 a 14 se leen como cero. No todos esos ceros necesitan bloques independientes.
- Haz pread desde 0 mediante 3. Obtienes AB, pero la posición que compartía con 4 no cambia. Ejecuta pwrite en 7 y observa el contenido sin mover A1.
- Prueba SEEK_CUR con un resultado negativo. La operación falla y el cursor anterior permanece. Luego pide SEEK_END -1 y compara con el índice del último byte.
14. Resumen
- Lseek selecciona una posición y devuelve el offset absoluto resultante; no transfiere bytes.
- SEEK_SET usa el comienzo, SEEK_CUR el cursor actual y SEEK_END la longitud del objeto.
- Un desplazamiento negativo puede ser válido; una posición negativa no lo es para el archivo regular estudiado.
- Off_t representa posiciones. Los tipos y las operaciones aritméticas deben contemplar sus rangos.
- Buscar después del final no amplía el archivo. Una escritura posterior sí puede crear una región que se lea como ceros.
- Los huecos distinguen tamaño lógico y almacenamiento asignado; leer ceros no demuestra por sí solo que exista un hueco.
- Los duplicados comparten posición; las aperturas independientes mantienen posiciones separadas.
- Pread y pwrite usan offsets absolutos sin mover el cursor de la apertura, con la salvedad de anexado documentada en Linux.
- Un pwrite parcial exige avanzar tanto el puntero como el offset del próximo intento.
- La E/S posicional evita carreras del cursor, pero no ofrece por sí sola bloqueo, transacciones ni persistencia.
- Los flujos no posicionables, como tuberías, requieren otro modo de organizar el acceso.
| Necesito… | Operación apropiada |
|---|---|
| Consultar la posición actual. | lseek(fd, 0, SEEK_CUR) |
| Ir al comienzo. | lseek(fd, 0, SEEK_SET) |
| Ir al último byte de un archivo no vacío. | lseek(fd, -1, SEEK_END) |
| Consultar longitud sin mover el cursor. | fstat y st_size, para un archivo regular. |
| Leer en una posición sin tocar el cursor. | pread |
| Modificar una región sin tocar el cursor. | pwrite, con apertura adecuada y sin O_APPEND para este uso en Linux. |
| Sumar almacenamiento realmente atribuido. | Consultar asignación con stat y du, no solo la posición o la longitud. |
15. Preguntas de repaso y práctica
Antes de desplegar cada respuesta, separa la posición de cada apertura del tamaño del objeto.
1. ¿Qué devuelve un lseek correcto: bytes leídos o una posición?
La nueva posición absoluta respecto del comienzo del objeto posicionable. Lseek no devuelve contenido ni comunica una cantidad transferida.
2. Un archivo mide diez bytes. ¿Dónde quedan SEEK_END 0 y SEEK_END -1?
En 10 y 9 respectivamente. Los datos existentes ocupan índices de 0 a 9; la posición 10 está justo después del último byte.
3. Desde la posición 6, ¿qué resultado produce SEEK_CUR -3?
La posición absoluta 3. El argumento negativo es válido porque la suma no queda antes del comienzo del archivo regular.
4. ¿Lseek a 1 MiB sobre un archivo vacío hace que mida 1 MiB?
No. Solo cambia la posición de esa apertura. El tamaño sigue siendo cero hasta que una operación como write o ftruncate lo modifique.
5. Si escribes un byte en el índice 1048576 de un archivo vacío, ¿cuál es el tamaño?
1.048.577 bytes. El índice del byte escrito empieza a contar en cero y la longitud llega una posición más allá.
6. ¿Un read desde más allá del final rebobina el cursor al tamaño?
No. En el caso del archivo regular devuelve cero con petición positiva y puede conservar la posición que ya estaba después del final.
7. ¿Todos los bytes cero de un archivo representan bloques sin asignar?
No. Pueden ser ceros escritos explícitamente o partes inicializadas de un bloque asignado. La representación de huecos es una propiedad de almacenamiento, no una deducción del valor de cada byte.
8. Si 4 es un dup de 3, ¿un lseek sobre 4 cambia lo que leerá 3?
Sí. Los dos referencian la misma descripción de archivo abierta y su posición. Si fueran dos open independientes, no compartirían ese cursor por el mero hecho de abrir el mismo nombre.
9. ¿Fstat y lseek al final consultan el tamaño con los mismos efectos?
No. En un archivo regular fstat consulta st_size sin mover el cursor. Lseek al final también cambia la posición de la apertura y puede afectar a quienes la compartan.
10. ¿Qué diferencia hay entre pread y lseek seguido de read?
Pread selecciona la región mediante un offset explícito y no cambia el cursor compartido. La secuencia de dos llamadas puede intercalarse con otros cambios de posición y termina modificando ese estado.
11. Un pwrite aceptó tres de diez bytes en el offset 100. ¿Qué queda?
Siete bytes que deben enviarse desde buf + 3 al offset 103. Avanzar solo el puntero y repetir offset 100 sobrescribiría una región incorrecta.
12. ¿Pwrite permite escribir mediante una apertura O_RDONLY?
No. La posición explícita no elimina los requisitos del modo de acceso. La apertura debe permitir escritura y el objeto debe admitir E/S posicional.
13. ¿Qué precaución requiere pwrite con O_APPEND en Linux?
Linux documenta que se anexa al final aunque se aporte otro offset. Para modificar una región por offset en estos ejemplos se abre sin O_APPEND. No se debe suponer que esa combinación se comporta igual en todas las plataformas.
14. ¿Pread convierte una región en una instantánea inmune a otros escritores?
No. Evita depender del cursor compartido, pero no impide que otro proceso modifique los datos. La coordinación de contenido requiere decisiones adicionales.
15. ¿Por qué se comprueba (off_t)-1 y no solo si el retorno es cero?
Porque cero es una posición válida y habitual. El valor que señala error es -1 representado como off_t; entonces se consulta errno.
16. ¿Se puede usar lseek para volver atrás en una tubería?
No. Una tubería es un flujo no posicionable y la operación falla típicamente con ESPIPE. Hay que guardar los datos necesarios en memoria u otro almacenamiento si se quiere revisarlos.
17. Con cabecera de 16 bytes y registros de 32, ¿dónde empieza el registro de índice 4?
En 16 + 4 × 32 = 144. Ocupa los índices 144 a 175. El cálculo requiere tipos y comprobaciones adecuados para evitar desbordamientos con índices grandes.
18. ¿SEEK_HOLE encuentra el siguiente byte cuyo valor sea cero?
No. Consulta regiones que el sistema de archivos informa como huecos, con las limitaciones de su implementación. Puede haber ceros en regiones asignadas y una cartografía de huecos poco precisa.
Para practicar
- Ejecuta posiciones.c y anota el cursor antes y después de cada read. Comprueba que CUR -3 usa la posición que dejó la lectura anterior.
- Modifica el salto END +5 por END +100. Compara la posición final y stat para verificar que la longitud no cambia. Después restablece el programa.
- Ejecuta el recorrido sobre un archivo vacío. Identifica qué saltos pueden tener éxito y cuál relativo al final debe fallar; no interpretes -1 como una posición alcanzada.
- Crea dos archivos dispersos con crear-hueco.c cambiando la constante a 4096 y a 1048576. Predice sus longitudes y compara bloques asignados sin asumir que coinciden con esas longitudes.
- Ejecuta crear-hueco dos veces con el mismo nombre. Comprueba que la segunda falla por exclusividad y que la primera X permanece en su ubicación.
- Usa parche-posicional para sustituir dos bytes al principio, en el centro y al final de un archivo de prueba. Distingue cuándo se conserva el tamaño y cuándo se amplía.
- Prueba offsets negativos, texto vacío, un texto de más de 64 bytes y el límite que excedería 1 GiB. Comprueba que el programa rechaza los argumentos sin crear el archivo solicitado.
- Adapta el programa del tema 4 para hacer pread mediante un duplicado. Verifica que el cursor compartido no cambia, a diferencia de una lectura ordinaria.
- Diseña en papel un formato con cabecera de 24 bytes y registros de 40. Calcula las posiciones de los índices 0, 1 y 7 y la longitud mínima para contenerlos completos.
- En el simulador busca después de EOF sin escribir, observa la longitud y luego escribe. Lee el hueco con pread y explica por qué los ceros no son EOF.
- En el simulador usa 3 y 4 para compartir un cursor y 5 como apertura independiente. Aplica una mezcla de lseek, read y pread y predice las tres posiciones antes de cada paso.
Con este tema termina el bloque de descriptores. En el tema 8 construiremos un cat y un cp propios integrando apertura, lectura, escritura, errores y cierre. La selección de posiciones que acabas de estudiar también permitirá interpretar por qué una copia secuencial puede conservar el contenido y modificar la representación de los huecos.