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

Tema 20

Los directorios son archivos

Una carpeta no contiene físicamente los bytes de todos sus hijos: organiza nombres que permiten llegar a otros objetos. Estudiaremos las entradas nombre → inodo, los cambios necesarios al crear, borrar y renombrar, el significado de punto y punto punto, y la diferencia entre tamaño de directorio y tamaño de un árbol. Dos programas en C, un laboratorio sobre una imagen ext2 y dos simuladores conectarán la vista de rutas con las estructuras que la sostienen.

1. Un archivo de un tipo especial

En los temas 17 a 19 separamos inodos, bloques y disponibilidad de recursos. Ahora abrimos conceptualmente uno de esos objetos: el directorio. Tiene identidad, propietario, permisos, marcas de tiempo y una representación que ocupa recursos. Su función es organizar el espacio de nombres.

Directorio: objeto del sistema de archivos que asocia nombres de componentes con otros objetos. En el modelo de inodos, cada entrada activa relaciona un nombre con un número de inodo del mismo sistema de archivos.

La palabra «archivo» se usa aquí en sentido amplio. Un directorio no es un archivo regular con una convención de texto: el kernel reconoce su tipo y aplica operaciones y restricciones específicas. En Linux, intentar leerlo con read como un archivo regular falla con EISDIR. No se modifica escribiendo manualmente líneas con nombres.

capas · tres contenidos distintos
directorio "curso" (inodo 40)
    contenido del directorio: "notas.txt" → inodo 81
                                        │
inodo 81: tipo regular, tamaño, permisos, mapa de datos
                                        │
contenido de notas.txt: "Repasar directorios\n"

Las aplicaciones usan operaciones como mkdir, open con creación, unlink, rmdir y rename. Para enumerar nombres existe una interfaz de directorios; la estudiaremos con opendir y readdir en el tema 21. Esa interfaz no entrega necesariamente los mismos bytes ni la misma estructura que hay en disco.

Un directorio de un sistema virtual también puede sintetizar sus entradas sin tener un archivo de bloques persistente detrás. La idea común es la asociación de nombres; la representación concreta depende del sistema de archivos.

2. Entradas: nombres que identifican objetos

Una entrada contiene un nombre local, no la ruta completa. En /home/ana/curso/notas.txt, el directorio curso necesita una entrada para notas.txt; no guarda obligatoriamente la cadena completa que empieza en la raíz. El recorrido de componentes se desarrollará en el tema 22.

EstructuraInformación característicaQué no representa
Entrada de directorioNombre local y referencia al objeto; puede incluir tipo y campos de organización.Una copia del contenido del archivo.
Inodo del objetoTipo, modo, tamaño, enlaces y representación del contenido.Un único nombre absoluto e inmutable.
RutaSecuencia de componentes interpretada desde un punto de partida.Identidad permanente del objeto.

En un directorio no puede haber dos entradas activas con el mismo nombre según las reglas de comparación del sistema. El mismo nombre sí puede aparecer en directorios diferentes. Dos nombres también pueden identificar un mismo archivo mediante enlaces duros, que estudiaremos en el tema 23.

ejemplo · nombres locales e identidad dentro de un volumen
directorio A                  directorio B
"datos.txt" → inodo 81         "datos.txt" → inodo 95
"respaldo"  → inodo 81

A/datos.txt y B/datos.txt: mismo nombre local, objetos diferentes.
A/datos.txt y A/respaldo: nombres diferentes, mismo objeto.

Bytes del nombre y límites

En las interfaces de Linux, un componente no puede contener / ni el byte nulo; la barra separa componentes y el nulo termina la cadena de la API. Espacios, saltos de línea y otros bytes pueden formar parte de un nombre. Por eso una lista de nombres no se analiza de manera general separando por espacios o por líneas.

El límite por componente depende del sistema. El formato ext tradicional admite hasta 255 bytes de nombre; bytes y caracteres Unicode no son equivalentes. Tampoco se debe suponer que todos los montajes comparan mayúsculas, minúsculas o normalizaciones del mismo modo. Los ejemplos de este tema usan nombres ASCII sencillos.

Un nombre que comienza con punto suele ocultarse en la presentación predeterminada de ls. Esa convención no cambia su tipo, sus permisos ni su pertenencia al directorio.

3. Punto, punto punto y jerarquía

. designa el directorio actual del recorrido y .. su padre en el espacio de nombres. En el formato clásico ext se materializan como entradas del directorio. Otros sistemas pueden proporcionar esa semántica sin almacenarlas de la misma manera.

modelo clásico · un subdirectorio dentro de curso
curso (inodo 40):
    "."       → 40
    ".."      → 12
    "practica" → 60

practica (inodo 60):
    "."       → 60
    ".."      → 40

Un directorio «vacío» no necesita carecer de estos nombres especiales: significa que no contiene entradas de usuario adicionales. Esa distinción es relevante para rmdir. En la raíz visible del proceso, subir con .. no permite escapar de esa raíz.

La jerarquía ordinaria de directorios evita ciclos: no se permite mover un directorio dentro de uno de sus descendientes y las aplicaciones no pueden crear enlaces duros arbitrarios a directorios. Los montajes y enlaces simbólicos añaden relaciones que estudiaremos más adelante.

