← Sistemas de Archivos y E/SBloque 9 · Montaje y la capa virtual · Tema 32 de 50

Tema 32

Estadísticas del volumen en C

Saber cuántos bytes parecen disponibles exige interpretar unidades, reservas y contadores del sistema de archivos. Construiremos un df propio con statvfs, consultaremos por descriptor y distinguiremos previsión de reserva. Dos programas en C, un laboratorio acotado, una escritura parcial reproducible y dos simuladores mostrarán por qué una consulta favorable no reemplaza comprobar cada operación.

1. Consultar el volumen que contiene una ruta

En los temas 30 y 31 conectamos sistemas de archivos y distinguimos sus proveedores. Ahora queremos saber qué capacidad informa el sistema que contiene una ruta y qué parte parece disponible para una creación o una escritura.

Estadísticas del volumen: contadores del sistema de archivos, no del tamaño de un solo archivo. Describen una observación del proveedor en un momento; no son una reserva que garantice escrituras futuras.

terminal · localizar primero el destino real
$ findmnt --target "$HOME" --output TARGET,SOURCE,FSTYPE,OPTIONS
$ df -B1 -- "$HOME"
$ df -i -- "$HOME"

La consulta corresponde al sistema que resuelve la ruta en el espacio de montajes actual. El archivo volumen.img del tema 30 pertenece al anfitrión; los archivos bajo su punto montado pertenecen al ext4 interno. Son capacidades y restricciones distintas.

2. statvfs y fstatvfs

interfaz · estadísticas por ruta o por descriptor
#include <sys/statvfs.h>

int statvfs(const char *ruta, struct statvfs *datos);
int fstatvfs(int fd, struct statvfs *datos);

Retorno: 0 si se pudo consultar; -1 y errno si hubo error.

statvfs acepta la ruta de un objeto existente del sistema de archivos, no exige escribir la ruta de la raíz del montaje. Si el archivo que planeas crear no existe, puedes consultar su directorio padre ya existente.

fstatvfs consulta mediante un descriptor abierto. Conservar una base y usar openat para crear respecto de ella evita resolver repetidamente el nombre de esa base, como vimos en el tema 29. No congela los contadores ni elimina la competencia por espacio. La referencia de campos e interfaces es statvfs(3).

3. Leer los campos sin confundir sus unidades

CampoQué expresaUso en el tema
f_bsizeTamaño de bloque informado por el sistema.Se muestra como dato; no sustituye la unidad de los contadores siguientes.
f_frsizeUnidad de fragmento para contar capacidad.Multiplicar f_blocks, f_bfree y f_bavail para obtener bytes.
f_blocksCapacidad en unidades f_frsize.Base del total del modelo.
f_bfreeBloques libres informados.Incluye espacio que puede no estar disponible para usuarios ordinarios.
f_bavailBloques disponibles para usuarios sin privilegios.Indicador inicial para el aviso de capacidad.
f_files / f_ffree / f_favailContadores totales, libres y disponibles de inodos.Recurso separado de los bloques de datos.
f_flag / f_namemaxFlags y longitud máxima de nombre informados.Detectar ST_RDONLY y observar límites adicionales.

f_bsize y f_frsize suelen coincidir, pero el programa no debe basar el cálculo en esa coincidencia. Los contadores no son ya bytes ni los bloques de 512 bytes de st_blocks.

4. Total, usado, libre y disponible

Empezaremos con un modelo de contadores no negativos que satisface f_bavail ≤ f_bfree ≤ f_blocks y f_frsize mayor que cero. Se comprueban esas relaciones antes de restar o multiplicar.

fórmulas · una sola unidad para los contadores de capacidad
unidad             = f_frsize
total_bytes        = f_blocks * unidad
libre_bytes        = f_bfree * unidad
disponible_bytes   = f_bavail * unidad
usado_bytes        = (f_blocks - f_bfree) * unidad
no_disponible_bytes = (f_bfree - f_bavail) * unidad

Ejemplo: unidad=4096, total=100000, libre=30000, disponible=20000
total_bytes      = 409600000
usado_bytes      = 286720000
libre_bytes      = 122880000
disponible_bytes =  81920000

