← Sistemas de Archivos y E/SBloque 7 · Enlaces duros y simbólicos · Tema 25 de 50

Tema 25

unlink, rename y archivos temporales

Borrar un nombre no siempre destruye su archivo y renombrar un temporal puede cambiar de una vez qué objeto publica una ruta. Separaremos entradas, referencias abiertas y contenido; crearemos temporales con mkstemp; y observaremos lectores que conservan una versión anterior después de un reemplazo. Dos programas en C, un laboratorio verificable y dos simuladores conectarán la vida de los objetos con la limpieza de recursos y los límites de la atomicidad.

1. Nombre, objeto y referencia abierta

Los temas 23 y 24 separaron enlaces duros, enlaces simbólicos y objetos. Ahora estudiaremos lo que ocurre cuando se retira una entrada mientras alguien conserva el archivo abierto, y cómo un nombre puede pasar a publicar otro objeto sin cambiar los descriptores ya obtenidos.

Nombre: asociación en un directorio. Objeto archivo: identidad, metadatos y contenido. Referencia abierta: acceso mantenido por un descriptor y su descripción de archivo abierto, aunque cambien las asociaciones de nombres.

tres relaciones · ninguna sustituye a las demás
directorio: "informe.txt" ──→ inodo 81 ──→ datos
                                 ↑
proceso: fd=3 ──→ descripción abierta

Quitar "informe.txt" retira una asociación del directorio.
Cerrar fd=3 retira una referencia del proceso.
Ni abrir ni duplicar un descriptor añade un enlace duro.

El número entero de descriptor solo tiene sentido en la tabla del proceso. Puede reutilizarse después de cerrar. El contador st_nlink, en cambio, cuenta nombres duros del archivo regular; no cuenta cuántos descriptores existen.

Dos descriptores duplicados pueden compartir una misma descripción abierta y su offset. Dos aperturas independientes del mismo nombre pueden tener descripciones y posiciones distintas. En ambos casos pueden mantener vivo el objeto después de que desaparezcan sus nombres.

unlink retira el nombre indicado. Si corresponde a un archivo regular, disminuye su contador de enlaces. Los demás nombres duros del mismo objeto continúan funcionando. Si corresponde a un simbólico final, se retira el enlace mismo, no el destino al que llevaría.

interfaz · eliminación de una entrada
#include <unistd.h>
int unlink(const char *ruta);

#include <fcntl.h>
int unlinkat(int dirfd, const char *ruta, int flags);

0: operación completada
-1: error, consultar errno

Con flags cero, unlinkat realiza la eliminación de nombre relativa a un directorio inicial. AT_REMOVEDIR selecciona la eliminación de un directorio vacío. Un archivo regular no se trata como directorio y una carpeta no se borra recursivamente mediante una única llamada unlink.

La posibilidad de retirar un nombre depende de su padre, del acceso de búsqueda en la ruta y de reglas como el sticky bit. Que el archivo tenga modo de solo lectura no impide por sí solo retirar su entrada. El bloque de permisos comenzará en el tema siguiente.

La referencia de unlink(2) describe eliminación, referencias abiertas y errores. Un retorno de éxito no significa que se hayan sobrescrito todos los bytes antiguos del soporte ni que ya exista una cantidad exacta de espacio libre global.

3. Un archivo abierto sin nombres

Si se retira el último enlace duro mientras el archivo sigue abierto, su nlink puede ser cero y el descriptor seguir admitiendo lecturas y escrituras según el modo con que se abrió. Una nueva apertura por el nombre eliminado falla, porque vuelve a consultar el directorio.

secuencia · descriptor y ruta tienen destinos diferentes
fd = open("trabajo", O_RDWR)    → objeto 81, nlink=1
unlink("trabajo")              → objeto 81, nlink=0
read(fd, ...)                  → puede leer datos de 81
open("trabajo", ...)           → ENOENT, si nadie recreó el nombre

crear otro "trabajo"           → objeto 95
fd todavía identifica 81; el nuevo nombre identifica 95

Los recursos se pueden recuperar cuando desaparecen las referencias que los retienen, con condiciones y trabajo diferido del sistema de archivos. Además de descriptores puede haber otras referencias, por ejemplo mapas de memoria. Los modelos de este tema simplifican esas relaciones y lo indicarán.

Por qué du y df pueden discrepar

Un recorrido por nombres no encuentra un archivo cuyo último nombre desapareció, pero su asignación puede seguir ocupada por una aplicación. Por eso retirar un registro grande no garantiza que df muestre inmediatamente la capacidad esperada: el proceso puede mantenerlo abierto e incluso seguir escribiendo en él.

En Linux, lsof +L1, si está instalado y se dispone de visibilidad suficiente, ayuda a localizar archivos abiertos con menos de un enlace. Las referencias de /proc/PID/fd también pueden aportar información. Permisos, espacios de nombres y políticas limitan lo que se observa; no toda discrepancia entre du y df tiene esta causa.