Entrada en disco y recorrido visible no son idénticos en todos los casos.

Al atravesar puntos de montaje, la capa de rutas combina varios sistemas de archivos. No se puede explicar cualquier resultado de .. leyendo solamente un número de inodo de una imagen aislada. Los laboratorios de este tema usan subdirectorios ordinarios sin montajes internos.

4. Qué cambia al crear

Crear un archivo regular nuevo requiere relacionar varias estructuras. El orden siguiente describe responsabilidades lógicas; no es una secuencia universal de escrituras físicas ni un protocolo completo de recuperación frente a fallos.

  1. Resolver y comprobar el padre. Localizar el directorio, validar acceso y verificar las condiciones del nombre final.
  2. Obtener un objeto nuevo. Reservar e inicializar un inodo u otra estructura equivalente.
  3. Incorporar el nombre. Añadir la entrada al contenido del directorio, reutilizando espacio o ampliándolo si hace falta.
  4. Actualizar metadatos. Ajustar referencias, contadores y marcas de tiempo pertinentes.

Un archivo vacío puede no necesitar bloques de datos propios. Aun así, su nombre puede forzar un bloque adicional para el directorio. Es la razón por la que en el tema 19 no asociamos «cero bytes de contenido» con «cero recursos».

Crear un directorio

mkdir crea un objeto de tipo directorio y añade su nombre al padre. En el modelo ext clásico, el nuevo directorio incorpora . y ... Esa relación explica también los cambios en los contadores de enlaces del padre. La referencia de mkdir(2) describe creación, modo inicial y errores.

terminal · preparar un árbol nuevo en Bash
$ LAB20=$(mktemp -d "$HOME/tema20.XXXXXX")
$ cd -- "$LAB20"
$ mkdir origen destino
$ (set -o noclobber; printf 'contenido\n' > origen/nota.txt)
$ ls -lai -- origen
$ stat -c 'nombre=%n tipo=%F inodo=%i enlaces=%h tamano=%s' -- origen origen/nota.txt

La variante con O_CREAT sin O_EXCL puede abrir un archivo ya existente en vez de crear otro. No hay que confundir una apertura con una creación efectiva. En nuestros programas se exige que los nombres de creación sean nuevos.

5. Qué cambia al eliminar un nombre

unlink elimina una entrada, disminuyendo el número de enlaces del archivo al que se refería. Si era el último nombre, el objeto todavía puede mantenerse por descriptores abiertos u otras referencias. El tema 17 ya permitió observar esa continuidad y el tema 25 la desarrollará.

transición · quitar una entrada no elimina todas las referencias
antes:   "nota.txt" → 81     "alias.txt" → 81     nlink(81)=2
unlink("nota.txt")
después:                    "alias.txt" → 81     nlink(81)=1

Se modifica el directorio y el contador del objeto.
El contenido de 81 sigue disponible por alias.txt.

No se exige borrar físicamente cada byte del nombre o del contenido. El sistema puede marcar una entrada como libre o absorber su espacio dentro de otro registro. Liberar capacidad y sobrescribir de forma segura el soporte son problemas distintos. La referencia de unlink(2) detalla la eliminación de nombres.

Eliminar un directorio

Se utiliza rmdir, o unlinkat con AT_REMOVEDIR, y se requiere un directorio vacío de entradas de usuario. Un directorio con un archivo oculto sigue sin estar vacío. El error habitual por contenido restante es ENOTEMPTY; POSIX también permite EEXIST para esa condición. Consulta rmdir(2).

Los permisos de modificación del espacio de nombres se evalúan principalmente sobre el directorio padre, además del acceso de búsqueda en la ruta y reglas como el sticky bit. Que el contenido de un archivo sea de solo lectura no prohíbe por sí solo retirar su nombre. El bloque de permisos explicará estas decisiones con detalle.

6. Renombrar y mover

Dentro del mismo sistema de archivos, rename cambia la relación entre un nombre y su objeto. Renombrar origen/nota.txt a origen/informe.txt mantiene el objeto y cambia las entradas necesarias en el padre. El nuevo nombre puede necesitar una disposición diferente si ocupa más bytes.

Moverlo a destino/informe.txt modifica ambos directorios: se retira el nombre de uno y se incorpora en el otro. No hace falta copiar el contenido del archivo para implementar ese cambio de ubicación en el espacio de nombres.

terminal · comprobar identidad antes y después
$ stat -c 'antes dev=%d inodo=%i bytes=%s' -- origen/nota.txt
$ mv -- origen/nota.txt destino/informe.txt
$ stat -c 'despues dev=%d inodo=%i bytes=%s' -- destino/informe.txt
$ cat -- destino/informe.txt

Destino existente y límites de atomicidad

rename puede sustituir un destino existente si se cumplen sus condiciones. Ese destino pasa a identificar el objeto de origen; el objeto anterior del destino pierde ese nombre. La atomicidad evita observar el nombre de destino temporalmente ausente durante la sustitución; no convierte dos consultas separadas de un observador en una instantánea.