La diferencia entre libre y disponible describe espacio libre que ese contador no ofrece como disponible ordinario. No significa universalmente «todos estos bytes son una reserva fija para root»: la interpretación depende del sistema, su configuración y sus políticas.

Algunos proveedores y herramientas contemplan situaciones más amplias, incluidas disponibilidades negativas o contadores no interpretables con este modelo. Los ejemplos las rechazan explícitamente en lugar de provocar una resta sin signo que parezca un valor enorme.

5. Qué porcentaje de uso estamos calculando

Para una presentación de la familia de df resulta útil comparar lo usado con lo usado más lo disponible ordinario. El denominador no tiene por qué ser el total físico informado. Por eso un volumen puede llegar al 100 % de ese indicador y aún tener bloques libres no disponibles para ese uso ordinario.

porcentaje declarado · no dividir si el denominador es cero
usados = f_blocks - f_bfree
denominador = usados + f_bavail
porcentaje = 100 * usados / denominador

Ejemplo anterior:
100 * 70000 / (70000 + 20000) = 77.777... %

usados / total daría 70 %, que responde a otra definición.

Nuestro mini-df muestra dos decimales; GNU df presenta normalmente un entero con sus reglas de redondeo y casos especiales. No se espera igualdad textual entre porcentajes. Con total, usado y disponible todos cero mostramos ND, no inventamos un porcentaje de ocupación.

6. Bytes exactos, MiB y desbordamientos

Un MB decimal equivale a 1 000 000 bytes; un MiB binario, a 1 048 576. Cambiar la etiqueta sin cambiar la división produce una comparación incorrecta. Los valores en bytes serán la referencia exacta del laboratorio.

aritmética · comprobar antes del producto entero
if (unidad != 0 && cantidad > UINTMAX_MAX / unidad) {
    /* El producto no cabe: no multiplicar como si fuera válido. */
}
bytes = cantidad * unidad;

MiB = bytes / 1048576.0L;   /* presentación aproximada */

Convertir a un tipo amplio después de multiplicar no corrige un desbordamiento que ya ocurrió en un tipo menor. El programa convierte cada operando y comprueba el producto. La visualización humana puede redondearse; los bytes exactos no se recalculan desde ese redondeo.

7. Tener bloques libres no garantiza crear otro archivo

Crear un archivo también necesita metadatos. En sistemas que informan contadores de inodos útiles, f_favail puede advertir que no quedan inodos disponibles para usuarios sin privilegios aunque f_bavail siga siendo grande.

terminal · comparar dos recursos distintos
$ df -B1 -- "$HOME"
$ df -i -- "$HOME"

Un sistema con contadores de inodos dinámicos o no significativos puede presentar cifras que no se interpretan como las de un ext4 preconfigurado. En el mini-df, f_files igual a cero no se transforma en una conclusión de «cero archivos posibles»; se marca el cálculo de usados como no disponible.

f_namemax, cuotas y límites por archivo son otras restricciones. Comprobar dos contadores tampoco reconstruye toda la política de una operación concreta.

8. Por qué df, du y stat no coinciden necesariamente

HerramientaPregunta principalMotivos de diferencias
df¿Qué capacidad y disponibilidad informa el sistema que contiene la ruta?Metadatos, políticas del sistema y objetos fuera del recorrido visible.
du¿Qué uso puede estimarse recorriendo estos nombres?Permisos, enlaces, montajes, archivos no enumerables y opciones del recorrido.
stat¿Qué atributos tiene este objeto?El tamaño lógico no equivale al espacio físico asignado.

Un archivo disperso puede tener tamaño lógico grande sin asignar bloques para todos sus huecos. Un archivo desvinculado pero todavía abierto puede seguir consumiendo espacio aunque du no lo encuentre por un nombre. Las referencias de du(1) y stat(2) ayudan a separar estas medidas.

Compresión, copias compartidas, snapshots o capas como OverlayFS agregan reglas del proveedor. Para comparar con df se fija la unidad con -B1 y se consulta la misma ruta. Véase df(1).

9. Consultar antes de escribir: aviso, no reserva

