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.
opendir(ruta) → DIR *
│
├─ readdir → entrada
├─ readdir → entrada
├─ ...
└─ readdir → NULL: distinguir final y error
│
closediropendir 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.
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ón | Objeto consultado | Uso 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.
#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));
}
#endifLos 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.
#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.
$ 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.
arbol/
├─ a.txt
└─ sub/
└─ b.txt
Preorden: arbol, a.txt, sub, b.txt
Postorden: a.txt, b.txt, sub, arbolEn 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.
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ón | Qué informa | Decisión del programa |
|---|---|---|
FTW_D | Directorio visitado antes de sus hijos. | Procesar metadatos si la tarea los necesita. |
FTW_DP | Directorio después de sus hijos, con FTW_DEPTH. | Puede servir para subtotales. |
FTW_F | Visita de archivo. | Comprobar st_mode para exigir archivo regular. |
FTW_SL | Enlace simbólico con recorrido físico. | No entrar en su destino. |
FTW_DNR | Directorio que no se pudo leer. | Marcar el resultado como incompleto. |
FTW_NS | No 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.
- Enlaces simbólicos: nuestros recorridos nftw procesan el enlace, sin seguirlo. Un enlace hacia un ancestro no provoca descender nuevamente en ese árbol.
- Montajes: se activa FTW_MOUNT. La restricción está ligada al sistema de archivos; no es un mecanismo general de aislamiento ni sustituye el control de un espacio de nombres con montajes complejos.
- Enlaces duros: mini-find emite cada ruta coincidente; mini-du cuenta una vez cada par de identidad observado.
- Raíz: nftw también la visita. Mini-du la incluye; mini-find puede seleccionarla si es un archivo regular que coincide.
- Cambios concurrentes: no se obtiene una instantánea. Los laboratorios suponen un árbol sin modificaciones durante la medición.
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.
#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.
$ 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).
#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;
}$ 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))
PYLas 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ón | Respuesta de estos programas | Interpretación |
|---|---|---|
| La raíz de mini-ls no se abre | Diagnóstico y estado 1. | No se realizó el listado solicitado. |
| Falla un fstatat durante el listado | Se informa y se continúa; estado final 1. | Las filas emitidas no son un listado completo garantizado. |
| FTW_DNR o FTW_NS | Se marca recorrido incompleto. | No se confunde «sin acceso» con «sin objetos». |
| Falla memoria o se desborda la suma | El callback detiene mini-du. | No se imprime un total normal. |
| No hay coincidencias | Mini-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 stdout | Estado 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.
$ 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-findAntes 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.
$ 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")
PYEl 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.
$ ./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.nulCompara 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
$ ./mini-ls arbol/docs/nota.txt
$ ./mini-du no-existe
$ ./mini-find no-existe '*.txt' > fallido.nul
$ ./mini-find arbol '*.inexistente' > vacio.nulLos 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.
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")$ python3 verificar-recorridos.pyLos 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.
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
| Paso | Ruta | Nivel | Clasificació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
| Ruta | dev:inodo | Tipo | Longitud | Asignados | Aporte | Decisió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
| Herramienta | Trabajo central | Memoria y límites |
|---|---|---|
| mini-ls | Una enumeración de N entradas y una consulta de metadatos por hijo procesado. | No almacena todas las filas; no ordena ni recorre descendientes. |
| mini-du | Recorrido 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-find | Recorrido, 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
opendir,readdiryclosedirgestionan un flujo de enumeración.readdirdevuelve NULL tanto al finalizar como al fallar; errno permite distinguirlo siguiendo su contrato.- Los nombres obtenidos deben procesarse o copiarse antes de reutilizar el flujo.
- La consulta de metadatos y la política de seguimiento de enlaces son decisiones separadas de enumerar.
fstatatrelativo al directorio evita concatenaciones en un búfer fijo, pero no elimina carreras sobre entradas.nftworganiza un recorrido de árbol y entrega ruta, clasificación y profundidad a un callback.- Preorden y postorden cambian cuándo se procesa un directorio respecto de sus descendientes.
- Mini-du deduplica objetos; mini-find conserva cada nombre coincidente.
- La longitud lógica regular y la asignación total son magnitudes diferentes.
- Los registros delimitados por nulo permiten representar rutas que contienen saltos de línea.
- Un resultado vacío con éxito y un recorrido fallido requieren interpretaciones distintas.
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
- Dibuja la secuencia de llamadas de un listado y señala dónde se distinguen final y error.
- Ejecuta mini-ls sobre el árbol preparado y comprueba nombres ocultos, FIFO y enlace roto.
- Añade un nombre con comillas o una barra inversa y observa su representación escapada.
- Compara atributos de un enlace con stat, lstat y fstatat sin seguimiento final.
- Obtén preorden y postorden del árbol del simulador sin asumir ese orden para el sistema real.
- Explica por qué el enlace hacia el ancestro no genera un ciclo con FTW_PHYS.
- Añade otro enlace duro a nota.txt y adapta la comprobación de nombres, sin aumentar el total lógico único.
- Compara el aporte de disperso.bin a longitud regular y asignación.
- Ejecuta mini-find con un patrón sin coincidencias y distingue su resultado del de una raíz inexistente.
- Prueba acceso denegado sobre un directorio propio con una cuenta sin privilegios y restituye sus permisos después.
- Sustituye el vector de identidades por una tabla hash conservando el par dev/inodo y la política de errores.
- 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.