1. Varios nombres, un mismo objeto
En los temas anteriores distinguimos entradas de directorio, inodos y resolución de rutas. Un enlace duro aprovecha esa separación: añade un nombre que identifica un objeto ya existente, sin crear otro archivo regular con una copia de su contenido.
Enlace duro o hard link: entrada de directorio que asocia un nombre con un objeto del mismo sistema de archivos. Cuando se crea otro enlace duro a un archivo, ambos nombres permiten acceder al mismo inodo y a sus datos.
directorio A: "informe.txt" ─┐
├─→ inodo 81 → datos del informe
directorio B: "copia.txt" ───┘
inodo 81: tipo regular, propietario, modo, tamaño, nlink=2
No hay un segundo inodo regular para copia.txt.
El directorio B sí necesita representar su nueva entrada.El primer nombre creado también es un enlace duro. Una vez que existen varios, no hay un nombre principal obligatorio y otro que dependa de él. Quitar el que llamábamos «original» no rompe el otro: ambos se relacionan directamente con el mismo objeto.
Un enlace duro no almacena una cadena que apunte al nombre anterior. Por eso renombrar A/informe.txt no obliga a actualizar el texto de B/copia.txt. La comparación con enlaces simbólicos se desarrollará en el tema 24.
Este tema se centra en archivos regulares. Las referencias especiales de directorios tienen reglas diferentes y no autorizan a construir una jerarquía arbitraria de enlaces duros a carpetas.
2. Comprobar identidad, no solo contenido
Dos archivos pueden contener los mismos bytes y seguir siendo objetos independientes. Comparar contenido con cmp responde si las lecturas coinciden, pero no demuestra que los nombres sean enlaces duros al mismo archivo.
| Comprobación | Qué indica | Qué no demuestra |
|---|---|---|
| Mismo tamaño | Las longitudes consultadas coinciden. | Que el contenido o la identidad sean iguales. |
| cmp sin diferencias | Coincidencia del contenido leído. | Que exista un solo objeto. |
| Mismo número de inodo aislado | Coincidencia de un número. | Identidad entre sistemas de archivos distintos. |
| Mismo par st_dev y st_ino | Identidad de los objetos consultados en el contexto del ensayo. | Identificador eterno ni una única ruta posible. |
Las comparaciones se realizan con el árbol estable y sin sustituciones concurrentes. Los números de inodo pueden reutilizarse después de la destrucción de un objeto. Abrir ambos archivos y consultar sus descriptores evita volver a resolver esos nombres al comparar, aunque no convierte las dos aperturas en una operación simultánea.
Si usas stat sobre un enlace simbólico, puedes terminar comparando su destino. No concluyas que las entradas mismas son enlaces duros solo porque una consulta con seguimiento alcanzó el mismo objeto. Nuestro comparador en C rechaza enlaces simbólicos en el componente final y exige archivos regulares.
La referencia inode(7) explica el ámbito del número de inodo y el campo de enlaces. Registrar dispositivo, inodo y política de seguimiento hace que la conclusión sea comprobable.
3. Crear enlaces desde la terminal
En GNU/Linux, ln crea enlaces duros por defecto. Para que el segundo argumento se interprete exactamente como nombre de destino, utilizaremos -T. No emplearemos opciones que retiren un destino existente.
$ LAB23=$(mktemp -d "$HOME/tema23.XXXXXX")
$ cd -- "$LAB23"
$ mkdir A B
$ (set -o noclobber; printf 'version1\n' > A/original.txt)
$ ln -T -- A/original.txt B/alias.txt
$ stat -c 'nombre=%n dev=%d ino=%i enlaces=%h bytes=%s modo=%a' -- A/original.txt B/alias.txt
$ ls -li -- A/original.txt B/alias.txtLos dos nombres deben mostrar la misma identidad y dos enlaces. Sus textos de ruta son distintos, pero el tipo del objeto continúa siendo archivo regular; no aparece la letra l de un enlace simbólico en el listado largo.
Repetir exactamente la orden de creación sobre B/alias.txt debe fallar porque ya existe. Crear otro enlace no es una instrucción para reemplazar el destino. En un script conviene tratar el fallo real de la operación, sin depender únicamente de una comprobación previa de existencia.
Comparar con una copia independiente
$ cp --no-clobber --reflink=never -T -- A/original.txt independiente.txt
$ cmp -- A/original.txt independiente.txt
$ stat -c 'nombre=%n dev=%d ino=%i enlaces=%h bytes=%s' -- A/original.txt B/alias.txt independiente.txtLa copia tiene identidad distinta aunque al principio su contenido coincida. Desactivamos reflinks para que la comparación no se centre en compartición copy-on-write de bloques. Una copia, un reflink y un enlace duro no ofrecen la misma relación entre cambios futuros.
La documentación de GNU ln describe el tratamiento del destino y sus opciones. Las órdenes de este tema usan fuentes regulares propias y nombres de destino nuevos; las opciones para seguir una fuente simbólica se estudiarán al comparar tipos de enlace.
4. Qué cuenta st_nlink
Para un archivo regular, st_nlink informa cuántos enlaces duros tiene. Crear otro nombre aumenta el contador; retirar uno lo disminuye. Cambiar contenido o abrir otro descriptor no crea por sí mismo una entrada de directorio.
crear A/original.txt → nlink=1
crear B/alias.txt con link → nlink=2
abrir los dos nombres → nlink=2
renombrar A/original.txt → nlink=2
retirar ese nombre → nlink=1
retirar B/alias.txt → nlink=0
Los descriptores abiertos son referencias diferentes de los enlaces.Un archivo abierto puede seguir existiendo con cero enlaces. La desaparición de su último nombre no implica que todos sus bytes se liberen en ese instante. El tema 25 profundizará en objetos abiertos sin nombre y archivos temporales; aquí basta con no confundir los dos contadores.
El contador tampoco dice dónde están los otros nombres. Si un recorrido encuentra dos entradas y st_nlink vale cinco, puede haber otros nombres fuera del subárbol observado. No se obtiene una lista de rutas invirtiendo directamente el número de inodo.
El valor de enlaces de un directorio tiene la interpretación especial estudiada en el tema 20 y puede variar con el formato. No se usa para contar todos sus archivos hijos. En este tema los cálculos y simuladores de nlink corresponden a archivos regulares.
5. Datos y metadatos compartidos
Como los nombres llegan al mismo objeto, una escritura en uno modifica los datos que se observan por el otro. También se comparten propietario, permisos, tamaño y atributos asociados al inodo; no hay un modo de acceso independiente por cada nombre duro.
$ printf 'extra\n' >> B/alias.txt
$ cat -- A/original.txt
$ cmp -- A/original.txt B/alias.txt
$ chmod 640 -- B/alias.txt
$ stat -c 'nombre=%n ino=%i enlaces=%h bytes=%s modo=%a' -- A/original.txt B/alias.txtLa copia independiente no incorpora el texto añadido ni el cambio de permisos efectuado después de crearla. Que una aplicación presente dos rutas como documentos separados no cambia la relación entre sus objetos.
Datos comunes no significa offset común
Dos llamadas open independientes sobre los dos nombres crean descripciones de archivo abierto distintas, con offsets propios. Los datos pertenecen al mismo archivo; la posición de lectura pertenece a la descripción abierta. Descriptores obtenidos por dup pueden compartir esa descripción, como vimos al estudiar descriptores.
Los permisos de búsqueda y modificación de las entradas siguen dependiendo de sus directorios padres. Dos rutas al mismo objeto pueden atravesar directorios con accesos diferentes. Compartir el modo del archivo no hace idénticas todas las condiciones para llegar a él.
6. Escribir y reemplazar son operaciones distintas
Muchos editores pueden guardar escribiendo sobre el archivo existente o preparando otro archivo y sustituyendo un nombre mediante renombrado. El efecto sobre un grupo de enlaces duros es diferente.
Escribir en A:
A → 81 → contenido modificado
B → 81 → el mismo contenido modificado
Crear temporal 95 y renombrarlo sobre A:
A → 95 → contenido nuevo
B → 81 → contenido anterior
El segundo caso cambia la asociación del nombre A.
No convierte automáticamente B en otro nombre del inodo 95.Reemplazar A reduce los enlaces del objeto anterior al retirar esa asociación. El objeto nuevo ocupa el nombre de destino. Si B era el único nombre restante del viejo, ahora ambos archivos tienen un enlace y contenidos que pueden diferir.
Renombrar un nombre a otro nombre ya existente del mismo objeto es otro caso: en Linux, rename termina con éxito sin cambiar esos dos enlaces. No debe modelarse como borrar uno de ellos. La referencia de rename(2) documenta esta condición.
La sustitución atómica de un nombre no acredita persistencia después de perder energía. El tema 35 desarrollará la sincronización de archivo y directorio. El objetivo aquí es identificar qué objeto recibe cada cambio, durante la ejecución.
7. Límites, montajes y errores
Un enlace duro no puede conectar objetos de sistemas de archivos diferentes. En Linux, intentar enlazar entre montajes distintos también puede devolver EXDEV aunque ambos expongan el mismo sistema de archivos. Comparar st_dev ayuda a interpretar el contexto, pero no sustituye intentar la operación y comprobar su resultado.
| Error | Situación habitual en link/linkat | Consecuencia práctica |
|---|---|---|
EEXIST | El nombre de destino ya existe. | No se reemplaza automáticamente. |
EXDEV | Origen y destino no pertenecen al mismo montaje admitido. | No hay enlace duro entre esas ubicaciones. |
ENOENT / ENOTDIR | Falta un componente o un prefijo no es directorio. | Revisar la resolución de ambas rutas. |
EACCES | Faltan permisos requeridos sobre las rutas o el destino. | Crear un nombre modifica su directorio padre. |
EPERM | Origen directorio, política de protección, atributos o falta de soporte. | No implica una única causa universal. |
EMLINK | Se alcanzó el límite de enlaces. | El límite depende de formato y características. |
ENOSPC / EDQUOT | No hay recursos o cuota para la nueva entrada. | No copiar los datos no significa costo cero. |
EROFS | Sistema de archivos de solo lectura. | La creación es una modificación del espacio de nombres. |
Linux incorpora protected_hardlinks, que puede limitar enlaces a archivos de otro propietario según credenciales, permisos y modo. No se reduce a «si puedo leerlo, puedo enlazarlo». El laboratorio usa archivos propios; no cambia esa política. Su contrato está en proc_sys_fs(5).
Las aplicaciones ordinarias no crean enlaces duros arbitrarios a directorios. Esa restricción preserva las propiedades de la jerarquía y evita ciclos de padres. Tampoco todo formato o almacenamiento remoto ofrece el mismo soporte para enlaces.
8. link y linkat desde C
#include <unistd.h>
int link(const char *origen, const char *destino);
#include <fcntl.h>
int linkat(int dir_origen, const char *origen,
int dir_destino, const char *destino, int flags);
Resultado: 0 si tiene éxito; -1 si falla, con errno.
El destino debe ser un nombre nuevo.link resuelve rutas relativas desde el cwd. linkat permite un directorio inicial explícito para cada ruta; con AT_FDCWD se usa el cwd. Una ruta absoluta ignora el descriptor correspondiente en la variante ordinaria.
Pasar dos descriptores de directorio facilita expresar «toma este nombre de A y crea aquel en B». No congela sus entradas ni garantiza confinamiento de rutas arbitrarias. En los ejemplos, las rutas relativas son nombres fijos dentro de un árbol privado.
En Linux, linkat con flags cero no sigue el enlace simbólico del componente final de origen; AT_SYMLINK_FOLLOW pide seguirlo. Los prefijos continúan resolviéndose. Nuestros ensayos exigen un origen regular creado por el propio programa para no mezclar esas políticas con la idea principal.
La referencia de link(2) especifica ambas llamadas y sus errores. Después de un fallo, guarda errno antes de llamar a otras funciones si necesitas conservar su causa. Que un chequeo previo viera el destino libre no garantiza que continúe libre cuando se intente crear el enlace.
9. Programa en C: comparar dos objetos abiertos
Este programa abre dos rutas, exige archivos regulares y consulta los descriptores con fstat. Rechaza el seguimiento del enlace simbólico final y no lee contenido. El resultado es una comparación de identidad, no de igualdad de bytes.
Devuelve 0 si son el mismo objeto, 1 si son distintos y 2 si hubo un error de uso, apertura, consulta, tipo o salida. O_NONBLOCK evita quedarse esperando a un escritor si se le pasa una FIFO; después se rechaza por tipo. El programa está destinado a los archivos regulares controlados del laboratorio, no a abrir dispositivos ajenos.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char **argv)
{
if (argc != 3) {
fprintf(stderr, "Uso: %s ARCHIVO_A ARCHIVO_B\n", argv[0]);
return 2;
}
int a = -1, b = -1, estado = 2;
struct stat x, y;
int flags = O_RDONLY | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC;
a = open(argv[1], flags);
if (a == -1) { perror("open A"); goto fin; }
b = open(argv[2], flags);
if (b == -1) { perror("open B"); goto fin; }
if (fstat(a, &x) == -1 || fstat(b, &y) == -1) {
perror("fstat"); goto fin;
}
if (!S_ISREG(x.st_mode) || !S_ISREG(y.st_mode)) {
fputs("Se requieren dos archivos regulares\n", stderr);
goto fin;
}
printf("A dev=%ju ino=%ju enlaces=%ju bytes=%jd\n",
(uintmax_t)x.st_dev, (uintmax_t)x.st_ino,
(uintmax_t)x.st_nlink, (intmax_t)x.st_size);
printf("B dev=%ju ino=%ju enlaces=%ju bytes=%jd\n",
(uintmax_t)y.st_dev, (uintmax_t)y.st_ino,
(uintmax_t)y.st_nlink, (intmax_t)y.st_size);
int mismos = x.st_dev == y.st_dev && x.st_ino == y.st_ino;
puts(mismos ? "identidad=misma" : "identidad=distinta");
estado = mismos ? 0 : 1;
fin:
if (b != -1 && close(b) == -1) { perror("close B"); estado = 2; }
if (a != -1 && close(a) == -1) { perror("close A"); estado = 2; }
if (fflush(stdout) == EOF) estado = 2;
return estado;
}$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 mismo-archivo.c -o mismo-archivo
$ ./mismo-archivo A/original.txt B/alias.txt
$ ./mismo-archivo A/original.txt independiente.txtEl segundo caso devuelve 1 de forma esperada: los objetos son distintos. No es una avería del programa. La comparación mantiene las referencias abiertas, pero no promete que las rutas sigan identificándolas después: el árbol puede cambiar mientras la aplicación continúa.
10. Programa en C: compartir y separar nombres
El siguiente programa crea un árbol nuevo con A y B, escribe version1, crea un alias en B y comprueba dos enlaces. Después modifica el mismo objeto por el alias y verifica que A observa Version1. Finalmente prepara version2 en otro archivo y lo renombra sobre el nombre de A.
El descriptor del archivo anterior se conserva durante la sustitución. Eso permite comprobar que B sigue identificando el objeto viejo y que el nombre de A ha pasado a identificar otro. El ensayo exige un directorio nuevo y no coordina modificaciones de otros procesos.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <fcntl.h>
#include <inttypes.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 escribir(int fd, const char *texto, size_t n)
{
size_t 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 int comprobar(int fd, const char *texto)
{
char b[32];
size_t n = strlen(texto), p = 0;
struct stat s;
if (n > sizeof b || 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 int mismos(const struct stat *a, const struct stat *b)
{
return a->st_dev == b->st_dev && a->st_ino == b->st_ino;
}
static int foto(const char *fase, int fd, nlink_t enlaces)
{
struct stat s;
if (fstat(fd, &s) == -1) return -1;
if (s.st_nlink != enlaces) { errno = EIO; return -1; }
printf("%s dev=%ju ino=%ju enlaces=%ju bytes=%jd modo=%03o\n",
fase, (uintmax_t)s.st_dev, (uintmax_t)s.st_ino,
(uintmax_t)s.st_nlink, (intmax_t)s.st_size,
(unsigned)(s.st_mode & 0777));
return 0;
}
int main(int argc, char **argv)
{
int raiz = -1, a = -1, b = -1, original = -1, alias = -1, nuevo = -1;
int estado = 1;
struct stat viejo, visto, reemplazo;
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);
b = openat(raiz, "B", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (a == -1 || b == -1) goto error;
original = openat(a, "original.txt", O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
if (original == -1 || escribir(original, "version1\n", 9) == -1) goto error;
if (foto("creado", original, 1) == -1 || fstat(original, &viejo) == -1)
goto error;
if (linkat(a, "original.txt", b, "alias.txt", 0) == -1) goto error;
if (fstatat(b, "alias.txt", &visto, AT_SYMLINK_NOFOLLOW) == -1) goto error;
if (!mismos(&viejo, &visto)) goto inesperado;
if (foto("enlazado", original, 2) == -1) goto error;
if (linkat(a, "original.txt", b, "alias.txt", 0) != -1 || errno != EEXIST)
goto inesperado;
if (foto("duplicado_rechazado", original, 2) == -1) goto error;
alias = openat(b, "alias.txt", O_RDWR | O_NOFOLLOW | O_CLOEXEC);
if (alias == -1) goto error;
ssize_t k;
do { k = pwrite(alias, "V", 1, 0); } while (k == -1 && errno == EINTR);
if (k != 1) { if (k == 0) errno = EIO; goto error; }
if (fchmod(alias, 0640) == -1 || comprobar(original, "Version1\n") == -1)
goto error;
if (fstat(original, &visto) == -1) goto error;
if ((visto.st_mode & 0777) != 0640) goto inesperado;
if (foto("escrito", original, 2) == -1) goto error;
nuevo = openat(a, "temporal.txt", O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
if (nuevo == -1 || escribir(nuevo, "version2\n", 9) == -1) goto error;
if (renameat(a, "temporal.txt", a, "original.txt") == -1) goto error;
if (fstatat(a, "original.txt", &reemplazo, AT_SYMLINK_NOFOLLOW) == -1 ||
fstatat(b, "alias.txt", &visto, AT_SYMLINK_NOFOLLOW) == -1) goto error;
if (mismos(&viejo, &reemplazo) || !mismos(&viejo, &visto)) goto inesperado;
if (comprobar(alias, "Version1\n") == -1 || comprobar(nuevo, "version2\n") == -1)
goto error;
if (foto("viejo_tras_reemplazo", original, 1) == -1 ||
foto("nuevo_tras_reemplazo", nuevo, 1) == -1) goto error;
puts("compartir=correcto reemplazar=correcto EEXIST=correcto");
estado = 0;
goto fin;
inesperado:
fputs("Identidad, modo o resultado inesperado\n", stderr);
goto fin;
error:
perror("operacion del ensayo");
fin:
if (cerrar(&nuevo) == -1) { perror("close nuevo"); estado = 1; }
if (cerrar(&alias) == -1) { perror("close alias"); estado = 1; }
if (cerrar(&original) == -1) { perror("close original"); estado = 1; }
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 grupo-enlaces.c -o grupo-enlaces
$ ./grupo-enlaces ensayo-cSe esperan enlaces 1, 2, 2, 2, 1, 1 en las seis fotos. Las primeras cinco corresponden al objeto viejo y la última a otro objeto. La segunda creación rechazada debe conservar el contador. Tras el éxito quedan dos archivos distintos: ensayo-c/A/original.txt con version2 y ensayo-c/B/alias.txt con Version1.
Las funciones de transferencia comprueban interrupciones, escrituras parciales y lecturas incompletas. Se conservan los objetos si algo falla y se comprueban los cierres. No se llama a fsync porque se observa el comportamiento durante la ejecución; no se certifica un protocolo de recuperación.
11. Laboratorio de identidad y contenido
Usa Linux o WSL con GCC, Python 3, GNU Coreutils y GNU Findutils. En WSL es preferible trabajar dentro del directorio personal de Linux para disponer de la semántica esperada de enlaces y permisos. No se necesitan privilegios ni cambios globales. Los archivos contienen unos pocos bytes.
Trabaja en el directorio aislado de la sección 3, guarda los dos fuentes y compílalos. Ejecuta las órdenes de las secciones 3 y 5 en ese orden: al final A y B siguen identificando el mismo objeto de 15 bytes, mientras la copia independiente conserva los nueve bytes iniciales.
$ python3 - <<'PY'
from pathlib import Path
import os
a, b, copia = Path("A/original.txt"), Path("B/alias.txt"), Path("independiente.txt")
identidad = lambda p: (p.stat().st_dev, p.stat().st_ino)
viejo = identidad(a)
assert viejo == identidad(b) != identidad(copia)
assert a.stat().st_nlink == b.stat().st_nlink == 2
assert a.read_bytes() == b.read_bytes() == b"version1\nextra\n"
assert copia.read_bytes() == b"version1\n"
with open("A/temporal.txt", "xb") as f:
f.write(b"version2\n")
os.replace("A/temporal.txt", a)
assert identidad(a) != viejo == identidad(b)
assert a.stat().st_nlink == b.stat().st_nlink == 1
assert a.read_bytes() == b"version2\n"
assert b.read_bytes() == b"version1\nextra\n"
print("El reemplazo de A no cambió el contenido ni la identidad de B")
PYNo vuelvas a ejecutar esta fase como si el estado inicial siguiera intacto. Para repetir la secuencia, crea otro directorio con mktemp. El uso de nombres nuevos permite revisar qué dejó cada operación.
Conservar y analizar las fotos del programa en C
$ ./grupo-enlaces ensayo-c > fases.txt
$ cat fases.txt
$ ./mismo-archivo ensayo-c/A/original.txt ensayo-c/B/alias.txtLa comparación final debe devolver 1 porque el reemplazo los separó. Las fotos incluyen el dispositivo para no comparar números de inodo fuera de su contexto. No se exige que marcas de tiempo consecutivas sean diferentes ni que un contador global de espacio cambie una cantidad fija.
Verificar el contrato sin depender de números de inodo concretos
El verificador siguiente comprueba las fotos y realiza otra secuencia sobre archivos nuevos propios. También abre el objeto viejo antes de retirar sus nombres: verifica el contador cero sin intentar consultar una ruta que ya no existe.
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
lineas = Path("fases.txt").read_text().splitlines()
fotos = []
for linea in lineas:
if " dev=" in linea:
fase, resto = linea.split(" ", 1)
fotos.append((fase, dict(c.split("=", 1) for c in resto.split())))
assert [f for f, _ in fotos] == ["creado", "enlazado", "duplicado_rechazado",
"escrito", "viejo_tras_reemplazo", "nuevo_tras_reemplazo"]
assert [int(d["enlaces"]) for _, d in fotos] == [1, 2, 2, 2, 1, 1]
identidades = [(d["dev"], d["ino"]) for _, d in fotos]
assert len(set(identidades[:5])) == 1
assert identidades[5] != identidades[0]
assert all(int(d["bytes"]) == 9 for _, d in fotos)
assert fotos[3][1]["modo"] == "640"
assert "compartir=correcto reemplazar=correcto EEXIST=correcto" in lineas
assert Path("ensayo-c/A/original.txt").read_bytes() == b"version2\n"
assert Path("ensayo-c/B/alias.txt").read_bytes() == b"Version1\n"
raiz = Path(tempfile.mkdtemp(prefix="prueba-enlaces-", dir="."))
a, b, c = raiz / "a", raiz / "b", raiz / "copia"
a.write_bytes(b"uno\n")
os.link(a, b)
shutil.copyfile(a, c)
def comparar(x, y, esperado):
p = subprocess.run(["./mismo-archivo", str(x), str(y)], capture_output=True)
assert p.returncode == esperado, (p.stdout, p.stderr)
comparar(a, b, 0)
comparar(a, c, 1) # mismos bytes no implica mismo objeto
assert a.read_bytes() == c.read_bytes()
try:
os.link(a, b)
except FileExistsError:
pass
else:
raise AssertionError("El destino existente debía rechazarse")
assert a.stat().st_nlink == 2
os.rename(a, b) # dos nombres del mismo objeto: operación sin cambios
assert a.exists() and b.exists() and a.stat().st_nlink == 2
with open(b, "r+b") as f:
f.write(b"U")
assert a.read_bytes() == b.read_bytes() == b"Uno\n"
assert c.read_bytes() == b"uno\n"
os.chmod(b, 0o640)
assert a.stat().st_mode & 0o777 == 0o640
os.symlink("a", raiz / "simbolico")
comparar(raiz / "simbolico", a, 2)
comparar(raiz, a, 2)
comparar(raiz / "ausente", a, 2)
with open(a, "rb") as abierto:
a.unlink()
assert b.stat().st_nlink == 1
b.unlink()
assert os.fstat(abierto.fileno()).st_nlink == 0
assert abierto.read() == b"Uno\n"
comparar(a, c, 2)
p = subprocess.run(["./grupo-enlaces", "ensayo-c"], capture_output=True)
assert p.returncode != 0
print("Correctos: fotos, contenido, identidad, enlaces, modos y errores")
print("Árbol adicional conservado:", raiz)$ python3 verificar-enlaces.pyLa lectura con cero enlaces anticipa el tema 25; no es una prueba de cuánto espacio físico libera el sistema al cerrar. El verificador conserva su directorio adicional para inspección y no depende de valores numéricos particulares de dispositivo o inodo.
Para observar un límite de montaje no hace falta forzar un ensayo en rutas del sistema. El simulador siguiente incluye otro volumen y rechaza el enlace. Si pruebas un caso real entre ubicaciones propias de montajes diferentes, registra el error efectivo; no crees ni modifiques montajes para ejecutar este laboratorio.
12. Contar espacio y localizar otros nombres
Un enlace adicional no duplica los bloques de datos del archivo. Sí puede consumir espacio de directorio, actualizar metadatos y requerir trabajo del sistema de archivos. «No copia contenido» y «no usa recursos» no son afirmaciones equivalentes.
Si cada uno de tres nombres muestra un archivo de un MiB, sumar sus longitudes por ruta produce tres MiB, aunque los tres pertenezcan a un objeto de un MiB lógico. El tema 21 deduplicó por identidad para evitar esa multiplicación en mini-du.
$ mkdir conteo
$ (set -o noclobber; printf 'datos\n' > conteo/a)
$ ln -T -- conteo/a conteo/b
$ du -B1 -- conteo/a conteo/b
$ du -l -B1 -- conteo/a conteo/b
$ du -s -B1 -- conteoEn GNU du, la invocación predeterminada cuenta normalmente una vez los archivos con varios enlaces; -l o --count-links cambia esa decisión. Las dos primeras órdenes comparan la política dentro de una invocación. Ejecutar du por separado para cada nombre reinicia la información del recorrido. La suma del directorio incluye además su propia representación.
Buscar nombres dentro de un alcance conocido
$ find -P conteo -xdev -samefile conteo/a -print0 > nombres.nul
$ python3 - <<'PY'
from pathlib import Path
datos = Path("nombres.nul").read_bytes()
assert datos.endswith(b"\0")
nombres = datos[:-1].split(b"\0")
assert set(nombres) == {b"conteo/a", b"conteo/b"}
for nombre in nombres:
print(repr(nombre))
PY-samefile y -print0 son opciones de la herramienta elegida, no funciones del inodo. El recorrido solo encuentra los nombres dentro de su alcance y accesibles en ese momento. -inum compara un número de inodo; usarlo sin considerar el sistema de archivos puede confundir objetos.
La contabilidad atribuida tampoco es una medida universal de bytes exclusivos recuperables. Compresión, snapshots, reflinks y otras referencias pueden alterar lo que se libera. No extrapoles el modelo simple de enlaces a todas las formas de compartición.
13. Simuladores de nombres y referencias
Enlazar, copiar, escribir y reemplazar
El primer modelo contiene tres directorios: A y B pertenecen al volumen 1; C al volumen 2. Inicialmente solo existe A/original.txt, con identidad 1:81. Cada objeto tiene contenido ASCII y un contador calculado a partir de sus nombres activos.
Un nombre no es una copia del objeto
| Nombre | dev:inodo | Contenido observado |
|---|
| dev | Inodo | nlink | Bytes lógicos | Contenido |
|---|
Crea el enlace a B y luego escribe en A: ambos nombres deben mostrar version2. Después usa «Reemplazar el nombre de origen» con otro contenido: A obtiene otra identidad, mientras B conserva la anterior. Reinicia y crea una copia en C: puede tener el mismo número de inodo 81, pero dev=2 la distingue del objeto 1:81.
Enlazar o renombrar hacia C se rechaza con EXDEV en el modelo. La copia sí puede crear un objeto independiente allí. Los nombres de destino se usan al enlazar, copiar o renombrar; el contenido solo se usa al escribir o reemplazar. Si renombrar encuentra otro nombre del mismo objeto, no cambia ninguno de los dos.
El modelo admite hasta 16 nombres de 1 a 24 bytes ASCII y contenido de 0 a 80 bytes imprimibles. No representa permisos, montajes alternativos, asignación de bloques, enlaces simbólicos ni aperturas. Cuando desaparece el último nombre, elimina el objeto porque aquí no quedan otras referencias. No accede al equipo.
Enlaces globales y referencias abiertas
El segundo modelo aísla un archivo regular. Puedes configurar cuántos enlaces tiene, cuántos de esos nombres se ven dentro de un subárbol y cuántas descripciones abiertas lo conservan. Las aperturas iniciales representan acciones realizadas antes de la foto.
st_nlink no cuenta descriptores
Con el estado inicial, abre una vez y retira los dos nombres visibles: todavía hay un enlace fuera del subárbol. Retíralo también: nlink pasa a cero, pero el objeto permanece abierto. Cierra la última referencia para que quede sin referencias en este modelo. Intentar abrir por un nombre que ya no se ve no recupera el objeto sin nombre.
Los contadores del panel son didácticos. No modelan dup, mapas de memoria, trabajo diferido de liberación ni recuperación de capacidad del dispositivo. El tema 25 desarrollará las referencias abiertas con más detalle. Configurar otro escenario reconstruye su estado y no es una operación sobre el archivo anterior.
14. Usos y límites de una copia basada en enlaces
Los enlaces duros permiten publicar otro nombre, mantener accesible un objeto después de retirar una entrada y compartir archivos sin cambios entre árboles de respaldo. Son útiles cuando la identidad compartida es una propiedad deseada y el montaje admite la operación.
También crean un riesgo funcional sencillo: dos rutas que parecen independientes pueden recibir cambios del mismo objeto. Si una versión histórica y el archivo de trabajo son enlaces duros, escribir sobre el archivo existente altera lo observado desde ambos árboles.
| Mecanismo | Identidad inicial | Escritura posterior en el objeto de trabajo |
|---|---|---|
| Enlace duro | La misma. | La observan todos sus nombres. |
| Copia independiente | Distinta. | No modifica automáticamente el otro objeto. |
| Reflink cuando está disponible | Distinta, aunque pueda compartir bloques inicialmente. | El mecanismo copy-on-write separa los cambios según su contrato. |
Un esquema de respaldo con enlaces necesita reglas de inmutabilidad y de actualización: los archivos compartidos no deben editarse sobre el mismo objeto si se pretende conservar versiones anteriores. Que cierto editor reemplace nombres no garantiza que todas las aplicaciones o herramientas actúen así.
Además, dos nombres en el mismo almacenamiento no proporcionan independencia ante pérdida del volumen. El enlace duro no es por sí solo una copia de seguridad completa ni una instantánea de todo el árbol. En el tema 49 construiremos un proyecto de respaldo incremental con políticas explícitas y pruebas.
Cómo documentar una prueba
- Declara la relación inicial. Rutas consultadas, identidad, contador y estado del montaje.
- Nombra la operación exacta. Crear enlace, copiar, escribir, renombrar, reemplazar o retirar.
- Comprueba identidad y contenido por separado. No uses tamaño o cmp como única prueba de enlaces.
- Registra el alcance. Qué nombres se recorrieron y si pueden quedar otros fuera de la vista.
- Expón lo que no se midió. Persistencia, bytes físicos liberados o modificaciones concurrentes si no formaron parte del ensayo.
15. Resumen
- Un enlace duro añade un nombre para un objeto existente; no crea una segunda copia regular de sus datos.
- El nombre creado primero no tiene una prioridad permanente sobre los demás.
- La identidad se comprueba con dispositivo e inodo; igualdad de contenido es otra propiedad.
- st_nlink cuenta enlaces duros del archivo regular, no descriptores abiertos.
- Los nombres del mismo objeto comparten datos, tamaño y metadatos del archivo.
- Dos aperturas independientes pueden tener offsets distintos aunque lleguen al mismo objeto.
- Escribir modifica el objeto compartido; reemplazar una entrada puede separar un nombre del grupo.
- link y linkat rechazan un destino existente y están sujetos a permisos, soporte y límites de montaje.
- Crear otro nombre puede consumir recursos de directorio aunque no duplique los bloques de datos.
- Un recorrido encuentra nombres dentro de su alcance; el contador no proporciona sus rutas.
- Un enlace duro no congela el contenido ni proporciona por sí solo un respaldo independiente.
16. Preguntas de repaso y práctica
1. ¿El primer nombre creado es el verdadero archivo y el segundo una referencia dependiente?
No. Ambos nombres son entradas que identifican el mismo objeto. No existe una prioridad permanente del primero sobre el segundo.
2. ¿Crear un enlace duro duplica el inodo regular?
No. Añade una entrada de directorio y aumenta el contador del objeto existente. Puede requerir recursos para representar esa entrada.
3. ¿Por qué cmp no demuestra que haya un enlace duro?
Porque una copia independiente también puede tener el mismo contenido. La comparación de identidad requiere el par de dispositivo e inodo con una política de consulta conocida.
4. ¿Basta con que ls -i muestre el mismo número?
No entre sistemas de archivos diferentes. El número de inodo tiene ámbito en su sistema; hay que considerar también el dispositivo y el contexto del montaje.
5. ¿Abrir diez veces un archivo aumenta st_nlink en diez?
No. Abrir crea referencias abiertas, no entradas de directorio. El contador de enlaces puede permanecer igual.
6. ¿Qué ocurre si se elimina el nombre que llamábamos original?
Disminuye el contador, pero los otros nombres siguen accediendo al mismo objeto. No dependen del texto del nombre retirado.
7. ¿Puede haber un archivo con cero enlaces?
Sí, mientras otras referencias lo mantengan, por ejemplo una apertura existente. No puede abrirse por un nombre ya retirado, pero el descriptor puede seguir siendo válido.
8. ¿Cada nombre puede tener un modo de permisos diferente para el archivo?
No para los atributos compartidos del mismo inodo. Los directorios atravesados por cada ruta sí pueden imponer condiciones de acceso diferentes.
9. ¿Dos open independientes sobre alias comparten necesariamente offset?
No. Cada llamada crea su propia descripción abierta con posición propia. Compartir datos del archivo es distinto de compartir una descripción mediante dup.
10. ¿Por qué un editor puede separar dos enlaces al guardar?
Si crea un archivo nuevo y reemplaza uno de los nombres, ese nombre pasa al nuevo objeto. Los demás continúan identificando el objeto anterior.
11. ¿Renombrar A sobre B elimina A si ya son nombres del mismo objeto?
En el caso documentado por rename en Linux, la operación termina sin cambios. No se retira uno de los dos enlaces.
12. ¿link reemplaza un destino existente?
No. Falla con EEXIST. Un intento rechazado no debe aumentar el contador ni modificar el destino.
13. ¿Se puede crear un enlace duro entre volúmenes diferentes?
No. En Linux también puede fallar entre montajes distintos del mismo sistema de archivos. La operación informa el resultado efectivo, habitualmente EXDEV.
14. ¿Por qué no se enlazan directorios arbitrariamente?
La jerarquía impone reglas de parentesco y evita ciclos. Las referencias especiales de directorio no son permiso para crear cualquier relación de enlaces duros.
15. ¿Poder leer un archivo ajeno siempre permite enlazarlo?
No. Intervienen permisos de rutas y destino, políticas como protected_hardlinks, atributos y soporte del sistema de archivos.
16. ¿Qué aporta linkat frente a link?
Permite expresar cada ruta relativa desde un descriptor de directorio explícito. No congela sus entradas ni convierte el punto inicial en una frontera automática.
17. ¿Por qué sumar tamaños de nombres puede sobrecontar?
Varios nombres pueden pertenecer a un único objeto. Para un total de objetos únicos se deduplica por identidad; una suma por nombres aplica otro contrato.
18. ¿Un nlink de cinco y dos nombres encontrados es una contradicción?
No. Puede haber tres nombres fuera del subárbol recorrido o del alcance accesible. El contador no contiene la lista de rutas.
19. ¿Un reflink es otro nombre del mismo inodo?
No. Tiene identidad distinta aunque comparta bloques inicialmente mediante copy-on-write. Es una forma de compartición diferente.
20. ¿Un enlace duro conserva una versión histórica inmutable?
No por sí solo. Una escritura sobre el objeto compartido se observa desde todos sus nombres. Un diseño de versiones necesita políticas adicionales de actualización e inmutabilidad.
Para practicar
- Crea tres nombres de un archivo en dos directorios y comprueba identidad y nlink después de cada creación.
- Haz una copia de contenido idéntico y demuestra que tiene otra identidad.
- Escribe por un alias y observa el cambio por los otros nombres, sin modificar la copia independiente.
- Cambia permisos por uno de los nombres y explica por qué los demás muestran el mismo modo.
- Abre dos nombres de manera independiente y comprueba que sus posiciones de lectura pueden avanzar por separado.
- Repite una creación sobre un destino existente y verifica que el fallo no altera el contador.
- Ejecuta el ensayo C y explica las seis fotos sin depender de números concretos de inodo.
- Sustituye un nombre mediante un temporal y contrasta el contenido y la identidad de los dos grupos resultantes.
- Renombra un nombre sobre otro del mismo objeto y comprueba que ambos permanecen.
- Compara GNU du con y sin --count-links sobre un grupo nuevo, conservando las opciones y el alcance.
- Usa find -samefile dentro de un subárbol y explica por qué no garantiza hallar todos los nombres del volumen.
- Diseña una regla de actualización para un respaldo con enlaces que preserve versiones anteriores sin escribir sobre objetos compartidos.
En el tema 24 crearemos y leeremos enlaces simbólicos, observaremos enlaces rotos y compararemos sus reglas, límites y usos con los enlaces duros.