Llamadas al sistema en C — teoría con open, read, write, lseek y una partida guardada encima de otra (y los puntos que no eran míos)
El verano pasado le dejé a mi amigo Max probar un jueguecito que había hecho en C para practicar: guardaba la partida en un fichero, partida.sav, con una línea por jugador. Max llegó al nivel 12 con 2750 puntos y guardó. Después jugué yo, llegué al nivel 3 y guardé encima. Cuando cargué la partida, el juego me saludó así:
Bytes leídos: 20 Partida cargada: Lia nivel 03 2750
Mi nombre, mi nivel… y los 2750 puntos de Max. No había ningún error de compilación ni ningún aviso. Este es el programa, reducido a lo esencial:
#include <stdio.h>
#include <fcntl.h> /* open y las constantes O_... */
#include <unistd.h> /* read, write, lseek, close */
#include <string.h> /* strlen */
int main(void) {
/* 1. La partida antigua de Max: una ranura de 20 bytes */
int fd = open("partida.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, "Max nivel 12 2750\n", 20);
close(fd);
/* 2. Guardo MI partida encima... pero sin O_TRUNC */
const char *mia = "Lia nivel 03"; /* solo 14 bytes */
fd = open("partida.sav", O_WRONLY | O_CREAT, 0644);
write(fd, mia, strlen(mia)); /* pisa los 14 primeros */
close(fd);
/* 3. Cargo la partida */
char buf[64];
fd = open("partida.sav", O_RDONLY);
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
buf[n] = '\0'; /* read no pone el '\0' */
printf("Bytes leídos: %zd\n", n);
printf("Partida cargada: %s", buf);
return 0;
}
¿Ves el fallo? En el segundo open falta O_TRUNC. Sin esa opción, abrir un fichero para escribir no lo vacía: el puntero empieza en el byte 0 y write va pisando byte a byte. Mi partida ocupaba 14 bytes ("Lia nivel 03") y la de Max 20, así que los 6 últimos (" 2750\n") sobrevivieron. Las cuentas cuadran: 20 − 14 = 6 bytes de Max, y read leyó los 20 del fichero entero.
Hoy vas a entender por qué pasa eso y a manejar ficheros como lo hace el propio sistema operativo: con llamadas al sistema. En el artículo anterior, sobre procesos, viste que fork() crea un proceso pidiéndoselo al kernel. Con los ficheros es igual: open, read, write, lseek y close son peticiones directas al kernel, sin la capa de fopen y fprintf. Al terminar tendrás todo listo para la práctica, donde guardaremos, copiaremos y editaremos un fichero de partidas de verdad.
Tabla de Contenidos
Funciones de biblioteca frente a llamadas al sistema
En C tienes dos formas de trabajar con ficheros:
| Funciones de biblioteca | Llamadas al sistema | |
|---|---|---|
| Ejemplos | fopen, fprintf, fgets, fclose | open, read, write, lseek, close |
| Cabeceras | <stdio.h> | <fcntl.h>, <unistd.h> (y <sys/stat.h> para los permisos) |
| Qué devuelven al abrir | Un puntero FILE * | Un número entero: el descriptor |
| Buffer | Sí, la biblioteca acumula y escribe a bloques | No: cada llamada va directa al kernel |
| Trabajan con | Texto con formato (%d, %s) | Bytes en bruto |
Las funciones de biblioteca están construidas encima de las llamadas al sistema: fprintf acaba llamando a write. ¿Por qué aprender lo de abajo si lo de arriba es más cómodo? Porque es el acceso más directo que tiene un programa, porque funciona igual con ficheros, con la terminal y con cualquier cosa que envíe o reciba bytes (dispositivos, tuberías, conexiones de red) y porque es lo que te preguntan en IC2.
Las llamadas al sistema que vamos a usar no son estándar de C, sino de POSIX (Linux, macOS…). Compilan sin problema con gcc -Wall -std=c11 en Linux; en Windows necesitas WSL o una máquina virtual.
Las cabeceras de este artículo son siempre las mismas: <fcntl.h> trae open y las constantes O_...; <unistd.h> trae read, write, lseek y close.
Descriptores de fichero: 0, 1, 2… y el siguiente libre
Cuando un proceso abre un fichero, el kernel no le da el fichero: le da un número entero, el descriptor. A partir de ahí, todo se hace con ese número: read(3, …) significa «lee del fichero que tengo abierto como 3».
Antes de que empiece main, todo proceso tiene ya tres descriptores abiertos, por convención:
| Descriptor | Nombre | Normalmente es |
|---|---|---|
| 0 | entrada estándar (stdin) | el teclado |
| 1 | salida estándar (stdout) | la terminal |
| 2 | error estándar (stderr) | la terminal |
Por eso el primer open de un programa devuelve casi siempre 3: el kernel da el descriptor libre más bajo, y 0, 1 y 2 ya están ocupados. Míralo:
#include <stdio.h>
#include <fcntl.h> /* open y las constantes O_... */
#include <unistd.h> /* read, write, lseek, close */
#include <string.h> /* strlen */
int main(void) {
/* 0, 1 y 2 ya están abiertos antes de que empiece main */
const char *msg = "Escrito con write(1, ...)\n";
write(1, msg, strlen(msg)); /* 1 = salida estándar */
const char *err = "Escrito con write(2, ...)\n";
write(2, err, strlen(err)); /* 2 = error estándar */
/* Cada open devuelve el descriptor libre más bajo */
int a = open("ranura1.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int b = open("ranura2.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int c = open("ranura3.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
printf("ranura1 -> fd %d, ranura2 -> fd %d, ranura3 -> fd %d\n", a, b, c);
close(b); /* el 4 queda libre */
int d = open("ranura4.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
printf("Tras cerrar el fd %d, ranura4 -> fd %d\n", b, d);
/* Un fichero que no existe: open devuelve -1 */
int e = open("ranura9.sav", O_RDONLY);
printf("open de ranura9.sav devuelve %d\n", e);
if (e == -1)
perror("Error al abrir ranura9.sav"); /* explica el motivo */
close(a);
close(c);
close(d);
return 0;
}
La salida de la única ejecución:
Escrito con write(1, ...) Escrito con write(2, ...) ranura1 -> fd 3, ranura2 -> fd 4, ranura3 -> fd 5 Tras cerrar el fd 4, ranura4 -> fd 4 open de ranura9.sav devuelve -1 Error al abrir ranura9.sav: No such file or directory
Vamos por partes:
- Las dos primeras líneas no usan
printf: sonwrite(1, …)ywrite(2, …). Escribir en el descriptor 1 es escribir en la terminal, sin abrir nada. El 2 también acaba en la terminal, pero es otro canal (si rediriges la salida con> salida.txt, el 2 seguiría saliendo por pantalla). ranura1,ranura2yranura3reciben 3, 4 y 5: los siguientes libres, en orden.- Al cerrar el 4, ese hueco queda libre, y el siguiente
openlo reutiliza:ranura4.savrecibe un 4, no un 6. Es la regla del «más bajo libre». ranura9.savno existe, así queopendevuelve −1. El −1 es la forma que tienen todas estas llamadas de decir «algo ha fallado», yperrortraduce el motivo a texto:No such file or directory.
No comprobar el −1. Si open falla y sigues adelante, haces read(-1, …), que también falla, y el programa sigue con datos que nunca se leyeron. Después de cada open, un if (fd == -1) { perror(...); return 1; }.
El texto que le pasas a perror va delante de los dos puntos; lo de detrás lo pone el sistema según el error. En la terminal del laboratorio puede salir en español (No existe el fichero o el directorio) si el sistema está en español; aquí lo verás en inglés.
Las tres tablas del kernel
Un descriptor es solo un índice. ¿Dónde está entonces la posición de lectura, el modo de apertura, el tamaño del fichero? En tres estructuras que mantiene el kernel:
- Tabla de descriptores (una por proceso): la posición 3 apunta a una entrada de la tabla siguiente. Es lo único que «es» tuyo.
- Tabla de ficheros abiertos (del sistema): una entrada por cada
open. Guarda el puntero de lectura/escritura (en qué byte vas) y el modo (O_RDONLY,O_WRONLY…). - Tabla de i-nodos: una entrada por fichero del disco, con su tamaño, sus permisos, su dueño y dónde están sus bloques.
Fíjate en el dibujo en dos detalles que vamos a comprobar con código al final del artículo: si el mismo proceso hace dos open del mismo fichero, tiene dos entradas (A y B) con dos punteros distintos; si hace un open y después fork, el hijo copia la tabla de descriptores, pero su fd 3 apunta a la misma entrada A, así que padre e hijo comparten el puntero.
Cuando no entiendas qué hace un programa con ficheros, pregúntate «¿cuántas entradas de ficheros abiertos hay y dónde está el puntero de cada una?». Casi todos los misterios se resuelven ahí.
open: abrir (o crear) un fichero
int open(const char *ruta, int flags); int open(const char *ruta, int flags, mode_t modo); /* obligatorio si hay O_CREAT */
flags dice cómo quieres abrirlo. Es obligatorio uno de los tres primeros, y se le pueden sumar opciones con el operador |:
| Flag | Significado |
|---|---|
O_RDONLY | Solo lectura |
O_WRONLY | Solo escritura |
O_RDWR | Lectura y escritura |
O_CREAT | Crea el fichero si no existe (necesita el tercer parámetro, los permisos) |
O_TRUNC | Si ya existía, lo vacía (tamaño 0) al abrir |
O_APPEND | Cada write va al final del fichero, sin pisar nada |
O_EXCL | Junto con O_CREAT: falla si el fichero ya existe |
¿Por qué |? Cada flag es un número con un solo bit encendido, y el OR los junta en un único int: O_WRONLY | O_CREAT | O_TRUNC es «escritura, créalo si hace falta y vacíalo». Ahora mira cómo se comportan los más importantes:
#include <stdio.h>
#include <fcntl.h> /* open y las constantes O_... */
#include <unistd.h> /* read, write, lseek, close */
#include <sys/stat.h> /* stat y los bits de permisos */
/* Lee el fichero entero y lo enseña con su tamaño */
static void mostrar(const char *nombre) {
char buf[128];
int fd = open(nombre, O_RDONLY);
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
buf[n] = '\0';
printf(" (%zd bytes)\n%s", n, buf);
}
int main(void) {
/* O_TRUNC: si ya existía, se vacía antes de escribir */
int fd = open("partidas.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, "Lia nivel 03 0450\n", 20);
close(fd);
printf("1) Tras O_TRUNC:");
mostrar("partidas.sav");
/* O_APPEND: cada write va al final, sin pisar nada */
fd = open("partidas.sav", O_WRONLY | O_APPEND);
write(fd, "Max nivel 12 2750\n", 20);
close(fd);
printf("2) Tras O_APPEND:");
mostrar("partidas.sav");
/* O_CREAT | O_EXCL: falla si el fichero ya existe */
fd = open("partidas.sav", O_WRONLY | O_CREAT | O_EXCL, 0644);
printf("3) O_CREAT | O_EXCL devuelve %d\n", fd);
if (fd == -1)
perror(" partidas.sav");
/* creat(nombre, modo) = open(nombre, O_WRONLY | O_CREAT | O_TRUNC, modo) */
fd = creat("copia.sav", 0644);
printf("4) creat devuelve el fd %d\n", fd);
close(fd);
/* Los permisos que pedimos con 0644 */
struct stat st;
stat("partidas.sav", &st);
printf("5) Permisos de partidas.sav: %o\n", st.st_mode & 0777);
return 0;
}
Salida:
1) Tras O_TRUNC: (20 bytes) Lia nivel 03 0450 2) Tras O_APPEND: (40 bytes) Lia nivel 03 0450 Max nivel 12 2750 3) O_CREAT | O_EXCL devuelve -1 partidas.sav: File exists 4) creat devuelve el fd 3 5) Permisos de partidas.sav: 644
Entre líneas:
- 1) Con
O_TRUNCel fichero empieza vacío y queda con los 20 bytes de Lia. Es la combinación de «guardar partida»: la que me faltaba en la historia del principio. - 2) Con
O_APPEND, elwritede Max se coloca detrás: 20 + 20 = 40 bytes. No hizo falta mover nada; el kernel lleva el puntero al final antes de cada escritura. - 3)
O_CREAT | O_EXCLes «créalo, pero solo si es nuevo». Comopartidas.savya existía,opendevuelve −1 yperrordiceFile exists. Sirve para no machacar una partida guardada sin querer. - 4)
creat(ruta, modo)es una llamada antigua que equivale exactamente aopen(ruta, O_WRONLY | O_CREAT | O_TRUNC, modo). Devuelve 3 porque los anteriores ya se cerraron dentro demostrar. - 5) Los permisos salen
644, los que pedimos con0644.
El tercer parámetro: los permisos en octal
El 0644 empieza por 0 porque está en octal (base 8): cada dígito son 3 bits, uno por permiso (lectura = 4, escritura = 2, ejecución = 1).
| Dígito | Para quién | Cuenta | Permisos |
|---|---|---|---|
| 6 | dueño | 4 + 2 | rw- |
| 4 | grupo | 4 | r-- |
| 4 | otros | 4 | r-- |
Así que 0644 es rw-r--r--, lo mismo que verías con ls -l. Ojo: sin el 0 delante, 644 sería un número decimal (1204 en octal) y los permisos saldrían rarísimos.
Usar O_CREAT sin el tercer parámetro. El compilador no se queja (con -Wall tampoco), pero open coge como permisos lo que haya en ese momento en la memoria: a veces sale un fichero que ni tú puedes leer. Con O_CREAT, siempre el modo.
El sistema aplica además la umask del usuario, que quita permisos. La habitual es 022 (quita la escritura a grupo y otros), por eso 0644 llega intacto. Si pidieras 0666, con esa umask saldría 644 igualmente.
read: leer bytes (y lo que read NO hace)
ssize_t read(int fd, void *buf, size_t cuantos);
read intenta leer cuantos bytes del descriptor fd y los copia en buf, a partir de donde esté el puntero. Devuelve:
- el número de bytes que ha leído de verdad (puede ser menos de los que pediste);
- 0 si el puntero ya estaba al final del fichero;
- −1 si hay un error.
Y después de leer, el puntero avanza esos bytes. Por eso se lee en bucle hasta que devuelve 0:
#include <stdio.h>
#include <fcntl.h> /* open y las constantes O_... */
#include <unistd.h> /* read, write, lseek, close */
int main(void) {
int fd = open("partida.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, "Lia nivel 03 0450\n", 20);
close(fd);
fd = open("partida.sav", O_RDONLY);
if (fd == -1) { /* comprueba siempre el -1 */
perror("partida.sav");
return 1;
}
char buf[9]; /* 8 bytes + sitio para el '\0' */
ssize_t leidos;
int vuelta = 1;
/* read devuelve los bytes leídos; 0 = fin de fichero */
while ((leidos = read(fd, buf, 8)) > 0) {
buf[leidos] = '\0'; /* lo pongo yo: read no lo hace */
printf("vuelta %d: read devuelve %zd -> \"", vuelta, leidos);
for (char *p = buf; *p != '\0'; p++) /* recorro hasta el '\0' */
if (*p == '\n')
printf("\\n"); /* el salto también es un byte */
else
putchar(*p);
printf("\"\n");
vuelta++;
}
printf("vuelta %d: read devuelve %zd (fin de fichero)\n", vuelta, leidos);
close(fd);
return 0;
}
La salida, vuelta a vuelta:
vuelta 1: read devuelve 8 -> "Lia ni" vuelta 2: read devuelve 8 -> "vel 03 0" vuelta 3: read devuelve 4 -> "450\n" vuelta 4: read devuelve 0 (fin de fichero)
| Vuelta | Puntero antes | read devuelve | Bytes leídos | Puntero después |
|---|---|---|---|---|
| 1 | 0 | 8 | Lia ni | 8 |
| 2 | 8 | 8 | vel 03 0 | 16 |
| 3 | 16 | 4 | 450\n | 20 |
| 4 | 20 | 0 | (nada: fin de fichero) | 20 |
Comprobación: 8 + 8 + 4 = 20 bytes, el tamaño de la ranura. En la vuelta 3 pedimos 8 pero solo quedaban 4: read no se inventa los otros 4, simplemente devuelve menos. Y el \n cuenta como un byte más.
El ‘\0’ lo pones tú
Fíjate en la línea buf[leidos] = '\0';. read copia bytes; no sabe nada de cadenas de C, así que no añade el carácter nulo. Si después usas buf como cadena (%s, strlen, o un bucle hasta el '\0' como aquí), tienes que cerrarla tú. Dos detalles que hacen que funcione:
- El buffer mide 9, uno más que los 8 que pedimos, para que siempre quepa el
'\0'. - Se pone en
buf[leidos], no enbuf[8]: en la vuelta 3 solo se leyeron 4 bytes, y lo que hay en las posiciones 4-7 son restos de la vuelta 2.
printf("%s", buf) justo después de read, sin el '\0'. A veces funciona (si de casualidad había un cero en memoria) y a veces imprime basura o restos de la lectura anterior. Lo verás en directo en los ejercicios.
write y close
ssize_t write(int fd, const void *buf, size_t cuantos); int close(int fd);
write escribe cuantos bytes de buf en la posición del puntero (o al final, con O_APPEND), avanza el puntero y devuelve cuántos ha escrito, o −1. Si el puntero está en mitad del fichero, sobrescribe: no inserta, no desplaza lo que hay detrás. Es exactamente lo que pasó con mi partida.
Dos reglas para el tercer argumento:
- Para escribir una cadena,
strlen(texto), nosizeof(texto): sitextoes unchar *,sizeofvale 8 (lo que mide un puntero), no la longitud. - Para escribir lo que acabas de leer, el valor que devolvió
read(leidos), no el tamaño del buffer. En la práctica copiaremos un fichero así.
close(fd) libera el descriptor (el número vuelve a estar libre, como viste con el 4) y la entrada de ficheros abiertos si nadie más la usa. Cuando un proceso termina, el kernel cierra todos sus descriptores, pero cerrar a mano es lo correcto: un proceso tiene un número máximo de descriptores abiertos y un programa largo que abre sin cerrar acaba quedándose sin ellos.
lseek: mover el puntero
Todo fichero abierto tiene su puntero, que marca dónde será el siguiente read o write. lseek lo mueve sin leer ni escribir nada:
off_t lseek(int fd, off_t desplazamiento, int desde);
El tercer argumento dice desde dónde se cuenta el desplazamiento (que puede ser negativo):
desde | Cuenta desde | Ejemplo con un fichero de 80 bytes |
|---|---|---|
SEEK_SET | el principio (byte 0) | lseek(fd, 40, SEEK_SET) → 40 |
SEEK_CUR | la posición actual | si estás en 43, lseek(fd, 9, SEEK_CUR) → 52 |
SEEK_END | el final (el tamaño) | lseek(fd, -2, SEEK_END) → 78 |
Devuelve la nueva posición (o −1). De ahí salen dos trucos muy usados: lseek(fd, 0, SEEK_CUR) te dice dónde estás sin moverte, y lseek(fd, 0, SEEK_END) te dice cuánto mide el fichero.
Vamos a probarlo con un fichero de cuatro partidas. Cada una ocupa exactamente 20 bytes (lo llamaremos ranura), así que la ranura 2 empieza en el byte 2 · 20 = 40:
#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"
int main(void) {
int fd = open("partidas.sav", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("partidas.sav");
return 1;
}
write(fd, PARTIDAS, 4 * TAM_RANURA); /* 80 bytes */
printf("Tras escribir, el puntero está en %ld\n", (long) lseek(fd, 0, SEEK_CUR));
/* SEEK_SET: desde el principio. Ranura 2 = byte 2 * 20 = 40 */
char nombre[4];
off_t pos = lseek(fd, 2 * TAM_RANURA, SEEK_SET);
read(fd, nombre, 3);
nombre[3] = '\0';
printf("SEEK_SET 40 -> %ld, leo \"%s\"\n", (long) pos, nombre);
printf("Tras leer 3 bytes, SEEK_CUR 0 -> %ld\n", (long) lseek(fd, 0, SEEK_CUR));
/* SEEK_CUR: desde donde estoy. 43 + 9 = 52, donde empieza el nivel */
char nivel[3];
pos = lseek(fd, 9, SEEK_CUR);
read(fd, nivel, 2);
nivel[2] = '\0';
printf("SEEK_CUR +9 -> %ld, nivel de Ane: %s\n", (long) pos, nivel);
/* SEEK_END: desde el final. El último byte (79) es el '\n' */
char c;
pos = lseek(fd, -2, SEEK_END); /* 80 - 2 = 78 */
read(fd, &c, 1);
printf("SEEK_END -2 -> %ld, último dígito: %c\n", (long) pos, c);
printf("SEEK_END 0 -> %ld = tamaño del fichero\n", (long) lseek(fd, 0, SEEK_END));
/* Más allá del final: el hueco se rellena con ceros */
pos = lseek(fd, 5, SEEK_END); /* 80 + 5 = 85 */
write(fd, "!", 1);
printf("SEEK_END +5 -> %ld; tras escribir 1 byte, tamaño %ld\n",
(long) pos, (long) lseek(fd, 0, SEEK_END));
unsigned char resto[6];
lseek(fd, 80, SEEK_SET);
read(fd, resto, 6);
printf("Bytes 80 a 85:");
for (int i = 0; i < 6; i++)
printf(" %d", resto[i]); /* 33 es el código de '!' */
printf("\n");
close(fd);
return 0;
}
Salida:
Tras escribir, el puntero está en 80 SEEK_SET 40 -> 40, leo "Ane" Tras leer 3 bytes, SEEK_CUR 0 -> 43 SEEK_CUR +9 -> 52, nivel de Ane: 05 SEEK_END -2 -> 78, último dígito: 0 SEEK_END 0 -> 80 = tamaño del fichero SEEK_END +5 -> 85; tras escribir 1 byte, tamaño 86 Bytes 80 a 85: 0 0 0 0 0 33
Sigue las flechas del dibujo con la salida delante:
- Tras escribir los 80 bytes, el puntero está al final: 80.
SEEK_SET 40salta a la ranura 2 yreadde 3 bytes leeAne. El puntero avanza: 40 + 3 = 43.SEEK_CUR +9cuenta desde 43: 43 + 9 = 52, justo donde está el nivel de Ane (05).SEEK_END -2cuenta desde el tamaño: 80 − 2 = 78. ¿Por qué −2 y no −1? Porque el último byte (el 79) es el\n. El último dígito de Teo (1200) está uno antes.SEEK_END 0devuelve 80: el tamaño.SEEK_END +5lleva el puntero más allá del final, a 85. Eso no da error. Al escribir el!allí, el fichero crece hasta 86 bytes y los bytes 80 a 84, que nunca escribimos, valen 0 (el 33 es el código ASCII de!). Es el «hueco de ceros».
lseek no lee ni escribe: solo cambia un número en la entrada de ficheros abiertos. Por eso puedes llevar el puntero fuera del fichero sin error; el problema (o la gracia) llega al escribir.
lseek devuelve off_t, un entero que puede ser de 64 bits. Para imprimirlo con printf se convierte a long y se usa %ld, como en el código.
Descriptores heredados con fork
Lo prometido en el dibujo de las tres tablas. Ahora que sabes que el puntero vive en la entrada de ficheros abiertos, ¿qué crees que pasa en estos dos casos?
- A) Un proceso abre el mismo fichero dos veces, escribe a Lia con el primer descriptor y a Max con el segundo.
- B) Un proceso abre el fichero una vez, hace
fork, el hijo escribe a Lia y, cuando termina, el padre escribe a Max.
#include <stdio.h>
#include <fcntl.h> /* open y las constantes O_... */
#include <unistd.h> /* read, write, lseek, close */
#include <sys/types.h> /* pid_t */
#include <sys/wait.h> /* waitpid */
/* Lee el fichero entero y lo enseña con su tamaño */
static void mostrar(const char *nombre) {
char buf[128];
int fd = open(nombre, O_RDONLY);
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
buf[n] = '\0';
printf(" (%zd bytes)\n%s", n, buf);
}
int main(void) {
/* A) Dos open del mismo fichero: dos punteros independientes */
int a = open("dos_open.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int b = open("dos_open.sav", O_WRONLY);
write(a, "Lia nivel 03 0450\n", 20); /* el puntero de a pasa a 20 */
write(b, "Max nivel 12 2750\n", 20); /* el de b sigue en 0: pisa a Lia */
close(a);
close(b);
printf("A) Dos open:");
mostrar("dos_open.sav");
/* B) Un solo open y después fork: padre e hijo comparten el puntero */
int fd = open("fork.sav", O_WRONLY | O_CREAT | O_TRUNC, 0644);
pid_t pid = fork();
if (pid == 0) { /* hijo: hereda el fd 3 */
write(fd, "Lia nivel 03 0450\n", 20);
close(fd); /* cierra SU copia del fd */
return 0;
}
waitpid(pid, NULL, 0); /* el padre espera al hijo */
printf("B) El padre ve el puntero en %ld\n", (long) lseek(fd, 0, SEEK_CUR));
write(fd, "Max nivel 12 2750\n", 20); /* escribe detrás, no encima */
close(fd);
printf("B) open + fork:");
mostrar("fork.sav");
return 0;
}
Salida:
A) Dos open: (20 bytes) Max nivel 12 2750 B) El padre ve el puntero en 20 B) open + fork: (40 bytes) Lia nivel 03 0450 Max nivel 12 2750
- En A, cada
opencreó su propia entrada con su propio puntero. El deaavanzó a 20, pero el debseguía en 0, así que Max pisó a Lia: 20 bytes y Lia desaparecida. Es otra forma de perder una partida. - En B, el hijo heredó el descriptor 3, que apunta a la misma entrada que el del padre. Cuando el hijo escribió sus 20 bytes, el puntero compartido pasó a 20, y el padre lo ve así (
El padre ve el puntero en 20). Suwriteva detrás: 40 bytes, nada pisado.
El waitpid del artículo de procesos es el que garantiza el orden: sin él, padre e hijo podrían escribir a la vez. Y cada uno hace su close: el hijo cierra su copia del descriptor, y la entrada sigue viva hasta que el padre cierra la suya.
Errores típicos
Abrir para escribir sin O_TRUNC cuando quieres reemplazar el contenido. Si lo nuevo es más corto, quedan restos de lo viejo (la partida de Max).
O_CREAT sin los permisos, o con 644 en lugar de 0644.
Usar el buffer como cadena sin buf[leidos] = '\0', o reservar justo los bytes que pides a read y no dejar sitio para el '\0'.
Escribir sizeof buf bytes en lugar de los leidos: copias basura del final del buffer.
lseek(fd, -1, SEEK_END) para leer el último carácter de un fichero de texto: el último byte suele ser el \n.
Abrir con O_WRONLY y después intentar leer (o al revés). read devuelve −1 con Bad file descriptor; si lo vas a hacer todo con el mismo descriptor, O_RDWR.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta código línea a línea y dibuja las variables y la memoria; elige C en el desplegable. No está pensado para trabajar con ficheros reales del disco, así que aquí simulamos uno: un struct con los bytes, el tamaño y el puntero. mi_lseek y mi_read hacen lo mismo que las de verdad, y así ves moverse el puntero en cada paso:
#include <stdio.h>
#define SET 0
#define CUR 1
#define END 2
struct Fichero { char datos[16]; int tam; int pos; }; /* pos = el puntero */
int mi_lseek(struct Fichero *f, int off, int whence) {
int base = whence == SET ? 0 : whence == CUR ? f->pos : f->tam;
f->pos = base + off; /* solo se mueve el puntero */
return f->pos;
}
int mi_read(struct Fichero *f, char *buf, int n) {
int i = 0;
while (i < n && f->pos < f->tam)
buf[i++] = f->datos[f->pos++]; /* copia un byte y avanza */
return i; /* 0 = fin de fichero */
}
int main(void) {
struct Fichero f = {"Lia 03\nMax 12\n", 14, 0};
char buf[8] = {0};
int n = mi_read(&f, buf, 3); /* "Lia": pos pasa a 3 */
mi_lseek(&f, 4, CUR); /* salta " 03\n": pos 7 */
n = mi_read(&f, buf, 3); /* "Max" */
int p = mi_lseek(&f, -3, END); /* 14 - 3 = 11 */
n = mi_read(&f, buf, 8); /* pido 8, solo quedan 3 */
printf("%d %d %c%c\n", p, n, buf[0], buf[1]);
return 0;
}
Al ejecutarlo, la última línea que imprime es:
11 3 12
Qué observar:
- En el primer
mi_read, mira cómof.pospasa de 0 a 3 mientrasbufse llena conL,i,a: leer mueve el puntero. - En
mi_lseek(&f, 4, CUR), solo cambiaf.pos(de 3 a 7).datosybufno se tocan: eso es todo lo que hacelseek. - En
mi_lseek(&f, -3, END), la base esf->tam(14), y 14 − 3 = 11, el byte del1de12. - En el último
mi_readse piden 8 bytes, pero elwhilese para cuandoposllega atam: devuelve 3, como elreadde verdad en la última vuelta.
Resumen
| Llamada | Qué hace | Devuelve | Ojo con |
|---|---|---|---|
open(ruta, flags[, modo]) | Abre o crea y da un descriptor | El descriptor libre más bajo, o −1 | O_TRUNC, y 0644 con O_CREAT |
read(fd, buf, n) | Lee hasta n bytes y avanza | Bytes leídos, 0 al final, −1 | No pone el '\0' |
write(fd, buf, n) | Escribe n bytes y avanza | Bytes escritos o −1 | Sobrescribe; strlen o leidos |
lseek(fd, desp, desde) | Mueve el puntero | La nueva posición o −1 | SEEK_END apunta tras el último byte |
close(fd) | Libera el descriptor | 0 o −1 | Cierra siempre lo que abres |
Tres ideas para llevarte:
- Un descriptor es solo un número (0, 1 y 2 vienen de serie); el puntero y el modo viven en la entrada de ficheros abiertos, y dos
openson dos punteros, pero unforkcomparte el mismo. readywritetrabajan con bytes y te dicen cuántos han movido: usa ese número, y pon tú el'\0'si vas a tratar lo leído como texto.- Con
lseeky registros de tamaño fijo puedes ir directo a cualquier dato: posición = tamaño del registro · número + desplazamiento dentro del registro.
En la práctica construimos el fichero de partidas completo: lo guardamos ranura a ranura, hacemos una copia de seguridad con un bucle de read y write y editamos los puntos de una partida sin tocar el resto. Después, en Linux, toca otra pieza que ya has visto asomar con el 0644: los permisos y el comando chmod.
¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Un comentario