Una aplicación puede rechazar un trabajo claramente demasiado grande o advertir al usuario. Pero entre consultar y escribir otros procesos pueden consumir espacio, una cuota puede limitar al solicitante o el sistema puede pasar a solo lectura.

secuencia · una observación favorable puede quedar antigua
Proceso A: fstatvfs(base) → disponible 1 MiB
Proceso A: necesita 64 KiB → admite intentar
Proceso B: consume casi todo el espacio de ese volumen
Proceso A: crea y escribe → progreso parcial, después ENOSPC

Un margen reduce el riesgo de una estimación ajustada; no bloquea a los competidores ni reserva metadatos. El patrón recuerda a TOCTOU, aunque conservar el descriptor de la base sí estabiliza qué directorio se utiliza.

La demanda tampoco es siempre igual al tamaño lógico de los datos: intervienen redondeo a unidades de asignación, metadatos y características del sistema. Se trata de una decisión de planificación, seguida de operaciones cuyos resultados se deben comprobar.

10. Escritura parcial, cuotas y errores diferidos

write puede devolver un número positivo menor que el solicitado. Ese progreso cuenta: se continúa por el resto. Si devuelve -1, se interpreta errno; EINTR puede requerir reintentar sin dar por escrito el bloque que no avanzó. Cero, en el bucle del ejemplo, se trata como falta inesperada de progreso.

ResultadoInterpretación posible
ENOSPCFalta de espacio o de recursos de asignación del sistema de archivos.
EDQUOTCuota aplicable agotada.
EROFSOperación de escritura sobre un sistema de solo lectura.
EFBIGLímite del tamaño del archivo o del proceso.
EIOError de entrada/salida.

Una consulta de espacio no sustituye esas comprobaciones. Las reglas de retorno y los errores se describen en write(2).

Un write exitoso no certifica persistencia tras un corte. Se comprueban fsync y close; algunos fallos pueden aparecer diferidos. En Linux no se reintenta close a ciegas, porque el descriptor puede haberse liberado y reutilizado. Véase close(2).

11. Preasignar es otra operación

Si el diseño necesita asegurar espacio para un rango de un archivo, posix_fallocate realiza una operación de asignación, no una simple consulta. Si tiene éxito, las escrituras en ese rango tienen una garantía específica frente a fallos por falta de espacio.

interfaz · retorno distinto al de muchas llamadas POSIX
int error = posix_fallocate(fd, 0, longitud);
if (error != 0) {
    /* error es el código devuelto; no leer errno como si fuera -1. */
}

Hay que comprobar soporte, límites y condiciones de emulación de la biblioteca. La garantía no elimina errores de E/S, ni sustituye autorización, sincronización o el protocolo de publicación. Véase posix_fallocate(3).

fallocate es una interfaz específica de Linux con modos adicionales; no todos significan lo mismo que preasignar en su modo básico. Véase fallocate(2).

12. Programa en C: un df propio con unidades explícitas

Guarda mini-df.c. Imprime contadores originales, bytes calculados y porcentaje declarado. No enumera todos los montajes ni intenta reproducir todos los casos especiales de GNU df; consulta una ruta existente por ejecución.

mini-df.c · validar relaciones y productos antes de calcular
#define _POSIX_C_SOURCE 200809L
#include <sys/statvfs.h>
#include <inttypes.h>
#include <stdio.h>

