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.
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
| Propiedad | Enlace duro adicional | Enlace simbólico |
|---|---|---|
| Relación | Otro nombre del mismo objeto. | Un objeto distinto con una ruta como contenido. |
| Identidad consultada sin seguimiento final | La del archivo compartido. | La del propio enlace. |
| Contador del destino | Aumenta al añadir el nombre. | No aumenta por crear el enlace simbólico. |
| Destino inexistente | No se puede enlazar un origen que no existe en el caso ordinario. | El enlace puede crearse y permanecer roto. |
| Directorios | No se crean relaciones arbitrarias desde aplicaciones ordinarias. | Puede apuntar a un directorio. |
| Límites de montaje | No conecta montajes distintos mediante el enlace. | Su ruta puede atravesar montajes al resolverse. |
| Renombrar el nombre del destino | No rompe los demás nombres del objeto. | Puede dejar de resolver la cadena guardada. |
| Recrear el nombre del destino | No 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.
/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.txtUn 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.
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.
$ 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.txtLa 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.
$ 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/relativoLos 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
| Interfaz | Componente final simbólico | Información obtenida |
|---|---|---|
stat | Se sigue. | Atributos del objeto resuelto, o error del recorrido. |
lstat | No se sigue en una ruta ordinaria sin barra final. | Atributos del enlace mismo. |
fstatat con AT_SYMLINK_NOFOLLOW | No se sigue el final según ese contrato. | Atributos relativos a un directorio inicial explícito. |
readlink / readlinkat | Se 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.
$ readlink -- accesos/roto
$ stat -- accesos/roto
$ stat -L -- accesos/roto
$ (set -o noclobber; printf 'aparecio\n' > datos/ausente.txt)
$ cat -- accesos/rotoLa 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.
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 81Quitar 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
#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.
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.
#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;
}$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 ver-enlace.c -o ver-enlace
$ ./ver-enlace accesos/relativo
$ ./ver-enlace accesos/rotoEl 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.
#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;
}$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 ciclo-simbolico.c -o ciclo-simbolico
$ ./ciclo-simbolico ensayo-cLa 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:
$ 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/movilEl 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.
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)$ python3 verificar-simbolicos.pyPath.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
- Listar: presentar el enlace puede mostrar su texto sin entrar en el destino; opciones de seguimiento cambian la consulta.
- Copiar: copiar el objeto enlace conserva su texto; seguirlo copia el objeto resuelto. Revisa las opciones concretas y los enlaces encontrados dentro de un árbol.
- Recorrer: seguir enlaces a directorios puede salir del subárbol o crear ciclos. El tema 21 eligió recorrido físico con FTW_PHYS.
- Abrir: O_NOFOLLOW de una apertura ordinaria afecta al final; RESOLVE_NO_SYMLINKS de openat2 permite prohibir seguimiento en todo el recorrido.
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.
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
| Nombre | Identidad propia | Tipo | nlink propio | Texto almacenado | Resolución | Contenido 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
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
| Necesidad | Relación a considerar | Condición que hay que aceptar |
|---|---|---|
| Otro nombre del mismo objeto, resistente al renombrado de los demás nombres | Enlace duro. | Compartición de cambios y límites de montaje. |
| Un nombre que siga una ruta publicada, incluso si después se sustituye su objeto | Enlace simbólico. | Dependencia de la ruta y posibles enlaces rotos. |
| Referencias internas de un paquete que se traslada completo | Simbólicos relativos. | Conservar las relaciones de directorios y revisar movimientos individuales. |
| Una referencia hacia una ubicación absoluta dentro del entorno visible | Simbólico absoluto. | Dependencia de esa raíz y ese espacio de montajes. |
| Contenido que debe cambiar independientemente | Copia 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
- Consulta el enlace mismo. Tipo, identidad, texto y longitud sin confundirlos con el destino.
- Declara la base del texto. Padre del enlace para relativos y raíz visible para absolutos.
- Registra la operación. Seguir, copiar el enlace, moverlo, sustituir el destino o retirar un nombre.
- Separa errores. ENOENT, EACCES y ELOOP no describen la misma situación.
- Reconoce el alcance temporal. Consultas sucesivas sobre rutas no son una instantánea frente a modificaciones concurrentes.
15. Resumen
- Un simbólico es un objeto propio que guarda una ruta; un duro es otro nombre del mismo objeto.
- Crear un simbólico no exige que exista el destino ni aumenta su contador de enlaces.
- El texto relativo se interpreta desde el padre del enlace; el absoluto desde la raíz correspondiente.
- lstat consulta el enlace final y stat intenta seguirlo según las reglas de la ruta.
- Readlink devuelve bytes del texto, no lee el contenido del destino ni resuelve toda una cadena.
- Readlink no termina la cadena con nulo; retorno igual a capacidad exige considerar truncamiento.
- Un enlace roto conserva su existencia propia y puede volver a funcionar al aparecer el nombre requerido.
- Mover un relativo puede cambiar su resolución; renombrar o recrear un destino puede romperlo o redirigirlo.
- Eliminar el enlace sin seguirlo no elimina su destino.
- Las herramientas deben declarar si siguen enlaces; leer el texto previamente no congela la resolución posterior.
- La elección depende de si se quiere compartir identidad, seguir una ruta o mantener datos independientes.
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
- Crea un enlace duro y uno simbólico hacia el mismo archivo y compara identidad propia, identidad seguida y nlink.
- Crea un simbólico roto y demuestra que existe sin seguirlo.
- Haz aparecer su destino y comprueba que cambia la resolución sin modificar el texto del enlace.
- Compara enlaces relativos y absolutos al trasladar un paquete completo.
- Mueve solo un enlace relativo entre dos directorios con archivos del mismo nombre y contenido diferente.
- Renombra un destino y recrea su nombre con otro archivo; observa la diferencia frente a un duro.
- Ejecuta el lector con enlaces de texto de 127, 128 y 129 bytes y comprueba su longitud completa.
- En el simulador de lectura, compara capacidad menor, igual y mayor que el texto.
- Construye una cadena y un ciclo; distingue leer un texto de resolver todos los enlaces.
- Retira un simbólico y comprueba que los bytes del destino se conservan.
- Explica por qué comprobar lstat y después abrir la ruta no forma una operación atómica.
- 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.