1. La interfaz de open
En el tema 4 separamos el número de descriptor, la descripción de archivo abierta y el objeto del sistema de archivos. open conecta esos niveles: resuelve una ruta, comprueba las condiciones de apertura, crea una descripción de archivo abierta y devuelve un descriptor que la referencia. Algunas banderas también piden modificar o crear el archivo durante esa operación.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
/* Forma habitual de la declaracion en la biblioteca: */
int open(const char *ruta, int banderas, ...);
/* Abrir un objeto que ya debe existir: */
int entrada = open("datos.txt", O_RDONLY);
/* Crear si hace falta y abrir para escritura: */
int salida = open("salida.txt", O_WRONLY | O_CREAT, 0666);
La ruta es una cadena terminada en byte nulo. Si es relativa, se interpreta desde el directorio de trabajo del proceso; si es absoluta, desde su raíz visible. Las banderas expresan el acceso y las condiciones adicionales. El argumento mode, de tipo mode_t, se proporciona cuando se solicita creación con O_CREAT; también es necesario con O_TMPFILE, que queda fuera de este laboratorio.
Si tiene éxito, open devuelve el menor descriptor libre, un entero no negativo. Si falla, devuelve -1 y establece errno. Una apertura correcta puede devolver 0: el criterio de error es fd == -1, no fd <= 0.
int fd = open("datos.txt", O_RDONLY | O_CLOEXEC);
if (fd == -1) {
perror("open datos.txt");
/* No leer, escribir ni cerrar fd como si la apertura hubiera funcionado. */
}
errno solo se consulta cuando el resultado indica un error. Una operación correcta no tiene la obligación de ponerlo a cero. Si vas a realizar otras llamadas antes de describir el fallo, conserva primero su valor en una variable local. Los ejemplos completos mostrarán ese cuidado.
La apertura no es una transacción de todo el programa
Si open crea un archivo vacío y después tu programa detecta un argumento inválido, esa creación ya pudo suceder. Si abre con truncado y después no logra escribir, el contenido anterior ya se perdió. El éxito o fracaso de una llamada posterior no deshace automáticamente los efectos de una apertura anterior.
Por eso la validación que no necesita abrir el recurso debe hacerse antes de open. Para reemplazar archivos sin exponer resultados incompletos existen patrones con un archivo temporal y rename, que estudiaremos más adelante. En este tema vamos a observar cuándo ocurre cada efecto.
2. Elegir un modo de acceso
De las banderas de acceso se elige una: O_RDONLY, O_WRONLY u O_RDWR. No son tres capacidades que debas sumar para construir cualquier combinación. En Linux, O_RDONLY tiene valor cero, lo que vuelve especialmente engañosas algunas expresiones.
| Modo | Permite mediante esa apertura | No pide por sí solo |
|---|---|---|
O_RDONLY | Leer. | Crear o modificar el contenido. |
O_WRONLY | Escribir. | Truncar, anexar o crear si falta. |
O_RDWR | Leer y escribir. | Truncar ni mantener posiciones separadas de lectura y escritura. |
Una apertura O_RDWR tiene una posición actual compartida por sus operaciones ordinarias. No hay un cursor de lectura y otro de escritura dentro de esa misma descripción de archivo abierta. Las operaciones pread y pwrite permitirán indicar una posición explícita sin mover ese cursor en el tema 7.
El operador | y la máscara O_ACCMODE
int fd = open("informe.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
/* Para consultar el modo de una apertura ya obtenida: */
int estado = fcntl(fd, F_GETFL);
if (estado == -1) {
/* Manejar error antes de interpretar los bits. */
} else if ((estado & O_ACCMODE) == O_RDONLY) {
/* Apertura de solo lectura. */
}
| es OR bit a bit. || es OR lógico y produce un valor de verdad, no una combinación de banderas. Tampoco escribas O_RDONLY | O_WRONLY para pedir lectura y escritura: al ser cero O_RDONLY, en Linux obtendrías O_WRONLY. Para ambos accesos existe O_RDWR.
La comprobación estado & O_RDONLY nunca detecta el modo de lectura cuando esa constante vale cero. Se extrae el campo con O_ACCMODE y se compara con la constante elegida. No hace falta memorizar números: utiliza los nombres definidos por la plataforma.
Acceso y permisos del objeto son preguntas diferentes
Los permisos del archivo determinan, junto con las credenciales y otras reglas, si se permite obtener el acceso solicitado. El modo de apertura limita lo que se puede hacer mediante esa referencia. Si un archivo permite leer y escribir, pero lo abriste O_RDONLY, un write sobre ese descriptor no se vuelve válido por los permisos del archivo: normalmente falla con EBADF.
Abrir con más acceso del necesario también puede impedir una operación que habría funcionado con menos. Si solo puedes leer un archivo, O_RDONLY puede tener éxito y O_RDWR fallar. Solicita la capacidad que de verdad necesita el programa.
3. Las banderas y sus combinaciones
Además del modo de acceso, se pueden pedir condiciones y comportamientos. La tabla siguiente reúne las que usaremos y distingue el momento en que importan.
| Bandera | Efecto principal | Detalle que importa |
|---|---|---|
O_CREAT | Crear un archivo regular si no existe. | Si ya existe, no lo vacía ni exige que sea nuevo. |
O_EXCL con O_CREAT | Exigir creación nueva. | Si la entrada final ya existe, se obtiene EEXIST. |
O_TRUNC | Reducir a cero un archivo regular existente al abrir para escritura. | El efecto ocurre durante open, sin esperar al primer write. |
O_APPEND | Situar cada escritura al final de archivo. | No significa que todas las lecturas ocurran al final. |
O_CLOEXEC | Crear el descriptor con FD_CLOEXEC activado. | Se cerrará cuando exec sustituya correctamente el programa. |
O_CREAT, O_EXCL y O_TRUNC expresan decisiones de la apertura; O_APPEND permanece como estado de la descripción de archivo abierta. O_CLOEXEC solicita una propiedad de la entrada de descriptor. Recordar dónde queda cada dato evita esperar que el truncado se repita en cada write o que una marca de cierre sea una propiedad del inodo.
Combinaciones que expresan intenciones distintas
/* Debe existir; conservar longitud y empezar a escribir desde posicion 0. */
open("datos.txt", O_WRONLY);
/* Crear si falta; si existe, conservarlo. */
open("datos.txt", O_WRONLY | O_CREAT, 0666);
/* Crear o dejar vacio, como una redireccion > habitual del shell. */
open("datos.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
/* Crear si falta y anexar cada escritura, como >>. */
open("datos.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
Los fragmentos muestran intenciones y omiten guardar el descriptor y comprobar errores; no son programas completos para copiar. En los laboratorios lo haremos correctamente. Para todos los ejemplos de truncado utilizaremos O_WRONLY u O_RDWR: no debes basarte en O_RDONLY | O_TRUNC, cuyo comportamiento no ofrece una receta portable y puede truncar en Linux.
O_CLOEXEC desde el momento de abrir
Solicitar O_CLOEXEC en open evita dejar una ventana entre obtener el descriptor y marcarlo después con fcntl. En un programa con varios hilos, otro hilo podría ejecutar una creación y sustitución de proceso durante esa ventana y transmitir una referencia que no debía conservar el programa nuevo.
No sustituye la organización del cierre, y no provoca el cierre al hacer fork por sí solo: actúa ante un exec correcto. La usaremos en los programas completos para que sus archivos de trabajo no se transmitan accidentalmente a un programa ejecutado después.
4. Crear un archivo y calcular sus permisos
Cuando O_CREAT necesita crear el archivo, el tercer argumento indica los permisos solicitados. En C se escriben normalmente en octal, con un cero inicial: 0666, 0644 o 0600. El literal decimal 666 no representa los mismos bits que el octal 0666.
En el modelo de permisos tradicionales, sin una ACL por defecto en el directorio, los permisos iniciales se obtienen mediante mode & ~umask. La máscara quita bits; no los añade. El argumento mode no cambia los permisos de un archivo que ya existía.
| mode solicitado | umask | Resultado | Permisos |
|---|---|---|---|
0666 | 0022 | 0644 | Dueño lee y escribe; grupo y otros leen. |
0666 | 0002 | 0664 | Dueño y grupo leen y escriben; otros leen. |
0666 | 0077 | 0600 | Solo el dueño lee y escribe. |
0600 | 0022 | 0600 | La máscara no concede accesos ausentes. |
0644 | 0002 | 0644 | No se convierte en 0664. |
No calcules el resultado mediante una resta aritmética. La fórmula es una operación de bits. Por ejemplo, restar 0022 a 0600 no produce el conjunto de permisos deseado. Tampoco se añaden permisos de ejecución a los documentos creados con 0666, aunque umask sea 0000.
Quién necesita permiso para crear
Para crear una entrada nueva se necesitan los permisos adecuados en el directorio padre, normalmente escritura y búsqueda, además de poder atravesar los directorios anteriores. Para abrir un archivo existente intervienen los permisos y reglas de ese archivo. Poder escribir un documento no implica poder crear otros nombres en su carpeta, ni poder crear una entrada implica poder leer todos los archivos existentes.
El mode establece el acceso futuro al objeto nuevo; no es una contraseña para saltarse las comprobaciones de la ruta. Es posible que una creación produzca un archivo con modo 0000 y aun así la apertura que lo creó conserve el acceso solicitado: la concesión de esa referencia y los permisos que verán aperturas posteriores son aspectos distintos.
El tercer argumento es obligatorio aunque esperes que ya exista
Si pasas O_CREAT, proporciona siempre mode. No puedes omitirlo porque «en mi equipo ese archivo ya está». Si falta y la biblioteca debe utilizarlo, se lee un argumento que el programa no proporcionó y el comportamiento del código es incorrecto. Una compilación no detecta necesariamente todos estos errores en funciones de argumentos variables.
Las ACL por defecto del directorio pueden cambiar cómo se derivan los permisos iniciales. El simulador y los cálculos de este tema excluyen esas ACL; las estudiaremos en el bloque de protección. Los bits de propietario, grupo, setgid y las credenciales también tienen reglas que no se reducen a una sola máscara de nueve bits.
5. Truncar, sobrescribir y conservar el resto
Supongamos que el archivo contiene los diez bytes ABCDEFGHIJ. Abrir con O_WRONLY conserva su tamaño y coloca la posición inicial en 0. Si después escribes los dos bytes XY, se sustituyen los dos primeros y el archivo pasa a contener XYCDEFGHIJ. El sufijo no desaparece por haber terminado tu texto nuevo.
Antes: ABCDEFGHIJ longitud 10
O_WRONLY, luego write("XY"): XYCDEFGHIJ longitud 10
O_WRONLY | O_TRUNC al abrir: longitud 0
Luego write("XY"): XY longitud 2
O_TRUNC solicita reducir a cero el archivo regular al obtener una apertura de escritura. No se limita a cambiar el cursor y no espera a que llames a write. Si abres correctamente con esa bandera y cierras sin escribir, el archivo queda vacío.
Se modifica el objeto existente
Truncar un archivo existente no equivale a quitar su nombre y crear otro inodo. Los otros enlaces duros al mismo objeto ven el nuevo tamaño. Las referencias ya abiertas también llegan al objeto truncado; sus posiciones actuales no se rebobinan por ese cambio. Una apertura que estaba en posición 8 puede continuar en 8 mientras el archivo mide cero.
Si se vuelve a escribir desde una posición que ahora queda después del final, podrían aparecer huecos, como vimos en el tema 3. Aquí basta con distinguir la longitud del objeto de las posiciones mantenidas por cada apertura.
O_CREAT no incluye O_TRUNC
Es una confusión frecuente: «abrir para crear salida» no implica descartar una versión anterior. O_CREAT pide un archivo nuevo solo cuando falta; si ya existe, no se borra. Para obtener el comportamiento de reemplazar el contenido desde cero se combina con O_TRUNC, o se usa otro patrón de reemplazo adecuado al problema.
O_TRUNC no hace persistente una escritura posterior ni convierte toda la secuencia en una operación atómica de publicación. Entre el truncado y el final de los writes, otro lector puede observar un archivo vacío o parcialmente escrito. La consistencia de un reemplazo completo se tratará en los temas de rename y escrituras atómicas.
6. Añadir con O_APPEND
O_APPEND conserva el contenido existente y hace que, antes de cada escritura, se seleccione el final actual del archivo. En un archivo regular local que implementa esta semántica, la selección del final y la escritura se realizan como un paso atómico respecto a esa colocación. No es lo mismo que consultar una vez el tamaño y luego escribir en una posición que podría quedar desactualizada.
int fd = open("app.log",
O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC,
0644);
O_APPEND no exige por sí solo que el archivo exista o se cree: ese papel corresponde a O_CREAT. Tampoco cambia el tamaño mientras no haya escrituras, salvo que lo hayas combinado con otra bandera como O_TRUNC. La posición inicial de la nueva apertura puede ser 0; lo que caracteriza al anexado es dónde se coloca cada write, no una promesa de que lseek siempre mostrará el tamaño al terminar open.
Por qué lseek al final no sustituye a O_APPEND
Imagina dos procesos con aperturas independientes. Ambos consultan el final cuando mide 100 y ambos se posicionan allí. Si después escriben, pueden usar posiciones que se solapan. Con O_APPEND no se reserva el final al iniciar el programa: se selecciona de nuevo para cada escritura.
Si ejecutas lseek(fd, 0, SEEK_SET) sobre una apertura con O_APPEND, la siguiente escritura ordinaria seguirá anexándose. En una apertura O_RDWR | O_APPEND, las lecturas sí pueden usar la posición que estableciste. La bandera gobierna la colocación de las escrituras.
Lo que no garantiza el anexado
Una línea lógica escrita mediante varias llamadas no se convierte por ello en un registro indivisible. Otros escritores pueden intercalar sus llamadas entre las tuyas. Además, write puede aceptar menos bytes de los solicitados; completar el resto puede requerir otra operación. Las garantías de colocación no equivalen a una transacción de varias llamadas ni a persistencia ante un corte de energía.
Los sistemas de archivos de red pueden introducir limitaciones adicionales; en particular, la emulación de anexado en NFS no ofrece automáticamente las mismas garantías frente a escritores concurrentes. El manual de open(2) documenta las banderas y estas condiciones. Ajusta el diseño a las garantías del sistema concreto donde escribas.
7. Crear exclusivamente con O_EXCL
Si necesitas un archivo que no exista previamente, usa O_CREAT | O_EXCL. Si la entrada ya existe, open falla con EEXIST en vez de abrirla o truncarla. La comprobación y la creación forman parte de la misma operación de apertura en un sistema que ofrece esa semántica.
int fd = open("resultado.txt",
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC,
0600);
if (fd == -1) {
if (errno == EEXIST) {
/* El nombre ya estaba ocupado: no se abrio para sobrescribirlo. */
} else {
/* Otro error de ruta, permisos o recursos. */
}
}
No es equivalente a preguntar primero si existe y después ejecutar O_CREAT sin exclusividad. Otro proceso puede crear el nombre entre la comprobación y el uso. Veremos esa clase de carrera con más detalle en el tema 29. Aquí la idea es expresar la condición en la operación que debe cumplirla.
Entrada existente y enlaces simbólicos
Con O_CREAT | O_EXCL, un enlace simbólico en el componente final hace que la creación exclusiva falle, incluso si su destino no existe. Se exige que el nombre final esté libre, no que «seguir el enlace termine en un archivo ausente». Esto no impide por sí solo seguir enlaces en todos los componentes anteriores de la ruta.
Sin O_EXCL, open sigue normalmente el enlace final y puede crear su destino si se pidió O_CREAT. No extrapoles la protección del componente final a todo el recorrido. Las interfaces para resolver rutas con restricciones adicionales se estudiarán cuando profundicemos en seguridad y directorios.
Exclusividad no significa bloqueo del archivo
Después de una creación exclusiva correcta, otro proceso con permisos suficientes puede abrir el nombre existente mediante una apertura ordinaria. O_EXCL no establece un bloqueo permanente que prohíba lectores o escritores. Sirve para decidir quién crea ese nombre en esa operación.
No uses O_EXCL sin O_CREAT como receta general de exclusividad. Sus efectos fuera de esa combinación no forman parte de este modelo y existen casos particulares dependientes del tipo de objeto. Para temporales únicos hay herramientas e interfaces como mktemp y mkstemp, que estudiaremos en el tema 25.
8. Una herramienta para observar open
El programa abrir-demo.c recibe un modo, una ruta y opcionalmente un texto. Primero valida los argumentos; después abre, muestra el tamaño y permisos del objeto, escribe el texto si se pidió y muestra el resultado. Siempre solicita O_CLOEXEC y cierra su descriptor una sola vez.
Los modos tienen nombres deliberadamente separados: escribir exige existencia y no trunca; crear crea si falta y conserva si existe; nuevo exige creación exclusiva; truncar crea o vacía; anexar crea o añade; mixta abre para leer y escribir sin truncar. El modo leer solo obtiene una apertura de lectura y muestra sus atributos: no imprime el contenido.
#define _POSIX_C_SOURCE 200809L
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void error(const char *operacion, const char *ruta, int codigo)
{
fprintf(stderr, "%s [%s]: %s\n", operacion, ruta, strerror(codigo));
}
static int ficha(int fd, const char *etapa)
{
struct stat s;
if (fstat(fd, &s) == -1)
return -1;
int marcas = fcntl(fd, F_GETFD);
if (marcas == -1)
return -1;
printf("%s: longitud=%jd permisos=%04jo cloexec=%s\n",
etapa, (intmax_t)s.st_size,
(uintmax_t)(s.st_mode & 0777),
(marcas & FD_CLOEXEC) ? "si" : "no");
return 0;
}
static int escribir_todo(int fd, const char *texto, size_t longitud)
{
size_t total = 0;
while (total < longitud) {
ssize_t n = write(fd, texto + total, longitud - total);
if (n == -1) {
if (errno == EINTR)
continue;
return -1;
}
if (n == 0) {
errno = EIO;
return -1;
}
total += (size_t)n;
}
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 3 && argc != 4) {
fprintf(stderr, "uso: %s MODO RUTA [TEXTO]\n", argv[0]);
fprintf(stderr, "modos: leer escribir crear nuevo truncar anexar mixta\n");
return 2;
}
int banderas;
if (strcmp(argv[1], "leer") == 0)
banderas = O_RDONLY;
else if (strcmp(argv[1], "escribir") == 0)
banderas = O_WRONLY;
else if (strcmp(argv[1], "crear") == 0)
banderas = O_WRONLY | O_CREAT;
else if (strcmp(argv[1], "nuevo") == 0)
banderas = O_WRONLY | O_CREAT | O_EXCL;
else if (strcmp(argv[1], "truncar") == 0)
banderas = O_WRONLY | O_CREAT | O_TRUNC;
else if (strcmp(argv[1], "anexar") == 0)
banderas = O_WRONLY | O_CREAT | O_APPEND;
else if (strcmp(argv[1], "mixta") == 0)
banderas = O_RDWR | O_CREAT;
else {
fprintf(stderr, "modo desconocido: %s\n", argv[1]);
return 2;
}
if (argc == 4 && (banderas & O_ACCMODE) == O_RDONLY) {
fprintf(stderr, "el modo leer no acepta TEXTO para escribir\n");
return 2;
}
banderas |= O_CLOEXEC;
const char *ruta = argv[2];
int fd = (banderas & O_CREAT)
? open(ruta, banderas, 0666)
: open(ruta, banderas);
if (fd == -1) {
int codigo = errno;
error("open", ruta, codigo);
return 1;
}
int estado = 0;
printf("open correcto: fd=%d\n", fd);
if (ficha(fd, "despues de open") == -1) {
int codigo = errno;
error("ficha", ruta, codigo);
estado = 1;
goto limpiar;
}
if (argc == 4) {
if (escribir_todo(fd, argv[3], strlen(argv[3])) == -1) {
int codigo = errno;
error("write", ruta, codigo);
estado = 1;
goto limpiar;
}
if (ficha(fd, "despues de write") == -1) {
int codigo = errno;
error("ficha", ruta, codigo);
estado = 1;
}
}
limpiar:
if (close(fd) == -1) {
int codigo = errno;
error("close", ruta, codigo);
estado = 1;
}
fd = -1;
return estado;
}
Cómo leer el programa
- La validación precede a open. Un modo desconocido o pedir texto en modo leer no provoca una apertura que pudiera crear o truncar.
- Los permisos solicitados son 0666. La umask heredada del shell determina el resultado en el modelo sin ACL. El programa no modifica la máscara del proceso.
- El argumento mode se entrega cuando corresponde. Las ramas con O_CREAT pasan 0666; las demás usan dos argumentos.
- La escritura comprueba cuánto se aceptó. El bucle evita suponer que una llamada escribe todo. En el tema 6 estudiaremos los retornos y sus causas en profundidad.
- Los fallos conservan errno. Se guarda el código antes de generar mensajes adicionales. Un fallo de limpieza tampoco se convierte en un resultado de éxito.
- La salida de limpieza tiene un solo close. Una vez adquirido el descriptor, los caminos de error pasan por ese cierre. No hay un reintento ciego de close.
Usamos texto de los argumentos para no introducir todavía un copiador de archivos, que construiremos en el tema 8. El programa no añade un salto de línea al texto escrito. Si le pasas XY escribe dos bytes; sus propios mensajes de diagnóstico y observación sí se imprimen por separado en la terminal.
9. Laboratorio de combinaciones
Trabaja en Bash sobre Linux o WSL, preferentemente dentro del directorio personal de la distribución. Los experimentos usan únicamente una carpeta de práctica nueva. No necesitan permisos de administrador ni modificar archivos del sistema.
$ LAB5=$(mktemp -d "$HOME/tema5.XXXXXX")
$ cd "$LAB5"
$ printf 'ABCDEFGHIJ' > datos.txt
$ mkdir carpeta
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 abrir-demo.c -o abrir-demo
Guarda abrir-demo.c en esa carpeta antes de compilar. Mantén la terminal para conservar LAB5. El usuario, el sufijo temporal y los números de descriptor pueden variar. Si aparecen permisos diferentes a los ejemplos, consulta umask y comprueba si tu directorio tiene ACL por defecto.
Conservar el sufijo al sobrescribir
$ ./abrir-demo escribir datos.txt XY
open correcto: fd=3
despues de open: longitud=10 permisos=0644 cloexec=si
despues de write: longitud=10 permisos=0644 cloexec=si
$ cat datos.txt; printf '\n'
XYCDEFGHIJ
$ ./abrir-demo crear datos.txt Z
$ cat datos.txt; printf '\n'
ZYCDEFGHIJ
El segundo comando de la herramienta abre otra descripción de archivo con posición inicial 0. O_CREAT no cambia el objeto porque ya existe. Por eso Z sustituye solo al primer byte y los otros nueve se conservan. Los permisos 0644 de la salida suponen la creación inicial habitual con umask 0022.
Observar que el truncado sucede al abrir
$ ./abrir-demo truncar datos.txt
open correcto: fd=3
despues de open: longitud=0 permisos=0644 cloexec=si
$ wc -c < datos.txt
0
$ ./abrir-demo truncar datos.txt XY
$ cat datos.txt; printf '\n'
XY
$ ./abrir-demo anexar datos.txt ZZ
$ cat datos.txt; printf '\n'
XYZZ
Crear con dos máscaras distintas
$ (umask 022; ./abrir-demo nuevo publico.txt hola)
$ (umask 077; ./abrir-demo nuevo privado.txt secreto)
$ stat -c '%a %n' publico.txt privado.txt
644 publico.txt
600 privado.txt
$ (umask 000; ./abrir-demo crear privado.txt X)
$ stat -c '%a %n' privado.txt
600 privado.txt
Los paréntesis crean subshells para que las máscaras elegidas no modifiquen la del shell interactivo. La última apertura recibe el mismo mode solicitado de 0666, pero privado.txt ya existía. Sus permisos no se recalculan y siguen en 0600. Su contenido sí cambia desde el inicio, porque el modo crear de la herramienta permite escribir sin truncar.
Exigir un nombre nuevo y reconocer EEXIST
$ ./abrir-demo nuevo unico.txt primera
$ LC_ALL=C ./abrir-demo nuevo unico.txt segunda
open [unico.txt]: File exists
$ echo $?
1
$ cat unico.txt; printf '\n'
primera
$ ln -s destino-ausente.txt enlace-roto
$ LC_ALL=C ./abrir-demo nuevo enlace-roto intento
open [enlace-roto]: File exists
El nombre unico.txt no se reemplaza por segunda y el enlace roto tampoco se atraviesa para crear un destino mediante la apertura exclusiva. Si luego pruebas ./abrir-demo crear enlace-roto texto, sin O_EXCL, el destino puede crearse. Haz la comparación solo dentro de esta carpeta de laboratorio.
Una observación con dos nombres del mismo objeto
$ printf 'contenido anterior' > original.txt
$ ln original.txt alias.txt
$ stat -c '%i %s %n' original.txt alias.txt
$ ./abrir-demo truncar alias.txt
$ stat -c '%i %s %n' original.txt alias.txt
Después de truncar, los dos nombres conservan el mismo inodo y muestran tamaño cero. No has creado una copia vacía independiente en alias.txt. Esta prueba relaciona la semántica de O_TRUNC con la identidad del objeto estudiada en los temas 2 y 4.
10. Experimento: O_APPEND frente a la posición
Vamos a comprobar una propiedad que no se deduce con mirar únicamente el tamaño: mover la posición a cero no hace que la siguiente escritura ordinaria deje de anexarse. Este programa abre un archivo existente con O_APPEND, consulta la posición inicial, ejecuta lseek a cero y escribe XY.
#define _POSIX_C_SOURCE 200809L
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <inttypes.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
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_WRONLY | O_APPEND | O_CLOEXEC);
if (fd == -1) {
perror("open");
return 1;
}
int estado = 1;
off_t posicion = lseek(fd, 0, SEEK_CUR);
if (posicion == (off_t)-1) {
perror("lseek inicial");
goto limpiar;
}
printf("posicion tras open: %jd\n", (intmax_t)posicion);
if (lseek(fd, 0, SEEK_SET) == (off_t)-1) {
perror("lseek a cero");
goto limpiar;
}
const char texto[] = "XY";
size_t total = 0;
while (total < sizeof texto - 1) {
ssize_t n = write(fd, texto + total, sizeof texto - 1 - total);
if (n == -1) {
if (errno == EINTR)
continue;
perror("write");
goto limpiar;
}
if (n == 0) {
fprintf(stderr, "write no hizo progreso\n");
goto limpiar;
}
total += (size_t)n;
}
struct stat s;
if (fstat(fd, &s) == -1) {
perror("fstat");
goto limpiar;
}
posicion = lseek(fd, 0, SEEK_CUR);
if (posicion == (off_t)-1) {
perror("lseek final");
goto limpiar;
}
printf("tras write: posicion=%jd longitud=%jd\n",
(intmax_t)posicion, (intmax_t)s.st_size);
estado = 0;
limpiar:
if (close(fd) == -1) {
perror("close");
estado = 1;
}
return estado;
}
$ cd "$LAB5"
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 append-posicion.c -o append-posicion
$ printf 'ABCDEFGHIJ' > anexado.txt
$ ./append-posicion anexado.txt
posicion tras open: 0
tras write: posicion=12 longitud=12
$ cat anexado.txt; printf '\n'
ABCDEFGHIJXY
El resultado muestra dos hechos distintos: open comenzó en 0 y write terminó en 12, después de anexar a los diez bytes existentes. El lseek a cero no eliminó la bandera de la apertura. Si modificas el programa para quitar O_APPEND, con el mismo contenido inicial la escritura producirá XYCDEFGHIJ y la posición quedará en 2.
El bucle de escritura es correcto para completar este pequeño texto, pero no promete que varias iteraciones sean un único registro frente a otros escritores. La demostración se hace sin escritores concurrentes y sobre un archivo regular del laboratorio.
11. Errores de apertura y sus causas
El mismo retorno -1 puede tener causas muy diferentes. Antes de decidir si conviene reintentar, cambiar la ruta o informar al usuario, conserva y examina errno. No codifiques los números: compara las constantes de <errno.h>.
| Error | Situación típica | Qué revisar |
|---|---|---|
ENOENT | Falta el archivo sin O_CREAT, o falta un directorio intermedio. | La ruta completa. O_CREAT no crea automáticamente carpetas. |
EEXIST | O_CREAT | O_EXCL encuentra el nombre ocupado. | Si la intención era exigir un objeto nuevo. |
EACCES | No se permite el acceso o atravesar/crear en algún directorio. | Credenciales, permisos, ACL y componentes de la ruta. |
ENOTDIR | Un componente que debe atravesarse no es directorio. | El primer componente incompatible del recorrido. |
EISDIR | Se intenta abrir un directorio para escritura ordinaria. | Que la ruta sea el tipo de objeto esperado. |
EROFS | Se pide modificar un sistema de archivos de solo lectura. | El montaje y la operación solicitada. |
EMFILE | No se puede asignar otro descriptor dentro del límite del proceso. | Fugas de descriptores y límite aplicable. |
ENFILE | Se agotan recursos globales de archivos abiertos. | La situación del sistema, no solo la tabla del proceso. |
ENOSPC, EDQUOT | La creación requiere recursos de espacio o supera una cuota. | Bloques, inodos y límites del sistema de archivos. |
EINTR | Una señal interrumpe una apertura que estaba bloqueada. | La política de señales y reintento de esa operación. |
No toda apertura termina inmediatamente. Por ejemplo, abrir una FIFO puede esperar a otro extremo, y dispositivos o sistemas remotos tienen sus propios comportamientos. No ejecutes una apertura bloqueante de un objeto desconocido solo para «ver qué descriptor devuelve». El laboratorio utiliza archivos regulares para aislar las reglas que estudiamos.
Producir errores de ruta sin cambiar el sistema
$ LC_ALL=C ./abrir-demo leer ausente.txt
open [ausente.txt]: No such file or directory
$ LC_ALL=C ./abrir-demo crear no-hay-carpeta/salida.txt hola
open [no-hay-carpeta/salida.txt]: No such file or directory
$ LC_ALL=C ./abrir-demo escribir carpeta
open [carpeta]: Is a directory
Las descripciones exactas pueden depender de la biblioteca y de la configuración regional. En los dos primeros casos aparece ENOENT, pero añadir O_CREAT solo resolvería la ausencia del componente final si su directorio padre ya existe. Para crear carpetas hay otra operación: mkdir.
Open correcto no garantiza write correcto
Una apertura puede funcionar y la escritura posterior fallar por espacio, cuota, error de E/S u otras condiciones. Incluso después de writes aparentemente correctos pueden aparecer errores al sincronizar o cerrar, según el sistema. Comprobar solo open y devolver éxito después de ignorar todo lo demás no demuestra que el trabajo se haya completado.
En Linux, /dev/full es una herramienta útil para observar este caso: permite abrir el dispositivo, pero las escrituras fallan con ENOSPC. Si está disponible, ejecuta ./abrir-demo escribir /dev/full XY y comprueba su estado de salida. No significa que tu disco se haya llenado; es el comportamiento de ese dispositivo virtual.
12. close y el cierre ordenado
#include <unistd.h>
int close(int fd); /* devuelve 0 si tiene exito; -1 si informa un error */
Close retira una entrada de la tabla y libera esa referencia. Si quedan duplicados o referencias heredadas, la apertura puede continuar viva. El número queda disponible para una apertura posterior; no mantengas la suposición de que ese entero seguirá señalando el recurso que acabas de cerrar.
Un propietario claro y una ruta de limpieza
Una función que adquiere un descriptor debe definir quién lo cerrará. Si lo devuelve para que otra parte siga utilizándolo, transfiere esa responsabilidad; si solo lo usa internamente, debe liberar su referencia en sus salidas normales y de error. Hacer dos close «por si acaso» puede cerrar otro objeto que reutilizó el mismo número.
El programa abrir-demo usa una etiqueta limpiar para que los errores posteriores a open lleguen al mismo punto. El goto de ese caso no oculta una vuelta arbitraria por el programa: concentra la liberación de un recurso adquirido. No hay que cerrar si open devolvió -1.
int estado = 0;
/* ... se obtuvo fd y se realizaron operaciones ... */
if (hubo_error_de_trabajo) {
estado = 1;
/* Informar el error principal conservado antes de limpiar. */
}
if (close(fd) == -1) {
int error_cierre = errno;
/* Informar tambien el error de cierre. */
(void)error_cierre;
estado = 1;
}
fd = -1; /* esta variable ya no se utilizara como una referencia valida */
El fragmento expresa una organización, no un programa completo: hubo_error_de_trabajo representa el resultado de las operaciones previas. Asignar -1 a una variable ayuda a expresar su estado, pero no modifica copias del entero guardadas en otras partes del programa.
Por qué no reintentamos close a ciegas en Linux
En Linux, el descriptor se libera al principio de la operación de cierre, antes de algunas etapas que todavía pueden informar errores. Si reintentas automáticamente con el mismo entero, otro hilo podría haberlo reutilizado y cerrarías su recurso. Esto también exige cuidado con EINTR: no copies sin más un bucle de reintento de read y lo pegues alrededor de close.
Las reglas históricas y de otras plataformas no son idénticas. Este curso y sus programas están orientados a Linux; para software portable hay que consultar el contrato del sistema objetivo. El manual de close(2) explica la liberación del descriptor y las diferencias en el tratamiento de interrupciones. Un error se registra y se comunica; reusar el entero sin un contrato claro no lo corrige.
Cerrar, vaciar búferes y persistir son operaciones distintas
Write trabaja con un descriptor y puede dejar datos en cachés del núcleo. Fwrite y fprintf añaden, además, el nivel de búfer de FILE* en la biblioteca. Close no vacía mágicamente un búfer de stdio que todavía no se entregó al núcleo, ni asegura por sí mismo que todos los datos estén en almacenamiento estable.
Si un requisito exige persistencia, hay que estudiar operaciones como fsync y su resultado. Para persistir la incorporación o sustitución de un nombre también puede ser necesario sincronizar el directorio correspondiente. No añadiremos aquí una falsa garantía de durabilidad a los ejemplos: su objetivo es observar creación, contenido y vida útil de las referencias.
Cuando termina un proceso, el núcleo recupera sus descriptores. Eso no sustituye liberar recursos a tiempo ni permite informar de los fallos que el programa decidió ignorar. Una herramienta pequeña que termina enseguida y un servicio que abre archivos durante semanas tienen la misma obligación de organizar bien qué referencias siguen vivas.
13. Simulador de aperturas y contenido
El simulador tiene un único nombre, demo.txt, y permite mantener varias aperturas independientes del mismo archivo. Elige el estado inicial y las banderas para la próxima apertura. Después selecciona un descriptor para leer, escribir, volver a posición cero o cerrar. Los cambios en los controles de banderas no alteran aperturas que ya existen.
Qué cambia al abrir y qué cambia al escribir
| fd | Apertura | Acceso | Posición | Append | CLOEXEC |
|---|
Historial de operaciones
El modelo reserva 0, 1 y 2 y permite usar 3 a 7. Todas las aperturas llegan al mismo objeto; no se modelan enlaces, hilos ni fallos parciales de E/S. Se supone que la resolución de la ruta y los permisos de acceso permiten abrir: mode y umask se muestran para estudiar creación, no para simular la autorización de aperturas posteriores. Tampoco hay ACL por defecto.
O_EXCL sin O_CREAT queda fuera del modelo y se rechaza como combinación no admitida, sin atribuir ese diagnóstico a todos los sistemas reales. O_RDONLY | O_TRUNC también se rechaza como receta no portable. Las escrituras del simulador aceptan de una vez todo el texto ASCII imprimible; en un programa real hay que comprobar el retorno de write.
Secuencias que conviene predecir
- Con el archivo inicial, abre O_WRONLY y escribe XY. El contenido queda XYCDEFGHIJ. Reinicia y añade O_TRUNC: antes de escribir el archivo ya mide cero.
- Abre dos descriptores del archivo existente. Lee con uno en modo O_RDWR; después abre otro con truncado. Comprueba que la longitud cambia para ambos, pero la posición que tenía el primero se conserva.
- Reinicia con el nombre ausente y sin O_CREAT: obtienes ENOENT. Activa O_CREAT y crea con mode 0666 y umask 0077: aparecen permisos 0600.
- Sin reiniciar, cambia mode y umask y vuelve a abrir con O_CREAT. El modo del archivo existente no cambia. Añade O_EXCL y comprueba que la nueva apertura falla sin borrar el contenido.
- Abre con O_APPEND, vuelve a cero y escribe XY dos veces. Cada escritura utiliza el final actual. Quitar el check de O_APPEND no modifica el descriptor abierto.
- Cierra todos los descriptores. El archivo y su contenido siguen presentes; una nueva apertura puede reutilizar el menor número libre.
14. Resumen y recetas habituales
- Open devuelve un descriptor no negativo o -1 con errno. El 0 también es un resultado válido.
- Se elige un modo de acceso: O_RDONLY, O_WRONLY u O_RDWR. Las opciones adicionales se combinan con OR bit a bit.
- O_CREAT crea si falta; no incluye truncado ni exige novedad. Con esa bandera se proporciona mode.
- Sin ACL por defecto, mode & ~umask determina los permisos iniciales. No cambia el modo de un objeto existente.
- O_TRUNC vacía un archivo regular al abrirlo para escritura, antes de cualquier write.
- Sin truncado, sobrescribir menos bytes puede conservar el sufijo anterior.
- O_APPEND selecciona el final para cada escritura; no es un lseek inicial ni una transacción de varios writes.
- O_CREAT | O_EXCL exige un nombre nuevo en la apertura, sin establecer un bloqueo permanente.
- O_CLOEXEC solicita el cierre del descriptor ante exec correcto desde el momento de su creación.
- Close elimina una referencia y permite reutilizar su número. Hay que comprobar errores y evitar reintentos ciegos en Linux.
- Cerrar, borrar un nombre y garantizar persistencia son operaciones diferentes.
| Intención | Acceso y banderas principales | Si ya existe |
|---|---|---|
| Leer un archivo existente. | O_RDONLY | Conserva el contenido. |
| Modificar desde el principio sin vaciar. | O_WRONLY | Conserva longitud y bytes que no se sobrescriban. |
| Crear si falta y conservar si existe. | O_WRONLY | O_CREAT | No trunca ni recalcula permisos. |
| Crear o vaciar para una salida nueva. | O_WRONLY | O_CREAT | O_TRUNC | Reduce a cero al abrir. |
| Crear o añadir registros. | O_WRONLY | O_CREAT | O_APPEND | Conserva y anexa al escribir. |
| Exigir un nombre nuevo. | O_WRONLY | O_CREAT | O_EXCL | Falla con EEXIST. |
| Leer y escribir sin vaciar. | O_RDWR | Conserva contenido; una posición para esa apertura. |
Añade O_CLOEXEC cuando la referencia no deba conservarse en un programa ejecutado después. Las recetas de creación necesitan además mode y un manejo de error adecuado. La tabla describe intenciones, no reemplaza la comprobación de resultados.
15. Preguntas de repaso y práctica
Antes de desplegar cada respuesta, intenta predecir el tamaño, contenido, permisos y referencias que quedarían.
1. ¿Abrir con O_WRONLY borra el contenido anterior?
No. Solicita escritura y crea una apertura con posición inicial 0. Si escribes dos bytes sobre un archivo de diez, sustituyes esos dos y puedes conservar el sufijo. Para vaciar al abrir se usa O_TRUNC con acceso de escritura.
2. ¿Qué diferencia hay entre O_CREAT y O_CREAT | O_EXCL?
O_CREAT permite abrir un archivo existente o crear si falta. Al añadir O_EXCL se exige que el nombre final esté libre y la creación sea nueva; si existe, la apertura falla con EEXIST.
3. ¿Qué pasa si abres con truncado y cierras sin escribir?
Si la apertura de escritura tuvo éxito sobre un archivo regular, su tamaño ya se redujo a cero durante open. Close no restaura el contenido anterior.
4. ¿Por qué O_RDONLY | O_WRONLY no pide lectura y escritura?
Porque los modos de acceso son alternativas dentro de un campo. En Linux O_RDONLY vale cero, así que esa expresión equivale a O_WRONLY. Para ambos accesos se utiliza O_RDWR.
5. ¿Por qué se consulta (flags & O_ACCMODE) == O_RDONLY?
Para extraer el campo de modo de acceso y compararlo con la constante adecuada. Una prueba flags & O_RDONLY no detectaría nada cuando O_RDONLY tiene valor cero.
6. ¿Es válido omitir mode con O_CREAT si crees que el archivo existe?
No. La bandera requiere proporcionar ese argumento. La existencia puede cambiar y la llamada debe cumplir su contrato siempre, no solo cuando casualmente no necesita crear.
7. Sin ACL por defecto, ¿qué permisos resultan de 0666 con umask 0077?
0600: se conservan lectura y escritura del dueño y se retiran los bits de grupo y otros. Se calcula con una máscara de bits, no una resta aritmética.
8. ¿Una reapertura O_CREAT con mode 0666 convierte un archivo 0600 en 0644?
No. Mode se usa para la creación efectiva, no para cambiar los permisos de un archivo existente. Para modificar sus permisos existe chmod y sus reglas de autorización.
9. ¿O_CREAT crea los directorios intermedios que faltan?
No. Puede crear el archivo final si el recorrido hasta su directorio padre es válido y está permitido. Si falta una carpeta intermedia, puede fallar con ENOENT.
10. ¿Por qué hacer lseek al final antes de escribir no sustituye a O_APPEND?
Porque otra apertura puede modificar el final entre ese posicionamiento y la escritura. O_APPEND integra la selección del final en cada escritura con las garantías que ofrezca el sistema de archivos.
11. Con O_APPEND, ¿lseek a cero hace que write sobrescriba el principio?
No en el caso ordinario del archivo regular del laboratorio. El siguiente write vuelve a seleccionar el final. La posición modificada puede servir para lecturas si la apertura también admite leer.
12. ¿O_APPEND convierte varias llamadas de una línea de registro en una transacción?
No. Otras escrituras pueden intercalarse entre las llamadas, y una escritura parcial puede requerir completar mediante otra operación. Además, anexado y persistencia son garantías diferentes.
13. ¿Qué ocurre con un enlace simbólico roto ante O_CREAT | O_EXCL?
El nombre final ya está ocupado por el enlace y se obtiene EEXIST. La creación exclusiva no sigue ese enlace final para crear el destino ausente. Los enlaces intermedios son otra cuestión.
14. ¿O_CLOEXEC cierra el descriptor en cuanto haces fork?
No. La marca se aplica al ejecutar correctamente una sustitución del programa mediante exec. El fork ordinario hereda las referencias como vimos en el tema 4.
15. ¿Puede open funcionar y write fallar?
Sí. Obtener la referencia no reserva por adelantado todos los recursos de futuras escrituras. Puede fallar write por espacio, cuota, E/S u otras condiciones; algunas también pueden comunicarse al sincronizar o cerrar.
16. ¿Por qué no se rodea close con el mismo bucle de EINTR que read?
En Linux el número puede quedar liberado aunque close informe un error. Un reintento con ese entero puede afectar a una referencia nueva que lo haya reutilizado. Hay que respetar el contrato de cierre del sistema objetivo y comunicar los errores.
17. ¿Truncar un enlace duro afecta al otro nombre del mismo objeto?
Sí. Se modifica el tamaño del archivo compartido, no una copia asociada únicamente al nombre utilizado. Las posiciones de otras aperturas no se rebobinan por ese truncado.
18. ¿Un close correcto demuestra que el contenido está persistido ante un corte de luz?
No. La vida de la referencia y la persistencia son cuestiones distintas. Para requisitos de durabilidad deben estudiarse las operaciones de sincronización y las garantías del sistema de archivos.
Para practicar
- Compila abrir-demo.c y construye una tabla con el contenido de datos.txt después de usar escribir, crear, truncar y anexar con el mismo texto XY. Restablece ABCDEFGHIJ antes de cada prueba.
- Ejecuta truncar sin TEXTO y comprueba el tamaño inmediatamente con stat. Explica por qué no se necesitó ningún write para cambiar el archivo.
- Crea archivos nuevos con umask 0002, 0022 y 0077 en subshells. Compara los resultados con mode 0666 y después modifica el programa para solicitar 0600.
- Solicita una creación exclusiva dos veces y comprueba el contenido tras el fallo. Compara ese resultado con el modo crear que no utiliza O_EXCL.
- Prueba las aperturas crear y nuevo sobre un enlace simbólico roto del laboratorio. Explica qué entrada existe y qué ruta se seguiría en cada caso.
- Compila append-posicion.c y verifica que el contenido termina en XY aunque se ejecutó lseek a cero. Quita O_APPEND, restablece el archivo y compara longitud y posición.
- Ejecuta abrir-demo con modo desconocido y con leer más un argumento de texto. Comprueba que no se creó una ruta ausente y que el estado de salida fue 2.
- Si existe /dev/full, ejecuta una escritura allí con la herramienta. Distingue el éxito de open del fallo de write y del estado final del proceso.
- En el simulador abre una referencia de lectura y otra que trunque el mismo objeto. Observa las posiciones antes y después. Explica por qué el tamaño pertenece al archivo y el offset a cada apertura.
- Llena los cinco descriptores de práctica del simulador y prueba una apertura con O_TRUNC. Comprueba que el error de límite del modelo no se presenta como una apertura correcta; libera un descriptor y repite.
- Revisa las salidas de error del programa C e identifica qué caminos pasan por limpiar y cuáles no deben ejecutar close. Justifica cada caso a partir de si se adquirió o no la referencia.
En el tema 6 pasaremos de obtener referencias a transferir datos: read, write, tipos de retorno, lecturas y escrituras parciales, interrupciones y fin de archivo. Ya sabes pedir una apertura con efectos explícitos y organizar su cierre sin confundirlo con borrado o persistencia.