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

Tema 24

Enlaces simbólicos y comparación con los duros

Un enlace simbólico guarda una ruta que se interpreta al usarla; no añade otro nombre directamente al inodo del destino. Crearemos enlaces relativos y absolutos, leeremos su texto sin confundirlo con el contenido del archivo y observaremos cómo se rompen o cambian de destino al mover nombres. Dos programas en C, un laboratorio y dos simuladores permitirán comparar identidad, contadores, límites y usos frente a los enlaces duros.

1. Un objeto que contiene una ruta

En el tema 23 creamos varios nombres para un mismo archivo mediante enlaces duros. Un enlace simbólico establece otra relación: su entrada identifica un objeto de tipo enlace, cuyo contenido es el texto de una ruta. Cuando una operación decide seguirlo, esa ruta se incorpora a la resolución.

Enlace simbólico o symlink: objeto del sistema de archivos que almacena una ruta para resolverla cuando se sigue el enlace. Tiene identidad propia y puede existir aunque su destino no exista o no sea accesible.

dos objetos · el texto se interpreta durante la resolución
directorio: "atajo" → inodo 90, tipo enlace
                          │
                          └─ texto: "datos/informe.txt"

Al seguir atajo:
    resolver datos/informe.txt → inodo 81, tipo regular → contenido

Crear atajo no incrementa nlink del inodo 81.

La creación no comprueba que la ruta guardada alcance un archivo. Ese comportamiento permite preparar un nombre que será válido más adelante, pero también permite enlaces rotos desde el primer momento.

El tamaño lógico consultado sobre el enlace corresponde a los bytes de su texto, sin incluir un terminador nulo. El tamaño de su destino es otra propiedad. Un sistema de archivos puede almacenar enlaces cortos dentro de sus metadatos; no se deduce un bloque de datos dedicado por el solo hecho de tener texto.

La descripción de estos objetos y su tratamiento por las llamadas del sistema se recoge en symlink(7). En este tema trabajaremos con enlaces ordinarios, no con los magic links de ciertos sistemas virtuales como proc.

2. Comparación con un enlace duro

PropiedadEnlace duro adicionalEnlace simbólico
RelaciónOtro nombre del mismo objeto.Un objeto distinto con una ruta como contenido.
Identidad consultada sin seguimiento finalLa del archivo compartido.La del propio enlace.
Contador del destinoAumenta al añadir el nombre.No aumenta por crear el enlace simbólico.
Destino inexistenteNo se puede enlazar un origen que no existe en el caso ordinario.El enlace puede crearse y permanecer roto.
DirectoriosNo se crean relaciones arbitrarias desde aplicaciones ordinarias.Puede apuntar a un directorio.
Límites de montajeNo conecta montajes distintos mediante el enlace.Su ruta puede atravesar montajes al resolverse.
Renombrar el nombre del destinoNo rompe los demás nombres del objeto.Puede dejar de resolver la cadena guardada.
Recrear el nombre del destinoNo redirige los enlaces del objeto anterior.Puede hacer que la misma cadena alcance otro objeto.

Ambas formas consumen recursos del sistema de archivos. El enlace duro necesita representar otra entrada; el simbólico necesita además su propio objeto y texto. Ninguna de las dos equivale por sí sola a una copia de seguridad del contenido.

Si stat sigue un simbólico y muestra la identidad del destino, eso no convierte su entrada en un enlace duro. Para reconocer la relación hay que separar la consulta del enlace y la consulta con seguimiento.

3. Destinos relativos y absolutos

La ubicación del enlace y el texto almacenado son datos diferentes. Si el texto es relativo, se interpreta desde el directorio que contiene el enlace. No se interpreta desde el cwd del proceso que lo creó ni desde el de quien intenta abrirlo.

ejemplo · el directorio padre del enlace determina la base
/proyecto/
├─ datos/nota.txt
└─ accesos/nota → ../datos/nota.txt

Abrir /proyecto/accesos/nota desde cualquier cwd:
    base del destino relativo = /proyecto/accesos
    resultado = /proyecto/datos/nota.txt

Un texto absoluto empieza desde la raíz visible del proceso al resolverlo. No significa que el enlace contenga una referencia permanente al volumen o al inodo. En otro contenedor o espacio de montajes, la misma cadena absoluta puede resolver de otra manera.

Qué se conserva al trasladar un árbol

Un enlace relativo puede seguir funcionando si se mueve todo el árbol y se conserva la relación entre sus directorios. Si solo se mueve el enlace a otro padre, la misma cadena puede resolver un destino diferente o dejar de resolver. Un absoluto no se adapta automáticamente al nuevo prefijo del árbol.

mover solo el enlace · el texto no cambia
A/dato.txt contiene "uno"       B/dato.txt contiene "dos"
A/movil → dato.txt

rename A/movil → B/movil

Antes: A/movil resuelve A/dato.txt
Después: B/movil resuelve B/dato.txt
readlink devuelve "dato.txt" en ambos casos.

El tema 22 explicó por qué no basta con cancelar componentes como texto para reproducir la resolución. Un .. en un destino relativo parte del padre del enlace y puede interactuar con otros enlaces o montajes del recorrido.

4. Crear y observar desde la terminal