Cerrar o hacer que una aplicación rote correctamente su archivo puede liberar la referencia. No se necesita terminar procesos ajenos para el laboratorio: los ejemplos cierran sus propios descriptores y no intentan medir una recuperación exacta de espacio físico.

4. Qué garantiza rename

rename cambia una asociación de nombres y puede trasladarla entre directorios del mismo montaje admitido. Si el destino existe y se cumplen las condiciones de tipo y acceso, lo sustituye de forma atómica en el espacio de nombres.

publicar otro objeto · destino existente
antes:
    "estado.txt"  → objeto 81, contenido viejo
    ".nuevo-..."  → objeto 95, contenido nuevo completo

rename(".nuevo-...", "estado.txt")

después:
    "estado.txt"  → objeto 95
    el nombre temporal desaparece
    el objeto 81 pierde ese enlace, pero puede seguir abierto

La sustitución no necesita una fase en que el nombre de destino esté ausente. Hacer primero unlink del destino y después rename introduciría un hueco observable que la operación de sustitución no requiere.

Esto no convierte todas las consultas de otro proceso en una instantánea. Dos aperturas sucesivas pueden quedar a lados diferentes del reemplazo. La garantía sobre el cambio de nombre no protege contra modificaciones posteriores del contenido por otro escritor.

Entre sistemas o montajes distintos puede fallar con EXDEV. La herramienta mv puede implementar copia y eliminación en ese caso, con otras propiedades. En nuestro ejemplo se crea el temporal en el mismo directorio que el destino para que su publicación no dependa de moverlo desde /tmp.

Si origen y destino ya son nombres duros del mismo objeto, la operación puede terminar sin cambios, como comprobamos en el tema 23. Los directorios tienen restricciones adicionales. La referencia de rename(2) define esos casos.

5. Lectores antiguos y nuevas aperturas

Un lector que abre estado.txt antes del reemplazo conserva su referencia al objeto viejo. Un lector que lo abre después obtiene el nuevo objeto. El descriptor antiguo no se redirige automáticamente por compartir el texto de la ruta con una apertura posterior.

AcciónObjeto obtenidoQué puede observar
Abrir antes de renameEl anterior.Su contenido y metadatos mientras la referencia sea válida.
Abrir después de renameEl publicado.El nuevo contenido preparado.
Volver a usar el descriptor antiguoEl anterior.No cambia de objeto por el renombrado.
Resolver un simbólico hacia estado.txt despuésEl que publique esa ruta entonces.El simbólico sigue el nombre, según sus reglas de resolución.

Para que un lector de un descriptor vea una versión estable, esa versión no debe ser modificada por otros escritores. Rename separa asociaciones; no hace inmutable el objeto anterior ni el nuevo. Un enlace duro al viejo también lo mantiene accesible por otro nombre.

El protocolo útil es preparar el contenido completo en otro objeto y publicar solo después de comprobar las escrituras. Escribir poco a poco sobre el objeto que ya publica estado.txt permite que un lector vea fases parciales, aunque nunca cambie su nombre.

6. Crear un temporal sin separar nombre y apertura

Inventar un nombre predecible, comprobar que no existe y abrirlo después deja una separación entre comprobación y uso. Otro actor puede ocuparlo en ese intervalo. Elegir un nombre aparentemente raro tampoco constituye por sí solo un contrato de creación exclusiva.

mkstemp: función que recibe una plantilla modificable terminada en seis X, crea exclusivamente un archivo con un nombre generado y devuelve su descriptor ya abierto. No es una función que solo sugiera una cadena libre.

patrón · usar el descriptor devuelto, no reabrir por el nombre
char plantilla[] = ".trabajo-XXXXXX";
int fd = mkstemp(plantilla);
if (fd == -1) {
    /* informar el error */
}
/* plantilla ahora contiene el nombre creado; fd identifica el objeto */

La plantilla debe ser memoria modificable, no un literal de cadena. En Linux actual el archivo se crea con modo base 0600, que puede restringirse por la máscara aplicable; si se requieren atributos particulares hay que fijarlos y comprobarlos según el contrato de la aplicación.

Mkstemp no devuelve necesariamente un descriptor con close-on-exec. En un ejemplo de un solo hilo puede fijarse con fcntl; cuando es necesario evitar la ventana respecto de exec en otros hilos, Linux/glibc ofrece mkostemp con O_CLOEXEC. El tema 29 profundizará en las separaciones peligrosas entre operaciones.

La referencia de mkstemp(3) documenta plantilla, exclusividad, modo y variantes. La función no elimina por sí sola un temporal nombrado al cerrar: la aplicación decide si retira el nombre, lo conserva o lo publica mediante rename.

7. Ciclo de vida y alternativas

PatrónNombre durante el usoResponsabilidad
mkstemp y unlink inmediatoDesaparece después de la creación.Trabajar mediante el descriptor; comprobar la retirada.
mkstemp y publicación por renameTemporal hasta publicar.Retirar el temporal si se cancela antes; no confundirlo con el destino después.
tmpfileNo ofrece una ruta para publicar como parte de su interfaz.Usar el flujo FILE * y cerrarlo correctamente.
O_TMPFILE de Linux cuando se admiteObjeto inicialmente sin nombre.Comprobar soporte y las reglas de enlace o publicación que se utilicen.