static int producto(uintmax_t cantidad, uintmax_t unidad, uintmax_t *salida)
{
    if (unidad != 0 && cantidad > UINTMAX_MAX / unidad) return -1;
    *salida = cantidad * unidad;
    return 0;
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Uso: %s ruta-existente\n", argv[0]); return 2;
    }
    struct statvfs v;
    if (statvfs(argv[1], &v) == -1) { perror("statvfs"); return 1; }
    uintmax_t unidad = (uintmax_t)v.f_frsize;
    uintmax_t bloques = (uintmax_t)v.f_blocks;
    uintmax_t libres = (uintmax_t)v.f_bfree;
    uintmax_t disponibles = (uintmax_t)v.f_bavail;
    printf("FRSIZE=%ju\nBSIZE=%ju\nBLOCKS=%ju\nBFREE=%ju\nBAVAIL=%ju\n",
           unidad, (uintmax_t)v.f_bsize, bloques, libres, disponibles);
    printf("FILES=%ju\nFFREE=%ju\nFAVAIL=%ju\nNAMEMAX=%ju\nREADONLY=%d\n",
           (uintmax_t)v.f_files, (uintmax_t)v.f_ffree,
           (uintmax_t)v.f_favail, (uintmax_t)v.f_namemax,
           !!(v.f_flag & ST_RDONLY));
    if (!unidad || libres > bloques || disponibles > libres) {
        fprintf(stderr, "Contadores no interpretables por este modelo no negativo\n");
        return 1;
    }
    uintmax_t total, usado, libre, disponible, no_disponible;
    uintmax_t usados = bloques - libres;
    if (producto(bloques, unidad, &total) ||
        producto(usados, unidad, &usado) ||
        producto(libres, unidad, &libre) ||
        producto(disponibles, unidad, &disponible) ||
        producto(libres - disponibles, unidad, &no_disponible)) {
        fprintf(stderr, "El producto no cabe en uintmax_t\n"); return 1;
    }
    printf("TOTAL_BYTES=%ju\nUSED_BYTES=%ju\nFREE_BYTES=%ju\nAVAIL_BYTES=%ju\n",
           total, usado, libre, disponible);
    printf("NO_DISP_BYTES=%ju\n", no_disponible);
    printf("TOTAL_MIB=%.3Lf\nAVAIL_MIB=%.3Lf\n",
           (long double)total / 1048576.0L,
           (long double)disponible / 1048576.0L);
    /* usados + disponibles <= bloques en el modelo validado. */
    uintmax_t denominador = usados + disponibles;
    if (denominador)
        printf("PCT_USO=%.2Lf\n", 100.0L * (long double)usados / denominador);
    else
        puts("PCT_USO=ND");
    if (v.f_files && v.f_ffree <= v.f_files)
        printf("INODOS_USADOS=%ju\n", (uintmax_t)(v.f_files - v.f_ffree));
    else
        puts("INODOS_USADOS=ND");
    return 0;
}

Las conversiones a uintmax_t corresponden al modelo no negativo del ejemplo. Si la relación libre/disponible no se puede interpretar así, muestra los valores originales y termina con error. INODOS_USADOS=ND y PCT_USO=ND expresan que ese cálculo no tiene un denominador o contador significativo.

13. Programa en C: consultar y tratar el fallo real

Guarda escribir-con-aviso.c. Acepta entre 1 byte y 1 MiB para limitar el ensayo, usa un directorio propio de confianza y crea exclusivamente dato.bin nuevo. Opcionalmente espera una línea tras LISTO para reproducir consumo concurrente sin usar retardos.

escribir-con-aviso.c · el precheck no sustituye a write, fsync ni close
#define _POSIX_C_SOURCE 200809L
#include <sys/statvfs.h>
#include <fcntl.h>
#include <unistd.h>
#include <inttypes.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

static int bytes_validos(const char *texto, uintmax_t *n)
{
    if (!*texto) return 0;
    for (const char *p = texto; *p; ++p)
        if (*p < '0' || *p > '9') return 0;
    errno = 0;
    *n = strtoumax(texto, NULL, 10);
    return errno != ERANGE && *n >= 1 && *n <= 1048576;
}