Si ambos nombres ya identifican el mismo archivo, la operación puede terminar sin cambiar nada. Para directorios existen restricciones de tipo y vaciedad del destino. Los detalles se describen en rename(2).

Un rename entre sistemas de archivos falla con EXDEV. La herramienta mv puede resolver ese caso mediante copia y eliminación; entonces no hay una única operación de renombrado ni conservación obligatoria de la identidad. Tampoco atomicidad durante la ejecución significa persistencia garantizada después de perder energía: el tema 35 tratará la sincronización necesaria.

7. Mover un directorio y sus descendientes

Supongamos que A/sub contiene mil archivos. Al moverlo a B/sub dentro del mismo sistema no es necesario reescribir las mil rutas dentro de los inodos: los hijos guardan nombres locales y los inodos no almacenan una ruta absoluta única.

modelo clásico · antes y después del cambio de padre
antes: A (40): "sub" → 60      B (50): sin "sub"
       sub (60): "." → 60, ".." → 40, "dato" → 81

rename A/sub → B/sub

después: A (40): sin "sub"     B (50): "sub" → 60
         sub (60): "." → 60, ".." → 50, "dato" → 81

Cambian las relaciones con los padres y los metadatos necesarios. La entrada dato sigue asociada al inodo 81. Un descriptor abierto del directorio puede seguir identificando el objeto 60 aunque su antigua ruta haya desaparecido.

En el modelo clásico sin excepciones, un directorio tiene dos enlaces iniciales y uno adicional por cada subdirectorio inmediato: nlink = 2 + cantidad_de_subdirectorios. Al trasladar un subdirectorio entre padres, el padre anterior pierde esa contribución y el nuevo la gana. No cambia por ello el número de enlaces del archivo regular que esté dentro.

Esa fórmula no es un contador universal de entradas. Los archivos regulares no incrementan así el número de enlaces del padre y algunos sistemas o características, como dir_nlink de ext4 para directorios muy grandes, requieren otra interpretación. Úsala para el modelo declarado y comprueba los valores reales.

8. Tamaño, enlaces y marcas de tiempo

stat directorio consulta el objeto directorio. Su st_size no es la suma recursiva de bytes de sus descendientes, ni necesariamente el número de entradas. Puede reflejar bloques y estructuras de su representación. Para sumar un árbol se necesita recorrerlo y definir qué contar, como vimos con du.

OperaciónDirectorio padreObjeto referido
Crear un archivo nuevoAñade entrada; puede crecer; cambia mtime/ctime.Nuevo inodo y metadatos iniciales.
Escribir en un archivo existenteNo añade ni retira entradas por esa escritura.Cambian datos, posiblemente tamaño, asignación y tiempos.
Eliminar un nombreRetira una entrada; puede conservar capacidad asignada.Disminuye su contador de enlaces.
Mover entre padresModifica las entradas de ambos padres.Conserva identidad en el mismo sistema; actualiza metadatos pertinentes.

mtime del directorio refleja modificaciones de sus entradas, no cualquier cambio en el contenido de sus descendientes. ctime es cambio de estado y no fecha de creación. La granularidad y las políticas pueden hacer que dos consultas consecutivas muestren el mismo valor numérico: no demuestres una operación exigiendo que cada marca de tiempo sea distinta.

Borrar nombres no obliga a encoger

Un directorio que creció puede conservar bloques después de eliminar muchas entradas. El espacio interior queda disponible para nuevas entradas sin que necesariamente disminuya st_size o st_blocks. No hay contradicción si el número de nombres baja y el tamaño informado permanece igual.

La identidad se compara mediante st_dev y st_ino, no por el número de inodo aislado entre volúmenes. Estos valores tampoco son identificadores eternos: un objeto eliminado puede dejar un número reutilizable.

9. Listas, registros variables e índices

Una organización sencilla recorre una lista de entradas hasta encontrar el nombre buscado. Si hay N nombres y no hay ayudas, una búsqueda fallida puede inspeccionar N entradas. Un índice por hash o un árbol reduce el trabajo de localización, pero todavía debe comparar nombres y resolver posibles colisiones.

No hay garantía de orden alfabético en el almacenamiento ni en la enumeración. ls normalmente ordena para presentar el resultado. Enumerar todas las entradas sigue necesitando producirlas, aunque un índice acelere la búsqueda de un nombre.

Un registro clásico con longitud variable

Para el laboratorio elegiremos una imagen ext2 con la característica filetype, directorios lineales y bloques de 1024 bytes. Cada registro comienza con una cabecera de ocho bytes; el nombre tiene longitud explícita y el registro se alinea a cuatro bytes.

OffsetBytesCampoInterpretación del formato elegido
04inodeReferencia little-endian; cero indica una entrada sin uso.
42rec_lenLongitud hasta el siguiente registro, incluyendo espacio interior.
61name_lenCantidad de bytes del nombre.
71file_typeTipo codificado del objeto.
8name_lennameBytes del nombre, sin terminador nulo obligatorio.
cálculo del mínimo · alineación y espacio reutilizable
minimo(nombre) = 4 × ceil((8 + longitud_en_bytes(nombre)) / 4)