tmpfile(3) proporciona un flujo temporal de lectura y escritura y gestiona su eliminación al cerrar o terminar el programa según su contrato. No se debe suponer que equivale a mkstemp con una ruta que luego pueda renombrarse.

Retirar pronto el nombre reduce el tiempo en que existe una entrada que pueda olvidarse, pero el objeto sigue consumiendo recursos mientras se conserve abierto. También impide usarlo después por su nombre normal. No resuelve todos los accesos posibles por mecanismos específicos del sistema.

Si un proceso termina antes de retirar un temporal nombrado, el cierre de sus descriptores no elimina ese nombre. Una rutina de limpieza debe reconocer qué creó y en qué fase quedó; no debe borrar archivos solo porque su nombre tenga un prefijo parecido.

8. Funciones comunes de los ejemplos

Guarda este encabezado junto a los dos fuentes siguientes. Las funciones completan transferencias, manejan EINTR, detectan fin prematuro y comprueban el contenido sin depender del offset compartido entre descriptores duplicados.

temporal-comun.h · transferencias y consultas de objetos abiertos
#ifndef TEMPORAL_COMUN_H
#define TEMPORAL_COMUN_H
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

static inline int cerrar(int *fd)
{
    if (*fd == -1) return 0;
    int copia = *fd;
    *fd = -1;
    return close(copia);
}

static inline int escribir_todo(int fd, const char *texto)
{
    size_t n = strlen(texto), p = 0;
    while (p < n) {
        ssize_t k = write(fd, texto + p, n - p);
        if (k > 0) { p += (size_t)k; continue; }
        if (k == -1 && errno == EINTR) continue;
        if (k == 0) errno = EIO;
        return -1;
    }
    return 0;
}

static inline int comprobar(int fd, const char *texto)
{
    char b[64];
    size_t n = strlen(texto), p = 0;
    struct stat s;
    if (n > sizeof b) { errno = EOVERFLOW; return -1; }
    if (fstat(fd, &s) == -1) return -1;
    if (s.st_size != (off_t)n) { errno = EIO; return -1; }
    while (p < n) {
        ssize_t k = pread(fd, b + p, n - p, (off_t)p);
        if (k > 0) { p += (size_t)k; continue; }
        if (k == -1 && errno == EINTR) continue;
        if (k == 0) errno = EIO;
        return -1;
    }
    if (memcmp(b, texto, n)) { errno = EIO; return -1; }
    return 0;
}

static inline int mismos(const struct stat *a, const struct stat *b)
{
    return a->st_dev == b->st_dev && a->st_ino == b->st_ino;
}

static inline int foto(const char *fase, int fd, nlink_t enlaces,
                       const struct stat *identidad)
{
    struct stat s;
    if (fstat(fd, &s) == -1) return -1;
    if (s.st_nlink != enlaces || !mismos(&s, identidad)) {
        errno = EIO;
        return -1;
    }
    printf("%s dev=%ju ino=%ju enlaces=%ju bytes=%jd\n", fase,
           (uintmax_t)s.st_dev, (uintmax_t)s.st_ino,
           (uintmax_t)s.st_nlink, (intmax_t)s.st_size);
    return 0;
}
#endif

La función cerrar retira el entero de la variable antes de llamar a close. En estos programas Linux no se reintenta el cierre del mismo número tras un error, porque ese número podría reutilizarse. Un error se informa y afecta al estado final.

La comprobación de contenido se limita a los textos breves de los ensayos. No es una función de comparación general de archivos arbitrarios. El contexto supone un directorio privado sin actores concurrentes del mismo usuario que alteren los nombres.

9. Programa en C: retirar el nombre y seguir usando el archivo

El programa crea un temporal en el directorio actual, escribe datos, duplica el descriptor y retira el nombre. Comprueba que la ruta dejó de existir, que ambas referencias siguen identificando el objeto con nlink cero y que el duplicado permite leer después de cerrar la primera referencia.

temporal-sin-nombre.c · mkstemp, unlink y una referencia duplicada
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include "temporal-comun.h"

