1. Una cadena necesita un contexto
En el tema 20 relacionamos nombres con objetos y en el 21 recorrimos árboles desde C. Ahora estudiaremos la búsqueda que permite que una llamada como open("curso/notas.txt", O_RDONLY) llegue al objeto correspondiente. La cadena por sí sola no identifica un archivo de manera permanente.
Resolución de rutas: procedimiento que interpreta una secuencia de componentes en un contexto de raíz, directorio inicial, montajes y reglas de acceso, hasta encontrar el objeto o el lugar donde una operación pretende crearlo.
| Forma | Punto inicial habitual | Ejemplo |
|---|---|---|
| Absoluta | Raíz visible del proceso. | /home/ana/curso/notas.txt. |
| Relativa con open | Directorio actual del proceso, cwd. | curso/notas.txt. |
| Relativa con openat | Directorio identificado por dirfd. | openat(base, "notas.txt", ...). |
| Relativa con AT_FDCWD | Directorio actual. | openat(AT_FDCWD, "curso/notas.txt", ...). |
Dos procesos con cwd distintos pueden usar la misma ruta relativa y alcanzar objetos distintos. Incluso una ruta absoluta depende del espacio de nombres de montajes y de la raíz visible del proceso. Un contenedor puede ver en /datos algo diferente de lo que ve su anfitrión.
Con openat ordinario, una ruta absoluta ignora dirfd. Además, .. y los enlaces pueden salir del directorio inicial: un descriptor de base establece un punto de partida, no una frontera automática. Más adelante compararemos esta semántica con las restricciones de openat2.
2. Resolver componente a componente
Para una ruta como /home/ana/curso/notas.txt, el kernel comienza en la raíz y busca cada nombre dentro del directorio alcanzado anteriormente. Un componente intermedio ordinario debe conducir a un directorio para continuar.
inicio: raíz (inodo 2)
buscar "home" en 2 → directorio 10
buscar "ana" en 10 → directorio 12
buscar "curso" en 12 → directorio 40
buscar "notas.txt" en 40 → archivo 81
La operación final usa el objeto 81 si sus condiciones se cumplen.
No se obtiene 81 calculando una fórmula sobre el texto de la ruta.Cada búsqueda relaciona un padre y un nombre. Puede resolverse con información en memoria o pedir ayuda al sistema de archivos. Dibujar cuatro pasos lógicos no demuestra cuatro lecturas físicas: nombres, metadatos y contenido tienen mecanismos de caché diferentes.
El siguiente algoritmo es conceptual, no una reproducción del código interno de Linux. La implementación real coordina modificaciones concurrentes, referencias, montajes y posibles revalidaciones.
elegir punto de inicio según la ruta y la API
para cada componente que deba atravesarse:
exigir un directorio y comprobar acceso de búsqueda
interpretar . y .. según el contexto
localizar el nombre
resolver enlaces o montajes según la política
aplicar la operación específica al componente finalNo es necesario enumerar todos los hijos con readdir para abrir uno. El sistema de archivos puede localizar el nombre mediante un índice y el VFS puede disponer ya de la asociación. Enumeración y búsqueda de un componente son operaciones relacionadas pero distintas.
3. Punto, punto punto y barras
. mantiene el directorio alcanzado y .. solicita su padre en el espacio de nombres. En la raíz visible, subir no permite ir por encima de ella. El kernel proporciona esa semántica aunque un formato no almacene entradas físicas para ambos nombres.
En Linux, barras consecutivas se tratan como separadores repetidos. Una barra final exige que lo resuelto allí sea un directorio, salvo particularidades de una operación que lo esté creando. Por eso abrir notas.txt/ no equivale a abrir notas.txt si es un archivo regular.
curso/./notas.txt → consulta dentro de curso
curso//notas.txt → separadores repetidos en Linux
curso/notas.txt/ → ENOTDIR
no-existe/../notas.txt → ENOENT, no se omite el componente inexistente
notas.txt/../curso → ENOTDIR
/.. → raíz visibleUna ruta vacía no significa el directorio actual para open ordinario: falla. Algunas APIs de Linux admiten una cadena vacía junto con opciones específicas, como AT_EMPTY_PATH; no hay que extrapolar esa excepción a todas las llamadas.
Normalizar texto no es resolver objetos
Eliminar pares nombre/.. sin consultar el sistema puede cambiar el significado cuando intervienen enlaces simbólicos, montajes o errores de acceso. También puede ocultar que un componente no existe o no es un directorio. Una función que simplifica cadenas no ejecuta por ello la resolución del kernel.
Las reglas de inicio, componentes, barras y errores se describen en path_resolution(7). Cuando una aplicación debe ser portable, tampoco debe asumir que todos los sistemas dan exactamente el mismo significado a cualquier forma con barras iniciales repetidas.
4. Permisos y componente final
Para buscar un nombre dentro de un directorio se requiere permiso de búsqueda, asociado al bit x, además de las políticas aplicables. Leer la lista de nombres y atravesar un directorio son capacidades diferentes. El bloque de permisos profundizará en propietarios, grupos y ACL.
Puede ser posible abrir un archivo por un nombre conocido sin poder enumerar su padre. A la inversa, ver un nombre en un listado no garantiza que se pueda atravesar el directorio para consultar o abrir el objeto. Los privilegios y otras políticas pueden modificar el resultado.
| Situación observada | Error habitual | Qué indica |
|---|---|---|
| Componente requerido inexistente | ENOENT | Falta una asociación necesaria, o un enlace lleva a un destino inexistente. |
| Archivo donde se necesita un directorio | ENOTDIR | No se puede continuar el recorrido. |
| Búsqueda o acceso denegado | EACCES | La operación no está autorizada por las comprobaciones aplicables. |
| Demasiadas expansiones de enlaces o enlace prohibido | ELOOP | Hay que considerar la política y la llamada que falló. |
El último componente depende de la operación
stat sigue un enlace final, mientras lstat consulta el enlace mismo. unlink retira el nombre del enlace y no borra su destino. open con creación puede aceptar que falte el último nombre si el padre existe y se cumplen las condiciones. No puede crear automáticamente los directorios intermedios ausentes.
O_NOFOLLOW en una apertura ordinaria impide seguir el enlace del componente final; no prohíbe enlaces en todo el prefijo. Las excepciones de O_PATH y las operaciones sobre enlaces merecen atención al leer el contrato. Nuestros ejemplos de apertura usan lectura ordinaria, sin O_PATH.
Si varias condiciones fallan en una ruta, no deduzcas todas ellas de un único errno: se informa un error de la operación, no un inventario completo de cada problema del árbol.
5. Cómo se expanden los enlaces simbólicos
Un enlace simbólico contiene una ruta que debe interpretarse cuando el recorrido lo sigue. Un destino relativo se interpreta desde el directorio que contiene el enlace, no desde el cwd del proceso que inició la operación. Un destino absoluto vuelve a la raíz visible, salvo políticas específicas como RESOLVE_IN_ROOT.
/home/ana/atajo → /srv/datos
solicitud: /home/ana/atajo/info.txt
1. llegar al enlace atajo
2. insertar su destino /srv/datos
3. conservar el sufijo info.txt
4. resolver /srv/datos/info.txtDespués de expandir el enlace se siguen procesando los componentes pendientes. Esto explica por qué simplificar atajo/.. como si fuera texto puede ser incorrecto:
/home/ana/atajo → /srv/datos
/home/ana/atajo/../nota.txt
│
└─ tras seguir el enlace, .. parte de /srv/datos
resultado: /srv/nota.txt
No equivale necesariamente a /home/ana/nota.txt.Un enlace roto produce error al seguirlo, pero sigue siendo consultable como enlace. Un bucle, como bucle → bucle, provoca expansiones repetidas. Linux limita a 40 los enlaces seguidos durante una resolución y devuelve ELOOP al exceder ese límite. También puede devolver ELOOP antes si la política prohíbe seguir alguno.
Este tema introduce el efecto sobre rutas; la creación, lectura y comparación de enlaces simbólicos se desarrollará en el tema 24. Un enlace no almacena una referencia permanente al inodo de su destino: si cambian los nombres, la misma cadena puede resolver otro objeto o dejar de resolver.
6. Montajes, raíz visible e identidad
Al llegar a un punto de montaje, el recorrido puede pasar del objeto que servía como punto de entrada a la raíz de otro montaje. El estado de una ruta en el VFS incluye la asociación con un montaje, además de la entrada correspondiente. Un número de inodo aislado no describe esa posición.
árbol de origen: /mnt/disco → directorio que sirve de punto de montaje
montaje activo: /mnt/disco → raíz del sistema montado
/mnt/disco/foto.jpg → búsqueda dentro del montaje
/mnt/disco/.. → padre visible del punto de montaje: /mntEl segundo caso no se explica leyendo únicamente el .. almacenado dentro de la raíz del volumen montado. La capa de montajes interviene en la navegación. Los montajes bind y los espacios de nombres añaden otras relaciones; el tema 30 desarrollará su administración.
La pareja (st_dev, st_ino) permite comparar identidad de objetos en nuestros ensayos, pero no es una ruta absoluta única ni identifica una posición de montaje sin contexto. Dos rutas pueden llegar al mismo objeto por relaciones diferentes.
La raíz y el cwd se mantienen como referencias a objetos del espacio de nombres, no como simples cadenas que el proceso deba volver a resolver en cada operación. De ahí que puedan seguir siendo útiles después de un renombrado.
7. Dentries y caché de nombres
Dentry: estructura del VFS que representa una asociación de un nombre con su padre y, cuando existe, con un inodo. La dcache conserva información de estas asociaciones para ayudar a resolver rutas.
Una dentry positiva está asociada a un objeto. Una dentry negativa registra una ausencia de nombre en ese padre. La segunda permite evitar repetir trabajo de búsqueda para un nombre que no existe, siempre que las reglas del sistema permitan confiar en esa información.
(padre 40, "notas.txt") → positivo: inodo 81
(padre 40, "nuevo.txt") → negativo: nombre ausente
(padre 50, "notas.txt") → otra asociación independiente
Crear nuevo.txt exige actualizar la información de ausencia.
Renombrar o borrar exige mantener coherentes las asociaciones afectadas.| Capa | Información característica | No demuestra |
|---|---|---|
| Caché de nombres | Asociaciones de componentes y padres. | Que el contenido del archivo ya esté en memoria. |
| Caché de inodos | Objetos y metadatos disponibles en memoria. | Que cada nombre posible del objeto esté localizado. |
| Caché de páginas | Páginas de contenido de archivos y otros usos de memoria. | Que una ruta futura vaya a resolver el mismo objeto. |
La caché no elimina la coordinación de permisos, cambios de nombres o revalidación. En sistemas remotos puede ser necesario consultar si una asociación sigue vigente. Las rutas rápidas del kernel pueden retroceder a un recorrido con más comprobaciones si no logran validar la información disponible.
La documentación de búsqueda de rutas del kernel explica dentries, coherencia y los recorridos RCU-walk y REF-walk. Son estrategias internas de concurrencia; no son opciones que una aplicación active para exigir un número fijo de lecturas.
Una segunda apertura más rápida no prueba por sí sola un acierto de dcache: también pudieron cambiar caché de metadatos, caché de contenido, planificación o carga del equipo. En el laboratorio no vaciaremos cachés globales ni presentaremos tiempos como contadores directos de búsquedas físicas.
8. Descriptores frente a rutas que cambian
Un descriptor de directorio puede ser un ancla para operaciones relativas. Si otra operación cambia su nombre en el árbol, el descriptor sigue refiriéndose al directorio abierto. Abrir después una cadena con el nombre antiguo vuelve a resolverla y puede fallar o alcanzar un reemplazo.
base = open("base", O_RDONLY | O_DIRECTORY)
rename("base", "movida")
mkdir("base", ...)
openat(base, "nota.txt", ...) → buscar dentro del directorio original
open("base/nota.txt", ...) → resolver el nuevo directorio llamado baseEso estabiliza el punto inicial, pero no congela sus hijos. Si alguien sustituye nota.txt dentro del directorio original, una nueva llamada openat puede obtener el reemplazo. Un descriptor ya abierto del archivo conserva una referencia al objeto que se abrió, con sus propias reglas de vida.
El cwd también puede cambiar mediante chdir o fchdir. En procesos con hilos, cambiarlo afecta a otros hilos que comparten ese contexto; usar descriptores explícitos evita depender de ese estado global para cada operación. La semántica de openat se detalla en open(2).
9. Programa en C: un directorio como ancla
Este programa crea un árbol privado nuevo. Cambia el cwd, renombra el directorio base y crea otro con su nombre anterior. Comprueba que el descriptor sigue llegando a la nota original, mientras la ruta que reutiliza el nombre llega al reemplazo.
Solo se escriben dos archivos de un byte, N y R. El ensayo es de un proceso sin modificaciones concurrentes. Mantiene un descriptor del cwd original para restaurarlo incluso si ocurre un error. Si falla, conserva los objetos del ensayo para inspección.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <fcntl.h>
#include <stdio.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_nota(int padre, char valor)
{
int fd = openat(padre, "nota.txt", O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
if (fd == -1) return -1;
ssize_t n;
do { n = write(fd, &valor, 1); } while (n == -1 && errno == EINTR);
int error = n == 1 ? 0 : n == 0 ? EIO : errno;
if (close(fd) == -1 && error == 0) error = errno;
if (error) { errno = error; return -1; }
return 0;
}
static int comprobar(int padre, const char *ruta, char esperado)
{
int fd = openat(padre, ruta, O_RDONLY | O_CLOEXEC);
if (fd == -1) return -1;
char c;
ssize_t n;
do { n = read(fd, &c, 1); } while (n == -1 && errno == EINTR);
int error = n == -1 ? errno : n != 1 || c != esperado ? EIO : 0;
if (close(fd) == -1 && error == 0) error = errno;
if (error) { errno = error; return -1; }
return 0;
}
int main(int argc, char **argv)
{
int cwd = -1, raiz = -1, base = -1, otro = -1, nueva = -1;
int estado = 1, temporal = -1;
struct stat inicial, actual;
if (argc != 2) {
fprintf(stderr, "Uso: %s DIRECTORIO_NUEVO\n", argv[0]);
return 1;
}
cwd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (cwd == -1) goto error;
if (mkdir(argv[1], 0700) == -1) goto error;
raiz = open(argv[1], O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (raiz == -1) goto error;
if (mkdirat(raiz, "base", 0700) == -1 ||
mkdirat(raiz, "otro", 0700) == -1) goto error;
base = openat(raiz, "base", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
otro = openat(raiz, "otro", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (base == -1 || otro == -1) goto error;
if (crear_nota(base, 'N') == -1 || fstat(base, &inicial) == -1) goto error;
if (fchdir(otro) == -1) goto error;
temporal = open("nota.txt", O_RDONLY | O_CLOEXEC);
if (temporal != -1 || errno != ENOENT) {
fputs("La ruta relativa al nuevo cwd no falló como se esperaba\n", stderr);
goto fin;
}
if (comprobar(base, "nota.txt", 'N') == -1) goto error;
if (renameat(raiz, "base", raiz, "movida") == -1) goto error;
if (mkdirat(raiz, "base", 0700) == -1) goto error;
nueva = openat(raiz, "base", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (nueva == -1 || crear_nota(nueva, 'R') == -1) goto error;
if (fstat(base, &actual) == -1) goto error;
if (actual.st_dev != inicial.st_dev || actual.st_ino != inicial.st_ino) {
fputs("Cambió la identidad del directorio abierto\n", stderr);
goto fin;
}
if (comprobar(base, "nota.txt", 'N') == -1 ||
comprobar(raiz, "movida/nota.txt", 'N') == -1 ||
comprobar(raiz, "base/nota.txt", 'R') == -1) goto error;
puts("cwd_independiente=correcto identidad=correcta ancla=N nombre_nuevo=R");
estado = 0;
goto fin;
error:
perror("operacion del ensayo");
fin:
if (cerrar(&temporal) == -1) { perror("close temporal"); estado = 1; }
if (cwd != -1 && fchdir(cwd) == -1) { perror("restaurar cwd"); estado = 1; }
if (cerrar(&nueva) == -1) { perror("close nueva"); estado = 1; }
if (cerrar(&otro) == -1) { perror("close otro"); estado = 1; }
if (cerrar(&base) == -1) { perror("close base"); estado = 1; }
if (cerrar(&raiz) == -1) { perror("close raiz"); estado = 1; }
if (cerrar(&cwd) == -1) { perror("close cwd"); estado = 1; }
if (fflush(stdout) == EOF) estado = 1;
return estado;
}$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 ancla-ruta.c -o ancla-ruta
$ ./ancla-ruta ensayo-anclaEl resultado demuestra estabilidad del punto inicial y diferencia entre objeto abierto y nombre reutilizado. No congela las entradas de base ni demuestra persistencia tras un corte de energía. Se conservan ensayo-ancla/movida/nota.txt con N y ensayo-ancla/base/nota.txt con R.
10. Restringir una resolución con openat2
Linux proporciona openat2 para solicitar restricciones durante la apertura. Recibe una estructura open_how inicializada a cero y un campo resolve. No es equivalente a inspeccionar una cadena antes de pasarla a open: las condiciones se aplican dentro del recorrido del kernel.
| Opción | Condición principal | Distinción útil |
|---|---|---|
RESOLVE_BENEATH | Rechaza resoluciones que escapen del directorio base. | Rechaza rutas y destinos simbólicos absolutos; no prohíbe todo enlace relativo. |
RESOLVE_IN_ROOT | Usa dirfd como raíz para esa resolución. | Reinterpreta rutas absolutas y mantiene .. en la raíz de esa operación. |
RESOLVE_NO_SYMLINKS | No permite seguir enlaces simbólicos en ningún componente. | Su alcance supera al O_NOFOLLOW final. |
RESOLVE_NO_MAGICLINKS | No permite resolver magic links, como ciertos enlaces de proc. | Se pide explícitamente cuando esa propiedad forma parte del contrato. |
RESOLVE_NO_XDEV | No permite atravesar puntos de montaje. | Incluye restricciones sobre montajes bind. |
RESOLVE_CACHED | Exige resolver con información disponible en caché sin el trabajo que la opción excluye. | Puede devolver EAGAIN; no informa cuántas lecturas físicas habría costado otra apertura. |
RESOLVE_BENEATH y RESOLVE_IN_ROOT son políticas distintas; no se combinan en los modos del ejemplo. Una ruta ../fuera.txt que escapa de la base se rechaza con BENEATH. Con IN_ROOT, .. en la raíz virtual se queda allí, por lo que ese mismo texto busca dentro de la base.
Las comprobaciones concurrentes pueden impedir asegurar una restricción y producir EAGAIN. La aplicación debe decidir un reintento acotado o informar el fallo; no debe retirar silenciosamente las condiciones para conseguir éxito. El ejemplo informa cada resultado sin reintentos automáticos.
La referencia de openat2(2) describe las condiciones y errores. Está disponible desde Linux 5.6, pero el kernel, las cabeceras y las políticas del entorno deben admitir la llamada y las opciones. Tener soporte no significa que toda restricción sea adecuada para cualquier aplicación.
11. Programa en C: comparar políticas de apertura
La herramienta siguiente abre un directorio base y consulta una ruta con uno de cinco modos: normal, final, debajo, sin-enlaces y en-raiz. Solo consulta identidad y tipo: no lee el contenido. O_NONBLOCK evita esperar a un escritor si la apertura ordinaria encuentra una FIFO; no es una restricción del tipo de objeto.
Los dos primeros modos usan openat. Los otros usan la llamada de sistema openat2 de Linux. Si no existe la llamada, se devuelve 77 con diagnóstico, sin reemplazarla por una apertura con menos restricciones. Otros fallos, incluidas denegaciones del entorno, se informan como errores; no se ocultan como falta de soporte.
#define _GNU_SOURCE
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <linux/openat2.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
static const char *etiqueta(int e)
{
switch (e) {
case ENOENT: return "ENOENT";
case ENOTDIR: return "ENOTDIR";
case EACCES: return "EACCES";
case ELOOP: return "ELOOP";
case EXDEV: return "EXDEV";
case ENOSYS: return "ENOSYS";
case EINVAL: return "EINVAL";
case EAGAIN: return "EAGAIN";
default: return "OTRO";
}
}
int main(int argc, char **argv)
{
if (argc != 4) {
fprintf(stderr, "Uso: %s BASE MODO RUTA\n", argv[0]);
return 1;
}
int flags = O_RDONLY | O_NONBLOCK | O_CLOEXEC;
unsigned long long resolve = 0;
int restringir = 0;
if (!strcmp(argv[2], "normal")) { /* openat sin restricciones extra */ }
else if (!strcmp(argv[2], "final")) flags |= O_NOFOLLOW;
else if (!strcmp(argv[2], "debajo")) {
restringir = 1;
resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
} else if (!strcmp(argv[2], "sin-enlaces")) {
restringir = 1;
resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS;
} else if (!strcmp(argv[2], "en-raiz")) {
restringir = 1;
resolve = RESOLVE_IN_ROOT | RESOLVE_NO_MAGICLINKS;
} else {
fputs("Modo no admitido\n", stderr);
return 1;
}
int base = open(argv[1], O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (base == -1) { perror("open base"); return 1; }
int fd = -1;
if (!restringir) fd = openat(base, argv[3], flags);
else {
struct open_how how = {0};
how.flags = (unsigned long long)flags;
how.resolve = resolve;
#ifdef SYS_openat2
fd = (int)syscall(SYS_openat2, base, argv[3], &how, sizeof how);
#else
errno = ENOSYS;
#endif
}
int estado = 0;
if (fd == -1) {
int e = errno;
fprintf(stderr, "error=%s errno=%d detalle=%s\n", etiqueta(e), e, strerror(e));
estado = restringir && e == ENOSYS ? 77 : 1;
} else {
struct stat s;
if (fstat(fd, &s) == -1) { perror("fstat"); estado = 1; }
else {
char tipo = S_ISREG(s.st_mode) ? 'f' : S_ISDIR(s.st_mode) ? 'd' : 'o';
printf("dev=%ju ino=%ju tipo=%c bytes=%jd\n",
(uintmax_t)s.st_dev, (uintmax_t)s.st_ino,
tipo, (intmax_t)s.st_size);
}
if (close(fd) == -1) { perror("close objeto"); estado = 1; }
}
if (close(base) == -1) { perror("close base"); estado = 1; }
if (fflush(stdout) == EOF) estado = 1;
return estado;
}$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 consultar-ruta.c -o consultar-rutaSi falta linux/openat2.h, se necesitan cabeceras de desarrollo apropiadas para compilar esta parte del laboratorio. La comprobación de SYS_openat2 solo cubre la ausencia de su número de llamada en cabeceras que sí tienen los tipos requeridos. El directorio base se abre con resolución ordinaria y debe ser el directorio de confianza previsto por la aplicación.
La restricción se aplica al recorrido solicitado, no valida el contenido ni los permisos de negocio de un archivo. Tampoco convierte un árbol modificable en una instantánea. El tipo o agrupa los objetos que no son regulares ni directorios; el ejemplo no pretende ser un clasificador completo.
12. Laboratorio de rutas y restricciones
Usa Linux o WSL, Bash, GCC y Python 3. Trabaja dentro de un directorio nuevo, preferentemente en el sistema Linux de WSL. No se modifican montajes, la raíz del proceso ni las cachés globales. Guarda los dos fuentes anteriores en este directorio antes de compilarlos.
$ LAB22=$(mktemp -d "$HOME/tema22.XXXXXX")
$ cd -- "$LAB22"
$ findmnt -T .
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 ancla-ruta.c -o ancla-ruta
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 consultar-ruta.c -o consultar-ruta
$ ./ancla-ruta ensayo-anclaComprueba N y R en los archivos conservados por el primer programa. Ejecutarlo otra vez sobre ensayo-ancla debe fallar al crear el directorio; no debe reutilizarlo ni sobrescribirlo.
$ python3 - <<'PY'
from pathlib import Path
import os
b = Path("base")
b.mkdir()
(b / "dir").mkdir()
(b / "directorio_real").mkdir()
(b / "directorio_real/sub").mkdir()
(b / "dir/nota.txt").write_bytes(b"N")
(b / "resultado.txt").write_bytes(b"B")
(b / "directorio_real/resultado.txt").write_bytes(b"R")
Path("fuera.txt").write_bytes(b"F")
os.symlink("dir/nota.txt", b / "enlace")
os.symlink("dir", b / "prefijo")
os.symlink("directorio_real/sub", b / "puerta")
os.symlink("../fuera.txt", b / "escapar")
os.symlink("ausente", b / "roto")
os.symlink("bucle", b / "bucle")
os.symlink("/dir/nota.txt", b / "absoluto")
print("Árbol preparado")
PYObservar apertura ordinaria y O_NOFOLLOW
$ ./consultar-ruta base normal dir/nota.txt
$ ./consultar-ruta base normal enlace
$ ./consultar-ruta base final enlace
$ ./consultar-ruta base final prefijo/nota.txt
$ ./consultar-ruta base normal escapar
$ ./consultar-ruta base normal dir/nota.txt/
$ ./consultar-ruta base normal roto
$ ./consultar-ruta base normal bucleLos dos primeros alcanzan la misma nota. El tercero falla con ELOOP al prohibir el enlace final; el cuarto tiene éxito porque el enlace aparece en el prefijo. escapar alcanza el archivo hermano de base: openat ordinario no contiene el recorrido. Los tres últimos muestran ENOTDIR, ENOENT y ELOOP respectivamente.
Estos errores son resultados esperados de casos independientes. Si haces la prueba a mano, consulta $? inmediatamente después de cada ejecución; un script que se detiene en el primer error no completará todos los casos.
Contrastar BENEATH, NO_SYMLINKS e IN_ROOT
$ ./consultar-ruta base debajo enlace
$ ./consultar-ruta base debajo escapar
$ ./consultar-ruta base sin-enlaces prefijo/nota.txt
$ ./consultar-ruta base en-raiz /dir/nota.txt
$ ./consultar-ruta base en-raiz absoluto
$ ./consultar-ruta base debajo /dir/nota.txtSi openat2 está disponible, BENEATH permite el enlace relativo que permanece dentro y rechaza el que escapa con EXDEV. NO_SYMLINKS rechaza también el enlace intermedio con ELOOP. IN_ROOT interpreta las dos formas absolutas dentro de base. BENEATH rechaza la ruta absoluta.
Un estado 77 significa llamada no disponible según el programa. Conserva el diagnóstico y omite las conclusiones de esa parte. Un error distinto exige revisar su causa; no es evidencia automática de que el kernel carezca de openat2.
Comprobar que punto punto no es una cancelación textual
$ ./consultar-ruta base normal puerta/../resultado.txt
$ ./consultar-ruta base normal resultado.txt
$ python3 - <<'PY'
from pathlib import Path
assert Path("base/puerta/../resultado.txt").read_bytes() == b"R"
assert Path("base/resultado.txt").read_bytes() == b"B"
print("La expansión del enlace cambia el padre desde el que se procesa ..")
PYVerificador de identidad y errores
Guarda el siguiente programa junto a los ejecutables. Compara resultados con objetos conocidos del árbol y comprueba errores por su etiqueta, sin depender del idioma del mensaje del sistema. No mide aciertos de caché ni tiempos de disco.
from pathlib import Path
import subprocess
def ejecutar(modo, ruta):
return subprocess.run(["./consultar-ruta", "base", modo, ruta],
capture_output=True, text=True)
def identidad(modo, ruta, esperado):
p = ejecutar(modo, ruta)
assert p.returncode == 0, p.stderr
campos = dict(c.split("=", 1) for c in p.stdout.split())
s = Path(esperado).stat()
assert (int(campos["dev"]), int(campos["ino"])) == (s.st_dev, s.st_ino)
def error(modo, ruta, etiqueta):
p = ejecutar(modo, ruta)
assert p.returncode == 1 and f"error={etiqueta} " in p.stderr, p
assert not p.stdout
nota = "base/dir/nota.txt"
for ruta in ["dir/nota.txt", "enlace", "dir//./nota.txt", str(Path(nota).resolve())]:
identidad("normal", ruta, nota)
identidad("final", "prefijo/nota.txt", nota)
identidad("normal", "escapar", "fuera.txt")
identidad("normal", "puerta/../resultado.txt", "base/directorio_real/resultado.txt")
identidad("normal", "resultado.txt", "base/resultado.txt")
for ruta, etiqueta in [("enlace", "ELOOP")]:
error("final", ruta, etiqueta)
for ruta, etiqueta in [("dir/nota.txt/", "ENOTDIR"), ("roto", "ENOENT"),
("bucle", "ELOOP"), ("", "ENOENT"),
("ausente/../resultado.txt", "ENOENT")]:
error("normal", ruta, etiqueta)
prueba = ejecutar("debajo", "dir/nota.txt")
if prueba.returncode == 77:
print("openat2 no disponible: se omiten las pruebas de restricciones")
else:
assert prueba.returncode == 0, prueba.stderr
identidad("debajo", "enlace", nota)
identidad("en-raiz", "/dir/nota.txt", nota)
identidad("en-raiz", "absoluto", nota)
identidad("en-raiz", "../dir/nota.txt", nota)
error("debajo", "escapar", "EXDEV")
error("debajo", "/dir/nota.txt", "EXDEV")
error("sin-enlaces", "prefijo/nota.txt", "ELOOP")
error("sin-enlaces", "enlace", "ELOOP")
print("Restricciones de openat2 comprobadas")
assert Path("ensayo-ancla/movida/nota.txt").read_bytes() == b"N"
assert Path("ensayo-ancla/base/nota.txt").read_bytes() == b"R"
p = subprocess.run(["./ancla-ruta", "ensayo-ancla"], capture_output=True)
assert p.returncode != 0
print("Correctos: identidad, ancla, cwd, enlaces, barras y errores")$ python3 verificar-rutas.pyLa prueba de permisos puede añadirse con una cuenta ordinaria sobre un directorio propio al que se quite temporalmente búsqueda, restaurándola al terminar. No se exige EACCES a un proceso privilegiado que pueda atravesarlo. Para este laboratorio no hacen falta cambios de privilegios.
13. Simuladores de resolución y caché
Seguir una ruta paso a paso
El primer modelo usa un árbol fijo, un solo volumen y una raíz visible. El cwd es /home/ana; el directorio base alternativo es /home/ana/curso. Las rutas absolutas empiezan siempre en la raíz del modelo. La selección del componente final compara consulta con seguimiento y consulta del propio enlace, como stat frente a lstat.
/ inodo 2
├─ home/ana/ inodo 12
│ ├─ nota.txt archivo 83
│ ├─ curso/ directorio 40
│ │ ├─ notas.txt archivo 81
│ │ ├─ practica/ directorio 60
│ │ └─ subida → ../nota.txt
│ ├─ atajo → /srv/datos
│ ├─ roto → no-existe
│ ├─ bucle → bucle
│ └─ secreto/ sin permiso de búsqueda en el modelo
│ └─ dato
└─ srv/ directorio 90
├─ nota.txt archivo 96
└─ datos/ directorio 91
└─ info.txt archivo 95Componentes, enlaces y errores
| Componente o acción | Posición | Interpretación |
|---|
La ruta inicial debe llegar a /srv/nota.txt, inodo 96. Prueba curso/subida, curso/notas.txt/, roto, bucle, secreto/dato y no-existe/../nota.txt. Cambia el tratamiento final para consultar el enlace roto en vez de seguirlo. Una barra final sigue imponiendo la condición de directorio.
Al cambiar campos, pulsa «Preparar resolución» para construir el nuevo recorrido. Solo se admiten letras ASCII, cifras, guion, guion bajo, punto y barra, con un máximo de 160 caracteres. El modelo no accede al equipo, no simula montajes ni resuelve permisos completos: secreto tiene una denegación fija y todos los demás directorios permiten búsqueda. El límite de 40 expansiones sí forma parte de su recorrido.
Una ausencia también puede estar en caché
El segundo panel representa un único directorio, inodo 40. Una tabla conserva asociaciones positivas o negativas y cuenta consultas al respaldo abstracto. Al crear o borrar, el modelo invalida la entrada afectada para que la consulta siguiente no reutilice información anterior.
Caché positiva, negativa y mutaciones
| Padre | Nombre | Estado de caché | Asociación |
|---|
Consulta dos veces nuevo.txt: la primera busca en el respaldo y la segunda reutiliza una ausencia. Créalo y consulta de nuevo: debe aparecer el inodo 82, sin conservar el ENOENT anterior. Repite la idea consultando y eliminando notas.txt. Vaciar caché conserva los objetos y los contadores acumulados; reiniciar restaura todo el escenario.
La invalidación explícita es una regla del modelo. Linux puede actualizar, convertir, retirar o revalidar dentries según la operación y el sistema de archivos; esta tabla no reproduce sus estructuras, su política de memoria ni su concurrencia. Una consulta al respaldo no equivale necesariamente a una lectura de dispositivo. Ambos simuladores son independientes.
14. Diagnosticar sin confundir las capas
- Conserva la cadena exacta y la llamada. Incluye barra final, política de enlace y si se crea o solo se consulta.
- Identifica el punto de partida. Raíz visible, cwd, dirfd y espacio de montajes relevante.
- Separa prefijo y componente final. Un error intermedio puede impedir llegar al objeto final.
- Examina enlaces y montajes. No canceles componentes de texto suponiendo que todos son directorios ordinarios.
- Interpreta la política. ELOOP puede ser un bucle o un enlace prohibido; EXDEV puede venir de una restricción de resolución.
- Compara objetos cuando corresponda. Usa identidad y descriptores, sin asumir que una ruta sea estable.
- Distingue evidencia de rendimiento. Tiempo, caché de nombres, metadatos y contenido no son la misma medición.
En un entorno controlado, herramientas como namei -l ruta ayudan a presentar los componentes; readlink consulta el texto de un enlace y realpath aplica su propia política de canonicalización. Lee sus opciones: no todas requieren que existan las mismas partes de la ruta.
Ni obtener una cadena canonicalizada ni comprobar permisos antes de abrir fija el árbol entre dos operaciones. Para requisitos sobre el recorrido, usa el contrato adecuado de la llamada que realiza la apertura. El tema 29 tratará las carreras entre comprobar y usar con más detalle.
No es necesario cambiar reservas, ejecutar como administrador o vaciar cachés para explicar un ENOENT o un ENOTDIR del laboratorio. Primero reconstruye qué componente se buscó, desde qué objeto y bajo qué reglas.
15. Resumen
- Una ruta requiere contexto: raíz visible, cwd o directorio de base y montajes.
- La resolución busca componentes sucesivos, sin exigir enumerar todo el directorio.
- Los componentes intermedios deben permitir continuar y superar las comprobaciones de búsqueda.
- La operación concreta decide qué hacer con el componente final y con su posible inexistencia.
- Los enlaces relativos se interpretan desde su padre; los absolutos vuelven a la raíz correspondiente.
- Una simplificación textual no reemplaza el recorrido de enlaces, montajes y errores.
- La dcache puede conservar tanto asociaciones positivas como ausencias.
- Caché de nombres, caché de inodos y caché de contenido describen información diferente.
- Un descriptor de directorio estabiliza el punto inicial frente a un renombrado, pero no congela sus hijos.
- openat ordinario no impone confinamiento; openat2 permite pedir restricciones durante la resolución.
- La falta de soporte o el fallo de una restricción no justifican retirar silenciosamente la política solicitada.
16. Preguntas de repaso y práctica
1. ¿La cadena curso/notas.txt identifica siempre el mismo archivo?
No. Depende del directorio inicial y del estado del espacio de nombres. Otro cwd o un cambio de entradas puede hacerla resolver otro objeto.
2. ¿Una ruta absoluta siempre empieza en la raíz del anfitrión?
No. Empieza en la raíz visible del proceso, salvo una política específica de la operación que cambie esa interpretación. Los espacios de nombres pueden ofrecer vistas distintas.
3. ¿openat usa dirfd para una ruta absoluta ordinaria?
No. La ruta absoluta ignora el directorio base en openat ordinario. RESOLVE_IN_ROOT de openat2 tiene otra semántica.
4. ¿Se necesita readdir para abrir un nombre conocido?
No. La búsqueda de un componente puede usar índices y asociaciones en memoria. Enumerar todos los hijos no es un paso obligatorio de cada apertura.
5. ¿Qué ocurre con ausente/../nota.txt?
Si ausente no existe y debe atravesarse, la resolución falla con ENOENT. No se cancela ese par de componentes sin comprobarlo.
6. ¿Por qué notas.txt/ puede fallar con ENOTDIR?
Porque la barra final exige que allí se resuelva un directorio. Un archivo regular no satisface esa condición.
7. ¿Leer un directorio y buscar dentro de él son el mismo permiso?
No. La lectura permite enumerar nombres; la búsqueda permite atravesarlo para localizar componentes. El resultado depende además de las demás políticas aplicables.
8. ¿Desde dónde se interpreta el destino relativo de un enlace?
Desde el directorio que contiene ese enlace. No se reinicia desde el cwd del proceso que solicitó la ruta.
9. ¿Por qué atajo/.. puede no equivaler a omitir ambos componentes?
Porque atajo puede llevar a otro directorio y entonces .. parte del destino alcanzado. También pueden intervenir montajes o fallos antes de llegar allí.
10. ¿Un enlace roto deja de ser consultable?
No. Puede consultarse el propio enlace con una operación que no siga su componente final. El error aparece al intentar llegar a un destino inexistente.
11. ¿O_NOFOLLOW prohíbe todos los enlaces de una ruta?
No en la apertura ordinaria mostrada. Afecta al componente final; un enlace intermedio puede seguirse. RESOLVE_NO_SYMLINKS tiene alcance sobre todo el recorrido.
12. ¿Qué es una dentry negativa?
Información de que un nombre no está asociado a un objeto en un padre concreto. Puede ayudar a responder búsquedas fallidas mientras sea válida.
13. ¿Un acierto de dcache implica que el contenido esté en memoria?
No. La asociación de nombre y el contenido pertenecen a capas diferentes. Resolver rápido no garantiza una lectura de datos sin E/S.
14. ¿Una ausencia almacenada puede conservarse sin cambios después de crear el nombre?
La información debe mantenerse coherente con la creación. El modelo la invalida; el sistema real puede actualizar o reorganizar las estructuras pertinentes.
15. ¿Un descriptor del padre sobrevive al renombrado de ese directorio?
Puede seguir refiriéndose al objeto abierto y servir como base relativa. La cadena con su nombre anterior se vuelve a resolver y puede llegar a un reemplazo.
16. ¿Un descriptor de directorio congela todas sus entradas?
No. Otros procesos pueden crear, eliminar o sustituir sus hijos. Una nueva apertura relativa consulta el estado que encuentre durante esa operación.
17. ¿RESOLVE_BENEATH equivale a prohibir cualquier enlace?
No. Puede permitir enlaces relativos cuya resolución permanezca dentro. Para prohibir seguir enlaces en todos los componentes se usa RESOLVE_NO_SYMLINKS.
18. ¿En qué se distingue RESOLVE_IN_ROOT?
Da a dirfd semántica de raíz para esa resolución: reinterpreta rutas absolutas y mantiene .. en esa raíz. BENEATH rechaza ciertas formas de escape en lugar de reinterpretarlas así.
19. ¿EAGAIN autoriza a repetir la apertura sin restricciones?
No. Se puede decidir un reintento acotado conservando el contrato o informar el fallo. Eliminar condiciones cambiaría las garantías solicitadas.
20. ¿El simulador de caché cuenta accesos físicos al disco?
No. Cuenta consultas a un respaldo abstracto. El sistema real puede tener metadatos disponibles, revalidar con otra capa o realizar trabajo diferente.
Para practicar
- Resuelve una misma cadena relativa desde dos directorios diferentes y dibuja cada búsqueda de componente.
- Compara una ruta ordinaria con la misma ruta terminada en barra y explica el tipo exigido.
- Construye un ejemplo donde cancelar nombre/.. produzca una ruta diferente debido a un enlace.
- Ejecuta ancla-ruta y comprueba el contenido de los dos archivos que conservan nombres parecidos.
- Explica por qué el descriptor base no impide modificar nota.txt dentro de él.
- Compara O_NOFOLLOW final con RESOLVE_NO_SYMLINKS sobre prefijo/nota.txt.
- Contrasta BENEATH e IN_ROOT con una ruta absoluta y con un .. inicial.
- Prueba el límite de enlaces con bucle y distingue ese error del rechazo por política.
- En el simulador, consulta una ausencia dos veces, crea el nombre y explica el cambio en la tabla.
- Describe qué se conserva al vaciar la caché del modelo y qué se restaura al reiniciarlo.
- Registra un fallo real del laboratorio con cadena, API, directorio inicial, política y errno.
- Explica por qué una segunda apertura más rápida no mide por sí sola un acierto de caché de nombres.
En el tema 23 estudiaremos los enlaces duros: cómo crear varios nombres para un mismo objeto, comprobar su identidad y seguir su contador de enlaces.