"a"       : 8 + 1 = 9   → mínimo 12
"nota.txt": 8 + 8 = 16  → mínimo 16
"curso"   : 8 + 5 = 13  → mínimo 16

Si rec_len=40 para "curso", el registro tiene 24 bytes adicionales.
El siguiente registro comienza a +40, no a +16.

Los registros del formato elegido no atraviesan el final de un bloque. Para avanzar se usa rec_len; no se busca un byte nulo después del nombre. La documentación del kernel sobre entradas describe el esquema clásico compartido con la familia ext y las variantes indexadas. No se debe aplicar este esquema sin comprobar características a cualquier directorio ext4 moderno.

Al insertar se puede dividir un registro con espacio sobrante. Al eliminar, una implementación puede ampliar la longitud del registro anterior para cubrir el hueco. Los bytes antiguos pueden quedar dentro del espacio libre sin constituir una entrada activa. Un analizador debe seguir los registros actuales, no buscar cadenas sueltas y declararlas archivos existentes.

10. Programa en C: observar un cambio de padre

El siguiente programa crea un directorio nuevo con dos padres, A y B. Dentro de A crea sub, mantiene un descriptor abierto y lo mueve a B. Después verifica que la identidad se conserva, que la ruta anterior desaparece y que .. del objeto movido identifica al nuevo padre.

No recorre entradas ni implementa un listado: consulta objetos concretos con fstat y fstatat. El uso de operaciones relativas a descriptores permite mantener un punto de referencia durante el cambio de nombres. El laboratorio es de un solo proceso y supone que nadie modifica su directorio privado.

mover-directorio.c · identidad, padre y condición de vaciedad
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <fcntl.h>
#include <inttypes.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 iguales(const struct stat *a, const struct stat *b)
{
    return a->st_dev == b->st_dev && a->st_ino == b->st_ino;
}

static int foto(const char *fase, int a, int b, int sub)
{
    struct stat x, y, z;
    if (fstat(a, &x) == -1 || fstat(b, &y) == -1 ||
        fstat(sub, &z) == -1) return -1;
    printf("%s,A=%ju,B=%ju,sub=%ju,ino_sub=%ju\n", fase,
           (uintmax_t)x.st_nlink, (uintmax_t)y.st_nlink,
           (uintmax_t)z.st_nlink, (uintmax_t)z.st_ino);
    return 0;
}