int main(int argc, char **argv)
{
    if (argc != 1) {
        fprintf(stderr, "Uso: %s\n", argv[0]);
        return 1;
    }
    char nombre[] = ".sin-nombre-XXXXXX";
    const char *texto = "contenido temporal\n";
    int fd = -1, copia = -1, nombrado = 0, estado = 1;
    struct stat inicial, consulta;
    fd = mkstemp(nombre);
    if (fd == -1) { perror("mkstemp"); return 1; }
    nombrado = 1;
    if (fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) goto error;
    if (escribir_todo(fd, texto) == -1 || fstat(fd, &inicial) == -1) goto error;
    if (foto("creado", fd, 1, &inicial) == -1) goto error;
    copia = fcntl(fd, F_DUPFD_CLOEXEC, 0);
    if (copia == -1) goto error;
    if (foto("duplicado", copia, 1, &inicial) == -1) goto error;
    if (unlink(nombre) == -1) goto error;
    nombrado = 0;
    if (lstat(nombre, &consulta) != -1 || errno != ENOENT) {
        fputs("El nombre retirado no devolvió ENOENT\n", stderr);
        goto fin;
    }
    if (foto("sin_nombre", fd, 0, &inicial) == -1 ||
        comprobar(fd, texto) == -1) goto error;
    if (cerrar(&fd) == -1) goto error;
    if (foto("solo_duplicado", copia, 0, &inicial) == -1 ||
        comprobar(copia, texto) == -1) goto error;
    if (cerrar(&copia) == -1) goto error;
    puts("nombre_ausente=correcto lectura=correcta referencias_cerradas=correcto");
    estado = 0;
    goto fin;
error:
    perror("operacion del temporal");
fin:
    if (cerrar(&copia) == -1) { perror("close copia"); estado = 1; }
    if (cerrar(&fd) == -1) { perror("close fd"); estado = 1; }
    if (nombrado) fprintf(stderr, "Temporal propio conservado para inspección: %s\n", nombre);
    if (fflush(stdout) == EOF) estado = 1;
    return estado;
}
terminal · compilar y ejecutar en el directorio privado
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 temporal-sin-nombre.c -o temporal-sin-nombre
$ ./temporal-sin-nombre

Las cuatro fotos muestran el mismo objeto, con enlaces 1, 1, 0, 0. Duplicar la referencia no elevó nlink. El cierre final no se comprueba intentando usar después el descriptor retirado: el número ya no identifica necesariamente aquel objeto.

Después del éxito no queda un nombre .sin-nombre-* creado por el programa. Si falla antes de unlink, informa el nombre propio conservado. Fijar close-on-exec después de mkstemp se usa aquí en un programa de un hilo sin fork/exec concurrente; no se presenta como solución de esa carrera en aplicaciones multihilo.

10. Programa en C: publicar o cancelar un temporal

Este ensayo exige un directorio nuevo, crea estado.txt con viejo y prepara un temporal de seis bytes con nuevo en ese mismo directorio. Cierra el temporal después de comprobar su contenido y antes de renombrarlo. Mantiene abierto el destino anterior para comparar las dos versiones después de publicar.

El modo opcional cancelar no publica: conserva el destino viejo y retira solo el temporal propio. La limpieza distingue si el nombre temporal todavía existe o si ya fue consumido por rename. Un error posterior a publicar se informa sin borrar el destino nuevo ni afirmar que no hubo cambios.

publicar-temporal.c · preparar completamente, publicar una entrada o cancelar
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <stdint.h>
#include "temporal-comun.h"

