← Sistemas de Archivos y E/SBloque 6 · Directorios · Tema 21 de 50

Tema 21

Recorrer directorios en C

Una entrada proporciona un nombre; para listar atributos, sumar uso o buscar archivos hay que decidir cómo consultar y recorrer sus objetos. Usaremos opendir, readdir, fstatat y nftw para construir tres herramientas: un listado de un nivel, un resumen de espacio y una búsqueda por patrón. Comprobaremos enlaces, archivos dispersos, nombres con saltos de línea, errores y resultados incompletos mediante un laboratorio y dos simuladores.

1. Abrir, enumerar y cerrar un directorio

En el tema 20 interpretamos registros de un directorio ext2. Para enumerar un directorio del sistema en ejecución no debemos analizar sus bloques: usamos una interfaz que oculta el formato concreto del montaje.

Flujo de directorio: estado de enumeración representado en C por DIR *. Permite obtener entradas sucesivas mediante readdir; no es un arreglo que contenga de antemano todos los nombres ni un descriptor entero.

ciclo de vida · biblioteca e interfaz de directorios
opendir(ruta) → DIR *
                  │
                  ├─ readdir → entrada
                  ├─ readdir → entrada
                  ├─ ...
                  └─ readdir → NULL: distinguir final y error
                  │
               closedir

opendir devuelve NULL si falla, por ejemplo por permisos, inexistencia o un componente que no sea directorio. closedir libera el flujo y su descriptor asociado; después no se vuelve a usar ese puntero. No hay que cerrar por separado el descriptor obtenido con dirfd mientras el flujo lo gestiona.

fdopendir permite partir de un descriptor ya abierto: si tiene éxito transfiere su gestión al flujo; si falla, el descriptor sigue siendo responsabilidad del llamador. Los contratos están en opendir(3) y closedir(3).

Enumerar nombres no equivale a ordenarlos. El orden puede depender del sistema, del historial de operaciones y de sus índices. Nuestros listados no prometen orden alfabético; cuando comparemos resultados usaremos conjuntos o listas ordenadas explícitamente.

2. Leer entradas sin confundir final y error

readdir devuelve un puntero a una struct dirent gestionada por la biblioteca. Su nombre se proporciona en d_name como cadena terminada en nulo. No se libera con free y hay que copiarla si se necesita conservarla tras posteriores lecturas del mismo flujo.

patrón · reiniciar errno antes de cada lectura
for (;;) {
    errno = 0;
    struct dirent *e = readdir(d);
    if (e == NULL) {
        if (errno != 0) {
            /* error: conservar diagnóstico y estado de fallo */
        }
        break;
    }
    /* procesar e antes de la próxima lectura */
}

La misma devolución NULL sirve para final y error. Reiniciar errno antes de cada llamada evita atribuirle un fallo dejado por otra operación del cuerpo del bucle. Consultarlo sin que readdir haya devuelto NULL tampoco identifica un error de lectura.

Si aparecen . y .., se omiten al recorrer hijos para no volver al directorio ni subir al padre. No se deben descartar todos los nombres que comiencen con punto: .config es un nombre de usuario válido.

d_type puede valer DT_UNKNOWN; no basta para todos los montajes. Tampoco se interpreta d_off como un offset físico del disco. Estas precauciones y la vida del puntero se describen en readdir(3).

El tamaño de struct dirent de una implementación no define el tamaño de un registro ext2. En el tema 20 el nombre en disco tenía longitud explícita; aquí la interfaz entrega una cadena. Son representaciones diferentes.

3. Del nombre a los metadatos

Para mostrar tamaño, tipo y contador de enlaces necesitamos consultar el objeto. Elegir qué objeto consultar es parte del diseño: si el nombre es un enlace simbólico, podemos examinar el propio enlace o seguir su destino.

OperaciónObjeto consultadoUso en este tema
stat(ruta, &s)El destino si el componente final es un enlace simbólico.Explicar la diferencia con lstat.
lstat(ruta, &s)El enlace final mismo.Oráculo de comprobación desde Python con lstat.
fstatat(dirfd, nombre, &s, AT_SYMLINK_NOFOLLOW)Objeto final sin seguir el enlace, relativo al directorio abierto.Listado de un nivel.
fstat(fd, &s)Objeto ya abierto.Alternativa cuando se necesita operar sobre un objeto identificado por descriptor.

Usar fstatat con el descriptor del directorio evita construir una ruta concatenando padre y nombre en un búfer fijo. No elimina todas las carreras: otro proceso puede cambiar una entrada entre readdir y la consulta. Si esta falla, se informa; no se inventan metadatos a partir de la entrada anterior.

El tipo se comprueba con S_ISREG, S_ISDIR y las demás macros sobre st_mode. La identidad se compara con (st_dev, st_ino). El número d_ino de una entrada no sustituye una consulta de atributos y puede diferir del objeto observado al atravesar un montaje.