int main(int argc, char **argv)
{
    uintmax_t pedido;
    if ((argc != 3 && argc != 4) ||
        (argc == 4 && strcmp(argv[3], "pausa")) ||
        !bytes_validos(argv[2], &pedido)) {
        fprintf(stderr, "Uso: %s directorio-propio bytes-1-a-1048576 [pausa]\n", argv[0]);
        return 2;
    }
    int base = open(argv[1], O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (base == -1) { perror("open directorio"); return 1; }
    int fd = -1, resultado = 1;
    uintmax_t escritos = 0;
    struct statvfs v;
    if (fstatvfs(base, &v) == -1) { perror("fstatvfs"); goto fin; }
    uintmax_t unidad = (uintmax_t)v.f_frsize, bloques = (uintmax_t)v.f_bavail;
    if (!unidad || bloques > UINTMAX_MAX / unidad) {
        fprintf(stderr, "Disponibilidad no representable\n"); goto fin;
    }
    uintmax_t disponible = bloques * unidad;
    printf("CONSULTA_BAVAIL_BYTES=%ju\nSOLICITADOS=%ju\n", disponible, pedido);
    if ((v.f_flag & ST_RDONLY) || pedido > disponible ||
        (v.f_files && !v.f_favail)) {
        puts("PRECHECK=RECHAZADO");
        resultado = 3; goto fin;
    }
    puts("PRECHECK=ADMITE_INTENTO");
    if (argc == 4) {
        puts("LISTO");
        if (fflush(stdout) == EOF) { perror("fflush"); goto fin; }
        if (getchar() != '\n') {
            fprintf(stderr, "Se esperaba una línea vacía\n"); goto fin;
        }
    }
    fd = openat(base, "dato.bin", O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
    if (fd == -1) { perror("openat nuevo"); goto fin; }
    unsigned char buffer[4096];
    memset(buffer, 'D', sizeof buffer);
    while (escritos < pedido) {
        uintmax_t restante = pedido - escritos;
        size_t cantidad = restante < sizeof buffer ? (size_t)restante : sizeof buffer;
        ssize_t n = write(fd, buffer, cantidad);
        if (n == -1 && errno == EINTR) continue;
        if (n <= 0) {
            if (n == -1) perror("write");
            else fprintf(stderr, "write no avanzó\n");
            goto fin;
        }
        escritos += (uintmax_t)n;
    }
    if (fsync(fd) == -1) { perror("fsync archivo"); goto fin; }
    if (fsync(base) == -1) { perror("fsync directorio"); goto fin; }
    resultado = 0;
fin:
    if (fd != -1) {
        printf("ESCRITOS=%ju\n", escritos);
        if (close(fd) == -1) { perror("close archivo"); resultado = 1; }
    }
    if (close(base) == -1) { perror("close directorio"); resultado = 1; }
    return resultado;
}

El estado 0 indica que completó las operaciones comprobadas, 1 un fallo de ejecución, 2 argumentos inválidos y 3 un rechazo de la política previa. ESCRITOS registra bytes entregados a write, no una certificación independiente de durabilidad.

El archivo parcial se conserva si falla después de crearlo; no hay reversión automática. La sincronización incluye el directorio por la creación de la entrada. Los alcances de fsync se documentan en fsync(2). En tmpfs, sincronizar tampoco hace persistente la instancia.

14. Laboratorio de lectura y escritura acotada

Usa Bash en Linux, compilador C y Python 3. El laboratorio básico no necesita sudo y escribe menos de 1 MiB por ejecución válida. Guarda los dos programas en el directorio nuevo.

terminal · compilar, consultar y comparar unidades
$ LAB32=$(mktemp -d "$HOME/tema32.XXXXXX")
$ cd -- "$LAB32"
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 mini-df.c -o mini-df
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 escribir-con-aviso.c -o escribir-con-aviso
$ ./mini-df .
$ findmnt --target . --output TARGET,SOURCE,FSTYPE,OPTIONS
$ df -B1 -- .
$ df -i -- .
$ ./mini-df /proc

En procfs pueden aparecer contadores sin capacidad útil y porcentaje ND; eso no significa que todo el equipo se haya quedado sin disco. df puede presentar casos especiales de otra forma. Si tu distribución ofrece implementaciones alternativas, identifica qué versión de df comparas.

Guarda verificar-tema32.py. Usa nombres nuevos y comprueba relaciones de la misma consulta, bytes escritos y errores de entrada. No exige que los libres de dos instantes sean idénticos.

verificar-tema32.py · comprobación del modelo y del archivo real
import os
from pathlib import Path
import subprocess
import tempfile

base = Path(tempfile.mkdtemp(prefix="casos32-", dir=".")).resolve()
inspector = Path("mini-df").resolve(strict=True)
escritor = Path("escribir-con-aviso").resolve(strict=True)

def ejecutar(*args):
    return subprocess.run(list(map(str, args)), capture_output=True, text=True, timeout=10)

p = ejecutar(inspector, base)
assert p.returncode == 0, p.stderr
c = dict(linea.split("=", 1) for linea in p.stdout.splitlines())
for clave in ("FRSIZE", "BSIZE", "BLOCKS", "BFREE", "BAVAIL",
              "TOTAL_BYTES", "USED_BYTES", "FREE_BYTES", "AVAIL_BYTES", "NO_DISP_BYTES"):
    c[clave] = int(c[clave])
u = c["FRSIZE"]
assert c["TOTAL_BYTES"] == c["BLOCKS"] * u
assert c["FREE_BYTES"] == c["BFREE"] * u
assert c["AVAIL_BYTES"] == c["BAVAIL"] * u
assert c["USED_BYTES"] + c["FREE_BYTES"] == c["TOTAL_BYTES"]
assert c["NO_DISP_BYTES"] == c["FREE_BYTES"] - c["AVAIL_BYTES"]
den = c["USED_BYTES"] + c["AVAIL_BYTES"]
if den:
    assert abs(float(c["PCT_USO"]) - 100 * c["USED_BYTES"] / den) <= 0.011
else:
    assert c["PCT_USO"] == "ND"
real = os.statvfs(base)
assert (real.f_frsize, real.f_blocks) == (u, c["BLOCKS"])
# No se exige que los libres de dos consultas sucesivas sean idénticos.
d = base / "normal"
d.mkdir(mode=0o700)
p = ejecutar(escritor, d, 8193)
assert p.returncode == 0, p.stdout + p.stderr
assert "ESCRITOS=8193" in p.stdout
assert (d / "dato.bin").read_bytes() == b"D" * 8193
p = ejecutar(escritor, d, 10)
assert p.returncode == 1, "O_EXCL debe rechazar el nombre existente"
assert (d / "dato.bin").stat().st_size == 8193
for i, entrada in enumerate(("0", "-1", "+1", "1x", "1.5", "1048577", "9" * 40)):
    d = base / f"invalido-{i}"
    d.mkdir()
    p = ejecutar(escritor, d, entrada)
    assert p.returncode == 2 and not (d / "dato.bin").exists(), entrada
assert ejecutar(inspector, base / "ausente").returncode == 1
print("Correctos: unidades, relaciones, porcentaje, escritura completa y rechazo de entradas")
print("Laboratorio conservado en", base)
terminal · ejecutar una vez y conservar resultados
$ python3 verificar-tema32.py
$ printf 'Laboratorio conservado en %s\n' "$LAB32"

Los bytes exactos y los contadores son la base de la comparación. Las columnas humanas de df y el porcentaje pueden diferir por redondeo, política del proveedor y tiempo transcurrido.

15. Práctica opcional: agotar solo un tmpfs de 1 MiB

Esta etapa requiere autorización para un montaje propio. Utiliza una instancia pequeña y nueva, separada del volumen anfitrión. No se llena HOME ni un disco real para demostrar ENOSPC. Guarda el programa de control como carrera-espacio.py dentro de LAB32.

terminal · preparar un destino con límite pequeño y explícito
$ mkdir -- "$LAB32/limitado"
$ MNT32="$LAB32/limitado"
$ sudo mount -t tmpfs -o "size=1M,nr_inodes=128,mode=0700,uid=$(id -u),gid=$(id -g),nodev,nosuid,noexec" tmpfs "$MNT32"
$ findmnt --mountpoint "$MNT32" --output TARGET,SOURCE,FSTYPE,OPTIONS
$ ./mini-df "$MNT32"
carrera-espacio.py · competir después de LISTO y conservar progreso parcial
import errno
import os
from pathlib import Path
import subprocess
import sys

if len(sys.argv) != 2:
    raise SystemExit("Uso: python3 carrera-espacio.py punto-tmpfs-nuevo-de-1MiB")
punto = Path(sys.argv[1]).resolve(strict=True)
tipo = subprocess.check_output(
    ["findmnt", "-rn", "--mountpoint", str(punto), "-o", "FSTYPE"], text=True).strip()
v = os.statvfs(punto)
assert tipo == "tmpfs" and v.f_blocks * v.f_frsize == 1024 * 1024
assert not list(punto.iterdir()), "El destino debe ser nuevo y vacío"
programa = Path("escribir-con-aviso").resolve(strict=True)
p = subprocess.Popen([str(programa), str(punto), "65536", "pausa"],
                     stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                     stderr=subprocess.PIPE, text=True)
try:
    cabecera = []
    for linea in p.stdout:
        cabecera.append(linea)
        if linea.strip() == "LISTO":
            break
    else:
        raise AssertionError("El escritor no llegó a LISTO")
    total = 0
    fd = os.open(punto / "relleno.bin", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    try:
        # Solo se llena este tmpfs privado de 1 MiB, nunca el volumen anfitrión.
        while total < 2 * 1024 * 1024:
            try:
                n = os.write(fd, b"R" * 4096)
            except OSError as exc:
                assert exc.errno == errno.ENOSPC
                break
            assert n > 0
            total += n
        else:
            raise AssertionError("No apareció ENOSPC dentro del límite del ensayo")
        assert total >= 32768
        os.ftruncate(fd, total - 32768)  # liberar 32 KiB para una escritura parcial
    finally:
        os.close(fd)
    salida, error = p.communicate("\n", timeout=10)
    assert p.returncode == 1, salida + error
    dato = punto / "dato.bin"
    assert 0 < dato.stat().st_size < 65536
    assert dato.read_bytes() == b"D" * dato.stat().st_size
    assert "write:" in error
    print("".join(cabecera) + salida, end="")
    print("Correcto: la consulta admitió el intento, pero quedó un archivo parcial")
    print("Bytes conservados:", dato.stat().st_size)
finally:
    if p.poll() is None:
        p.kill()
    p.communicate()
terminal · ejecutar la prueba y desmontar normalmente
$ python3 carrera-espacio.py "$MNT32"
$ ./mini-df "$MNT32"
$ ls -l -- "$MNT32"
$ cd -- "$LAB32"
$ sudo umount "$MNT32"

El control verifica que el destino exacto es tmpfs, de 1 MiB y vacío. Llena esa instancia, libera 32 KiB y permite continuar al escritor que había observado más espacio. El resultado esperado es una escritura parcial y un estado de error, no un archivo completo de 64 KiB.

Las cifras exactas de asignación pueden depender del proveedor; se comprueba progreso mayor que cero y menor que lo pedido. El ensayo conserva los datos hasta el desmontaje. Si falla, se inspeccionan sus archivos y referencias antes de retirar el montaje.

16. Simuladores de capacidad y competencia

Calcular con contadores y unidades diferentes

Modelo de contadores enteros no negativos de hasta 64 bits. Usa BigInt para conservar exactitud y rechaza productos que no cabrían en el modelo uintmax_t de 64 bits. El formato MiB es una presentación redondeada a tres decimales.

De contadores a bytes y porcentaje

relaciones y porcentaje declarado
MagnitudUnidades f_frsizeBytes exactosMiB

Una consulta favorable y una operación posterior

Modelo simplificado en MiB enteros. La escritura puede consumir el espacio que queda tras el competidor, limitada además por la condición elegida. Omite redondeo, metadatos y errores diferidos; el margen es una política, no una reserva.

Previsión frente a resultado real

consulta, cambio de condiciones y operación

Reduce el consumo competidor a cero y compara éxito con falta de inodos. Después selecciona cuota: puede haber muchos bytes libres globales y un límite distinto para el solicitante.

17. Diagnóstico y resumen

SíntomaQué comprobar
La capacidad calculada es demasiado grande.Unidad f_frsize, conversiones y desbordamiento antes del producto.
Quedan bloques libres, pero no se puede escribir.f_bavail, inodos, cuotas, permisos, solo lectura y error real de la operación.
df y du no coinciden.Qué mide cada uno, unidad, objetos abiertos sin nombre, metadatos y proveedor.
El porcentaje no es usados/total.Definición del denominador y diferencia entre libre y disponible.
La consulta admitió el intento y quedó un parcial.Competencia, progresos de write y tratamiento del estado incompleto.
El write terminó, pero fsync falló.No publicar un éxito que el protocolo todavía no pudo confirmar.

18. Preguntas de repaso y práctica

1. ¿statvfs informa el tamaño de un solo archivo?

No. Informa estadísticas del sistema de archivos que contiene la ruta consultada.

2. ¿Se puede consultar un nombre que aún no existe para crear allí?

No se usa esa ruta inexistente como si ya identificara el sistema. Se puede abrir y consultar el directorio padre existente.

3. ¿Qué cambia al usar fstatvfs?

La consulta se refiere al sistema del objeto abierto por descriptor. No congela los contadores.

4. ¿Por qué usar f_frsize para los bytes?

Porque f_blocks, f_bfree y f_bavail están expresados en esa unidad. No se asume igualdad con f_bsize.

5. ¿f_bfree y f_bavail son intercambiables?

No. El segundo informa disponibilidad para usuarios sin privilegios; puede ser menor que el libre.

6. ¿La diferencia siempre es una reserva fija para root?

No es una interpretación universal. Se observa la política y el proveedor del sistema concreto.

7. ¿70 % y 77,78 % pueden describir los mismos contadores?

Sí, si se usa total como denominador en el primero y usado más disponible en el segundo. Hay que declarar la definición.

8. ¿Qué porcentaje mostramos con denominador cero?

ND. No dividimos por cero ni afirmamos una ocupación que el contador no permite interpretar.

9. ¿Convertir después del producto evita overflow?

No. Se convierten los operandos y se comprueba el rango antes de multiplicar.

10. ¿Un MiB equivale a un MB decimal?

No. Son 1 048 576 y 1 000 000 bytes respectivamente.

11. ¿Tener datos libres garantiza crear un archivo?

No. También pueden faltar inodos, cuota, permisos u otros recursos.

12. ¿f_files cero significa siempre que no puede crearse ningún archivo?

No. Algunos proveedores no presentan contadores útiles con esa interpretación. El programa marca el cálculo como no disponible.

13. ¿Por qué df y du pueden discrepar?

Miden mediante mecanismos y alcances distintos; influyen metadatos, nombres accesibles, archivos abiertos desvinculados y opciones.

14. ¿Un margen de espacio impide la competencia?

No. Es una política de previsión; otro proceso puede consumir el recurso después de la consulta.

15. ¿Un write corto significa que no escribió nada?

No. Su retorno positivo indica el progreso real y el siguiente intento empieza por el resto.

16. ¿ENOSPC y EDQUOT son el mismo diagnóstico?

No. Uno indica falta de recursos de espacio; el otro corresponde a una cuota aplicable.

17. ¿El programa deshace un archivo parcial?

No. Lo conserva para inspección y devuelve error. Un diseño de publicación debe decidir cómo tratar ese estado.

18. ¿fsync del archivo cubre necesariamente la entrada de directorio?

No. La durabilidad de una creación puede requerir sincronizar también el directorio según la interfaz y el sistema.

19. ¿posix_fallocate informa fallos mediante -1 y errno?

No. Devuelve directamente un número de error distinto de cero. Se sigue su contrato específico.

20. ¿La prueba opcional llena el disco del usuario?

No. Verifica una instancia tmpfs propia de 1 MiB y limita el consumo a ese destino.

Para practicar

  1. Calcula total, usado, libre y disponible con una unidad de 4096 y contadores pequeños.
  2. Cambia f_bsize sin cambiar f_frsize y comprueba qué resultados deben mantenerse.
  3. Construye un caso donde f_bfree sea positivo y f_bavail sea cero.
  4. Compara usados/total con usados/(usados+disponibles) para el mismo ejemplo.
  5. Prueba denominador cero y un producto que exceda 64 bits en el simulador.
  6. Compara mini-df con df -B1 y explica diferencias de redondeo o de instante.
  7. Observa qué informa procfs sin interpretarlo como capacidad de un disco físico.
  8. Ejecuta el escritor con 1, 4096 y 8193 bytes en directorios nuevos.
  9. Comprueba que repetir dato.bin no lo sobrescribe.
  10. Simula falta de inodos y cuota aunque queden bytes libres.
  11. Realiza la carrera opcional y registra cuántos bytes quedaron en el parcial.
  12. Diseña un protocolo que separe archivo de trabajo incompleto y publicación final.

En el tema 33 estudiaremos fallos, fsck y journaling: qué puede quedar inconsistente tras un corte y qué garantías aportan los mecanismos de recuperación.