int main(int argc, char **argv)
{
    if ((argc != 2 && argc != 3) ||
        (argc == 3 && strcmp(argv[2], "cancelar"))) {
        fprintf(stderr, "Uso: %s DIRECTORIO_NUEVO [cancelar]\n", argv[0]);
        return 1;
    }
    int cancelar = argc == 3;
    int dir = -1, viejo = -1, temporal = -1, lector = -1;
    int nombrado = 0, publicado = 0, estado = 1;
    char *plantilla = NULL;
    const char sufijo[] = "/.estado-XXXXXX";
    size_t largo = strlen(argv[1]);
    struct stat id_viejo, id_nuevo, consulta;
    if (largo > SIZE_MAX - sizeof sufijo) {
        fputs("Ruta demasiado larga para representarla\n", stderr);
        return 1;
    }
    if (mkdir(argv[1], 0700) == -1) { perror("mkdir nuevo"); return 1; }
    dir = open(argv[1], O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir == -1) goto error;
    viejo = openat(dir, "estado.txt", O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
    if (viejo == -1 || escribir_todo(viejo, "viejo\n") == -1 ||
        fstat(viejo, &id_viejo) == -1) goto error;
    if (foto("antes", viejo, 1, &id_viejo) == -1) goto error;
    plantilla = malloc(largo + sizeof sufijo);
    if (!plantilla) goto error;
    memcpy(plantilla, argv[1], largo);
    memcpy(plantilla + largo, sufijo, sizeof sufijo);
    temporal = mkstemp(plantilla);
    if (temporal == -1) goto error;
    nombrado = 1;
    if (fcntl(temporal, F_SETFD, FD_CLOEXEC) == -1 ||
        escribir_todo(temporal, "nuevo\n") == -1 ||
        comprobar(temporal, "nuevo\n") == -1 ||
        fstat(temporal, &id_nuevo) == -1) goto error;
    if (foto("preparado", temporal, 1, &id_nuevo) == -1) goto error;
    if (cerrar(&temporal) == -1) goto error;
    if (cancelar) {
        if (comprobar(viejo, "viejo\n") == -1 ||
            foto("cancelado", viejo, 1, &id_viejo) == -1) goto error;
        puts("publicacion=cancelada destino=viejo");
        estado = 0;
        goto fin;
    }
    if (renameat(dir, plantilla + largo + 1, dir, "estado.txt") == -1) goto error;
    nombrado = 0;
    publicado = 1;
    lector = openat(dir, "estado.txt", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (lector == -1 || fstat(lector, &consulta) == -1) goto error;
    if (mismos(&id_viejo, &consulta) || !mismos(&id_nuevo, &consulta)) {
        fputs("Identidad inesperada después de publicar\n", stderr);
        goto fin;
    }
    if (comprobar(viejo, "viejo\n") == -1 || comprobar(lector, "nuevo\n") == -1 ||
        foto("lector_viejo", viejo, 0, &id_viejo) == -1 ||
        foto("lector_nuevo", lector, 1, &id_nuevo) == -1) goto error;
    puts("publicacion=realizada lector_anterior=viejo nueva_apertura=nuevo");
    estado = 0;
    goto fin;
error:
    perror("operacion de publicación");
fin:
    if (cerrar(&temporal) == -1) { perror("close temporal"); estado = 1; }
    if (nombrado && unlinkat(dir, plantilla + largo + 1, 0) == -1) {
        perror("retirar temporal propio");
        estado = 1;
    }
    if (cerrar(&lector) == -1) { perror("close lector"); estado = 1; }
    if (cerrar(&viejo) == -1) { perror("close viejo"); estado = 1; }
    if (cerrar(&dir) == -1) { perror("close dir"); estado = 1; }
    free(plantilla);
    if (fflush(stdout) == EOF) { perror("fflush stdout"); estado = 1; }
    if (estado && publicado)
        fputs("El reemplazo ya ocurrió; no se revierte el destino\n", stderr);
    return estado;
}
terminal · publicación y cancelación sobre árboles distintos
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 publicar-temporal.c -o publicar-temporal
$ ./publicar-temporal publicado
$ ./publicar-temporal cancelado cancelar

El descriptor viejo observa nlink cero después del reemplazo, pero sigue leyendo viejo. La nueva apertura identifica el objeto que era temporal y lee nuevo. En cancelado queda estado.txt con viejo y ningún .estado-* del ensayo.

La ruta usada para mkstemp y el descriptor dir deben seguir correspondiendo al mismo directorio. El programa lo asegura como condición de su árbol privado y estable; no pretende resistir actores que renombren los padres durante el ensayo. Tampoco conserva automáticamente propietario, ACL u otros atributos de un destino previo: el nuevo objeto tiene sus propios metadatos.

Este programa no llama a fsync. Su éxito comprueba contenido e identidad durante la ejecución y publicación por rename. La persistencia del archivo y de la entrada del directorio se tratará explícitamente en el tema 35; cerrar sin errores no sustituye ese protocolo.

11. Laboratorio y comprobaciones

Usa Linux o WSL con GCC y Python 3. En WSL trabaja preferentemente dentro del directorio personal del sistema Linux. Guarda el encabezado y los dos fuentes en un directorio privado nuevo antes de compilar. No se necesita ejecutar como administrador ni tocar archivos de otros procesos.

terminal · crear un espacio de trabajo y compilar
$ LAB25=$(mktemp -d "$HOME/tema25.XXXXXX")
$ cd -- "$LAB25"
$ findmnt -T .
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 temporal-sin-nombre.c -o temporal-sin-nombre
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 publicar-temporal.c -o publicar-temporal

Los ensayos anteriores pueden ejecutarse a mano sobre nombres nuevos. El verificador siguiente crea sus propios directorios con sufijos distintos y ejecuta ambos programas, de modo que no depende de que publicado o cancelado sigan libres.

Verificar identidad y limpieza por fase

verificar-temporales.py · lecturas, contadores y resultado de la publicación
from pathlib import Path
import os
import subprocess
import tempfile

def ejecutar(*args):
    return subprocess.run(args, capture_output=True, text=True)

def fotos(texto):
    resultado = {}
    for linea in texto.splitlines():
        if " dev=" in linea:
            fase, resto = linea.split(" ", 1)
            resultado[fase] = dict(c.split("=", 1) for c in resto.split())
    return resultado

def identidad(foto):
    return foto["dev"], foto["ino"]

antes = set(Path(".").glob(".sin-nombre-*"))
p = ejecutar("./temporal-sin-nombre")
assert p.returncode == 0, p.stderr
f = fotos(p.stdout)
assert list(f) == ["creado", "duplicado", "sin_nombre", "solo_duplicado"]
assert [int(d["enlaces"]) for d in f.values()] == [1, 1, 0, 0]
assert len({identidad(d) for d in f.values()}) == 1
assert all(int(d["bytes"]) == len(b"contenido temporal\n") for d in f.values())
assert "lectura=correcta" in p.stdout
assert set(Path(".").glob(".sin-nombre-*")) == antes

base = Path(tempfile.mkdtemp(prefix="prueba-temporales-", dir="."))
destino = base / "publicado"
p = ejecutar("./publicar-temporal", str(destino))
assert p.returncode == 0, p.stderr
f = fotos(p.stdout)
assert list(f) == ["antes", "preparado", "lector_viejo", "lector_nuevo"]
assert identidad(f["antes"]) == identidad(f["lector_viejo"])
assert identidad(f["preparado"]) == identidad(f["lector_nuevo"])
assert identidad(f["antes"]) != identidad(f["preparado"])
assert int(f["lector_viejo"]["enlaces"]) == 0
assert int(f["lector_nuevo"]["enlaces"]) == 1
assert all(int(d["bytes"]) == 6 for d in f.values())
assert (destino / "estado.txt").read_bytes() == b"nuevo\n"
assert sorted(x.name for x in destino.iterdir()) == ["estado.txt"]

cancelado = base / "cancelado"
p = ejecutar("./publicar-temporal", str(cancelado), "cancelar")
assert p.returncode == 0, p.stderr
f = fotos(p.stdout)
assert list(f) == ["antes", "preparado", "cancelado"]
assert identidad(f["antes"]) == identidad(f["cancelado"])
assert int(f["cancelado"]["enlaces"]) == 1
assert (cancelado / "estado.txt").read_bytes() == b"viejo\n"
assert sorted(x.name for x in cancelado.iterdir()) == ["estado.txt"]

p = ejecutar("./publicar-temporal", str(destino))
assert p.returncode != 0 and p.stderr
assert (destino / "estado.txt").read_bytes() == b"nuevo\n"
p = ejecutar("./publicar-temporal", str(base / "invalido"), "otro-modo")
assert p.returncode != 0
assert not (base / "invalido").exists()

# Una comprobación independiente de lectores antiguos y nuevas aperturas.
viejo = base / "lectura.txt"
temporal = base / "preparado.txt"
viejo.write_bytes(b"antes\n")
with open(viejo, "rb") as lector:
    id_antes = os.fstat(lector.fileno())
    temporal.write_bytes(b"despues\n")
    os.replace(temporal, viejo)
    assert os.fstat(lector.fileno()).st_nlink == 0
    assert lector.read() == b"antes\n"
    assert viejo.read_bytes() == b"despues\n"
    s = viejo.stat()
    assert (s.st_dev, s.st_ino) != (id_antes.st_dev, id_antes.st_ino)

print("Correctos: referencias, nlink, identidad, publicación, cancelación y limpieza")
print("Árbol de pruebas conservado:", base)
terminal · ejecutar las comprobaciones completas
$ python3 verificar-temporales.py

Las comparaciones se hacen mientras el objeto antiguo sigue abierto, sin depender de que su número de inodo no se reutilice después. El verificador conserva su árbol de trabajo. No provoca cortes de energía, no certifica fallos de almacenamiento y no mide bytes recuperados al cerrar.

Observar por qué un temporal nombrado necesita limpieza

El siguiente ejemplo conserva deliberadamente un nombre después de cerrar el descriptor. Es un archivo nuevo de este laboratorio y su ruta se presenta para inspección; no se hace una limpieza basada en un comodín.

terminal · cerrar no retira el nombre de un mkstemp
$ python3 - <<'PY'
import os
import tempfile
from pathlib import Path
fd, nombre = tempfile.mkstemp(prefix="nombrado-", dir=".")
os.close(fd)
assert Path(nombre).is_file()
print("Nombre conservado tras close:", nombre)
os.unlink(nombre)  # solo el archivo exacto recién creado por este ensayo
assert not Path(nombre).exists()
PY

Tempfile de Python se usa solo para ilustrar el mismo contrato de nombre y descriptor en el laboratorio. El programa principal estudia la interfaz mkstemp de C y su manejo explícito de errores.

12. Errores y estado después de cada fase

FaseFallo posibleEstado que hay que distinguir
Crear temporalPermisos, cuota, espacio, descriptores o plantilla inválida.No asumir que se obtuvo un descriptor ni un nombre propio válido.
Escribir o cerrar antes de publicarTransferencia incompleta o error de E/S.El destino anterior debe conservarse; el temporal propio puede requerir limpieza.
Retirar el temporal sin nombre públicoFalla unlink.No afirmar que quedó anónimo; su nombre puede seguir existiendo.
Publicar con renameAcceso, tipo, montaje o condiciones del sistema.Informar resultado y revisar el contrato del sistema; no anunciar publicación exitosa.
Comprobar o cerrar después de publicarFallo posterior al reemplazo.El destino puede ser ya el nuevo. No se debe borrar como si aún fuera un temporal.
Entregar el informeFalla stdout.Puede haberse completado el trabajo aunque no se haya entregado todo el diagnóstico.

En sistemas remotos puede haber incertidumbre sobre si una operación se ejecutó antes de que el cliente recibiera un error. El patrón de este tema se verifica en el sistema local del laboratorio; una aplicación distribuida necesita un protocolo que contemple esos resultados inciertos.

La rutina de limpieza no es una transacción inversa universal. Si ya se sustituyó un destino, retirarlo para «deshacer» un fallo de informe podría destruir el resultado publicado y dejar el nombre ausente. El ejemplo conserva el nuevo estado y señala que el reemplazo ocurrió.

Cancelar antes de publicar es distinto de fallar después de publicar. Una interfaz de aplicación debe reflejar esa diferencia, en vez de devolver solo una frase ambigua que sugiera que cualquier error dejó todo intacto.

13. Simuladores de referencias y publicación

Retirar una entrada, duplicar y volver a abrir

El primer modelo comienza con trabajo → inodo 81, contenido viejo y fd=3 abierto. La tabla distingue descriptores de descripciones abiertas. Cada apertura independiente obtiene un offset propio; una duplicación comparte descripción y posición.

La vida del objeto no es la vida del nombre

nombre y referencias del proceso
Descriptores abiertos
fdDescripciónInodoOffset
Archivos regulares retenidos
InodonlinkDescripciones abiertasContenido

Duplica fd=3 y lee por ambos para ver el offset compartido. Retira trabajo: nlink pasa a cero y una apertura nueva falla, pero los descriptores continúan leyendo viejo. Recrea trabajo y ábrelo: aparece otro objeto con nuevo y otra descripción. Los descriptores antiguos no se redirigen.

El modelo admite hasta 16 descriptores simultáneos y usa números crecientes para facilitar el seguimiento; un kernel real puede reutilizar números cerrados. Solo representa archivos regulares, una entrada posible, referencias abiertas y offsets. No incluye mapas de memoria, fallos de E/S, permisos, hilos ni bytes físicos liberados.

Preparar, publicar o cancelar

El segundo modelo representa la sustitución de estado.txt, que inicialmente publica viejo. Un lector mantiene abierta esa versión. Avanza por creación del temporal, escritura, comprobación y cierre, y publicación. Puedes simular un fallo antes de publicar; las condiciones del escenario se fijan al prepararlo.

El nombre público cambia en una sola fase

estado de la publicación y referencias
Nombre o referenciaObjetoContenido observado

Antes de la última fase, una apertura actual del destino sigue viendo viejo. Después ve nuevo, mientras el lector anterior conserva 81. No hay una fase con estado.txt ausente. Cancelar después de publicar no retira el resultado. Si se cierra el lector anterior tras el reemplazo, el objeto viejo queda sin referencias en este modelo.

El fallo de rename se modela como un fallo local anterior al cambio, seguido de limpieza correcta del temporal. No representa respuestas inciertas de sistemas remotos ni fallos de la propia limpieza. Los dos simuladores son independientes y no acceden al sistema de archivos del equipo.

14. Atomicidad, concurrencia y persistencia

La palabra «atómico» necesita indicar qué cambio se considera indivisible. Rename permite sustituir una entrada sin publicar un estado intermedio en el que ese destino existente falte. No promete que varios archivos cambien juntos ni que dos lecturas separadas obtengan una instantánea común.

PropiedadQué aporta el patrón de este temaQué requiere más trabajo
Contenido preparado antes de ser visible por el nombre públicoEscritura completa y comprobada en otro objeto.Evitar modificaciones posteriores sobre la versión publicada.
Sustitución de un nombreRename en el montaje admitido, sin unlink previo del destino.Definir condiciones de tipo, acceso y sustitución permitida.
Varios escritoresCada uno puede preparar un temporal distinto.Bloqueo, comparación de versión u otra coordinación si no se acepta que el último reemplazo gane.
Persistencia tras perder energíaNo se certifica con estos programas.Sincronización, orden y tratamiento de errores según el sistema de archivos.

Si dos escritores leen la misma versión, calculan cambios y publican sin coordinarse, una publicación puede perder el resultado de la otra. Que cada rename sea atómico no resuelve el problema de actualizaciones perdidas ni constituye una transacción de lectura, modificación y escritura.

La sincronización de un archivo tampoco implica automáticamente la persistencia de su nombre. La referencia de fsync(2) distingue el archivo y su directorio. El tema 35 desarrollará el patrón con escritura, fsync del temporal, rename y fsync del directorio, incluyendo qué significa fallar en cada punto.

No se debe añadir una afirmación de durabilidad a partir de que el programa haya podido volver a leer el archivo inmediatamente. Esa lectura puede usar el estado en memoria del sistema, y un ensayo sin cortes no valida la recuperación ante uno.

Qué conviene registrar en una prueba

  1. Identidades y enlaces. Qué objeto conserva cada descriptor y qué nombres siguen existiendo.
  2. Fase alcanzada. Creación, escritura, cierre, publicación, cancelación o limpieza.
  3. Resultado de cada operación. Transferencias completas, errno y fallos de salida cuando corresponda.
  4. Alcance del entorno. Montaje local o remoto y presencia de otros escritores.
  5. Propiedad comprobada. Lectura durante la ejecución, cambio de nombre o persistencia, sin intercambiarlas.

15. Resumen

16. Preguntas de repaso y práctica

1. ¿Unlink y close hacen lo mismo?

No. Unlink retira un nombre del directorio; close retira una referencia abierta del proceso. La vida del objeto depende de las referencias que queden.

2. ¿Un archivo abierto puede tener st_nlink cero?

Sí. Puede perder todos sus nombres mientras una referencia abierta lo mantiene. Fstat sobre el descriptor sigue consultando ese objeto.

3. ¿Abrir de nuevo la ruta eliminada recupera ese objeto?

No. La nueva apertura vuelve a consultar el espacio de nombres. Puede fallar o alcanzar otro objeto si alguien recreó la ruta.

4. ¿Duplicar un descriptor crea un enlace duro?

No. Añade una referencia de la tabla de descriptores a una descripción abierta. El contador de nombres duros no aumenta.

5. ¿Por qué un duplicado puede compartir el offset?

Porque ambos descriptores se refieren a la misma descripción de archivo abierto. Dos llamadas open independientes pueden tener posiciones distintas.

6. ¿Borrar el nombre libera de inmediato todos los bloques?

No necesariamente. Puede haber otros nombres, referencias abiertas, mapas u otras condiciones y trabajo diferido del sistema.

7. ¿Por qué du puede dejar de ver un archivo y df seguir mostrando su uso?

Du recorre nombres; un objeto sin nombre puede seguir retenido por un proceso y consumir capacidad del sistema de archivos.

8. ¿Conviene quitar el destino antes de renombrar el temporal?

No para el patrón de sustitución mostrado. Introduciría un intervalo de ausencia que rename sobre un destino existente no necesita.

9. ¿El lector anterior pasa automáticamente al nuevo archivo?

No. Su descriptor mantiene el objeto que abrió. Una nueva apertura del nombre publicado puede obtener otro objeto.

10. ¿Rename garantiza que nadie modifique después los datos?

No. La sustitución de una entrada no hace inmutables los objetos. Se necesita una política de escritores para conservar versiones estables.

11. ¿Por qué crear el temporal junto al destino?

Para que la publicación no dependa de trasladarlo desde un montaje diferente. Rename entre montajes puede fallar con EXDEV.

12. ¿Mkstemp devuelve solo un nombre disponible?

No. Crea el archivo de forma exclusiva y devuelve su descriptor ya abierto, además de modificar la plantilla con el nombre elegido.

13. ¿Puede pasarse un literal como plantilla de mkstemp?

No. La función modifica las seis X finales; debe recibir memoria modificable como un arreglo de caracteres.

14. ¿Cerrar un mkstemp nombrado elimina su entrada?

No. La aplicación debe decidir si retira, conserva o publica el nombre. Tmpfile ofrece otro contrato.

15. ¿Por qué comprobar close antes de publicar?

Porque también puede informar un error. El patrón del ejemplo no publica si detecta un fallo en la preparación o el cierre del temporal.

16. ¿Un error después de publicar permite borrar el destino como limpieza?

No. Ese nombre puede ser ya el resultado publicado. Retirarlo podría destruir el nuevo estado; hay que informar la fase alcanzada.

17. ¿El archivo nuevo conserva automáticamente los atributos del anterior?

No. Tiene sus propios metadatos. Una aplicación debe decidir y comprobar qué atributos deben trasladarse antes de publicar.

18. ¿Dos rename atómicos evitan actualizaciones perdidas?

No. Dos escritores pueden calcular desde la misma versión y publicar resultados que se sustituyen. La coordinación del protocolo es otra propiedad.

19. ¿Cerrar sin errores garantiza supervivencia ante un corte?

No por sí solo. Se debe razonar sobre sincronización del archivo, del directorio y errores de cada fase. El tema 35 profundiza ese protocolo.

20. ¿Se prueba la liberación consultando un descriptor después de cerrarlo?

No. El entero puede reutilizarse y ya no mantiene la referencia anterior. La comprobación del ensayo consiste en cerrar correctamente las referencias propias, sin reutilizarlas.

Para practicar

  1. Dibuja nombres, inodos, descripciones abiertas y descriptores antes y después de unlink.
  2. Ejecuta temporal-sin-nombre y explica los contadores 1, 1, 0, 0 sin confundirlos con la cantidad de descriptores.
  3. En el simulador, duplica un descriptor y comprueba el offset común al alternar lecturas.
  4. Retira y recrea trabajo; compara los objetos de las referencias antiguas y de una apertura nueva.
  5. Ejecuta publicar-temporal y contrasta identidad, nlink y contenido del lector anterior y el nuevo.
  6. Ejecuta el modo cancelar y comprueba que solo queda el destino viejo.
  7. Prueba un directorio de destino ya existente y verifica que el programa lo rechaza sin modificar sus archivos.
  8. Explica qué residuo puede quedar si una aplicación termina después de mkstemp y antes de retirar o publicar el nombre.
  9. En el segundo simulador, provoca fallo de escritura y fallo de rename; registra qué objeto publica el destino en cada caso.
  10. Describe una situación con dos escritores en la que se pierde una actualización aunque cada rename sea atómico.
  11. Compara mkstemp, tmpfile y el patrón de unlink inmediato según necesidad de nombre y responsabilidad de limpieza.
  12. Separa qué prueba una lectura inmediata correcta de lo que exigiría validar recuperación después de un corte.

En el tema 26 comenzaremos el bloque de permisos: propietario, grupo y otros, los bits rwx y las decisiones de acceso sobre archivos y directorios.