Llamadas al sistema en C — práctica guardando, copiando y editando partidas con open, read, write y lseek (y la copia que medía 96 bytes)
La primera copia de seguridad que hice de mis partidas guardadas medía 96 bytes. El original medía 80. Abrí los dos ficheros y el final de la copia tenía 16 bytes repetidos de la línea anterior, como un eco. El fallo estaba en una sola palabra: había escrito write(out, buf, 32) en lugar de write(out, buf, leidos). En la última vuelta del bucle solo se habían leído 16 bytes, pero yo escribí los 32 del buffer, y los otros 16 eran restos de la vuelta anterior. Las cuentas cuadran: 3 vueltas · 32 = 96. La lección que me llevé, y que vas a ver repetida en los tres programas: en las llamadas al sistema, el número que devuelven importa tanto como lo que hacen.
Esta es la práctica de la teoría de llamadas al sistema. Son tres programas de nivel medio sobre el mismo fichero, partidas.sav, con cuatro partidas de 20 bytes cada una: el primero lo guarda ranura a ranura, el segundo hace una copia de seguridad con un bucle de read y write, y el tercero edita los puntos de una partida sin tocar el resto. Los datos se parecen a los de verdad: nombres de distinta longitud, niveles de una y dos cifras, un fichero que no existe y una ranura que no está.
Tabla de Contenidos
Antes de empezar
Conviene tener claro:
- Que
opendevuelve un descriptor (normalmente 3) o −1, y qué significanO_RDONLY,O_WRONLY,O_RDWR,O_CREATyO_TRUNC. - Que
readywritedevuelven cuántos bytes han movido y quereaddevuelve 0 al llegar al final. - Que
readno pone el'\0'. - Cómo mueve
lseekel puntero conSEEK_SET,SEEK_CURySEEK_END.
Si alguno te suena dudoso, repasa la teoría antes de seguir.
Cada programa crea sus ficheros en la carpeta desde la que lo ejecutas. Compila con gcc -Wall -std=c11 guardar.c -o guardar, ejecuta con ./guardar y mira el resultado con cat partidas.sav o con ls -l (te dice el tamaño en bytes, que es lo que vamos a comprobar todo el rato).
Programa 1: guardar las partidas
Tenemos cuatro jugadores con su nivel y sus puntos en tres arrays. Hay que guardarlos en partidas.sav con un formato fijo de 20 bytes por partida, comprobar que cada write escribe lo que debe y, al final, enseñar el fichero por pantalla leyéndolo con read y escribiéndolo en el descriptor 1. Lo que lo complica: los nombres y los números tienen longitudes distintas (3 y 12, 450 y 2750), y aun así todas las ranuras tienen que medir lo mismo.
La idea
Si todas las partidas miden lo mismo, la posición de cualquiera se calcula: la ranura n empieza en el byte 20 · n. Eso es lo que aprovecharán los programas 2 y 3, así que aquí nos aseguramos de que el formato sea fijo:
Para conseguir esos 20 bytes exactos usamos snprintf (que escribe con formato en un array, como printf pero en memoria) y después pasamos el resultado a write. Los pasos: abrir con O_TRUNC (guardar = empezar de cero), un bucle que formatea y escribe cada ranura, cerrar, y otra vez abrir para leer y volcar a la pantalla.
Antes de programar, piensa qué devuelve snprintf: el número de caracteres que ha escrito, sin contar el '\0'. Ese número es justo el que necesita write.
El código
#include <stdio.h>
#include <fcntl.h> /* open y las constantes O_... */
#include <unistd.h> /* read, write, lseek, close */
#define TAM_RANURA 20
int main(void) {
const char *nombres[] = {"Lia", "Max", "Ane", "Teo"};
int niveles[] = {3, 12, 5, 7};
int puntos[] = {450, 2750, 980, 1200};
/* O_TRUNC: cada vez que guardamos, el fichero empieza de cero */
int fd = open("partidas.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("partidas.sav");
return 1;
}
char ranura[64]; /* de sobra: snprintf nunca se sale */
for (int i = 0; i < 4; i++) {
/* %-6s: nombre a la izquierda en 6 huecos; %02d y %04d: ceros delante */
int n = snprintf(ranura, sizeof ranura, "%-6snivel %02d %04d\n",
nombres[i], niveles[i], puntos[i]);
ssize_t escritos = write(fd, ranura, n); /* n = 20: sin el '\0' */
if (escritos != TAM_RANURA) {
perror("write");
close(fd);
return 1;
}
printf("Ranura %d: write devuelve %zd, el puntero queda en %ld\n",
i, escritos, (long) lseek(fd, 0, SEEK_CUR));
}
close(fd);
/* Releer el fichero y volcarlo a la salida estándar (fd 1) */
fd = open("partidas.sav", O_RDONLY);
if (fd == -1) {
perror("partidas.sav");
return 1;
}
char buf[64];
ssize_t leidos;
printf("--- partidas.sav ---\n");
fflush(stdout); /* vacía printf antes de usar write */
while ((leidos = read(fd, buf, sizeof buf)) > 0)
write(1, buf, leidos); /* leidos, no sizeof buf */
close(fd);
return 0;
}
La línea clave es el formato de snprintf. Vamos a verla de cerca:
for (int i = 0; i < 4; i++) {
/* %-6s: nombre a la izquierda en 6 huecos; %02d y %04d: ceros delante */
int n = snprintf(ranura, sizeof ranura, "%-6snivel %02d %04d\n",
nombres[i], niveles[i], puntos[i]);
ssize_t escritos = write(fd, ranura, n); /* n = 20: sin el '\0' */
if (escritos != TAM_RANURA) {
perror("write");
close(fd);
return 1;
}
printf("Ranura %d: write devuelve %zd, el puntero queda en %ld\n",
i, escritos, (long) lseek(fd, 0, SEEK_CUR));
}
%-6s: el nombre, alineado a la izquierda y rellenado con espacios hasta 6 caracteres."Lia"se convierte en"Lia "(3 letras + 3 espacios). Sin el-, los espacios irían delante.niveles texto fijo (5 letras y un espacio).%02d: el nivel con 2 cifras como mínimo, con ceros delante: 3 →03, 12 →12.%04d: los puntos con 4 cifras: 450 →0450.\nal final.
Cuenta: 6 + 6 + 2 + 1 + 4 + 1 = 20. Y snprintf devuelve 20, que es lo que pasamos a write. El array ranura mide 64, mucho más de lo necesario: así snprintf nunca tiene que cortar nada, y además deja sitio para el '\0' que añade (y que no escribimos en el fichero).
La traza del bucle:
i | Nombre | snprintf devuelve | write devuelve | Puntero tras el write |
|---|---|---|---|---|
| 0 | Lia | 20 | 20 | 20 |
| 1 | Max | 20 | 20 | 40 |
| 2 | Ane | 20 | 20 | 60 |
| 3 | Teo | 20 | 20 | 80 |
La segunda parte es un cat casero:
char buf[64];
ssize_t leidos;
printf("--- partidas.sav ---\n");
fflush(stdout); /* vacía printf antes de usar write */
while ((leidos = read(fd, buf, sizeof buf)) > 0)
write(1, buf, leidos); /* leidos, no sizeof buf */
close(fd);
Lee de 64 en 64 y escribe en el descriptor 1 (la pantalla) los bytes que ha leído. Con 80 bytes, la primera vuelta lee 64, la segunda 16 y la tercera 0, que termina el while.
Salida
Ranura 0: write devuelve 20, el puntero queda en 20 Ranura 1: write devuelve 20, el puntero queda en 40 Ranura 2: write devuelve 20, el puntero queda en 60 Ranura 3: write devuelve 20, el puntero queda en 80 --- partidas.sav --- Lia nivel 03 0450 Max nivel 12 2750 Ane nivel 05 0980 Teo nivel 07 1200
Comprobación: cada write devolvió 20 y el puntero avanzó de 20 en 20 hasta 80 (4 · 20 = 80). Las cuatro líneas tienen sus columnas perfectamente alineadas, aunque Lia y Max tienen niveles de una y dos cifras: %02d y %04d se encargan de que 3 ocupe lo mismo que 12. Y el volcado sale después de la línea --- partidas.sav --- gracias al fflush (en la siguiente sección, por qué).
Preguntas que te puedes hacer
**¿Por qué fflush(stdout) antes del bucle de write?** Porque printf no escribe directamente: guarda el texto en un buffer de la biblioteca y lo manda con write(1, …) más tarde (al ver un \n si la salida es la terminal, o al llenarse el buffer si rediriges a un fichero). Nuestro write(1, …) va directo al kernel. Si mezclas los dos sin vaciar el buffer, el texto de printf podría salir después del volcado. fflush lo vacía en ese momento.
**¿Por qué comparo escritos con TAM_RANURA y no con −1?** Porque write puede escribir menos de lo que pides (por ejemplo, si el disco se llena) sin devolver −1. Comparar con lo esperado detecta las dos cosas: el error y la escritura a medias.
**¿Por qué O_TRUNC?** Por la historia de la teoría: si el fichero ya existía y era más largo (por ejemplo, con 5 partidas), sin O_TRUNC sobrescribiríamos las 4 primeras y la quinta seguiría ahí. Guardar debe dejar exactamente lo que guardas.
¿Por qué el volcado lee de 64 en 64 y no de 20 en 20? Porque aquí no nos importan las ranuras, solo copiar bytes a la pantalla. El tamaño del buffer es libre: con 64 son 2 lecturas con datos; con 20 serían 4. El resultado es el mismo porque escribimos leidos.
¿Y si un nombre tuviera más de 6 letras? %-6s fija un mínimo, no un máximo: «Teodoro» ocuparía 7 y la ranura mediría 21. Por eso el programa comprueba que write devolvió 20. Si quisieras cortarlo, el formato sería %-6.6s.
Escribir con write(fd, ranura, sizeof ranura). Escribiría los 64 bytes del array (los 20 buenos, el '\0' y 43 bytes de basura) y el fichero mediría 256 bytes en lugar de 80.
Pruébalo tú
- Añade una quinta jugadora,
{"Sam", 1, 50}, y comprueba que el tamaño final es 100 y que su línea esSam nivel 01 0050. - Pon un nombre de 7 letras y mira qué hace la comprobación de
escritos. Después arréglalo con%-6.6s.
Programa 2: copia de seguridad
Antes de tocar un fichero de partidas, lo sensato es copiarlo. El programa crea partidas.sav (las mismas cuatro partidas, 80 bytes) y lo copia en partidas.bak con una función copiar que lee y escribe a trozos de 32 bytes. Tiene que contar los bytes copiados, decir cuánto mide la copia y portarse bien si el fichero de origen no existe. Lo que lo complica: 80 no es múltiplo de 32.
La idea
El patrón de copia es siempre el mismo y merece la pena aprendérselo de memoria:
- Abrir el origen para leer y el destino para escribir (con
O_CREAT | O_TRUNC). - Mientras
readdevuelva más de 0: escribir en el destino esos bytes. - Cerrar los dos.
Con un buffer de 32 y un fichero de 80, el bucle da 3 vueltas con datos (32, 32 y 16) y una cuarta en la que read devuelve 0. El dibujo enseña el peligro: en la vuelta 3, la mitad del buffer son restos de la vuelta 2.
Antes de programar, piensa qué tiene que pasar si falla el segundo open (el del destino). El primero ya está abierto: hay que cerrarlo antes de salir.
El código
#include <stdio.h>
#include <fcntl.h> /* open y las constantes O_... */
#include <unistd.h> /* read, write, lseek, close */
#define TAM_RANURA 20
#define PARTIDAS "Lia nivel 03 0450\n" "Max nivel 12 2750\n" \
"Ane nivel 05 0980\n" "Teo nivel 07 1200\n"
#define TAM_BUF 32
/* Crea el fichero de partidas con las cuatro ranuras de ejemplo */
static int preparar(const char *nombre) {
int fd = open(nombre, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror(nombre);
return -1;
}
write(fd, PARTIDAS, 4 * TAM_RANURA);
close(fd);
return 0;
}
/* Copia origen en destino a trozos; devuelve los bytes copiados o -1 */
static long copiar(const char *origen, const char *destino) {
int in = open(origen, O_RDONLY);
if (in == -1) {
perror(origen);
return -1;
}
int out = open(destino, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (out == -1) {
perror(destino);
close(in); /* no dejes el otro abierto */
return -1;
}
char buf[TAM_BUF];
ssize_t leidos;
long total = 0;
int vuelta = 1;
while ((leidos = read(in, buf, TAM_BUF)) > 0) {
ssize_t escritos = write(out, buf, leidos); /* solo lo que se leyó */
if (escritos != leidos) {
perror("write");
break;
}
total += escritos;
printf(" vuelta %d: leídos %zd, escritos %zd, total %ld\n",
vuelta, leidos, escritos, total);
vuelta++;
}
printf(" vuelta %d: read devuelve %zd -> fin\n", vuelta, leidos);
close(in);
close(out);
return total;
}
int main(void) {
if (preparar("partidas.sav") == -1)
return 1;
printf("Copia de partidas.sav en partidas.bak:\n");
long n = copiar("partidas.sav", "partidas.bak");
printf("Copiados: %ld bytes\n", n);
int fd = open("partidas.bak", O_RDONLY);
printf("partidas.bak mide %ld bytes\n", (long) lseek(fd, 0, SEEK_END));
close(fd);
printf("Copia de un fichero que no existe:\n");
n = copiar("partidas_2019.sav", "viejas.bak");
printf("copiar devuelve %ld\n", n);
return 0;
}
Zoom a la función copiar, que es la que importa:
static long copiar(const char *origen, const char *destino) {
int in = open(origen, O_RDONLY);
if (in == -1) {
perror(origen);
return -1;
}
int out = open(destino, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (out == -1) {
perror(destino);
close(in); /* no dejes el otro abierto */
return -1;
}
char buf[TAM_BUF];
ssize_t leidos;
long total = 0;
int vuelta = 1;
while ((leidos = read(in, buf, TAM_BUF)) > 0) {
ssize_t escritos = write(out, buf, leidos); /* solo lo que se leyó */
if (escritos != leidos) {
perror("write");
break;
}
total += escritos;
printf(" vuelta %d: leídos %zd, escritos %zd, total %ld\n",
vuelta, leidos, escritos, total);
vuelta++;
}
printf(" vuelta %d: read devuelve %zd -> fin\n", vuelta, leidos);
close(in);
close(out);
return total;
}
- **Los dos
open.** El origen conO_RDONLY: solo lo vamos a leer, y así no podemos estropearlo por accidente. El destino conO_WRONLY | O_CREAT | O_TRUNC, 0644: si no existe se crea, y si existe (una copia vieja) se vacía. - **El
close(in)del segundoif.** Si falla el destino, el origen ya estaba abierto. Sin eseclose, cada llamada fallida dejaría un descriptor ocupado. - **El
while.** La asignación va entre paréntesis,(leidos = read(...)) > 0: primero se guarda lo que devuelveready después se compara. Sin los paréntesis,leidosvaldría el resultado de la comparación (1 o 0). - **
write(out, buf, leidos).** El corazón de la práctica: se escriben los bytes leídos, noTAM_BUF. - **
escritos != leidos.** Si el disco se llena,writeescribe menos; la copia estaría incompleta y lo decimos conperror. - **La función devuelve
long** con el total, o −1 si no pudo abrir algo. Asímainpuede distinguir «he copiado 0 bytes» (un fichero vacío) de «no he podido copiar».
| Vuelta | read devuelve | write escribe | total | Puntero del origen |
|---|---|---|---|---|
| 1 | 32 | 32 | 32 | 32 |
| 2 | 32 | 32 | 64 | 64 |
| 3 | 16 | 16 | 80 | 80 |
| 4 | 0 | — | 80 | 80 |
Salida
Copia de partidas.sav en partidas.bak: vuelta 1: leídos 32, escritos 32, total 32 vuelta 2: leídos 32, escritos 32, total 64 vuelta 3: leídos 16, escritos 16, total 80 vuelta 4: read devuelve 0 -> fin Copiados: 80 bytes partidas.bak mide 80 bytes Copia de un fichero que no existe: partidas_2019.sav: No such file or directory copiar devuelve -1
Comprobación: 32 + 32 + 16 = 80, y lseek(fd, 0, SEEK_END) sobre la copia confirma que mide 80 bytes, igual que el original. En la segunda llamada, partidas_2019.sav no existe: perror(origen) imprime el nombre del fichero y el motivo, y copiar devuelve −1 sin crear viejas.bak (el destino ni se llega a abrir).
Preguntas que te puedes hacer
¿Por qué un buffer de 32 y no del tamaño del fichero? Porque no sabes cuánto mide el fichero que te van a pasar: podría tener 80 bytes o 2 GB. El bucle funciona con cualquier tamaño. En programas reales se usan buffers de 4096 u 8192 bytes (menos llamadas al sistema), pero con 32 se ven bien las vueltas.
**¿Por qué no pongo el '\0' en el buffer?** Porque aquí nunca lo tratamos como cadena: lo pasamos tal cual a write. El '\0' solo hace falta si vas a usar %s, strlen y compañía. De hecho, si el fichero fuera una imagen, podría tener ceros en medio, y una copia «de cadenas» se cortaría ahí.
**¿Qué pasa si read devuelve −1 a mitad?** El while termina (−1 no es mayor que 0) y la línea de después imprime read devuelve -1. Una versión más completa lo comprobaría y devolvería −1 también.
**¿Por qué printf con dos espacios delante?** Solo para que las vueltas se vean «dentro» de la llamada a copiar en la salida. No tiene nada que ver con los ficheros.
write(out, buf, TAM_BUF). Es mi copia de 96 bytes: la última vuelta escribe 16 bytes buenos y 16 de restos, y la copia queda más grande que el original. Si comparas los tamaños con ls -l, lo detectas al momento.
Pruébalo tú
- Cambia
TAM_BUFa 7 y predice cuántas vueltas habrá y cuánto leerá la última (80 = 7 · 11 + 3). Compruébalo. - Haz que
copiarse niegue a machacar una copia existente usandoO_EXCL, y prueba a ejecutarlo dos veces.
Programa 3: editar una partida
Ane ha conseguido 3100 puntos y hay que actualizar su ranura, la 2, sin reescribir el fichero entero. El programa lee la ranura 2, cambia sus puntos, la vuelve a leer para comprobarlo, lee la última ranura sin saber cuántas hay e intenta leer una ranura que no existe (la 7). Lo que lo complica: hay que leer y escribir en el mismo fichero, y caer exactamente en los 4 bytes de los puntos.
La idea
Gracias al formato fijo, todo es aritmética:
- La ranura n empieza en
n * TAM_RANURA. - Sus puntos empiezan 15 bytes más allá:
n * TAM_RANURA + POS_PUNTOS. Para la ranura 2, 2 · 20 + 15 = 55. - La última ranura empieza 20 bytes antes del final:
lseek(fd, -TAM_RANURA, SEEK_END).
Se abre con O_RDWR (leer y escribir con el mismo descriptor) y sin O_TRUNC, que borraría las partidas.
Antes de programar, piensa en cuántos bytes tienes que escribir. Si escribes 5 (los 4 dígitos y el '\0'), te comes el \n de la ranura y la siguiente partida acaba pegada a esta.
El código
#include <stdio.h>
#include <fcntl.h> /* open y las constantes O_... */
#include <unistd.h> /* read, write, lseek, close */
#include <string.h> /* strlen */
#define TAM_RANURA 20
#define PARTIDAS "Lia nivel 03 0450\n" "Max nivel 12 2750\n" \
"Ane nivel 05 0980\n" "Teo nivel 07 1200\n"
#define POS_PUNTOS 15 /* los puntos empiezan en el byte 15 de la ranura */
/* Crea el fichero de partidas con las cuatro ranuras de ejemplo */
static int preparar(const char *nombre) {
int fd = open(nombre, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror(nombre);
return -1;
}
write(fd, PARTIDAS, 4 * TAM_RANURA);
close(fd);
return 0;
}
/* Lee la ranura n en buf (21 bytes); devuelve lo que devuelve read */
static ssize_t leer_ranura(int fd, int n, char *buf) {
lseek(fd, (off_t) n * TAM_RANURA, SEEK_SET); /* salto directo, sin leer las de antes */
ssize_t leidos = read(fd, buf, TAM_RANURA);
buf[leidos > 0 ? leidos : 0] = '\0';
return leidos;
}
/* Sobrescribe los 4 dígitos de puntos de la ranura n */
static void cambiar_puntos(int fd, int n, int nuevos) {
char cifras[8];
snprintf(cifras, sizeof cifras, "%04d", nuevos); /* siempre 4 dígitos */
off_t pos = lseek(fd, (off_t) n * TAM_RANURA + POS_PUNTOS, SEEK_SET);
write(fd, cifras, strlen(cifras)); /* 4 bytes, ni uno más */
printf("Escribo \"%s\" en el byte %ld\n", cifras, (long) pos);
}
int main(void) {
if (preparar("partidas.sav") == -1)
return 1;
int fd = open("partidas.sav", O_RDWR); /* leer Y escribir */
if (fd == -1) {
perror("partidas.sav");
return 1;
}
char ranura[TAM_RANURA + 1];
leer_ranura(fd, 2, ranura);
printf("Antes: %s", ranura);
cambiar_puntos(fd, 2, 3100);
leer_ranura(fd, 2, ranura);
printf("Después: %s", ranura);
/* La última ranura, sin saber cuántas hay: 20 bytes antes del final */
off_t pos = lseek(fd, -TAM_RANURA, SEEK_END);
read(fd, ranura, TAM_RANURA);
ranura[TAM_RANURA] = '\0';
printf("Última (byte %ld): %s", (long) pos, ranura);
/* Una ranura que no existe: read devuelve 0 */
ssize_t n = leer_ranura(fd, 7, ranura);
printf("Ranura 7: read devuelve %zd (vacía)\n", n);
printf("Tamaño: %ld bytes (no ha crecido)\n", (long) lseek(fd, 0, SEEK_END));
close(fd);
return 0;
}
Las dos funciones, de cerca:
/* Lee la ranura n en buf (21 bytes); devuelve lo que devuelve read */
static ssize_t leer_ranura(int fd, int n, char *buf) {
lseek(fd, (off_t) n * TAM_RANURA, SEEK_SET); /* salto directo, sin leer las de antes */
ssize_t leidos = read(fd, buf, TAM_RANURA);
buf[leidos > 0 ? leidos : 0] = '\0';
return leidos;
}
/* Sobrescribe los 4 dígitos de puntos de la ranura n */
static void cambiar_puntos(int fd, int n, int nuevos) {
char cifras[8];
snprintf(cifras, sizeof cifras, "%04d", nuevos); /* siempre 4 dígitos */
off_t pos = lseek(fd, (off_t) n * TAM_RANURA + POS_PUNTOS, SEEK_SET);
write(fd, cifras, strlen(cifras)); /* 4 bytes, ni uno más */
printf("Escribo \"%s\" en el byte %ld\n", cifras, (long) pos);
}
- **
leer_ranura** salta conSEEK_SETal principio de la ranura (no hace falta leer las anteriores) y lee 20 bytes. El(off_t)convierte antes de multiplicar, para que con ficheros enormes la cuenta no se desborde en unint. El'\0'se pone enleidossi se leyó algo y en 0 si no: sireaddevuelve 0 (ranura que no existe) o −1,bufqueda como cadena vacía en lugar de con basura. Por esobuftiene que medir 21. - **
cambiar_puntos** formatea los puntos con%04d(siempre 4 cifras: 3100 →3100, 50 →0050), salta an · 20 + 15y escribestrlen(cifras)bytes: 4, sin el'\0'. Imprime la posición que devuelvelseekpara que veas dónde ha escrito.
Las posiciones por las que pasa el puntero:
| Paso | Llamada | Puntero |
|---|---|---|
| Leer ranura 2 | lseek(fd, 40, SEEK_SET) + read de 20 | 40 → 60 |
| Cambiar puntos | lseek(fd, 55, SEEK_SET) + write de 4 | 55 → 59 |
| Releer ranura 2 | lseek(fd, 40, SEEK_SET) + read de 20 | 40 → 60 |
| Última ranura | lseek(fd, -20, SEEK_END) + read de 20 | 60 → 80 |
| Ranura 7 | lseek(fd, 140, SEEK_SET) + read | 140 (devuelve 0) |
Salida
Antes: Ane nivel 05 0980 Escribo "3100" en el byte 55 Después: Ane nivel 05 3100 Última (byte 60): Teo nivel 07 1200 Ranura 7: read devuelve 0 (vacía) Tamaño: 80 bytes (no ha crecido)
Comprobación: 0980 pasa a 3100 en el byte 55 (2 · 20 + 15 = 55) y el resto de la línea de Ane no cambia. La última ranura empieza en el byte 60 (80 − 20 = 60) y es la de Teo. La ranura 7 empezaría en 7 · 20 = 140, más allá de los 80 bytes: lseek no se queja (moverse fuera es legal) y read devuelve 0. Y el tamaño sigue siendo 80: hemos sobrescrito, no añadido.
Preguntas que te puedes hacer
**¿Por qué O_RDWR y no abrir dos veces, una para leer y otra para escribir?** Se puede, pero tendrías dos entradas de ficheros abiertos con dos punteros (lo vimos en la teoría) y tendrías que hacer lseek en cada uno. Con O_RDWR es un solo descriptor y un solo puntero.
¿Por qué no hay que leer las ranuras 0 y 1 para llegar a la 2? Porque el formato es fijo: sabemos que la 2 empieza en el 40. Con líneas de longitud variable (un CSV normal) no podrías: tendrías que leer desde el principio contando \n. Esa es la gran ventaja de los registros de tamaño fijo.
¿Por qué leer la ranura 7 no da error? Porque un lseek más allá del final es válido, y un read desde ahí simplemente no encuentra bytes: devuelve 0, igual que al terminar un fichero. Si allí escribieras, el fichero crecería y quedaría un hueco de ceros entre el 80 y el 140.
**¿Por qué compruebo leidos > 0 antes de poner el '\0'?** Porque si read devolviera −1, buf[-1] = '\0' escribiría fuera del array. Con el operador ternario, en ese caso el '\0' va en buf[0].
Abrir con O_RDWR | O_TRUNC «por costumbre». El fichero se vacía al abrir, la ranura 2 no existe y write en el 55 crea un fichero de 59 bytes con 55 ceros delante.
Para saber cuántas ranuras tiene el fichero sin leerlo, lseek(fd, 0, SEEK_END) / TAM_RANURA. Con 80 bytes, 80 / 20 = 4.
Pruébalo tú
- Escribe
cambiar_nivel(fd, n, nuevo)que cambie los 2 dígitos del nivel (están en el byte 12 de la ranura) y sube a Teo al nivel 8. - Imprime el número de ranuras con el truco de
SEEK_ENDy recorre todas desde la última hasta la primera usandolseekhacia atrás.
Errores típicos en esta práctica
- **Escribir el tamaño del buffer en lugar de
leidos:** la copia crece con basura (la mía de 96 bytes). - **Usar
sizeofcon arrays más grandes de lo que se escribe:** 64 bytes por ranura en lugar de 20. - **Escribir el
'\0'en el fichero:** constrlen(cifras) + 1osizeof cifraste comes el byte siguiente (el\n). - **Olvidar
closeen los caminos de error:** si el segundoopenfalla, el primero sigue abierto. - **Abrir con
O_TRUNCcuando quieres editar:** borra lo que ibas a modificar. - **No poner paréntesis en
(leidos = read(...)) > 0:**leidosacaba valiendo 1 o 0.
Míralo paso a paso en Python Tutor
Python Tutor dibuja las variables y los arrays en cada paso; elige C. Como no trabaja con ficheros reales del disco, aquí el «fichero» es un array de 14 bytes y la copia se hace con un buffer de 4, con los mismos pasos que copiar:
#include <stdio.h>
int main(void) {
char origen[] = "Lia 03\nMax 12\n"; /* 14 bytes, como un fichero */
char destino[16] = {0};
char buf[4]; /* el buffer de la copia */
int tam = 14, pos = 0, total = 0, leidos;
do {
leidos = 0; /* "read" de hasta 4 bytes */
while (leidos < 4 && pos < tam)
buf[leidos++] = origen[pos++];
for (int i = 0; i < leidos; i++) /* "write" de leidos bytes */
destino[total + i] = buf[i];
total += leidos;
} while (leidos > 0); /* 4, 4, 4, 2 y 0 */
printf("Copiados %d bytes\n%s", total, destino);
return 0;
}
Su salida:
Copiados 14 bytes Lia 03 Max 12
Qué observar:
- En la primera vuelta del
do, mira cómobufse llena conL,i,ay el espacio, ypospasa de 0 a 4. - En la cuarta vuelta solo quedan 2 bytes:
leidosvale 2, y enbuf[2]ybuf[3]siguen los restos de la vuelta anterior. Fíjate en que elforsolo copia 2. - Mira
destinocrecer de 4 en 4 (4, 8, 12) y después solo 2 (14). - En la quinta vuelta
leidosvale 0, elforno hace nada y eldo-whiletermina: el mismo final que elreadque devuelve 0.
Resumen
- Con un formato fijo (
%-6s,%02d,%04d) todas las ranuras miden lo mismo y la posición de cualquier dato se calcula. writedevuelve cuántos bytes escribió: compáralo con los que esperabas, no solo con −1.- El bucle de copia es
while ((leidos = read(...)) > 0) write(out, buf, leidos);, y el tercer argumento dewritees siempreleidos. - Para editar un fichero en su sitio:
O_RDWR, sinO_TRUNC,lseeka la posición exacta ywritede los bytes justos. lseekmás allá del final no es un error:readdevuelve 0 ywritecrea un hueco de ceros.- Cierra todo lo que abres, también en los caminos de error.
Checklist: ¿lo has practicado?
- ¿Has ejecutado los tres programas y comprobado las salidas?
- ¿Sabrías calcular en qué byte están los puntos de la ranura 3?
- ¿Puedes explicar por qué la copia con
write(out, buf, 32)mediría 96 bytes? - ¿Sabes qué devuelve
readcuando lees una ranura que no existe? - ¿Has intentado los «Pruébalo tú» sin mirar la teoría?
Si has respondido que sí a casi todo, estás listo para los ejercicios de llamadas al sistema, donde ya no te doy el código hecho. ¡Nos vemos allí!
Siguiente paso: ejerciciosLlamadas al sistema en C — ejercicios con contadores de bytes, O_APPEND, un ranking de partidas y tres fallos de lseek y read (y la partida que se negó a ir al principio)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