La referencia de stat(2) detalla las variantes. Nuestros programas no siguen enlaces encontrados durante el recorrido; un enlace roto sigue siendo un objeto que se puede listar sin consultar su destino.

4. Nombres, salida y encabezado común

Un nombre puede contener espacios, comillas, saltos de línea y bytes de control. Para una salida dirigida a personas lo representaremos entre comillas, escapando bytes que puedan alterar la presentación. Para intercambiar rutas entre programas, el buscador usará un byte nulo como separador.

Guarda este encabezado junto a los tres fuentes de las secciones siguientes. La función escribe un escape hexadecimal por byte, no interpreta Unicode ni produce una codificación JSON. Los diagnósticos también representan los nombres de esta manera.

recorrido-comun.h · representación de nombres y diagnósticos
#ifndef RECORRIDO_COMUN_H
#define RECORRIDO_COMUN_H
#include <stdio.h>
#include <string.h>

static inline void citar(FILE *salida, const char *nombre)
{
    fputc('"', salida);
    for (const unsigned char *p = (const unsigned char *)nombre; *p; ++p) {
        if (*p >= 32 && *p <= 126 && *p != '"' && *p != '\\')
            fputc(*p, salida);
        else
            fprintf(salida, "\\x%02x", (unsigned)*p);
    }
    fputc('"', salida);
}

static inline void diagnostico(const char *operacion, const char *ruta, int error)
{
    fprintf(stderr, "%s: ", operacion);
    citar(stderr, ruta);
    fprintf(stderr, ": %s\n", strerror(error));
}
#endif

Los programas comprueban al final el estado de la salida estándar: imprimir resultados también puede fallar. Un estado distinto de cero significa que no se debe tratar la salida como un resultado completo. No usamos la presentación de ls como formato de entrada para otra herramienta.

5. Un ls propio de un nivel

mini-ls enumera los hijos inmediatos, incluidos los ocultos, salvo . y ... Consulta atributos sin seguir enlaces finales y presenta tipo, identidad, número de enlaces y longitud. No ordena, no baja a subdirectorios y no reproduce todas las opciones de GNU ls.

mini-ls.c · enumeración con metadatos relativos al padre
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/stat.h>
#include "recorrido-comun.h"

static char tipo(mode_t m)
{
    if (S_ISREG(m)) return 'f';
    if (S_ISDIR(m)) return 'd';
    if (S_ISLNK(m)) return 'l';
    if (S_ISFIFO(m)) return 'p';
    if (S_ISCHR(m)) return 'c';
    if (S_ISBLK(m)) return 'b';
    if (S_ISSOCK(m)) return 's';
    return '?';
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Uso: %s DIRECTORIO\n", argv[0]);
        return 1;
    }
    DIR *d = opendir(argv[1]);
    if (!d) { diagnostico("opendir", argv[1], errno); return 1; }
    int estado = 0;
    int padre = dirfd(d);
    if (padre == -1) {
        diagnostico("dirfd", argv[1], errno);
        estado = 1;
        goto fin;
    }
    for (;;) {
        errno = 0;
        struct dirent *e = readdir(d);
        if (!e) {
            if (errno) {
                diagnostico("readdir", argv[1], errno);
                estado = 1;
            }
            break;
        }
        if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
            continue;
        struct stat s;
        if (fstatat(padre, e->d_name, &s, AT_SYMLINK_NOFOLLOW) == -1) {
            diagnostico("fstatat", e->d_name, errno);
            estado = 1;
            continue;
        }
        printf("%c dev=%ju ino=%ju links=%ju size=%jd nombre=",
               tipo(s.st_mode), (uintmax_t)s.st_dev, (uintmax_t)s.st_ino,
               (uintmax_t)s.st_nlink, (intmax_t)s.st_size);
        citar(stdout, e->d_name);
        putchar('\n');
        if (ferror(stdout)) { estado = 1; break; }
    }
fin:
    if (closedir(d) == -1) {
        diagnostico("closedir", argv[1], errno);
        estado = 1;
    }
    if (fflush(stdout) == EOF) estado = 1;
    return estado;
}

La longitud de un enlace simbólico se refiere al texto almacenado por el enlace, no al tamaño del destino. La longitud de un directorio tampoco suma sus hijos. El primer argumento de opendir sí se resuelve normalmente: si la raíz solicitada es un enlace a un directorio, puede seguirlo. La política de no seguir aquí se aplica a las entradas consultadas con fstatat.

terminal · compilar y listar un directorio conocido
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 mini-ls.c -o mini-ls
$ ./mini-ls .

6. De un nivel a un árbol

Un listado de un nivel obtiene hijos inmediatos. Un recorrido recursivo además entra en los directorios encontrados. La raíz del recorrido tiene profundidad cero; sus hijos tienen profundidad uno. La profundidad no depende de cuántas barras tuviera la ruta inicial.

árbol y dos órdenes · hermanos en orden didáctico fijo
arbol/
├─ a.txt
└─ sub/
   └─ b.txt