En GNU/Linux, ln -s crea un enlace simbólico. Usaremos -T para que el segundo argumento sea el nombre exacto que queremos crear, sin interpretarlo como un directorio de destino al que añadir otro nombre. Todos los nombres de creación son nuevos.

terminal · árbol privado y enlaces relativos
$ LAB24=$(mktemp -d "$HOME/tema24.XXXXXX")
$ cd -- "$LAB24"
$ mkdir datos accesos
$ (set -o noclobber; printf 'uno\n' > datos/nota.txt)
$ ln -sT -- ../datos/nota.txt accesos/relativo
$ ln -sT -- ../datos/ausente.txt accesos/roto
$ ln -sT -- ../datos accesos/carpeta
$ ls -ld -- accesos/relativo accesos/roto accesos/carpeta
$ readlink -- accesos/relativo
$ cat -- accesos/relativo
$ cat -- accesos/carpeta/nota.txt

La creación de accesos/roto tiene éxito aunque su destino no exista. El texto pasado como primer argumento se almacena tal como lo proporcionamos: no se ajusta automáticamente por estar ejecutando ln desde el directorio del laboratorio.

terminal · enlace absoluto y un nombre duro de comparación
$ ln -sT -- "$LAB24/datos/nota.txt" accesos/absoluto
$ ln -T -- datos/nota.txt datos/duro.txt
$ stat -c 'propio %n tipo=%F dev=%d ino=%i enlaces=%h bytes=%s' -- accesos/relativo datos/nota.txt datos/duro.txt
$ stat -Lc 'seguido %n tipo=%F dev=%d ino=%i enlaces=%h bytes=%s' -- accesos/relativo

Los dos nombres duros comparten identidad y elevan a dos el contador del archivo. El simbólico tiene otra identidad; con seguimiento se muestra el objeto alcanzado. GNU stat sin -L consulta el enlace en estos casos. No se generaliza esa opción predeterminada a cualquier herramienta.

Repetir una creación sobre un enlace existente, incluso roto, falla con EEXIST. Que una comprobación que sigue enlaces no encuentre el destino no significa que el nombre del enlace esté libre.

5. stat, lstat y lectura del enlace

InterfazComponente final simbólicoInformación obtenida
statSe sigue.Atributos del objeto resuelto, o error del recorrido.
lstatNo se sigue en una ruta ordinaria sin barra final.Atributos del enlace mismo.
fstatat con AT_SYMLINK_NOFOLLOWNo se sigue el final según ese contrato.Atributos relativos a un directorio inicial explícito.
readlink / readlinkatSe lee el propio enlace final.Bytes de la ruta almacenada, no el contenido del destino.

Los enlaces de los componentes intermedios pueden seguirse aunque no se siga el último. Una barra final también impone una condición de directorio y puede hacer que la consulta siga la resolución del enlace para satisfacerla. No añadas / al nombre de un enlace si pretendes observar simplemente el objeto enlace.

Un lstat correcto seguido de un stat que devuelve ENOENT es evidencia, en un árbol estable, de un enlace cuyo destino no se encuentra. Si el segundo devuelve EACCES, hay una denegación de acceso; no es correcto concluir sin más que el destino no existe.

La referencia de stat(2) distingue las consultas. Un programa debe conservar qué interfaz utilizó antes de interpretar el número de inodo, el tipo o el tamaño que observó.

6. Enlaces rotos, cadenas y ciclos

Un enlace roto sigue siendo un objeto con nombre y texto. Puede copiarse como enlace, renombrarse, leerse con readlink y eliminarse. Si aparece después un objeto en la ruta requerida, puede comenzar a funcionar sin modificar el enlace.

terminal · el mismo enlace pasa de roto a resoluble
$ readlink -- accesos/roto
$ stat -- accesos/roto
$ stat -L -- accesos/roto
$ (set -o noclobber; printf 'aparecio\n' > datos/ausente.txt)
$ cat -- accesos/roto

La consulta con seguimiento antes de crear el destino debe fallar. Es un caso esperado e independiente de las órdenes posteriores. El nombre «roto» se conserva como etiqueta del ensayo aunque después su destino exista.

Una cadena como primero → segundo → datos/nota.txt requiere expandir varios enlaces. Un ciclo como a → b y b → a no proporciona un destino final. Linux limita a 40 los enlaces seguidos durante una resolución ordinaria y puede devolver ELOOP al excederlo.

Readlink lee un solo texto: readlink primero devuelve segundo, no resuelve automáticamente toda la cadena. Herramientas con opciones de canonicalización realizan otra tarea y tienen sus propias reglas de existencia.

Un enlace que resuelve hoy no garantiza que siga haciéndolo después. La ruta se vuelve a interpretar en cada operación que la sigue, con el estado de nombres, permisos y montajes que encuentre entonces.

7. Mover, reemplazar y eliminar nombres

Renombrar el destino puede romper un simbólico que guardaba su nombre anterior. Un enlace duro al mismo objeto continúa accediendo a él. Si se recrea el nombre anterior con otro archivo, el simbólico puede pasar a ese nuevo objeto mientras el duro sigue en el viejo.

identidad y ruta · tres momentos del mismo conjunto
Inicial:
    dato.txt → inodo 81
    duro.txt → inodo 81
    simbolico → texto "dato.txt", inodo propio 90