int main(int argc, char **argv)
{
    int raiz = -1, a = -1, b = -1, sub = -1, estado = 1;
    struct stat inicial, actual, padre, esperado;
    if (argc != 2) {
        fprintf(stderr, "Uso: %s DIRECTORIO_NUEVO\n", argv[0]);
        return 1;
    }
    if (mkdir(argv[1], 0700) == -1) { perror("mkdir raiz"); return 1; }
    raiz = open(argv[1], O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (raiz == -1) goto error;
    if (mkdirat(raiz, "A", 0700) == -1 ||
        mkdirat(raiz, "B", 0700) == -1) goto error;
    a = openat(raiz, "A", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (a == -1) goto error;
    b = openat(raiz, "B", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (b == -1) goto error;
    if (mkdirat(a, "sub", 0700) == -1) goto error;
    sub = openat(a, "sub", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (sub == -1) goto error;
    if (fstat(sub, &inicial) == -1 ||
        fstatat(sub, "..", &padre, 0) == -1 ||
        fstat(a, &esperado) == -1) goto error;
    if (!iguales(&padre, &esperado)) goto inesperado;
    if (foto("antes", a, b, sub) == -1) goto error;
    if (renameat(a, "sub", b, "sub") == -1) goto error;
    if (fstat(sub, &actual) == -1 ||
        fstatat(b, "sub", &esperado, AT_SYMLINK_NOFOLLOW) == -1) goto error;
    if (!iguales(&inicial, &actual) || !iguales(&inicial, &esperado))
        goto inesperado;
    if (fstatat(sub, "..", &padre, 0) == -1 ||
        fstat(b, &esperado) == -1) goto error;
    if (!iguales(&padre, &esperado)) goto inesperado;
    if (fstatat(a, "sub", &actual, 0) != -1 || errno != ENOENT)
        goto inesperado;
    if (foto("despues", a, b, sub) == -1) goto error;

    /* B contiene sub: no se puede eliminar como directorio vacío. */
    if (unlinkat(raiz, "B", AT_REMOVEDIR) != -1 ||
        (errno != ENOTEMPTY && errno != EEXIST)) goto inesperado;
    if (cerrar(&sub) == -1) goto error;
    if (unlinkat(b, "sub", AT_REMOVEDIR) == -1) goto error;
    if (cerrar(&a) == -1 || cerrar(&b) == -1) goto error;
    if (unlinkat(raiz, "A", AT_REMOVEDIR) == -1 ||
        unlinkat(raiz, "B", AT_REMOVEDIR) == -1) goto error;
    puts("identidad=correcta padre=correcto ENOTEMPTY=correcto");
    estado = 0;
    goto fin;
inesperado:
    fprintf(stderr, "Relación o resultado inesperado\n");
    goto fin;
error:
    perror("operacion del laboratorio");
fin:
    if (cerrar(&sub) == -1) { perror("close sub"); estado = 1; }
    if (cerrar(&a) == -1) { perror("close A"); estado = 1; }
    if (cerrar(&b) == -1) { perror("close B"); estado = 1; }
    if (cerrar(&raiz) == -1) { perror("close raiz"); estado = 1; }
    if (fflush(stdout) == EOF) estado = 1;
    return estado;
}
terminal · compilar y ejecutar en Linux
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 mover-directorio.c -o mover-directorio
$ ./mover-directorio ensayo-c

En un sistema que use el conteo clásico, A y B pasan de 3, 2 a 2, 3; sub conserva dos enlaces y su identidad. El programa muestra esos contadores sin imponer la fórmula a todos los sistemas. Las comprobaciones obligatorias son identidad, cambio de padre, ausencia de la ruta anterior y rechazo de la eliminación de B mientras contiene sub.

Después del éxito queda ensayo-c vacío. Si falla alguna operación, pueden quedar objetos propios del ensayo para inspección. Una segunda ejecución sobre ese nombre debe fallar al intentar crearlo. No se sincroniza almacenamiento: se observan relaciones durante la ejecución, no recuperación después de una caída.

11. Programa en C: interpretar un volcado ext2

El segundo programa recibe un archivo regular con los bytes extraídos del contenido de un directorio de nuestra imagen. No abre un directorio del anfitrión para leerlo como datos. Su formato de entrada está limitado a directorios lineales ext2 con filetype, bloques de 1024 bytes y sin estructuras adicionales como colas de checksum.

La lectura de enteros se hace byte a byte en little-endian; no se convierte el búfer a un struct de C que pudiera incorporar relleno o alineación del compilador. Cada registro se valida antes de acceder al nombre y los bytes no imprimibles se presentan escapados.

leer-dir-ext2.c · lector de registros de la imagen preparada
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

enum { BLOQUE = 1024 };

static uint16_t le16(const unsigned char *p)
{
    return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}

static uint32_t le32(const unsigned char *p)
{
    return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
           ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}

static void nombre(const unsigned char *p, unsigned n)
{
    putchar('"');
    for (unsigned i = 0; i < n; ++i) {
        unsigned c = p[i];
        if (c >= 32 && c <= 126 && c != '"' && c != '\\')
            putchar((int)c);
        else
            printf("\\x%02x", c);
    }
    putchar('"');
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Uso: %s VOLCADO_DE_DIRECTORIO\n", argv[0]);
        return 1;
    }
    FILE *f = fopen(argv[1], "rb");
    if (!f) { perror("fopen"); return 1; }
    unsigned char b[BLOQUE];
    uintmax_t numero = 0;
    int estado = 1;
    size_t n;
    while ((n = fread(b, 1, sizeof b, f)) != 0) {
        if (n != sizeof b) {
            fprintf(stderr, "Bloque incompleto o error de lectura\n");
            goto fin;
        }
        size_t p = 0;
        while (p < sizeof b) {
            if (sizeof b - p < 8) goto formato;
            uint32_t ino = le32(b + p);
            unsigned rec = le16(b + p + 4);
            unsigned len = b[p + 6], tipo = b[p + 7];
            if (rec < 8 || rec % 4 || rec > sizeof b - p ||
                len > rec - 8) goto formato;
            if (ino != 0) {
                if (len == 0 || tipo > 7) goto formato;
                for (unsigned i = 0; i < len; ++i)
                    if (b[p + 8 + i] == 0 || b[p + 8 + i] == '/')
                        goto formato;
            }
            printf("bloque=%ju offset=%zu inode=%" PRIu32
                   " rec_len=%u name_len=%u tipo=%u nombre=",
                   numero, p, ino, rec, len, tipo);
            if (ino) nombre(b + p + 8, len);
            else fputs("(libre)", stdout);
            putchar('\n');
            p += rec;
        }
        ++numero;
    }
    if (ferror(f)) { perror("fread"); goto fin; }
    if (numero == 0) { fprintf(stderr, "Volcado vacío\n"); goto fin; }
    estado = 0;
    goto fin;
formato:
    fprintf(stderr, "Registro inválido o formato no admitido\n");
fin:
    if (fclose(f) == EOF) { perror("fclose"); estado = 1; }
    if (fflush(stdout) == EOF) estado = 1;
    return estado;
}
terminal · compilar el lector
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 leer-dir-ext2.c -o leer-dir-ext2

Una entrada libre puede conservar bytes antiguos; por eso no se presenta su nombre como activo. El programa rechaza longitud cero de registro, desalineación, salto fuera del bloque y nombre fuera del registro. No verifica que cada inodo esté asignado ni que el tipo coincida con su inodo: eso requiere consultar otras estructuras.

No es un verificador completo del sistema de archivos ni reconoce automáticamente características. Un archivo ajeno podría parecer sintácticamente válido y aun así no representar un directorio del formato declarado. Antes de usar el lector hay que preparar y comprobar la imagen según el laboratorio siguiente.

12. Laboratorio de operaciones y representación

Trabaja en el directorio privado creado en la sección 4. Necesitas Bash, GCC, Python 3, GNU Coreutils y e2fsprogs. En WSL usa preferentemente una ubicación dentro del sistema Linux y registra el montaje con findmnt -T .. Guarda los dos fuentes anteriores y compílalos.

Observar entradas y tamaño en el sistema anfitrión

En este ensayo se consultan metadatos antes y después de crear, escribir, renombrar y borrar. No exige valores particulares de tiempos ni que el directorio crezca o se reduzca con cada operación.

terminal · fotos independientes del padre y del hijo
$ mkdir observacion
$ stat -c 'inicial %n ino=%i size=%s blocks=%b links=%h mtime=%y ctime=%z' -- observacion
$ (set -o noclobber; : > observacion/a)
$ stat -c 'creado %n ino=%i size=%s blocks=%b links=%h mtime=%y ctime=%z' -- observacion observacion/a
$ printf 'datos\n' >> observacion/a
$ stat -c 'escrito %n ino=%i size=%s blocks=%b links=%h mtime=%y ctime=%z' -- observacion observacion/a
$ mv -- observacion/a observacion/nombre-mas-largo
$ ls -lai -- observacion
$ rm -- observacion/nombre-mas-largo
$ stat -c 'retirado %n ino=%i size=%s blocks=%b links=%h' -- observacion

Identifica qué foto corresponde al directorio y cuál al archivo. Escribir en a cambia el archivo sin añadir otro nombre. El renombrado cambia la entrada, aunque no necesite nuevos bloques. Después de retirarla, el directorio puede conservar su tamaño.

Crear una imagen con características conocidas

La imagen siguiente es un archivo nuevo de 16 MiB. mke2fs -d incorpora un árbol pequeño y el formato ext2 evita el journal. Forzamos filetype y desactivamos índices y checksums de metadatos para ajustarnos al lector. No se monta la imagen y no se necesitan privilegios.

terminal · preparar el contenido e inicializar solo la imagen nueva
$ command -v mke2fs dumpe2fs debugfs
$ mkdir -p semilla/curso/practica
$ (set -o noclobber; printf 'Una nota\n' > semilla/curso/nota.txt)
$ (set -o noclobber; : > semilla/curso/a)
$ python3 - <<'PY'
with open("directorios.ext2", "xb") as f:
    f.truncate(16 * 1024 * 1024)
PY
$ mke2fs -q -t ext2 -b 1024 -I 256 -m 0 -O filetype,^dir_index,^metadata_csum -d semilla directorios.ext2
$ LC_ALL=C dumpe2fs -h directorios.ext2 > formato.txt
$ cat formato.txt

Comprueba Block size: 1024, la presencia de filetype y la ausencia de dir_index y metadata_csum. Las opciones están descritas en mke2fs(8). Si la herramienta no admite la configuración o informa otro formato, conserva el diagnóstico y no interpretes la imagen con supuestos distintos.

Extraer los bytes del directorio y contrastar dos vistas

terminal · consultas de solo lectura y extracción a nombres nuevos
$ LC_ALL=C debugfs -R 'stat /curso' directorios.ext2
$ LC_ALL=C debugfs -R 'ls -l /curso' directorios.ext2
$ LC_ALL=C debugfs -R 'dump /curso curso-antes.bin' directorios.ext2
$ ./leer-dir-ext2 curso-antes.bin > entradas-antes.txt
$ cat entradas-antes.txt

Deben aparecer como activas ., .., a, nota.txt y practica, sin imponer un orden de inserción ni números de inodo. Contrasta el tipo 1 para archivos regulares y 2 para directorios. Suma los rec_len de cada bloque: deben cubrir exactamente 1024 bytes.

Eliminar una entrada dentro de la imagen

Solo la orden con -w siguiente modifica la imagen propia. La orden rm de debugfs elimina el nombre y ajusta el objeto; no se usa su orden de bajo nivel unlink, que no tiene la misma responsabilidad completa sobre los contadores.

terminal · retirar nota.txt y volver a examinar registros
$ LC_ALL=C debugfs -w -R 'rm /curso/nota.txt' directorios.ext2
$ LC_ALL=C debugfs -R 'dump /curso curso-despues.bin' directorios.ext2
$ LC_ALL=C debugfs -R 'ls -l /curso' directorios.ext2
$ ./leer-dir-ext2 curso-despues.bin > entradas-despues.txt
$ diff -u -- entradas-antes.txt entradas-despues.txt

diff devuelve 1 si hay diferencias; aquí es una salida esperada, no un fallo del laboratorio. nota.txt ya no debe figurar como entrada activa. Compara longitudes de registros y capacidad total, sin exigir que el volcado se acorte. Encontrar sus bytes antiguos con otra herramienta no demuestra que siga existiendo.

debugfs(8) documenta consultas, extracción y modificaciones. Revisa diagnósticos y consulta el estado final; el estado de salida por sí solo no certifica que una orden interna haya tenido éxito. La imagen exterior tiene su propia asignación en el anfitrión, distinta de los bloques interiores del directorio.

Comprobar el rechazo de un registro inválido

terminal · dañar una copia del volcado, sin modificar la imagen
$ python3 - <<'PY'
from pathlib import Path
b = bytearray(Path("curso-antes.bin").read_bytes())
b[4:6] = b"\x00\x00"  # rec_len cero: impediría avanzar
with open("registro-invalido.bin", "xb") as f:
    f.write(b)
PY
$ ./leer-dir-ext2 registro-invalido.bin

El lector debe terminar con estado distinto de cero y un diagnóstico de registro inválido, sin quedar en un ciclo. Un archivo truncado a medio bloque y un nombre que exceda rec_len - 8 también deben rechazarse. Estas comprobaciones validan el recorrido de registros, no la consistencia global del volumen.

13. Simuladores de nombres y registros

Modificar el espacio de nombres

El primer modelo tiene dos directorios del mismo volumen, A y B. Solo crea archivos regulares, con un nombre por objeto y sin descriptores abiertos. Cada creación asigna un inodo nuevo. Escribir añade 1024 bytes al objeto, mientras que renombrar puede cambiar nombre y padre sin copiar contenido. Si el destino existe, se sustituye su objeto.

Entradas e inodos separados

revisiones de las entradas, no marcas de tiempo reales
Entradas activas de usuario
PadreNombreInodo referido
Objetos regulares del modelo
InodoTipoBytes lógicosEnlaces

Crea A/nota.txt, escribe dos veces y muévelo a B/informe.txt. Verifica que conserva inodo y 2048 bytes, y que la revisión de A no aumentó durante las escrituras de contenido. Crea otro objeto en A y muévelo al mismo destino para observar una sustitución. Los campos de destino se usan únicamente al renombrar.

La revisión es un contador didáctico de modificaciones de entradas, no un reloj ni una lectura de mtime. Se limitan los nombres a 24 bytes ASCII, el total a 16 archivos y la longitud por archivo a 1 MiB. Esas restricciones facilitan el ensayo y no declaran límites de un sistema real. El modelo omite permisos, bloques, fallos y referencias adicionales.

Dividir y reutilizar registros

El segundo modelo representa un único bloque de 128 bytes con cabeceras de ocho y alineación a cuatro. Es una escala didáctica inspirada en el formato clásico, no un tamaño de bloque ext2 válido. . y .. están protegidos. Al insertar se divide el primer registro con espacio suficiente; al borrar, el anterior absorbe su longitud.

Registros de longitud variable

capacidad y entradas activas
Offsetrec_lenInodoNombreMínimoBytes adicionales

Inserta nota.txt, a y curso. Elimina el nombre intermedio y compara offsets y longitudes. La suma de rec_len siempre debe ser 128; el número de entradas puede bajar sin cambiar la capacidad. Prueba un nombre que no quepa y observa que la operación no altera el estado.

El espacio adicional pertenece a la representación del directorio y no equivale a bloques disponibles para contenido de cualquier archivo. El modelo no compacta el bloque ni asigna otro, no muestra bytes residuales y es independiente del simulador de nombres anterior.

14. Qué demuestra cada observación

ObservaciónConclusión válidaLímite
ls muestra un nombre nuevoLa enumeración encontró esa entrada en ese momento.No describe su posición en disco ni una instantánea frente a cambios concurrentes.
stat conserva dev e inodo tras renameLas consultas del ensayo identifican el mismo objeto.No prueba una identidad eterna ni mide escrituras físicas.
st_size del directorio no baja al borrarSu longitud informada se conserva.No demuestra que el nombre siga activo.
El lector sigue registros válidosEl volcado cumple las comprobaciones locales del formato elegido.No valida mapas de bits, enlaces ni características no admitidas.
rename termina correctamenteSe completó la operación del espacio de nombres.No acredita por sí sola persistencia después de una caída.

Las actualizaciones de directorios participan en la consistencia global. Nombre, inodo, contador de enlaces y asignación deben quedar relacionados correctamente. La caché puede diferir trabajo y el journal puede organizar su recuperación, pero el orden lógico de este tema no se traduce en una cantidad fija de accesos al dispositivo.

Para describir un ensayo, registra montaje o características de imagen, operación realizada, evidencia de nombres, identidad consultada y alcance de la comprobación. Separa errores observados de resultados esperados. No deduzcas rutas, referencias activas o tiempo físico únicamente a partir de cadenas encontradas en un volcado.

15. Resumen

16. Preguntas de repaso y práctica

1. ¿En qué sentido un directorio es un archivo?

Es un objeto con tipo, identidad, metadatos y representación. Su contenido organiza asociaciones de nombres; las aplicaciones deben usar operaciones específicas de directorio.

2. ¿Guarda una entrada la ruta absoluta de su archivo?

No en el modelo estudiado. Guarda un nombre local y una referencia al objeto. La ruta se interpreta combinando componentes desde un punto inicial.

3. ¿Puede haber dos entradas activas iguales en un mismo directorio?

No según sus reglas de comparación de nombres. El mismo nombre local sí puede existir en directorios distintos.

4. ¿Dos nombres diferentes pueden identificar el mismo inodo?

Sí. Los enlaces duros permiten varias entradas para un mismo objeto dentro del sistema de archivos. Sus detalles se estudiarán en el tema 23.

5. ¿Vacío significa que el directorio no tiene punto ni punto punto?

No. Significa que no tiene entradas de usuario adicionales. El formato clásico conserva las relaciones especiales de directorio actual y padre.

6. ¿Un archivo vacío puede hacer crecer el directorio padre?

Sí. Su nueva entrada necesita espacio aunque el archivo no tenga bloques de contenido propios. El padre puede requerir otro bloque de representación.

7. ¿Escribir bytes en un hijo modifica necesariamente las entradas del padre?

No. La escritura cambia el objeto referido. El conjunto de nombres del padre puede permanecer igual; su mtime no es un monitor recursivo del contenido.

8. ¿Por qué un unlink puede dejar el contenido accesible?

Puede quedar otro enlace o una referencia abierta. La operación retira un nombre; la vida del objeto depende de las referencias restantes.

9. ¿Puede rmdir eliminar un directorio con un archivo oculto?

No mientras contenga esa entrada. Oculto es una convención de presentación y no significa ausente.

10. ¿Renombrar dentro del mismo sistema copia todos los datos?

No necesita hacerlo. Modifica asociaciones de nombres y metadatos manteniendo la identidad del objeto. Puede requerir trabajo de organización de entradas.

11. ¿Qué ocurre si rename cruza sistemas de archivos?

La llamada falla con EXDEV. Una herramienta como mv puede implementar copia y eliminación, con propiedades distintas de una única operación rename.

12. ¿Mover A/sub a B/sub obliga a modificar todos sus hijos?

No. Se actualizan las relaciones necesarias con los padres y los metadatos pertinentes; los nombres locales de los hijos pueden permanecer iguales.

13. ¿Por qué nlink=2+k no cuenta todos los archivos del directorio?

En el modelo clásico k cuenta subdirectorios inmediatos, cuya relación de padre contribuye al contador. Los archivos regulares no aportan esa contribución. Hay sistemas y características con otra contabilidad.

14. ¿Qué significa que st_size del directorio permanezca igual después de borrar?

Que conserva la longitud informada de su representación. Puede tener espacio interno reutilizable y menos entradas activas sin haberse reducido.

15. ¿Cómo se calcula el mínimo de un registro para un nombre de nueve bytes?

La cabecera suma ocho: 8+9=17. Al alinear a cuatro resulta 20 bytes. rec_len puede ser mayor si el registro contiene espacio adicional.

16. ¿Por qué no se usa strlen sobre el nombre del volcado?

El formato guarda una longitud explícita y no exige un byte nulo al final. Buscarlo podría leer el siguiente registro o salir del bloque.

17. ¿Encontrar nota.txt como una cadena en disco prueba que existe?

No. Sus bytes pueden haber quedado en espacio reutilizable después de borrar. Hay que interpretar registros activos y, para validar el volumen, relacionarlos con otras estructuras.

18. ¿El lector sirve para cualquier directorio ext4?

No. Solo admite el formato y las condiciones del laboratorio. Índices, checksums, datos inline y otras características necesitan tratamiento específico.

19. ¿Por qué se comparan st_dev y st_ino juntos?

El número de inodo tiene ámbito dentro del sistema de archivos. Dos volúmenes pueden usar el mismo número para objetos diferentes.

20. ¿Un rename correcto garantiza supervivencia ante un corte?

No por sí solo. La operación y la persistencia requieren distinguir caché, sincronización del archivo y sincronización del directorio, que se desarrollarán en el tema 35.

Para practicar

  1. Dibuja las entradas y los inodos de dos directorios que tengan un nombre local igual para objetos diferentes.
  2. Representa dos nombres de un archivo y explica qué cambia al retirar uno.
  3. Registra la identidad de un archivo antes y después de renombrarlo y moverlo entre dos directorios del laboratorio.
  4. Ejecuta mover-directorio y comprueba identidad, cambio de padre y contadores de enlaces observados.
  5. Explica por qué la fórmula clásica de enlaces da valores distintos para un directorio con cuatro archivos y otro con cuatro subdirectorios.
  6. Compara stat del directorio con du del árbol. Declara qué mide cada resultado.
  7. Calcula el tamaño mínimo de registros para nombres de 1, 4, 5, 8, 9 y 24 bytes.
  8. En el simulador, llena el bloque de 128 bytes, elimina una entrada intermedia y reutiliza el espacio sin ampliar el bloque.
  9. Verifica que cada bloque del volcado queda cubierto por la suma de sus rec_len y contrasta los inodos con debugfs.
  10. Después de borrar una entrada en la imagen, compara registros activos, longitud total y posibles bytes residuales.
  11. Prueba el lector con rec_len cero, bloque truncado y name_len fuera del registro; conserva estado y diagnóstico.
  12. Explica por qué no es correcto tratar una estructura de readdir como si fuera directamente la estructura del disco.

En el tema 21 usaremos opendir, readdir, stat y nftw para recorrer directorios desde C y construir versiones propias de ls, du y find.