Preorden:  arbol, a.txt, sub, b.txt
Postorden: a.txt, b.txt, sub, arbol

En preorden se procesa un directorio antes de sus descendientes. En postorden, después. El segundo resulta útil si cada directorio debe recibir un subtotal ya calculado de sus hijos. Para un único total global, como el de nuestro mini-du, cualquiera de ambos sirve si no se cuenta dos veces.

Una implementación propia debe administrar rutas o descriptores, errores, profundidad y recursos. Mantener abierto cada ancestro consume descriptores proporcionalmente a la profundidad. Guardar todo el árbol en memoria consume espacio proporcional a su tamaño. Una cola explícita puede evitar recursión de C, pero no elimina esas decisiones.

En este tema delegaremos la mecánica del árbol en nftw. La aplicación seguirá siendo responsable de definir qué cuenta, qué selecciona y qué considera un recorrido completo.

7. La interfaz nftw

nftw invoca una función de devolución, o callback, para los objetos que visita. El callback recibe la ruta, metadatos, una clasificación y datos de ubicación. La macro _XOPEN_SOURCE 700, definida antes de los encabezados, expone la interfaz usada en Linux/glibc.

contrato · estructura de una visita
int visitar(const char *ruta, const struct stat *s,
            int clase, struct FTW *ubicacion);

nftw(raiz, visitar, 16, FTW_PHYS | FTW_MOUNT);

ubicacion->level: profundidad respecto de la raíz
ubicacion->base:  posición del nombre final dentro de ruta

callback devuelve 0: continuar
callback devuelve no cero: detener el recorrido
nftw devuelve -1: error propio de la función
ClasificaciónQué informaDecisión del programa
FTW_DDirectorio visitado antes de sus hijos.Procesar metadatos si la tarea los necesita.
FTW_DPDirectorio después de sus hijos, con FTW_DEPTH.Puede servir para subtotales.
FTW_FVisita de archivo.Comprobar st_mode para exigir archivo regular.
FTW_SLEnlace simbólico con recorrido físico.No entrar en su destino.
FTW_DNRDirectorio que no se pudo leer.Marcar el resultado como incompleto.
FTW_NSNo se obtuvieron los metadatos.No acceder a los campos de s.

El número 16 limita los directorios que la implementación mantiene abiertos simultáneamente; no limita el recorrido a 16 niveles. Un límite pequeño puede obligar a cerrar y reabrir directorios, aumentando trabajo.

Las opciones seleccionan políticas: FTW_PHYS no sigue enlaces simbólicos y FTW_MOUNT restringe el recorrido al sistema de archivos de inicio. FTW_DEPTH cambia al postorden. No usamos FTW_CHDIR, por lo que nuestros callbacks no necesitan cambiar el directorio de trabajo. El contrato completo está en nftw(3).

8. Enlaces, montajes e identidad

Antes de programar la suma o la búsqueda, fija estas decisiones. De otro modo dos programas pueden producir resultados diferentes y ambos estar aplicando contratos distintos.

ejemplo · nombres, objetos y destinos
arbol/a.txt       → objeto (dev=1, ino=81)
arbol/alias.txt   → objeto (dev=1, ino=81)  enlace duro
arbol/atajo       → enlace que contiene "a.txt"

Buscar nombres *.txt: puede devolver a.txt y alias.txt.
Sumar objetos únicos: cuenta el objeto 81 una vez.
Sumar uso físico sin seguir enlaces: también cuenta el propio atajo.

El tamaño lógico y los bloques asignados no son intercambiables. Como vimos en el tema 19, un archivo disperso puede tener muchos bytes lógicos con pocos bloques; aquí mini-du mostrará ambas magnitudes con alcances distintos y explícitos.

9. Un du propio con deduplicación

Este programa suma st_blocks × 512 de todos los objetos únicos visitados, incluidos directorios y enlaces simbólicos. Además suma st_size solo de los archivos regulares únicos. Esa segunda cifra no es la opción --apparent-size completa de GNU du: no incluye longitudes de enlaces ni de otros tipos.

Para deduplicar guardamos pares (st_dev, st_ino). Un vector lineal mantiene el ejemplo legible, con búsqueda O(U) por objeto y costo O(U²) en el peor caso para U objetos únicos. No es adecuado para millones de entradas; una tabla hash mejora esa parte.

mini-du.c · un resumen del árbol con unidades y referencias explícitas
#define _XOPEN_SOURCE 700
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <ftw.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/stat.h>
#include "recorrido-comun.h"

struct identidad { dev_t dev; ino_t ino; };
static struct identidad *vistos;
static size_t usados, capacidad;
static uintmax_t asignados, logicos;
static int incompleto;