Renombrar dato.txt a guardado.txt:
    guardado.txt → 81; duro.txt → 81
    simbolico → "dato.txt" → ENOENT

Crear otro dato.txt, inodo 95:
    simbolico → "dato.txt" → 95
    guardado.txt y duro.txt siguen en 81

Quitar el enlace no es quitar su destino

unlink o una orden de retirada sobre el nombre del enlace, sin barra final, elimina esa entrada y su objeto si no quedan otras referencias. No ejecuta la eliminación sobre el archivo al que apuntaba. Una orden recursiva o con opciones distintas exige revisar su propio contrato; el laboratorio no necesita eliminación recursiva.

Reemplazar el nombre del destino puede hacer que un simbólico siga la versión nueva, mientras un descriptor ya abierto continúa en el objeto anterior. Reemplazar el enlace mismo es otra operación: cambia el objeto que representa el nombre del enlace. Estos casos se conectan con rename y la vida de archivos abiertos del tema 25.

El texto de un enlace ordinario no se reescribe con write sobre el enlace: una apertura habitual lo sigue. Para cambiar qué texto publica un nombre se prepara otro enlace y se sustituye la entrada según la política requerida.

8. symlink y readlink desde C

interfaces · creación y lectura sin resolver el destino
#include <unistd.h>
#include <fcntl.h>

int symlink(const char *texto_destino, const char *nombre_enlace);
int symlinkat(const char *texto_destino, int dirfd, const char *nombre_enlace);

ssize_t readlink(const char *nombre_enlace, char *buf, size_t capacidad);
ssize_t readlinkat(int dirfd, const char *nombre_enlace,
                   char *buf, size_t capacidad);

En symlinkat, dirfd establece dónde resolver un nombre relativo para crear el enlace. No cambia ni resuelve el texto_destino: se guarda como fue recibido. La creación devuelve cero o -1 con errno, y no sobrescribe un nombre existente.

Readlink devuelve cuántos bytes colocó en el búfer. No añade un terminador nulo. Si el resultado iguala la capacidad, el texto puede haber quedado truncado; no hay un error separado que lo anuncie. Nunca se debe pasar ese búfer sin terminar a printf con %s.

frontera de lectura · la igualdad no demuestra que el texto esté completo
Texto almacenado: "dato.txt"  (8 bytes)

capacidad=4 → devuelve 4 → "dato"        texto incompleto
capacidad=8 → devuelve 8 → "dato.txt"    todavía no prueba ausencia de más bytes
capacidad=9 → devuelve 8 → "dato.txt"    resultado menor que capacidad

En ningún caso readlink añade '\0'.

Un tamaño obtenido con lstat puede ayudar a dimensionar, pero no debe sustituir la comprobación de la lectura: el enlace pudo cambiar entre llamadas y algunos sistemas virtuales no ofrecen ese tamaño como se esperaría de un enlace ordinario. El lector siguiente crece de forma acotada y exige un resultado menor que la capacidad.

Los contratos se describen en symlink(2) y readlink(2). El límite de memoria que usaremos es una decisión del ejemplo y no un tamaño máximo universal de enlaces.

9. Programa en C: leer sin truncar silenciosamente

El lector empieza con 128 bytes de capacidad, duplica si la lectura la llena y se detiene si llega a un MiB sin poder confirmar que el texto cabe. Reserva un byte adicional para terminar la cadena, aunque la presentación usa la longitud explícita y escapa bytes no imprimibles.

Primero consulta el enlace con lstat, después lee su texto y por último intenta consultar el destino con stat. Son observaciones separadas sobre una ruta que podría cambiar. El laboratorio supone un árbol estable: el crecimiento del búfer evita aceptar truncamiento, pero no convierte la secuencia en una instantánea.

ver-enlace.c · texto completo o diagnóstico, con destino separado
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

static void citar(const char *p, size_t n)
{
    putchar('"');
    for (size_t i = 0; i < n; ++i) {
        unsigned c = (unsigned char)p[i];
        if (c >= 32 && c <= 126 && c != '"' && c != '\\')
            putchar((int)c);
        else printf("\\x%02x", c);
    }
    putchar('"');
}