/* 1: nuevo; 0: repetido; -1: no se pudo registrar. */
static int registrar(const struct stat *s)
{
    for (size_t i = 0; i < usados; ++i)
        if (vistos[i].dev == s->st_dev && vistos[i].ino == s->st_ino)
            return 0;
    if (usados == capacidad) {
        if (capacidad > SIZE_MAX / 2) { errno = EOVERFLOW; return -1; }
        size_t nueva = capacidad ? capacidad * 2 : 64;
        if (nueva > SIZE_MAX / sizeof *vistos) {
            errno = EOVERFLOW;
            return -1;
        }
        void *p = realloc(vistos, nueva * sizeof *vistos);
        if (!p) return -1;
        vistos = p;
        capacidad = nueva;
    }
    vistos[usados++] = (struct identidad){ s->st_dev, s->st_ino };
    return 1;
}

static int visitar(const char *ruta, const struct stat *s,
                   int clase, struct FTW *ubicacion)
{
    (void)ubicacion;
    if (clase == FTW_NS || clase == FTW_DNR) {
        fputs("Visita incompleta: ", stderr);
        citar(stderr, ruta);
        fputc('\n', stderr);
        incompleto = 1;
        return 0;
    }
    int nuevo = registrar(s);
    if (nuevo == -1) {
        diagnostico("registrar identidad", ruta, errno);
        return 1;
    }
    if (!nuevo) return 0;
    if (s->st_blocks < 0 || (uintmax_t)s->st_blocks > UINTMAX_MAX / 512 ||
        (S_ISREG(s->st_mode) && s->st_size < 0)) {
        diagnostico("contabilidad", ruta, EOVERFLOW);
        return 1;
    }
    uintmax_t a = (uintmax_t)s->st_blocks * 512;
    uintmax_t l = S_ISREG(s->st_mode) ? (uintmax_t)s->st_size : 0;
    if (a > UINTMAX_MAX - asignados || l > UINTMAX_MAX - logicos) {
        diagnostico("suma", ruta, EOVERFLOW);
        return 1;
    }
    asignados += a;
    logicos += l;
    return 0;
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Uso: %s RAIZ\n", argv[0]);
        return 1;
    }
    int r = nftw(argv[1], visitar, 16, FTW_PHYS | FTW_MOUNT);
    if (r == -1) diagnostico("nftw", argv[1], errno);
    int estado = (r != 0 || incompleto);
    if (!estado)
        printf("asignados=%ju logicos_regulares=%ju objetos=%zu\n",
               asignados, logicos, usados);
    else
        fputs("No se publica un total completo del árbol\n", stderr);
    free(vistos);
    if (fflush(stdout) == EOF) estado = 1;
    return estado;
}

Se comprueban desbordamientos en las multiplicaciones, en la suma y al ampliar el vector. Un callback que devuelve 1 detiene el recorrido y propaga el fallo. Si se encuentra un directorio ilegible, se continúa para informar otras incidencias, pero no se publica un total normal.

La deduplicación es global para una sola raíz. No produce subtotales por directorio ni decide a qué carpeta atribuir un archivo con varios nombres. Tampoco calcula espacio exclusivo recuperable: compresión, reflinks y snapshots pueden hacer que la contabilidad atribuida no equivalga a capacidad física exclusiva.

terminal · compilar y consultar una raíz
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 mini-du.c -o mini-du
$ ./mini-du .

10. Un find propio por nombre

El buscador selecciona archivos regulares cuyo nombre final coincide con un patrón de fnmatch. No sigue enlaces simbólicos, no cruza al sistema de archivos de otro montaje y no deduplica nombres de enlaces duros: buscar rutas y sumar objetos son tareas diferentes.

Con flags cero, fnmatch admite comodines como *, ? y expresiones entre corchetes. El punto inicial no se excluye de forma especial. El patrón se aplica al nombre final obtenido mediante ubicacion->base, no a toda la ruta. Consulta fnmatch(3).

mini-find.c · archivos regulares coincidentes y salida delimitada por nulo
#define _XOPEN_SOURCE 700
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <fnmatch.h>
#include <ftw.h>
#include <sys/stat.h>
#include "recorrido-comun.h"

static const char *patron;
static int incompleto;

static int visitar(const char *ruta, const struct stat *s,
                   int clase, struct FTW *ubicacion)
{
    if (clase == FTW_NS || clase == FTW_DNR) {
        fputs("Visita incompleta: ", stderr);
        citar(stderr, ruta);
        fputc('\n', stderr);
        incompleto = 1;
        return 0;
    }
    if (!S_ISREG(s->st_mode)) return 0;
    int r = fnmatch(patron, ruta + ubicacion->base, 0);
    if (r == FNM_NOMATCH) return 0;
    if (r != 0) {
        fputs("No se pudo evaluar el patrón\n", stderr);
        return 1;
    }
    size_t n = strlen(ruta);
    if (fwrite(ruta, 1, n, stdout) != n || fputc('\0', stdout) == EOF)
        return 1;
    return 0;
}

int main(int argc, char **argv)
{
    if (argc != 3) {
        fprintf(stderr, "Uso: %s RAIZ PATRON\n", argv[0]);
        return 1;
    }
    patron = argv[2];
    int r = nftw(argv[1], visitar, 16, FTW_PHYS | FTW_MOUNT);
    if (r == -1) diagnostico("nftw", argv[1], errno);
    int estado = (r != 0 || incompleto);
    if (fflush(stdout) == EOF) estado = 1;
    if (estado) fputs("Búsqueda incompleta o error de salida\n", stderr);
    return estado;
}
terminal · compilar y guardar registros binarios
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 mini-find.c -o mini-find
$ ./mini-find . '*.txt' > encontrados.nul
$ python3 - <<'PY'
from pathlib import Path
for ruta in Path("encontrados.nul").read_bytes().split(b"\0"):
    if ruta:
        print(repr(ruta))
PY

Las comillas de '*.txt' impiden que Bash expanda el patrón antes de ejecutar el programa. La salida nula conserva los bytes de cada ruta, incluidos saltos de línea. Se muestra mediante repr para inspección humana. El código de salida debe comprobarse antes de consumir un archivo de resultados como completo.

El buscador emite resultados a medida que avanza. Si ocurre un error posterior, lo ya emitido puede ser una parte válida de una búsqueda incompleta. El consumidor debe descartar o marcar esa salida; no basta con contar líneas o ignorar stderr.

11. Errores y recorridos incompletos

Una carpeta puede permitir enumerar nombres y no permitir consultar todos sus hijos; también puede desaparecer una entrada entre ambas operaciones. En una visita FTW_NS no se usan los campos de struct stat porque no están definidos para esa consulta.

SituaciónRespuesta de estos programasInterpretación
La raíz de mini-ls no se abreDiagnóstico y estado 1.No se realizó el listado solicitado.
Falla un fstatat durante el listadoSe informa y se continúa; estado final 1.Las filas emitidas no son un listado completo garantizado.
FTW_DNR o FTW_NSSe marca recorrido incompleto.No se confunde «sin acceso» con «sin objetos».
Falla memoria o se desborda la sumaEl callback detiene mini-du.No se imprime un total normal.
No hay coincidenciasMini-find devuelve 0 y salida vacía si el recorrido fue completo.Es un resultado válido, diferente de una búsqueda fallida.
Falla escritura o vaciado de stdoutEstado distinto de cero.La entrega del resultado no está completa.

Los callbacks no suponen que errno contenga una causa portable y específica para todas las clasificaciones FTW_DNR/FTW_NS. Informan la ruta y el carácter incompleto. En cambio, después de un retorno -1 de una función que documenta errno, capturan el error para el diagnóstico.

Estos programas inspeccionan, no modifican el árbol. No constituyen un recorrido resistente a adversarios que cambien enlaces, renombren padres o alteren montajes mientras se ejecutan. Para operar sobre objetos concretos se necesitan diseños basados en descriptores y una política de resolución, que conectaremos con los temas 22 y 29.

12. Laboratorio con un árbol controlado

Usa Linux con GCC, Python 3, GNU Coreutils y GNU Findutils. En WSL trabaja preferentemente dentro del directorio personal de Linux para disponer de enlaces y FIFO con semántica habitual. No hacen falta privilegios ni imágenes de disco. Los datos ocupan pocos MiB y el laboratorio no intenta agotar recursos.

terminal · directorio nuevo y compilación
$ LAB21=$(mktemp -d "$HOME/tema21.XXXXXX")
$ cd -- "$LAB21"
$ findmnt -T .
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 mini-ls.c -o mini-ls
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 mini-du.c -o mini-du
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 mini-find.c -o mini-find

Antes de compilar, guarda el encabezado y los tres fuentes en ese directorio. El árbol se construye en un nombre nuevo, sin reutilizar contenido de otro ensayo.

terminal · árbol con enlaces, huecos y nombres especiales
$ python3 - <<'PY'
from pathlib import Path
import os

p = Path("arbol")
p.mkdir()
(p / "docs").mkdir()
(p / "sub").mkdir()
(p / "docs/nota.txt").write_bytes(b"uno\n")
os.link(p / "docs/nota.txt", p / "alias.txt")
(p / ".oculto.txt").write_bytes(b"oculto\n")
(p / "con espacio.txt").write_bytes(b"dos")
(p / "salto\nlinea.txt").write_bytes(b"x")
(p / "sub/datos.bin").write_bytes(os.urandom(8192))
(p / "sub/vacio").touch(exist_ok=False)
with open(p / "disperso.bin", "xb") as f:
    f.truncate(4 * 1024 * 1024)
    f.seek(-1, os.SEEK_END)
    f.write(b"Z")
    f.flush()
    os.fsync(f.fileno())
os.symlink("docs", p / "atajo")
os.symlink("no-existe", p / "roto")
os.symlink("..", p / "sub/volver")
os.mkfifo(p / "canal", 0o600)
print("Árbol preparado")
PY