static const char *etiqueta(int e)
{
    switch (e) {
        case ENOENT: return "ENOENT";
        case ENOTDIR: return "ENOTDIR";
        case EACCES: return "EACCES";
        case ELOOP: return "ELOOP";
        default: return "OTRO";
    }
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Uso: %s ENLACE_SIN_BARRA_FINAL\n", argv[0]);
        return 1;
    }
    struct stat propio, destino;
    if (lstat(argv[1], &propio) == -1) { perror("lstat"); return 1; }
    if (!S_ISLNK(propio.st_mode)) {
        fputs("El objeto final consultado no es un enlace simbólico\n", stderr);
        return 1;
    }
    enum { LIMITE = 1024 * 1024 };
    size_t capacidad = 128;
    char *b = NULL;
    int estado = 1;
    for (;;) {
        char *nuevo = realloc(b, capacidad + 1);
        if (!nuevo) { perror("realloc"); goto fin; }
        b = nuevo;
        ssize_t n;
        do { n = readlink(argv[1], b, capacidad); }
        while (n == -1 && errno == EINTR);
        if (n == -1) { perror("readlink"); goto fin; }
        if ((size_t)n < capacidad) {
            b[n] = '\0';
            printf("enlace dev=%ju ino=%ju enlaces=%ju size_lstat=%jd leidos=%zd texto=",
                   (uintmax_t)propio.st_dev, (uintmax_t)propio.st_ino,
                   (uintmax_t)propio.st_nlink, (intmax_t)propio.st_size, n);
            citar(b, (size_t)n);
            putchar('\n');
            break;
        }
        if (capacidad == LIMITE) {
            fputs("No se confirma texto completo dentro del límite del ejemplo\n", stderr);
            goto fin;
        }
        capacidad *= 2;
    }
    if (stat(argv[1], &destino) == -1) {
        int e = errno;
        printf("destino_error=%s errno=%d\n", etiqueta(e), e);
    } else {
        char tipo = S_ISREG(destino.st_mode) ? 'f' : S_ISDIR(destino.st_mode) ? 'd' : 'o';
        printf("destino dev=%ju ino=%ju tipo=%c enlaces=%ju bytes=%jd\n",
               (uintmax_t)destino.st_dev, (uintmax_t)destino.st_ino,
               tipo, (uintmax_t)destino.st_nlink, (intmax_t)destino.st_size);
    }
    estado = 0;
fin:
    free(b);
    if (fflush(stdout) == EOF) estado = 1;
    return estado;
}
terminal · compilar e inspeccionar enlaces existentes
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 ver-enlace.c -o ver-enlace
$ ./ver-enlace accesos/relativo
$ ./ver-enlace accesos/roto

El estado cero significa que se inspeccionó el enlace y se entregó el informe, incluso si la sección del destino contiene un error. Para saber si este se resolvió hay que examinar destino frente a destino_error. Un ciclo también puede leerse correctamente y producir ELOOP solo al seguirlo.

Pasar un archivo regular o un nombre inexistente es un error del uso del lector y devuelve 1. La salida escapada es una presentación de bytes, no JSON. No se exige que size_lstat coincida con leidos si otro proceso cambia el enlace entre llamadas.

10. Programa en C: mover y recrear destinos

El segundo programa crea un árbol privado con A y B. Ambos contienen un archivo llamado dato.txt, con contenidos distintos. Crea dos simbólicos relativos en A, mueve uno a B y verifica que conserva su identidad pero cambia el archivo alcanzado.

Después renombra el dato de A, observa el enlace restante roto, recrea dato.txt y comprueba que el mismo enlace llega al nuevo objeto. Un nombre duro conservado en la raíz sigue llegando al contenido anterior. Por último elimina el simbólico y verifica que su destino continúa existiendo.

ciclo-simbolico.c · texto estable, resolución dependiente de los nombres
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

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

static int crear(int padre, const char *texto)
{
    int fd = openat(padre, "dato.txt", O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
    if (fd == -1) return -1;
    size_t p = 0, n = strlen(texto);
    int error = 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;
        error = k == 0 ? EIO : errno;
        break;
    }
    if (close(fd) == -1 && error == 0) error = errno;
    if (error) { errno = error; return -1; }
    return 0;
}

static int contenido(int padre, const char *nombre, const char *esperado)
{
    int fd = openat(padre, nombre, O_RDONLY | O_CLOEXEC);
    if (fd == -1) return -1;
    char b[32];
    struct stat s;
    size_t n = strlen(esperado), p = 0;
    int error = 0;
    if (n > sizeof b) error = EOVERFLOW;
    else if (fstat(fd, &s) == -1) error = errno;
    else if (s.st_size != (off_t)n) error = EIO;
    while (!error && p < n) {
        ssize_t k = read(fd, b + p, n - p);
        if (k > 0) { p += (size_t)k; continue; }
        if (k == -1 && errno == EINTR) continue;
        error = k == 0 ? EIO : errno;
    }
    if (!error && memcmp(b, esperado, n)) error = EIO;
    if (close(fd) == -1 && error == 0) error = errno;
    if (error) { errno = error; return -1; }
    return 0;
}

static int texto_enlace(int padre, const char *nombre)
{
    char b[64];
    ssize_t n;
    do { n = readlinkat(padre, nombre, b, sizeof b); }
    while (n == -1 && errno == EINTR);
    if (n == -1) return -1;
    if (n != 8 || memcmp(b, "dato.txt", 8)) { errno = EIO; return -1; }
    return 0;
}

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