El enlace sub/volver apunta hacia un ancestro y sirve para comprobar que el recorrido físico no entra en su destino. La FIFO verifica que seleccionar un archivo regular exige comprobar st_mode; ninguno de los programas abre su contenido para leerlo.

terminal · ejecutar y contrastar las políticas
$ ./mini-ls arbol
$ ./mini-du arbol
$ du -s -x -B1 -- arbol
$ ./mini-find arbol '*.txt' > propios.nul
$ find -P arbol -xdev -type f -name '*.txt' -print0 > referencia.nul

Compara el campo asignados con GNU du expresado en bytes. No compares logicos_regulares directamente con el uso asignado ni con una suma por nombres que cuente dos veces los enlaces duros. La referencia de GNU du describe unidades y política de enlaces.

En este árbol, mini-find debe seleccionar cinco rutas con extensión .txt, incluido el oculto y el nombre con salto de línea. Las dos rutas al archivo de cuatro bytes aparecen como nombres diferentes. No debe añadir una copia de docs/nota.txt por atravesar atajo.

Probar errores sin confundirlos con resultados vacíos

terminal · casos que se ejecutan de manera independiente
$ ./mini-ls arbol/docs/nota.txt
$ ./mini-du no-existe
$ ./mini-find no-existe '*.txt' > fallido.nul
$ ./mini-find arbol '*.inexistente' > vacio.nul

Los tres primeros deben fallar; el cuarto debe devolver éxito con cero registros. Consulta $? inmediatamente después de cada ejecución si haces la prueba a mano. Redirigir stdout puede crear un archivo vacío incluso cuando el programa falla: la existencia de ese archivo no demuestra éxito.

Para probar permisos con una cuenta ordinaria, crea un subdirectorio propio sin acceso y verifica que el recorrido se marca incompleto; restituye sus permisos al terminar. No ejecutes esa prueba como root esperando el mismo resultado: los privilegios pueden permitir el acceso. No cambies permisos de directorios ajenos.

13. Verificar conjuntos y contabilidad

El verificador siguiente ejecuta nuevamente las herramientas, compara conjuntos de rutas y calcula una referencia independiente mediante os.scandir y lstat. Está pensado para el árbol pequeño anterior, sin montajes internos ni cambios concurrentes. Su referencia de suma también deduplica por identidad, porque esa política es parte del contrato.

verificar-recorridos.py · comprobaciones de resultado, no de orden de enumeración
from pathlib import Path
import os
import stat
import subprocess

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

def registros(datos):
    assert not datos or datos.endswith(b"\0"), "registro sin terminador"
    return datos[:-1].split(b"\0") if datos else []

propios = registros(ejecutar("./mini-find", "arbol", "*.txt"))
referencia = registros(ejecutar("find", "-P", "arbol", "-xdev",
                               "-type", "f", "-name", "*.txt", "-print0"))
assert len(propios) == len(set(propios)) == 5
assert set(propios) == set(referencia)
assert b"arbol/salto\nlinea.txt" in propios
assert b"arbol/.oculto.txt" in propios
assert not ejecutar("./mini-find", "arbol", "*.inexistente")

vistos = set()
asignados = logicos = 0

def sumar(ruta):
    global asignados, logicos
    s = os.lstat(ruta)
    clave = (s.st_dev, s.st_ino)
    if clave in vistos:
        return
    vistos.add(clave)
    asignados += s.st_blocks * 512
    if stat.S_ISREG(s.st_mode):
        logicos += s.st_size
    if stat.S_ISDIR(s.st_mode):
        with os.scandir(ruta) as entradas:
            for e in entradas:
                sumar(e.path)

sumar("arbol")
campos = dict(c.split("=", 1) for c in
              ejecutar("./mini-du", "arbol").decode("ascii").split())
assert int(campos["asignados"]) == asignados
assert int(campos["logicos_regulares"]) == logicos == 4202511
assert int(campos["objetos"]) == len(vistos)
gnu = int(ejecutar("du", "-s", "-x", "-B1", "--", "arbol").split()[0])
assert asignados == gnu

listado = ejecutar("./mini-ls", "arbol")
assert b'nombre="salto\\x0alinea.txt"' in listado
assert b'nombre=".oculto.txt"' in listado
assert b'l dev=' in listado  # enlaces propios, incluido el roto
assert b'p dev=' in listado  # FIFO: no se leyó como archivo regular

for args in [("./mini-ls", "arbol/docs/nota.txt"),
             ("./mini-du", "no-existe"),
             ("./mini-find", "no-existe", "*.txt")]:
    p = subprocess.run(args, capture_output=True)
    assert p.returncode != 0 and p.stderr

print("Correctos: conjuntos, nombres, tipos, deduplicación, sumas y errores")
terminal · verificación sobre el árbol sin modificar
$ python3 verificar-recorridos.py

Los 4202511 bytes corresponden a contenido lógico regular único de los datos preparados, no a su asignación física. El valor asignado se obtiene del sistema en cada ejecución y no se fija como constante. El verificador utiliza recursión de Python por brevedad; no es una implementación para profundidades arbitrarias.

Si añades otro archivo o modificas contenido, adapta las expectativas del árbol. Una diferencia con GNU du en otro entorno requiere revisar alcance, soporte de contadores, montajes y compartición; no se corrige reemplazando el resultado por una cifra esperada.

14. Simuladores de recorrido y suma

Observar los callbacks de un árbol

El primer modelo muestra un recorrido físico. Los hermanos tienen un orden fijo para que puedas comparar preorden y postorden; el sistema real no tiene que enumerarlos así. El enlace sub/volver apunta a su padre y se visita como enlace, sin entrar en él.

árbol del modelo · distinto del árbol del laboratorio
arbol/                 dev=1, ino=10
├─ a.txt               ino=81, longitud=4, asignados=4096
├─ alias.txt           otro nombre del mismo objeto 81
├─ sub/                ino=11
│  ├─ .b.txt           ino=82, longitud=1024, asignados=4096
│  └─ volver → ..       ino=83, longitud=2, asignados=0
├─ disperso.bin        ino=84, longitud=4194304, asignados=4096
└─ restringido/        ino=12
   └─ privado.txt      ino=85, longitud=3, asignados=4096

Cada directorio del modelo: longitud=4096, asignados=4096.

Orden, profundidad y errores de acceso

estado del recorrido en memoria
PasoRutaNivelClasificación

En preorden, observa la raíz antes de sus descendientes. Cambia a postorden y comprueba que queda al final y se clasifica como FTW_DP. Al denegar acceso, privado.txt no recibe visita: el resultado es incompleto aunque el recorrido llegue al final.

Elegir qué se suma y cuántas veces

El segundo modelo usa el árbol completo con acceso permitido. Compara asignación de todos los objetos con longitud lógica de los regulares. Puedes contar una vez por identidad o una vez por nombre. Solo cambia la política; el alias no crea un segundo contenido.

Contabilidad de rutas y objetos

suma según el contrato elegido
Rutadev:inodoTipoLongitudAsignadosAporteDecisión

Con objetos únicos, la asignación total es 28672 bytes; por nombres pasa a 32768, porque el objeto 81 aporta dos veces. La longitud regular única es 4195335 bytes y por nombres sube a 4195339. El archivo disperso aporta cuatro MiB lógicos y solo 4096 bytes asignados en este modelo.

Las cifras de asignación son datos didácticos, no mediciones de un montaje. No hay caché, actividad concurrente, compresión ni snapshots. La simulación de acceso denegado del primer panel no afecta este segundo cálculo, que representa siempre el árbol completo.

15. Costos y límites de los ejemplos

HerramientaTrabajo centralMemoria y límites
mini-lsUna enumeración de N entradas y una consulta de metadatos por hijo procesado.No almacena todas las filas; no ordena ni recorre descendientes.
mini-duRecorrido y comparación con el vector de identidades; peor caso cuadrático en objetos únicos.O(U) para identidades, además de recursos de nftw.
mini-findRecorrido, comprobación de tipo y evaluación del patrón.No conserva la lista completa; la salida puede ser parcial si falla después.

Contar consultas no proporciona directamente tiempo de dispositivo. Nombres, inodos y datos pueden estar en caché; un sistema remoto añade sus costos; el patrón y la cantidad de objetos influyen en el trabajo de CPU. Para medir, conserva entorno, opciones y errores, y separa primera ejecución de repeticiones con caché caliente.

El vector de mini-du es global porque nftw no recibe un puntero de contexto de usuario en su callback estándar. El programa realiza un solo recorrido, sin hilos ni recorridos anidados. Una biblioteca reutilizable necesitaría otro diseño para administrar estado, cancelación y concurrencia.

Un recorrido no es una instantánea. El árbol puede cambiar, un número de inodo puede reutilizarse y una entrada puede desaparecer antes de consultarla. Las comprobaciones de estos ejemplos detectan fallos observables, pero no convierten en consistente una vista de un árbol que otros procesos modifican continuamente.

Para ampliar las herramientas, puedes añadir ordenación con copias de nombres, una tabla hash de identidades, subtotales en postorden o un recorrido basado en descriptores. Cada mejora debe conservar una política explícita de enlaces, montajes y errores.

16. Resumen

17. Preguntas de repaso y práctica

1. ¿DIR * es el número de un descriptor?

No. Es un puntero al estado de un flujo de directorio gestionado por la biblioteca. dirfd permite consultar su descriptor asociado sin transferir la responsabilidad de cierre.

2. ¿Cómo se distingue final de error en readdir?

Se pone errno a cero antes de llamar. Si devuelve NULL, errno distinto de cero indica error; cero indica final del flujo.

3. ¿Por qué se reinicia errno en cada iteración?