int main(int argc, char **argv)
{
    int raiz = -1, a = -1, b = -1, estado = 1;
    struct stat enlace, movido, viejo, actual;
    if (argc != 2) {
        fprintf(stderr, "Uso: %s DIRECTORIO_NUEVO\n", argv[0]);
        return 1;
    }
    if (mkdir(argv[1], 0700) == -1) { perror("mkdir raiz"); return 1; }
    raiz = open(argv[1], O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (raiz == -1) goto error;
    if (mkdirat(raiz, "A", 0700) == -1 || mkdirat(raiz, "B", 0700) == -1)
        goto error;
    a = openat(raiz, "A", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (a == -1) goto error;
    b = openat(raiz, "B", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (b == -1) goto error;
    if (crear(a, "uno\n") == -1 || crear(b, "dos\n") == -1) goto error;
    if (linkat(a, "dato.txt", raiz, "duro.txt", 0) == -1) goto error;
    if (fstatat(a, "dato.txt", &viejo, 0) == -1) goto error;
    if (symlinkat("dato.txt", a, "movil") == -1 ||
        symlinkat("dato.txt", a, "fijo") == -1) goto error;
    if (fstatat(a, "movil", &enlace, AT_SYMLINK_NOFOLLOW) == -1) goto error;
    if (!S_ISLNK(enlace.st_mode) || viejo.st_nlink != 2) goto inesperado;
    if (texto_enlace(a, "movil") == -1 || contenido(a, "movil", "uno\n") == -1)
        goto error;
    if (symlinkat("otro", a, "movil") != -1 || errno != EEXIST) goto inesperado;
    if (renameat(a, "movil", b, "movil") == -1) goto error;
    if (fstatat(b, "movil", &movido, AT_SYMLINK_NOFOLLOW) == -1) goto error;
    if (!mismos(&enlace, &movido)) goto inesperado;
    if (texto_enlace(b, "movil") == -1 || contenido(b, "movil", "dos\n") == -1)
        goto error;
    puts("mover_enlace=misma_identidad texto=dato.txt destino=dos");

    if (renameat(a, "dato.txt", a, "guardado.txt") == -1) goto error;
    if (fstatat(a, "fijo", &actual, 0) != -1 || errno != ENOENT) goto inesperado;
    if (texto_enlace(a, "fijo") == -1 ||
        fstatat(a, "fijo", &actual, AT_SYMLINK_NOFOLLOW) == -1) goto error;
    puts("enlace_roto=consultable texto=dato.txt");
    if (crear(a, "tres\n") == -1 || contenido(a, "fijo", "tres\n") == -1 ||
        contenido(raiz, "duro.txt", "uno\n") == -1) goto error;
    if (fstatat(a, "fijo", &actual, 0) == -1) goto error;
    if (mismos(&viejo, &actual)) goto inesperado;
    puts("recrear_destino=otro_objeto simbolico=tres duro=uno");
    if (unlinkat(a, "fijo", 0) == -1 || contenido(a, "dato.txt", "tres\n") == -1)
        goto error;
    puts("retirar_enlace=destino_conservado EEXIST=correcto");
    estado = 0;
    goto fin;
inesperado:
    fputs("Identidad, tipo o resultado inesperado\n", stderr);
    goto fin;
error:
    perror("operacion del ensayo");
fin:
    if (cerrar(&b) == -1) { perror("close B"); estado = 1; }
    if (cerrar(&a) == -1) { perror("close A"); estado = 1; }
    if (cerrar(&raiz) == -1) { perror("close raiz"); estado = 1; }
    if (fflush(stdout) == EOF) estado = 1;
    return estado;
}
terminal · compilar y ejecutar sobre un nombre nuevo
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 ciclo-simbolico.c -o ciclo-simbolico
$ ./ciclo-simbolico ensayo-c

La función texto_enlace usa capacidad 64 porque solo valida el texto fijo de ocho bytes que acaba de crear el propio ensayo. No es un lector general de enlaces. El programa anterior sí administra un tamaño desconocido y comprueba truncamiento posible.

Los archivos se conservan para inspección. Una repetición sobre ensayo-c debe fallar al crear el directorio, sin reutilizar el árbol. El programa comprueba cierres y transferencias, pero no usa fsync: verifica relaciones durante la ejecución, no persistencia tras una caída.

11. Laboratorio y verificación

Usa Linux o WSL con GCC, Python 3 y GNU Coreutils. Trabaja en el directorio nuevo de la sección 4 y ejecuta sus órdenes en el orden indicado. En WSL es preferible el directorio personal de Linux. El ensayo no necesita privilegios ni cambios de montajes o políticas globales.

Guarda los dos fuentes anteriores, compílalos y ejecuta ciclo-simbolico sobre ensayo-c. Para observar un ciclo sin depender de los archivos anteriores, crea estos nombres adicionales:

terminal · inspeccionar cadenas y ciclos sin seguirlos al leer
$ ln -sT -- relativo accesos/cadena
$ ln -sT -- ciclo-b accesos/ciclo-a
$ ln -sT -- ciclo-a accesos/ciclo-b
$ ./ver-enlace accesos/cadena
$ ./ver-enlace accesos/ciclo-a
$ ./ver-enlace ensayo-c/B/movil

El primer informe debe leer el texto relativo y resolver el archivo de datos. El segundo debe leer ciclo-b y registrar ELOOP en la consulta del destino. El tercero debe mostrar dato.txt y llegar al archivo de B con dos, aunque antes del movimiento la misma cadena llegaba a uno.

Verificar identidad, traslado del árbol y fronteras del búfer

El verificador crea un paquete nuevo propio. Contrasta un relativo y un absoluto al renombrar el paquete completo, observa la diferencia frente a un enlace duro y prueba textos de 127, 128, 129 y 257 bytes. La igualdad entre longitud y capacidad debe provocar otro intento en el lector, aunque el caso exacto pudiera caber.

verificar-simbolicos.py · relaciones y errores sin números de inodo fijos
from pathlib import Path
import os
import stat
import subprocess
import tempfile

def informe(ruta):
    p = subprocess.run(["./ver-enlace", str(ruta)], capture_output=True)
    assert p.returncode == 0, p.stderr
    return p.stdout

def identidad(ruta, seguir=True):
    s = os.stat(ruta, follow_symlinks=seguir)
    return s.st_dev, s.st_ino

assert Path("ensayo-c/A/dato.txt").read_bytes() == b"tres\n"
assert Path("ensayo-c/A/guardado.txt").read_bytes() == b"uno\n"
assert Path("ensayo-c/duro.txt").read_bytes() == b"uno\n"
assert identidad("ensayo-c/A/guardado.txt") == identidad("ensayo-c/duro.txt")
assert Path("ensayo-c/B/movil").read_bytes() == b"dos\n"
assert os.readlink("ensayo-c/B/movil") == "dato.txt"
assert not os.path.lexists("ensayo-c/A/fijo")

raiz = Path(tempfile.mkdtemp(prefix="prueba-simbolicos-", dir="."))
p = raiz / "paquete"
(p / "datos").mkdir(parents=True)
(p / "accesos").mkdir()
nota = p / "datos/nota.txt"
nota.write_bytes(b"viejo\n")
os.link(nota, p / "duro")
os.symlink("../datos/nota.txt", p / "accesos/relativo")
texto_absoluto = str(nota.resolve())
os.symlink(texto_absoluto, p / "accesos/absoluto")
id_enlace = identidad(p / "accesos/relativo", seguir=False)
assert identidad(nota) != id_enlace
assert nota.stat().st_nlink == 2  # los simbólicos no lo aumentan

movido = raiz / "reubicado"
p.rename(movido)
r = movido / "accesos/relativo"
a = movido / "accesos/absoluto"
assert r.read_bytes() == b"viejo\n"
assert identidad(r, seguir=False) == id_enlace
assert os.readlink(a) == texto_absoluto
assert a.is_symlink() and not a.exists()
assert b"destino_error=ENOENT " in informe(a)

nota = movido / "datos/nota.txt"
viejo = identidad(nota)
nota.rename(movido / "datos/guardado.txt")
assert r.is_symlink() and not r.exists()
assert os.readlink(r) == "../datos/nota.txt"
assert stat.S_ISLNK(os.lstat(r).st_mode)
nota.write_bytes(b"nuevo\n")
assert r.read_bytes() == b"nuevo\n"
assert identidad(r) != viejo == identidad(movido / "duro")
r.unlink()
assert nota.read_bytes() == b"nuevo\n"

for n in (127, 128, 129, 257):
    enlace = raiz / f"largo-{n}"
    texto = "x" * n
    os.symlink(texto, enlace)
    salida = informe(enlace)
    assert f"leidos={n} ".encode() in salida
    assert b'texto="' + texto.encode() + b'"\n' in salida

os.symlink("b", raiz / "a")
os.symlink("a", raiz / "b")
assert b"destino_error=ELOOP " in informe(raiz / "a")
try:
    os.symlink("otro", raiz / "a")
except FileExistsError:
    pass
else:
    raise AssertionError("Un enlace existente no debía sobrescribirse")

for ruta in (nota, raiz / "ausente"):
    q = subprocess.run(["./ver-enlace", str(ruta)], capture_output=True)
    assert q.returncode == 1 and q.stderr
q = subprocess.run(["./ciclo-simbolico", "ensayo-c"], capture_output=True)
assert q.returncode != 0
print("Correctos: texto, identidad, relativos, absolutos, enlaces rotos y búferes")
print("Árbol adicional conservado:", raiz)
terminal · ejecutar después del ensayo C
$ python3 verificar-simbolicos.py

Path.exists() sigue el enlace; is_symlink(), os.lstat y os.path.lexists permiten observar su existencia propia. Esa distinción evita concluir que un nombre roto está disponible para una nueva creación.

Los enlaces largos del verificador pueden apuntar a componentes que no son nombres válidos por longitud. Eso no impide almacenar el texto como enlace si cabe en los límites del formato. La prueba exige que el lector entregue el texto completo; no exige que todos esos destinos puedan resolverse.

Si el sistema de archivos no admite la longitud o las operaciones preparadas, conserva el error y registra la limitación del entorno. No cambies los valores informados por el programa para hacerlos coincidir con el modelo.

12. Permisos, herramientas y restricciones

En Linux, los permisos ordinarios de un enlace simbólico suelen mostrarse como 0777 y no conceden acceso al contenido del destino. El acceso real depende de los directorios atravesados, del objeto resuelto y de otras políticas. Cambiar permisos siguiendo un enlace suele actuar sobre el destino, no sobre un modo independiente del enlace.

La propiedad del enlace sí puede importar para retirar o renombrar nombres en directorios con sticky bit y para la política protected_symlinks. La existencia de un enlace no autoriza a atravesar un directorio ni a leer cualquier archivo al que apunte.

Declarar qué sigue cada herramienta

No se detecta el riesgo de seguir enlaces únicamente leyendo una cadena y aprobándola antes de abrir: el árbol puede cambiar entre operaciones. El tema 22 presentó restricciones durante la resolución y el tema 29 tratará las carreras entre comprobar y usar.

Observar un enlace estable por descriptor en Linux

Cuando se necesita conservar la referencia al objeto enlace, Linux permite abrirlo con O_PATH y O_NOFOLLOW. Después readlinkat con nombre vacío puede leer ese objeto. Es una extensión específica, distinta del lector portable basado en ruta que usamos arriba.

esquema Linux · comprobar todos los retornos en una implementación completa
fd = open(ruta, O_PATH | O_NOFOLLOW | O_CLOEXEC);
fstat(fd, &s);                 /* comprobar S_ISLNK(s.st_mode) */
n = readlinkat(fd, "", buf, capacidad);
                              /* comprobar error y posible truncamiento */
close(fd);

Conservar el enlace no congela el árbol al que apunta su texto. Una consulta posterior del destino todavía puede observar otro objeto o fallar. Los magic links tienen semántica adicional y no se deben modelar solo como un archivo de texto con una ruta.

13. Simuladores de identidad y de lectura

La misma cadena puede resolver otro objeto

El primer modelo contiene dos directorios, A y B, con archivos llamados dato.txt. A contiene uno y B contiene dos. El nombre duro.txt comparte el objeto de A. Los enlaces movil y fijo guardan dato.txt; absoluto guarda /proyecto/A/dato.txt. La tabla separa identidad propia y resolución.

Enlace propio, texto y destino observado

estado de un único volumen didáctico
NombreIdentidad propiaTiponlink propioTexto almacenadoResoluciónContenido final

Mueve movil y verifica que su identidad y texto no cambian, pero ahora resuelve dos. Renombra el dato de A: fijo y absoluto quedan sin destino mientras duro.txt continúa en uno. Recrea dato.txt: los simbólicos llegan a otro objeto con tres. Retirar fijo no elimina ese archivo.

Reinicia y traslada todo el paquete a /traslado. Los relativos mantienen la relación interna y el absoluto conserva su cadena /proyecto/A/dato.txt, que deja de encontrarse en este árbol. El modelo no busca en el equipo, no representa permisos, cambios concurrentes ni montajes adicionales y omite objetos sin nombres de la tabla. No son cifras reales de espacio.

Cuánto devuelve readlink

El segundo panel representa un texto conocido y un búfer con capacidad elegida. Muestra qué puede concluir el llamador a partir del retorno y, por separado, lo que sabemos porque el modelo tiene el texto completo. Se restringe a ASCII imprimible para que cada carácter corresponda a un byte.

Lectura completa frente a truncamiento posible

bytes copiados, retorno y ausencia de terminador

Con dato.txt prueba capacidades 4, 8 y 9. En 8 no se perdió texto, pero el llamador todavía no puede confirmarlo solo por el retorno: una cadena más larga podría haber producido el mismo número de bytes. El lector en C amplía y repite para resolver esa ambigüedad en un árbol estable.

El simulador no crea un enlace real ni reproduce carreras entre lecturas. La representación entre comillas es solo una forma de mostrar los bytes recibidos, no significa que las comillas o el byte nulo hayan sido escritos por readlink.

14. Elegir la relación adecuada

NecesidadRelación a considerarCondición que hay que aceptar
Otro nombre del mismo objeto, resistente al renombrado de los demás nombresEnlace duro.Compartición de cambios y límites de montaje.
Un nombre que siga una ruta publicada, incluso si después se sustituye su objetoEnlace simbólico.Dependencia de la ruta y posibles enlaces rotos.
Referencias internas de un paquete que se traslada completoSimbólicos relativos.Conservar las relaciones de directorios y revisar movimientos individuales.
Una referencia hacia una ubicación absoluta dentro del entorno visibleSimbólico absoluto.Dependencia de esa raíz y ese espacio de montajes.
Contenido que debe cambiar independientementeCopia independiente o mecanismo de copia apropiado.No confundir otro nombre con otra versión de datos.

Un simbólico hacia una ruta de «versión actual» puede seguir cambios futuros del nombre; uno usado como archivo histórico puede dejar de señalar la versión esperada. Un duro conserva el objeto, pero sus datos siguen siendo modificables. La decisión depende de si se quiere seguir un nombre, compartir un objeto o separar los datos.

Ni los enlaces duros ni los simbólicos proporcionan por sí solos independencia ante la pérdida del almacenamiento. Para respaldos se necesitan reglas sobre contenido, mutabilidad y ubicación. El proyecto del tema 49 usará enlaces duros con un contrato explícito.

Cómo interpretar una observación

  1. Consulta el enlace mismo. Tipo, identidad, texto y longitud sin confundirlos con el destino.
  2. Declara la base del texto. Padre del enlace para relativos y raíz visible para absolutos.
  3. Registra la operación. Seguir, copiar el enlace, moverlo, sustituir el destino o retirar un nombre.
  4. Separa errores. ENOENT, EACCES y ELOOP no describen la misma situación.
  5. Reconoce el alcance temporal. Consultas sucesivas sobre rutas no son una instantánea frente a modificaciones concurrentes.

15. Resumen

16. Preguntas de repaso y práctica

1. ¿Qué contiene un enlace simbólico ordinario?

Una cadena de bytes que representa una ruta. No almacena una referencia permanente al inodo del destino ni una copia de su contenido.

2. ¿Por qué el simbólico tiene otra identidad?

Porque su nombre identifica un objeto de tipo enlace, distinto del archivo o directorio al que puede llegar al seguir su texto.

3. ¿Crear tres simbólicos aumenta nlink del destino en tres?

No. El contador del destino cuenta sus enlaces duros, no las rutas guardadas en otros objetos simbólicos.

4. ¿Puede crearse antes de que exista su destino?

Sí. La creación almacena el texto sin comprobar el destino. Puede permanecer roto hasta que aparezca una ruta resoluble.

5. ¿Desde dónde se interpreta ../datos/nota.txt?

Desde el directorio que contiene el enlace cuyo texto es ese, no desde el cwd de quien lo crea o lo usa.

6. ¿Mover un enlace relativo conserva siempre su destino?

No. El texto se conserva, pero otro padre puede hacer que resuelva otro objeto o falle. Trasladar todo un árbol puede conservar sus relaciones internas.

7. ¿Un absoluto se adapta al mover el paquete?

No automáticamente. Sigue almacenando la ruta anterior, interpretada desde la raíz visible del entorno en el que se use.

8. ¿Qué tamaño informa lstat de un enlace ordinario?

Los bytes de su texto, sin terminador nulo. No es el tamaño del contenido del destino ni necesariamente una medida de su asignación física.

9. ¿Un stat fallido demuestra que no existe el enlace?

No. Puede existir el enlace y fallar la resolución de su destino. lstat o una consulta sin seguimiento permite comprobar su objeto propio.

10. ¿EACCES y ENOENT describen lo mismo?

No. Uno informa denegación de acceso y el otro ausencia de un componente requerido. No se debe clasificar toda consulta fallida como destino inexistente.

11. ¿Readlink devuelve el contenido del archivo apuntado?

No. Devuelve la ruta almacenada en el enlace final. Una cadena de enlaces requiere otras resoluciones para alcanzar su destino.

12. ¿Readlink añade un byte nulo?

No. El programa debe usar la longitud devuelta o añadir un terminador en memoria propia con espacio disponible.

13. ¿Retorno igual a capacidad confirma una lectura completa?

No. Puede ser coincidencia exacta o truncamiento. Se amplía y repite, o se informa que el límite del programa impide confirmarlo.

14. ¿Un tamaño previo de lstat elimina la necesidad de comprobar readlink?

No. El enlace podría cambiar entre llamadas y algunos objetos virtuales no ofrecen ese tamaño como una guía suficiente. El retorno de lectura siempre se comprueba.

15. ¿Qué ocurre si se renombra el destino y luego se recrea su nombre anterior?

El simbólico puede quedar roto durante la ausencia y después llegar al nuevo objeto. Un duro al objeto viejo continúa identificándolo.

16. ¿Retirar el nombre del simbólico borra su destino?

No con una operación sobre el enlace mismo sin barra final. El destino conserva su propia entrada y referencias.

17. ¿Los permisos 0777 del enlace autorizan a leer el destino?

No. En Linux esos bits ordinarios no conceden ese acceso. Se aplican permisos de directorios, del destino y otras políticas.

18. ¿Por qué un simbólico puede atravesar sistemas de archivos?

Porque su texto se resuelve como ruta y puede cruzar montajes. No intenta crear otro nombre directo del inodo de un volumen distinto.

19. ¿El descriptor O_PATH del enlace congela su destino?

No. Puede mantener una referencia al enlace propio para operaciones específicas de Linux, pero el texto todavía se resuelve sobre un árbol que puede cambiar.

20. ¿Qué relación elegir para contenido independiente?

Una copia u otro mecanismo que proporcione independencia según su contrato. Ningún nombre adicional, duro o simbólico, garantiza por sí solo una versión separada del contenido.

Para practicar

  1. Crea un enlace duro y uno simbólico hacia el mismo archivo y compara identidad propia, identidad seguida y nlink.
  2. Crea un simbólico roto y demuestra que existe sin seguirlo.
  3. Haz aparecer su destino y comprueba que cambia la resolución sin modificar el texto del enlace.
  4. Compara enlaces relativos y absolutos al trasladar un paquete completo.
  5. Mueve solo un enlace relativo entre dos directorios con archivos del mismo nombre y contenido diferente.
  6. Renombra un destino y recrea su nombre con otro archivo; observa la diferencia frente a un duro.
  7. Ejecuta el lector con enlaces de texto de 127, 128 y 129 bytes y comprueba su longitud completa.
  8. En el simulador de lectura, compara capacidad menor, igual y mayor que el texto.
  9. Construye una cadena y un ciclo; distingue leer un texto de resolver todos los enlaces.
  10. Retira un simbólico y comprueba que los bytes del destino se conservan.
  11. Explica por qué comprobar lstat y después abrir la ruta no forma una operación atómica.
  12. Describe una política de copia o recorrido que conserve enlaces y otra que siga destinos, declarando sus consecuencias.

En el tema 25 estudiaremos unlink, rename y archivos temporales: la vida de objetos abiertos sin nombre, la sustitución atómica de entradas y el uso de mkstemp.