Porque otra operación del cuerpo puede haberlo modificado. Dejar un error anterior podría hacer que el final normal pareciera un fallo de readdir.

4. ¿Se puede guardar el puntero d_name y leer la siguiente entrada?

No se debe confiar en que siga conservando el nombre anterior. Para almacenarlo se copia su contenido a memoria propia antes de posteriores lecturas del mismo flujo.

5. ¿Se descarta cualquier nombre que empiece con punto?

No. Se omiten exactamente . y .. para procesar hijos. Los demás nombres ocultos siguen siendo entradas de usuario.

6. ¿Por qué no basta con d_type para todo?

Puede no estar disponible o valer DT_UNKNOWN. Para consultar atributos y decidir el tipo con la política elegida se usa stat, lstat o fstatat.

7. ¿Qué evita fstatat relativo al padre?

Evita concatenar la ruta del padre y el nombre en un búfer fijo. No impide que el nombre sea reemplazado entre la enumeración y la consulta.

8. ¿Qué hace AT_SYMLINK_NOFOLLOW?

Hace que la consulta del componente final obtenga atributos del enlace mismo. No es una política general para todos los componentes de cualquier ruta.

9. ¿La raíz forma parte de un recorrido nftw?

Sí. Tiene profundidad cero. El resumen la cuenta y el buscador puede seleccionarla si es un archivo regular cuyo nombre coincide.

10. ¿nopenfd=16 impide bajar al nivel 17?

No. Limita los directorios abiertos simultáneamente por la implementación. Un árbol más profundo puede requerir cierres y reaperturas.

11. ¿Qué cambia FTW_DEPTH?

Los directorios se procesan después de sus descendientes y se clasifican como FTW_DP. Es útil para combinar subtotales ya obtenidos.

12. ¿Se puede usar s cuando clase es FTW_NS?

No. Los metadatos no se obtuvieron y sus campos no están definidos para esa visita. Se informa el carácter incompleto.

13. ¿Cómo se detiene un recorrido estándar desde el callback?

Devolviendo un valor no nulo. Sin activar extensiones especiales de retorno, ese valor se propaga como resultado de nftw.

14. ¿Por qué mini-du necesita dev además de inodo?

Porque el número de inodo tiene ámbito en su sistema de archivos. La identidad se compara mediante el par, no con el número aislado.

15. ¿Mini-find debe eliminar rutas de enlaces duros repetidos?

No según su contrato. Busca nombres: dos rutas coincidentes deben aparecer aunque identifiquen el mismo contenido. Mini-du aplica otra política para sumar.

16. ¿Cuál es el costo del vector de identidades?

Usa memoria O(U) y puede requerir O(U²) comparaciones para U objetos únicos. Una tabla hash permite mejorar la búsqueda de identidades.

17. ¿Por qué se entrecomilla '*.txt' en Bash?

Para pasar el patrón al programa sin expansión previa por el shell. El programa lo evalúa con fnmatch sobre cada nombre final.

18. ¿Por qué un registro por línea no basta para rutas?

Porque un nombre puede contener saltos de línea. Un separador nulo permite conservar cada ruta sin esa ambigüedad.

19. ¿Un archivo de resultados vacío demuestra que no hubo coincidencias?

No. La redirección puede crearlo antes de ejecutar un programa que falla. También se debe comprobar el estado de salida y conservar los diagnósticos.

20. ¿El recorrido representa un instante único del árbol?

No. Las entradas y metadatos se observan en momentos distintos. Las pruebas de igualdad del laboratorio requieren que el árbol permanezca estable.

Para practicar

  1. Dibuja la secuencia de llamadas de un listado y señala dónde se distinguen final y error.
  2. Ejecuta mini-ls sobre el árbol preparado y comprueba nombres ocultos, FIFO y enlace roto.
  3. Añade un nombre con comillas o una barra inversa y observa su representación escapada.
  4. Compara atributos de un enlace con stat, lstat y fstatat sin seguimiento final.
  5. Obtén preorden y postorden del árbol del simulador sin asumir ese orden para el sistema real.
  6. Explica por qué el enlace hacia el ancestro no genera un ciclo con FTW_PHYS.
  7. Añade otro enlace duro a nota.txt y adapta la comprobación de nombres, sin aumentar el total lógico único.
  8. Compara el aporte de disperso.bin a longitud regular y asignación.
  9. Ejecuta mini-find con un patrón sin coincidencias y distingue su resultado del de una raíz inexistente.
  10. Prueba acceso denegado sobre un directorio propio con una cuenta sin privilegios y restituye sus permisos después.
  11. Sustituye el vector de identidades por una tabla hash conservando el par dev/inodo y la política de errores.
  12. Diseña una extensión que publique subtotales por directorio en postorden y explica cómo atribuirías los enlaces duros.

En el tema 22 estudiaremos cómo el kernel resuelve una ruta componente a componente, cómo intervienen los directorios y qué papel desempeñan las cachés de nombres y las referencias a objetos abiertos.