← Sistemas de Archivos y E/SBloque 4 · El búfer y la caché de disco · Tema 14 de 50

Tema 14

Escritura diferida y páginas sucias

Un programa puede terminar de escribir antes de que el almacenamiento haya terminado su trabajo. La caché de páginas permite agrupar cambios y solapar la aplicación con la E/S, pero también acumula datos pendientes y puede obligar al escritor a esperar. Seguiremos la vida de una página modificada, observaremos los parámetros de Linux, construiremos dos programas en C y separaremos tiempo de aceptación, sincronización y observación. Un simulador mostrará cómo crece la cola, qué ocurre al modificar una página durante su vaciado y qué versiones se pierden si desaparece la RAM.

1. Aceptar bytes y completar almacenamiento

En el tema anterior medimos lecturas y comprobamos que la residencia en caché cambia el trabajo necesario. En una escritura bufferizada aparece otra separación: el proceso entrega bytes al kernel y el almacenamiento puede recibirlos más tarde. Entre ambos momentos existe información modificada en memoria que todavía necesita ser escrita hacia su respaldo.

Escritura diferida o write-back: organización en la que se aceptan modificaciones en la caché y se propagan posteriormente al almacenamiento. Una página sucia contiene modificaciones pendientes de ese trabajo. El retorno de una escritura ordinaria no constituye, por sí solo, una confirmación de durabilidad.

recorrido conceptual · escritura bufferizada de un archivo regular
búfer del proceso
       │ write(fd, datos, n)
       ▼
caché de páginas: contenido nuevo y trabajo pendiente
       │                   ▲
       │ writeback         │ otra escritura puede modificarlo
       ▼                   │
sistema de archivos → capa de bloques → dispositivo
                                             │
                                  medio y cachés del dispositivo

El número positivo que devuelve write() indica cuántos bytes aceptó esa llamada. Puede ser menor que lo solicitado; el programa debe continuar con el resto. El éxito tampoco impide que un fallo de E/S anterior aparezca más tarde. El manual de write(2) documenta las transferencias parciales y la posible comunicación diferida de errores.

Que otro proceso pueda leer inmediatamente el contenido nuevo es una propiedad de visibilidad. Ambos procesos pueden estar consultando la misma caché. Reabrir el archivo y verificar sus bytes demuestra lo que se observa durante esa ejecución; no reproduce una recuperación después de un corte eléctrico.

Evento observadoQué permite afirmarQué falta por demostrar
write() retorna nLa llamada aceptó esos n bytes.La persistencia de los cambios.
read() devuelve los bytes nuevosEl lector ve ese contenido en ese momento.La supervivencia a la pérdida de memoria.
stat informa el tamaño esperadoEl estado visible del archivo tiene ese tamaño.La durabilidad de datos, metadatos y nombre.
Termina close()Se cerró el descriptor; también se comprueba su resultado.Una confirmación equivalente a sincronizar el archivo.

El alcance principal de este tema es la E/S bufferizada sobre archivos regulares en Linux. Un archivo en tmpfs, uno en un montaje remoto o un archivo situado dentro de una máquina virtual añaden condiciones distintas. Anotar dónde vive el archivo forma parte de describir cualquier resultado.

2. La vida de una página sucia

Una página limpia tiene un contenido que no necesita propagarse por modificaciones pendientes de la caché. Si el proceso escribe en ella, pasa a necesitar writeback. Cuando el kernel envía una versión hacia el respaldo, existe E/S en curso; cuando esa E/S termina correctamente, esa versión ya recorrió esa etapa. La aplicación puede haber vuelto a modificar la página mientras tanto.

  1. Residencia: la página está disponible en caché, o se incorpora para atender la operación.
  2. Modificación: se copian los bytes nuevos y se registra trabajo pendiente.
  3. Selección y envío: el writeback toma trabajo y lo entrega al sistema de archivos y a las capas inferiores.
  4. Finalización: se registra el resultado de la E/S. Si hubo otra modificación, todavía puede quedar trabajo por enviar.
  5. Reutilización: una página limpia puede permanecer para futuras lecturas; escribirla no exige expulsarla inmediatamente de la caché.

Una finalización no debe borrar una modificación posterior

versiones didácticas · una misma página
instante    RAM       versión en vuelo       respaldo del modelo
   0         v0              —                       v0
   1         v1              —                       v0   escritura A
   2         v1              v1                      v0   comienza envío
   3         v2              v1                      v0   escritura B
   4         v2              —                       v1   termina envío de v1
   5         v2              v2                      v1   nuevo envío
   6         v2              —                       v2   termina envío de v2

En el instante 4 no corresponde declarar que todo está actualizado: la E/S terminada corresponde a v1, mientras la memoria contiene v2. Este ejemplo usa números de versión para razonar; no describe una estructura de datos pública que las aplicaciones puedan consultar con ese formato.

Las categorías “sucia” y “en writeback” tampoco deben tratarse como casillas mutuamente excluyentes en toda situación. Puede existir una nueva modificación mientras sigue en curso una escritura anterior. Los detalles de folios, marcas y bloqueos pertenecen a la implementación; para el programa importa no confundir “se inició un envío” con “ya no queda nada pendiente”.

Residencia y suciedad son preguntas distintas.

mincore(), utilizado en los temas anteriores, observa residencia. Una página residente puede estar limpia, modificada o implicada en writeback. Su bit de residencia no responde qué cambios sobrevivirían a un fallo.

3. Por qué diferir y agrupar escrituras

Si cada actualización pequeña esperara a completar el almacenamiento, el proceso pagaría repetidamente latencias que a menudo pueden compartirse. La caché permite que la CPU continúe, que varias escrituras cercanas se agrupen y que actualizaciones repetidas de una misma región se combinen antes de propagarse.

Supongamos que una aplicación actualiza cien veces el mismo contador de ocho bytes. Entregó 800 bytes a través de sus llamadas, pero no creó cien páginas distintas de datos pendientes. Si las actualizaciones se agrupan antes del writeback, puede bastar propagar una versión final de la región. Si se intercalan envíos y nuevas modificaciones, la misma región puede requerir varias escrituras. La relación depende de cuándo ocurre cada evento.

PatrónQué creceConsecuencia posible
Ampliar un archivo secuencialmenteEl tamaño y la cantidad de regiones modificadas distintas.Puede acumularse una cola amplia de páginas.
Reescribir siempre el mismo rangoLos bytes lógicos entregados; el rango sigue acotado.Varias actualizaciones pueden combinarse.
Modificar mientras hay un envíoEl trabajo pendiente después de ese envío.Puede hacer falta volver a escribir una página.
Sincronizar después de cada cambioLas esperas y la frecuencia de puntos de confirmación.Disminuye la oportunidad de agrupar actualizaciones.

Tres cantidades que no son intercambiables

Los bytes lógicos son los aceptados por las llamadas del programa. El conjunto de páginas pendientes representa regiones de memoria que requieren trabajo. Los bytes de E/S del dispositivo incluyen los efectos del sistema de archivos, metadatos y otras capas. Compresión, asignación diferida, journaling y amplificación pueden cambiar su relación. El laboratorio medirá las primeras cantidades observables y evitará inventar la tercera.

Incluso modificar un solo byte puede implicar una unidad de almacenamiento mayor. Tampoco hay una igualdad universal entre “llamadas a write” y “operaciones del disco”. El tamaño de bloque que el programa entrega, el tamaño de página y la geometría de E/S son conceptos relacionados, pero diferentes.

La ventaja tiene una contrapartida.

Cuanto más trabajo permanece únicamente en memoria volátil, más cambios pueden quedar sin confirmar ante una pérdida de esa memoria. Diseñar una aplicación exige elegir puntos de durabilidad según sus datos y su protocolo; un número alto de MiB/s por sí solo no toma esa decisión.

4. Quién inicia el writeback

El vaciado no espera necesariamente a que la aplicación cierre el archivo. Puede iniciarse mientras el descriptor sigue abierto y mientras el proceso continúa produciendo datos. También puede quedar trabajo pendiente después de que el proceso haya terminado. La caché pertenece al kernel y su ciclo de vida no coincide con el de una variable del programa.

Intervienen trabajadores del kernel, la presión de memoria, el control de páginas modificadas y solicitudes explícitas de sincronización. Los trabajadores pueden agrupar trabajo asociado al respaldo correspondiente. No conviene enseñar un único nombre de hilo como una interfaz estable: los nombres que muestran las herramientas y la organización interna dependen del kernel.

Inicio, progreso y finalización

Iniciar writeback significa poner trabajo en marcha. El progreso depende del sistema de archivos y del servicio que recibe de las capas inferiores. La finalización comunica qué ocurrió con ese trabajo. Un dispositivo ocupado puede hacer que la cola tarde en bajar aunque el kernel ya haya empezado a vaciar.

Para seguir la implementación de Linux, el código de mm/page-writeback.c muestra el balance de páginas modificadas y la regulación del productor. No es un simple temporizador global: se consideran límites y tasas de progreso del contexto de writeback. En entornos con control de recursos, los límites locales también importan; la documentación de cgroup v2 sobre writeback describe esa interacción para sistemas de archivos compatibles.

línea temporal · dos actividades que pueden solaparse
aplicación:   escribe A │ calcula │ escribe B │ escribe C │ espera │ sigue
writeback:        selecciona A │ envía A y B │ completa │ envía C

No hay una barrera implícita después de cada write().
El escritor puede acabar esperando cuando el trabajo pendiente lo exige.

Ese solapamiento explica por qué sumar “tiempo que tardó write” y una estimación independiente de “tiempo de disco” puede contar dos veces intervalos compartidos. En los experimentos separaremos ventanas reales del mismo proceso y explicitaremos lo que queda fuera de cada una.

5. Umbrales y relojes de Linux

Los parámetros se consultan en /proc/sys/vm. La siguiente tabla resume su función; los valores de tu sistema son los que hay que registrar. Los pares en bytes y porcentaje son alternativas: al configurar uno se desactiva la representación contraparte, que aparece como cero.

ParámetroFunciónUnidad
dirty_background_bytes
dirty_background_ratio
Umbral para writeback de fondo.Bytes o porcentaje de memoria disponible para este fin.
dirty_bytes
dirty_ratio
Límite relacionado con la intervención del proceso que genera escrituras.Bytes o porcentaje; no es un porcentaje directo de MemTotal.
dirty_expire_centisecsAntigüedad para que los datos sean elegibles en el vaciado periódico.Centésimas de segundo.
dirty_writeback_centisecsIntervalo de activación periódica de los trabajadores.Centésimas de segundo; cero desactiva esa activación periódica.

Elegible no significa persistido dentro de un plazo. Desactivar la activación periódica tampoco desactiva los otros motivos de writeback. Consulta las definiciones en la documentación oficial de los sysctl de memoria virtual.

Leer el contexto sin cambiarlo

terminal · consulta de parámetros, sin privilegios de administración
$ for nombre in dirty_background_bytes dirty_background_ratio dirty_bytes dirty_ratio dirty_expire_centisecs dirty_writeback_centisecs; do printf '%s=' "$nombre"; cat "/proc/sys/vm/$nombre"; done
$ getconf PAGESIZE
$ uname -r

Si un intervalo leído fuese 500 centésimas, su conversión sería cinco segundos. Ese cálculo convierte unidades; no predice el instante de finalización de los datos. Registrar el valor real permite que otra persona entienda las condiciones del ensayo sin asumir una configuración por defecto.

Para interpretar un experimento, anota también si estás en una VM o un contenedor y qué montaje contiene el archivo. Un umbral global no explica por sí solo todos los límites efectivos de un proceso. En este curso observaremos los parámetros existentes: los cambios de política requieren un diagnóstico específico del sistema y quedan fuera de este laboratorio.

6. Regulación y equilibrio del escritor

La memoria no es una cola de capacidad infinita. Si el productor genera nuevas páginas pendientes más rápido de lo que el almacenamiento las completa, la cola crece. Cuando ese crecimiento necesita regularse, el kernel puede hacer esperar al escritor. Esa espera puede ocurrir dentro de una llamada bufferizada ordinaria.

Throttling del escritor: regulación del ritmo de modificación de páginas para que el trabajo pendiente no crezca indefinidamente frente al servicio disponible. En Linux intervienen estimaciones y controles por contexto; no hay que interpretar el límite configurado como una barrera exacta de una sola página.

Un balance de caudales para pensar

modelo de cola · aproximación, no ecuación completa del kernel
pendiente siguiente ≈ pendiente actual
                     + páginas nuevas que requieren salida
                     - páginas cuyo trabajo se completó

Si se añaden 12 páginas/tick y se completan 4 páginas/tick:
    crecimiento inicial aproximado = 8 páginas/tick.

Con 24 páginas de margen, ese exceso consume el margen en unos 3 ticks.
Luego el productor debe reducir su ritmo, esperar o recibir un error.

La expresión cuenta páginas y distingue actualizaciones sobre páginas ya pendientes. No sería correcto sustituir “páginas nuevas” por todos los bytes entregados a write() cuando se reescribe siempre el mismo rango. Tampoco supone que el servicio real sea constante: puede variar por otras cargas, metadatos, colas del dispositivo y planificación.

Por eso una copia puede empezar con una tasa muy alta y después bajar. Al principio consume margen de memoria; luego el ritmo observado refleja mejor las restricciones del conjunto. Una ejecución pequeña puede terminar antes de esa fase. Aumentar indiscriminadamente el tamaño para forzarla convertiría una demostración en una carga del sistema; aquí preferimos un ensayo acotado y un simulador donde la presión es controlable.

Lo que se mideInterpretación cuidadosa
Una ráfaga muy rápidaEl proceso entregó bytes rápidamente durante esa ventana; parte del trabajo pudo quedar pendiente.
Una ráfaga lentaHay latencia; para atribuirla a throttling hacen falta más señales.
Sincronización final cortaPuede haber avanzado mucho writeback antes de llegar a ella.
Sincronización final largaHabía trabajo o esperas que resolver; no identifica por sí sola la capa responsable.

La asignación de bloques, la reserva de memoria, los fallos de página y la actividad ajena también influyen en la latencia. Una gráfica temporal ayuda a formular hipótesis; una única muestra lenta no identifica el mecanismo interno que la causó.

7. Observar sin confundir contadores

/proc/meminfo presenta información del sistema. Dirty informa memoria pendiente de escritura y Writeback, memoria en proceso de escritura. Las unidades indicadas como kB corresponden a cantidades de 1024 bytes. La referencia de proc_meminfo(5) describe los campos. No son contadores privados del archivo del experimento.

terminal · una fotografía global
$ awk '/^(MemTotal|MemAvailable|Cached|Dirty|Writeback):/ {print}' /proc/meminfo

Una foto puede encontrar Writeback: 0 kB aunque hayan ocurrido escrituras entre dos consultas. El muestreo periódico pierde eventos cortos. También puede registrar una subida causada por otro proceso. Restar dos fotos no convierte una magnitud global en la suciedad de nuestro archivo.

Preguntas que cada instrumento sí puede contestar

InstrumentoPregunta útilLímite
Resultado de write/pwrite¿Cuántos bytes aceptó mi programa?No cuenta bytes persistidos ni operaciones físicas.
CLOCK_MONOTONIC¿Cuánto transcurrió entre dos puntos?No asigna ese tiempo automáticamente a una causa.
fstat y comprobación de contenido¿Qué tamaño y bytes son visibles?No son una prueba de recuperación tras un fallo.
Dirty / Writeback¿Qué informa el sistema en estas fotos?Incluyen otras cargas y no forman un historial completo.
findmnt -T¿Qué montaje contiene la ruta?No describe por completo el almacenamiento del anfitrión de una VM.

Los totales de memoria no son casilleros que siempre puedan sumarse sin solapamientos. No calcularemos “suciedad exacta del archivo = Dirty después − Dirty antes”, ni “bytes durables = tamaño − Dirty”. Son operaciones numéricas posibles, pero no describen esas propiedades.

Una ausencia de crecimiento también es un resultado.

Con pocos MiB y un almacenamiento rápido, el writeback puede seguirle el ritmo al productor. Si el montaje es de memoria, el recorrido cambia todavía más. Documenta la observación y sus condiciones; el experimento no exige ver una gran montaña de páginas sucias.

8. Herramientas comunes del laboratorio

Trabajaremos con Linux, un compilador C, Bash y Python 3 para el análisis. Guarda los tres archivos de código que siguen en un directorio de trabajo. El escritor crea un archivo nuevo con O_CREAT | O_EXCL y permisos 0600: rechaza rutas existentes, incluidos enlaces, y limita la entrega a 128 MiB. No se usan privilegios ni se vacía la caché global.

El encabezado comparte validación numérica, lectura de las fotos y un reloj monotónico. Un fallo al leer /proc se comunica como error; no se reemplaza silenciosamente por un cero que parezca una medición válida. El formato CSV usa enteros para los contadores y segundos con punto decimal.

medir-sucias.h · utilidades compartidas por los dos programas
#ifndef MEDIR_SUCIAS_H
#define MEDIR_SUCIAS_H
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

struct foto { unsigned long long dirty, writeback; };

static long numero(const char *s, long minimo, long maximo)
{
    char *fin;
    long n;
    if (!s[0] || strspn(s, "0123456789") != strlen(s)) return -1;
    errno = 0;
    n = strtol(s, &fin, 10);
    if (errno || *fin || n < minimo || n > maximo) return -1;
    return n;
}

static double ahora(void)
{
    struct timespec t;
    if (clock_gettime(CLOCK_MONOTONIC, &t) == -1) {
        perror("clock_gettime");
        exit(EXIT_FAILURE);
    }
    return (double)t.tv_sec + (double)t.tv_nsec / 1000000000.0;
}

static int fotografia(struct foto *f)
{
    FILE *p = fopen("/proc/meminfo", "r");
    char linea[256];
    unsigned long long n;
    int vistos = 0, fallo;
    if (!p) return -1;
    while (fgets(linea, sizeof linea, p)) {
        if (sscanf(linea, "Dirty: %llu kB", &n) == 1) {
            f->dirty = n; vistos |= 1;
        } else if (sscanf(linea, "Writeback: %llu kB", &n) == 1) {
            f->writeback = n; vistos |= 2;
        }
    }
    fallo = ferror(p);
    if (fclose(p) == EOF) fallo = 1;
    if (fallo || vistos != 3) { errno = EIO; return -1; }
    return 0;
}

static int pausa_ms(long ms)
{
    struct timespec restante = {ms / 1000, (ms % 1000) * 1000000L};
    while (nanosleep(&restante, &restante) == -1) {
        if (errno != EINTR) return -1;
    }
    return 0;
}
#endif

La lectura de /proc/meminfo no es una transacción que congele todo el sistema. Los campos pueden cambiar mientras se generan o se leen. El programa busca los nombres y exige que ambos aparezcan, pero eso no convierte la foto en una observación atómica de cada página.

Se utiliza nanosleep() para espaciar observaciones o ráfagas. Se reintenta el tiempo restante ante EINTR; la planificación puede prolongar la espera. Por eso los resultados incluyen tiempos medidos en lugar de dar por hecho que “100 ms solicitados” equivalen a intervalos exactos entre muestras.

9. Un escritor acotado con dos patrones

escritura-diferida.c recibe una ruta nueva, MiB lógicos, un patrón y una pausa por ráfaga. Cada ráfaga entrega 1 MiB mediante bloques de 64 KiB. En secuencial recorre offsets crecientes; en reuso reescribe siempre los mismos 64 KiB. En ambos casos se cuentan los bytes realmente aceptados y se atienden escrituras parciales.

escritura-diferida.c · escritura bufferizada, fotos y sincronización final separada
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <fcntl.h>
#include <stdint.h>
#include <sys/stat.h>
#include <unistd.h>
#include "medir-sucias.h"

enum { BLOQUE = 64 * 1024, POR_MIB = 16 };

static int escribir_todo(int fd, const unsigned char *b, size_t n, off_t pos)
{
    size_t hechos = 0;
    while (hechos < n) {
        ssize_t k = pwrite(fd, b + hechos, n - hechos, pos + (off_t)hechos);
        if (k > 0) { hechos += (size_t)k; continue; }
        if (k == -1 && errno == EINTR) continue;
        if (k == 0) errno = EIO;
        return -1;
    }
    return 0;
}

static int sincronizar(int fd)
{
    int r;
    do { r = fsync(fd); } while (r == -1 && errno == EINTR);
    return r;
}

int main(int argc, char **argv)
{
    unsigned char buffer[BLOQUE];
    long mib, pausa;
    int fd = -1, estado = EXIT_FAILURE, reuso;
    struct stat st;
    struct foto f;
    unsigned long long bytes = 0;
    double inicio, t, duracion, escritura = 0.0, sincronizacion, antes_sync;
    if (argc != 5 || (mib = numero(argv[2], 1, 128)) == -1 ||
        (pausa = numero(argv[4], 0, 20)) == -1 ||
        (strcmp(argv[3], "secuencial") && strcmp(argv[3], "reuso"))) {
        fprintf(stderr, "Uso: %s NUEVO MiB(1..128) secuencial|reuso pausa_ms(0..20)\n", argv[0]);
        return EXIT_FAILURE;
    }
    reuso = strcmp(argv[3], "reuso") == 0;
    memset(buffer, 'A', sizeof buffer);
    fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
    if (fd == -1) { perror("crear archivo nuevo"); return EXIT_FAILURE; }
    inicio = ahora();
    if (fotografia(&f) == -1) { perror("meminfo inicial"); goto salir; }
    if (printf("fase,rafaga,bytes_logicos,transcurrido_s,operacion_s,dirty_kib,writeback_kib\n"
               "inicio,0,0,%.9f,0,%llu,%llu\n", ahora() - inicio,
               f.dirty, f.writeback) < 0) goto salida_csv;
    for (long r = 1; r <= mib; ++r) {
        t = ahora();
        for (int b = 0; b < POR_MIB; ++b) {
            off_t pos = reuso ? 0 : (off_t)bytes;
            if (escribir_todo(fd, buffer, sizeof buffer, pos) == -1) {
                perror("pwrite"); goto salir;
            }
            bytes += sizeof buffer;
        }
        duracion = ahora() - t;
        escritura += duracion;
        if (fotografia(&f) == -1) { perror("meminfo"); goto salir; }
        if (printf("rafaga,%ld,%llu,%.9f,%.9f,%llu,%llu\n", r, bytes,
                   ahora() - inicio, duracion, f.dirty, f.writeback) < 0)
            goto salida_csv;
        if (pausa_ms(pausa) == -1) { perror("nanosleep"); goto salir; }
    }
    antes_sync = ahora();
    if (sincronizar(fd) == -1) { perror("fsync final"); goto salir; }
    sincronizacion = ahora() - antes_sync;
    if (fotografia(&f) == -1) { perror("meminfo final"); goto salir; }
    if (printf("sincronizado,%ld,%llu,%.9f,%.9f,%llu,%llu\n", mib, bytes,
               ahora() - inicio, sincronizacion, f.dirty, f.writeback) < 0)
        goto salida_csv;
    if (fstat(fd, &st) == -1) { perror("fstat"); goto salir; }
    if (st.st_size != (reuso ? (off_t)BLOQUE : (off_t)bytes)) {
        fprintf(stderr, "Tamaño visible inesperado\n"); goto salir;
    }
    if (fprintf(stderr, "logicos=%llu visibles=%lld write_s=%.9f "
                "fsync_s=%.9f intervalo_hasta_sync_s=%.9f\n", bytes,
                (long long)st.st_size, escritura, sincronizacion,
                antes_sync + sincronizacion - inicio) < 0) goto salir;
    if (fflush(stdout) == EOF) goto salida_csv;
    estado = EXIT_SUCCESS;
    goto salir;
salida_csv:
    fprintf(stderr, "No se pudo escribir el CSV\n");
salir:
    if (close(fd) == -1) { perror("close"); estado = EXIT_FAILURE; }
    return estado;
}

Qué incluye cada tiempo

operacion_s en una fila rafaga incluye las dieciséis transferencias de esa ráfaga, sus reintentos y el trabajo del bucle. Excluye la lectura de /proc, la salida CSV y la pausa posterior. write_s suma esas ventanas. No es tiempo puro de CPU: incluye cualquier espera que ocurra durante ellas.

fsync_s mide solamente la sincronización final. El intervalo desde inicio hasta su retorno incluye ráfagas, fotos, salida y pausas. Comienza después de crear el archivo, por lo que no incluye open(); termina antes de la foto y del cierre finales. El CSV transcurrido_s se toma después de cada foto, de modo que su última fila queda algo después del final de la sincronización.

El trabajo puede avanzar durante las pausas.

Si aumentas la pausa, el writeback dispone de más tiempo antes de llegar al fsync final. Que fsync_s baje no significa que el dispositivo haya ganado velocidad. Parte del trabajo pudo completarse antes, durante los intervalos que el programa decidió esperar.

El bloque se inicializa antes del cronómetro con bytes A. El patrón repetitivo facilita verificar el contenido, pero no representa todas las cargas: un sistema con compresión puede tratarlo de forma especial. Para comparar rendimientos físicos harían falta datos y un diseño específicos; aquí comparamos entrega lógica, rango visible y trabajo diferido.

pwrite() facilita elegir el rango sin cambiar el offset compartido. El archivo no se abre con O_APPEND. En reuso, 16 MiB lógicos se traducen en 256 actualizaciones del mismo bloque de 64 KiB; el tamaño final sigue siendo 65536 bytes. No se deduce que el dispositivo haya escrito solo 64 KiB: pueden haberse intercalado varios envíos.

Si falla una transferencia puede quedar un archivo parcial. El programa informa el error y no intenta borrar automáticamente esa evidencia. El estado de éxito solo se alcanza si terminaron las escrituras, la sincronización, las comprobaciones y la salida; también se revisa close(). Un error al escribir el informe no revierte el contenido ya entregado.

10. Un observador independiente

El escritor toma fotos entre ráfagas. Para observar también los intervalos, un segundo proceso consulta los campos periódicamente. No conoce el descriptor del escritor ni atribuye páginas a su archivo: produce una serie temporal global que se puede comparar con las marcas del ensayo.

observar-sucias.c · hasta 40 fotos, separadas entre 10 y 100 ms
#define _POSIX_C_SOURCE 200809L
#include "medir-sucias.h"

int main(int argc, char **argv)
{
    long muestras, intervalo;
    struct foto f;
    double inicio;
    if (argc != 3 || (muestras = numero(argv[1], 1, 40)) == -1 ||
        (intervalo = numero(argv[2], 10, 100)) == -1) {
        fprintf(stderr, "Uso: %s muestras(1..40) intervalo_ms(10..100)\n", argv[0]);
        return EXIT_FAILURE;
    }
    inicio = ahora();
    if (puts("muestra,transcurrido_s,dirty_kib,writeback_kib") == EOF)
        return EXIT_FAILURE;
    for (long i = 0; i < muestras; ++i) {
        if (fotografia(&f) == -1) { perror("meminfo"); return EXIT_FAILURE; }
        if (printf("%ld,%.9f,%llu,%llu\n", i, ahora() - inicio,
                   f.dirty, f.writeback) < 0) return EXIT_FAILURE;
        if (i + 1 < muestras && pausa_ms(intervalo) == -1) {
            perror("nanosleep"); return EXIT_FAILURE;
        }
    }
    if (fflush(stdout) == EOF) { perror("salida CSV"); return EXIT_FAILURE; }
    return EXIT_SUCCESS;
}

Con 40 muestras y 100 ms se solicitan 39 pausas, unos 3,9 segundos más el trabajo y las demoras de planificación. No es un temporizador de tiempo real ni un detector de todos los eventos del dispositivo. Los límites evitan convertir el ejemplo en un monitor indefinido.

Si el CSV se redirige a otro archivo, el observador también genera algo de escritura. La salida por stdio puede agruparse y volcarse al final. Eso limita la perturbación, pero no la elimina. Para series pequeñas el informe es diminuto frente al archivo de prueba; aun así, los campos siguen siendo globales.

Los dos programas usan un origen monotónico propio. Sus columnas transcurrido_s no comparten un cero exacto. Puedes comparar tendencias y orden de las muestras de cada serie; para superponerlas con precisión habría que registrar un origen común o marcas absolutas monotónicas, y cuantificar la incertidumbre de arranque.

11. Laboratorio reproducible e interpretación

Preparación y primera ejecución

Crea un directorio nuevo en un sistema de archivos local con espacio disponible, entra en él y guarda allí medir-sucias.h, escritura-diferida.c y observar-sucias.c. mktemp usa el directorio actual como base: elige previamente una ubicación adecuada. En WSL comprueba si estás dentro del sistema de archivos Linux o sobre un montaje de Windows y anótalo.

terminal · directorio propio, contexto y compilación
$ LAB14=$(mktemp -d ./tema14.XXXXXX)
$ cd "$LAB14"
Guarda aquí los tres archivos de código antes de compilar.
$ findmnt -T .
$ df -h .
$ gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror escritura-diferida.c -o escritura-diferida
$ gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror observar-sucias.c -o observar-sucias
$ ./escritura-diferida sec-16.bin 16 secuencial 0 > sec-16.csv 2> sec-16.log
$ ./escritura-diferida reuso-16.bin 16 reuso 0 > reuso-16.csv 2> reuso-16.log
$ cat sec-16.log reuso-16.log
$ wc -c sec-16.bin reuso-16.bin
16777216 sec-16.bin
   65536 reuso-16.bin
16842752 total

Los tamaños son comprobables; los tiempos y los contadores dependen de tu ejecución. Cada CSV del escritor tiene una cabecera, una fila inicial, dieciséis filas de ráfaga y una fila final: diecinueve líneas. Los informes deben indicar 16777216 bytes lógicos en ambos patrones, aunque sus tamaños visibles difieren.

Observar una ejecución espaciada

Arranca el observador en segundo plano y conserva su PID para esperar exclusivamente a ese proceso. La ejecución de 32 MiB solicita pausas por un total de 640 ms, además del trabajo. El observador puede seguir tomando fotos después de que el escritor termine. Si el escritor tarda más que la ventana del monitor, la serie no cubre todo el ensayo; el comando no promete ese solapamiento completo.

terminal · escritor y observador concurrentes, con ambos estados comprobados
$ ./observar-sucias 40 50 > observacion.csv & OBS14=$!
$ EST14=0; ./escritura-diferida espaciado-32.bin 32 secuencial 20 > espaciado-32.csv 2> espaciado-32.log || EST14=$?; MON14=0; wait "$OBS14" || MON14=$?; test "$EST14" -eq 0 && test "$MON14" -eq 0
$ head -n 5 observacion.csv
$ cat espaciado-32.log

Esperar al monitor evita dejar la toma de muestras a medio camino antes de analizarla. Las variables de estado permiten comprobar ambos procesos incluso si uno falla. No se mata ningún proceso ajeno ni se cambia la configuración de memoria del sistema.

Analizar las ventanas sin mezclarlas

Guarda el siguiente analizador como analizar-sucias.py. Exige una secuencia completa de ráfagas de 1 MiB y una confirmación final. Un archivo truncado o duplicado no produce un promedio aparentemente válido. Los contadores se presentan como rangos globales observados; la tasa se calcula con bytes lógicos y se etiqueta así.

analizar-sucias.py · integridad de la serie y resumen de ventanas
import csv
import math
import statistics
import sys

def analizar(ruta):
    with open(ruta, newline="", encoding="utf-8") as archivo:
        filas = list(csv.DictReader(archivo))
    if len(filas) < 3 or filas[0]["fase"] != "inicio" or filas[-1]["fase"] != "sincronizado":
        raise ValueError("faltan inicio, ráfagas o sincronización")
    rafagas = filas[1:-1]
    n = len(rafagas)
    if not 1 <= n <= 128:
        raise ValueError("cantidad de ráfagas fuera de rango")
    tiempos = []
    anterior = -1.0
    for i, fila in enumerate(filas):
        tiempo = float(fila["transcurrido_s"])
        operacion = float(fila["operacion_s"])
        if not math.isfinite(tiempo) or not math.isfinite(operacion) or tiempo < anterior or operacion < 0:
            raise ValueError("tiempos inválidos o fuera de orden")
        anterior = tiempo
        for clave in ("dirty_kib", "writeback_kib"):
            if int(fila[clave]) < 0:
                raise ValueError("contador negativo")
        if 1 <= i <= n:
            if fila["fase"] != "rafaga" or int(fila["rafaga"]) != i or int(fila["bytes_logicos"]) != i * 1048576:
                raise ValueError("ráfaga faltante, duplicada o bytes inesperados")
            tiempos.append(operacion)
    if int(filas[0]["bytes_logicos"]) != 0 or int(filas[0]["rafaga"]) != 0:
        raise ValueError("inicio inconsistente")
    if int(filas[-1]["bytes_logicos"]) != n * 1048576 or int(filas[-1]["rafaga"]) != n:
        raise ValueError("final inconsistente")
    escritura = sum(tiempos)
    sync = float(filas[-1]["operacion_s"])
    print(ruta)
    print(f"  MiB lógicos={n}; write_s={escritura:.6f}; fsync_s={sync:.6f}")
    if escritura > 0:
        print(f"  tasa lógica en ráfagas={n / escritura:.2f} MiB/s")
    print(f"  ráfaga mediana={statistics.median(tiempos):.6f}s; máxima={max(tiempos):.6f}s")
    print(f"  última foto a los {anterior:.6f}s, después de fsync")
    for clave in ("dirty_kib", "writeback_kib"):
        valores = [int(f[clave]) for f in filas]
        print(f"  {clave} global: mínimo={min(valores)}, máximo={max(valores)}")

if len(sys.argv) < 2:
    sys.exit("Uso: python3 analizar-sucias.py CSV [CSV ...]")
for ruta in sys.argv[1:]:
    try:
        analizar(ruta)
    except (OSError, ValueError, KeyError, TypeError, csv.Error) as error:
        sys.exit(f"{ruta}: serie inválida: {error}")
terminal · análisis y comprobación independiente del contenido
$ python3 analizar-sucias.py sec-16.csv reuso-16.csv espaciado-32.csv
$ python3 -c 'from pathlib import Path; pares=[("sec-16.bin",16*1048576),("reuso-16.bin",65536),("espaciado-32.bin",32*1048576)]; assert all(Path(p).stat().st_size == n and Path(p).read_bytes() == b"A"*n for p,n in pares); print("Tamaños y contenido visibles correctos")'

Cómo redactar la conclusión del ensayo

  1. Identifica el entorno. Kernel, montaje, VM o máquina física, carga concurrente y parámetros consultados.
  2. Cuenta lo entregado. Los patrones de 16 MiB aceptaron los mismos bytes lógicos, pero tocaron rangos distintos.
  3. Delimita los tiempos. Separa suma de ráfagas, sincronización final e intervalo total. Las pausas y observaciones están fuera de la suma de ráfagas.
  4. Describe las fotos. Informa el rango de Dirty y Writeback sin asignarlo exclusivamente al archivo.
  5. Expón lo que falta. El ensayo no mide operaciones físicas, una fase sostenida garantizada ni recuperación ante un corte.

Para repetir, elige nombres nuevos: el escritor rechaza reutilizar el nombre de un archivo existente. Alterna el orden de los patrones y conserva cada CSV con su contexto. No mezcles todas las filas como si fueran ejecuciones independientes: las ráfagas de un mismo proceso comparten el estado del ensayo.

Un benchmark acotado no garantiza saturación.

Si todos los tiempos son pequeños y los contadores apenas cambian, el resultado sigue siendo válido para esa carga. No infieras el ancho de banda sostenido del disco ni cambies umbrales globales para obligar al equipo a parecerse a un ejemplo.

12. Fallos, cortes y límites de las garantías

Un proceso terminado no equivale a una máquina sin energía

Si termina el proceso, el kernel puede seguir teniendo sus páginas y continuar el writeback. Incluso una terminación abrupta no borra la caché del sistema. Cortar la energía o perder el estado de una máquina virtual plantea un escenario diferente: desaparece memoria que el sistema necesitaba para completar cambios. Por eso matar al escritor y reabrir el archivo no prueba durabilidad frente a un fallo de la máquina.

El resultado real de una caída depende de datos, metadatos, orden de escrituras, protocolo de recuperación y garantías del almacenamiento. Un archivo puede tener versiones parcialmente actualizadas; tampoco debemos asumir que toda escritura aceptada se pierde. Parte del trabajo puede haber llegado ya a las capas persistentes.

Errores que llegan después

La aplicación puede haber recibido éxito al entregar datos y encontrar después un fallo al escribir más, sincronizar o cerrar. Una implementación que solo cuenta retornos positivos de write() y descarta los demás estados puede informar éxito indebidamente. Si falla la sincronización, no corresponde imprimir “guardado correctamente” como si el compromiso estuviera confirmado.

Los ejemplos conservan un estado de fracaso ante cualquier error relevante. Una salida CSV incompleta debe tratarse como un ensayo fallido; que las primeras filas parezcan razonables no valida el resto. Reintentar automáticamente toda la operación exige saber qué parte se aplicó y qué garantías ofrece el protocolo de la aplicación.

La sincronización final del laboratorio

fsync() espera la sincronización del archivo y comunica errores. Su alcance no incluye automáticamente la persistencia de la entrada del directorio de un archivo recién creado. Esa distinción se documenta en fsync(2). El próximo tema estudiará las llamadas y los protocolos de durabilidad con más detalle.

En este laboratorio usamos una sincronización final del archivo como frontera medible y comprobamos su retorno. No estamos implementando un protocolo completo de publicación durable de un nombre. Las garantías presuponen además que el sistema de archivos y el dispositivo cumplen el contrato de sincronización; el cronómetro no verifica por sí mismo su comportamiento frente a una pérdida real de energía.

SituaciónConclusión que se evitaRazón
Desciende Dirty“Mi archivo ya es durable”.La foto es global y el trabajo puede estar en otras etapas.
Se envió una versión de una página“La última versión ya terminó”.La página pudo modificarse después del envío.
Termina el proceso sin error“Todo sobrevivirá a un corte”.La salida del proceso no es una barrera general de almacenamiento.
El sistema tiene journal“Toda actualización de usuario es atómica y durable”.La recuperación de estructuras no reemplaza el protocolo de datos de la aplicación.

No hace falta provocar cortes reales para comprender estas diferencias. El simulador permite perder exclusivamente la memoria de un modelo, con estados visibles y versiones conocidas, sin intervenir sobre el equipo.

13. Simulador de páginas y writeback

La caché del modelo contiene dieciséis páginas. En cada tick primero terminan los envíos que cumplen su latencia de dos ticks, después actúa el productor y por último se selecciona nuevo trabajo. El servicio limita tanto los inicios como las finalizaciones por tick. Cada envío captura una versión; si la página vuelve a modificarse, completar ese envío conserva la modificación posterior como pendiente.

El umbral de fondo selecciona trabajo cuando la cola lo alcanza. En activaciones periódicas también se seleccionan páginas que cumplen la edad indicada. El límite del productor detiene nuevas operaciones al alcanzar el número máximo de páginas pendientes del modelo. Son reglas didácticas deterministas: Linux utiliza una regulación más compleja y sus parámetros no operan con estos ticks ni con este límite rígido.

Laboratorio de versiones y cola pendiente

Los parámetros se aplican con Reiniciar. Las acciones usan la configuración registrada en el estado.

estado · páginas pendientes contadas una sola vez
PáginaVersión RAMVersión en vueloVersión respaldoEstado
Historial de acciones

    Recorridos propuestos

    1. Cola que crece. Conserva los valores iniciales y avanza varios ticks con productor. Compara aceptadas, completadas y operaciones detenidas por el límite.
    2. Servicio detenido. Elige cero envíos por tick y reinicia. El productor llena su margen y deja de avanzar. Ningún botón de vaciado puede crear servicio inexistente.
    3. Actualizaciones que se combinan. Selecciona reuso y reinicia. Muchas escrituras incrementan la versión de la misma página; la cola no crece como en el patrón secuencial.
    4. Redirty. Con reuso, fondo 1 y servicio 1, da un tick para iniciar un envío y otro con productor para modificar la página durante ese envío. En el tercer tick compara la versión que completa con la versión nueva de RAM. La tabla conserva los cambios posteriores.
    5. Antigüedad. Usa productor 1, fondo 8, límite 12, edad 4 y período 2. Da un tick con productor y luego ticks sin productor. La página puede salir por edad en una activación periódica aun sin alcanzar fondo.
    6. Pérdida de memoria. Mientras haya versiones pendientes, simula el corte. La tabla conserva exclusivamente lo que el respaldo del modelo había completado; los envíos en curso desaparecen.

    Alcance del modelo: una petición de vaciado mantiene la selección activa hasta que no quedan páginas pendientes, pero solo avanza la E/S al dar ticks. No implementa fsync(). No modela errores, journal, directorios, sectores parcialmente escritos, cachés de dispositivo ni tiempos reales. “Respaldo” es una capa ideal persistente del simulador, no una lectura de tu disco.

    “Modificada sin enviar” cuenta una versión actual aún no capturada. “En vuelo” cuenta envíos anteriores; una página puede aparecer en ambos grupos. “Pendientes” es la unión, sin duplicarla. Estos contadores didácticos no son una reconstrucción exacta de Dirty y Writeback en Linux. Tras un corte se cuenta cuántas páginas tenían una versión RAM más reciente que el respaldo, no cuántas llamadas a write() se perdieron.

    14. Resumen

    15. Preguntas de repaso y práctica

    1. ¿Qué confirma un retorno positivo de write()?

    Confirma la cantidad de bytes aceptados por esa llamada, que puede ser menor que la solicitada. En una escritura bufferizada ordinaria no confirma por sí solo su persistencia frente a un fallo de la máquina.

    2. ¿Leer inmediatamente los bytes nuevos prueba durabilidad?

    No. La lectura puede obtenerlos de la caché compartida. Es una comprobación de contenido visible durante la ejecución, sin reproducir la pérdida de memoria del sistema.

    3. ¿Qué diferencia existe entre una página limpia y una sucia?

    La sucia tiene modificaciones que requieren propagación hacia su respaldo. Una limpia puede seguir residente y ser útil para lecturas; escribirla no obliga a expulsarla de la caché.

    4. ¿Qué ocurre si una página cambia durante su writeback?

    El envío iniciado corresponde a una versión anterior. Su finalización no debe eliminar el trabajo de la modificación posterior, que puede requerir otro envío.

    5. ¿Por qué 16 MiB lógicos pueden dejar un archivo de 64 KiB?

    El patrón reuso entrega repetidamente bloques de 64 KiB al offset cero. Se acumulan bytes lógicos aceptados, pero no se amplía el rango visible. Eso no determina cuántas veces se escribió físicamente esa región.

    6. ¿Qué ventaja ofrece agrupar actualizaciones?

    Permite combinar cambios cercanos o repetidos y compartir trabajo de las capas inferiores, además de solapar la ejecución con la E/S. La ventaja depende del patrón y del momento en que se propagan las versiones.

    7. ¿El writeback comienza siempre al cerrar el archivo?

    No. Puede iniciarse mientras el archivo sigue abierto y continuar después de que termine el proceso. La caché y los trabajadores pertenecen al kernel.

    8. ¿dirty_background_ratio se aplica directamente a MemTotal?

    No. La base de memoria disponible para esta política no equivale a toda la RAM física. Tampoco un porcentaje global explica todos los límites efectivos de contextos y grupos de recursos.

    9. ¿Qué relación tienen las variantes bytes y ratio?

    Son alternativas dentro de cada par de configuración. Al activar una representación, la contraparte aparece como cero; no son dos límites independientes que deban sumarse.

    10. ¿dirty_expire_centisecs garantiza una fecha de persistencia?

    No. Expresa antigüedad para elegibilidad en el vaciado periódico. La selección, el envío y la finalización requieren trabajo posterior y dependen de las condiciones de E/S.

    11. ¿Poner a cero el período elimina todo writeback?

    No. Desactiva esa activación periódica, pero siguen existiendo otros motivos de vaciado, como los límites de trabajo pendiente y las solicitudes explícitas.

    12. ¿Por qué un escritor bufferizado puede esperar?

    La memoria y el servicio son limitados. Puede esperar por regulación del ritmo, asignaciones, metadatos y otros recursos. Medir una llamada lenta no identifica automáticamente cuál de esas causas intervino.

    13. ¿Qué pasa si el productor genera 12 páginas por tick y solo salen 4?

    En el modelo simple de páginas nuevas, la cola crece inicialmente unas ocho páginas por tick. El margen de memoria se consume y luego hace falta regular el productor; no puede sostener ese exceso indefinidamente.

    14. ¿Dirty después menos Dirty antes mide mi archivo?

    No. Son fotos globales con otros procesos y actividad concurrente. Entre las fotos también pudo avanzar writeback del propio ensayo. La diferencia no atribuye páginas a un archivo.

    15. ¿Qué significa observar Writeback igual a cero?

    Que esa foto no mostró memoria en esa categoría. No demuestra que no haya ocurrido E/S entre las muestras ni confirma que todos los cambios de nuestro archivo sean durables.

    16. ¿Por qué las pausas pueden reducir fsync_s?

    Permiten que trabajo de fondo avance antes de la sincronización final. El intervalo total incluye esas pausas; una espera final menor no prueba una mejora del almacenamiento.

    17. ¿Matar el proceso reproduce un corte eléctrico?

    No. El kernel puede conservar sus páginas y continuar escribiéndolas. La pérdida de RAM del sistema elimina un estado que la terminación de un proceso normalmente deja disponible.

    18. ¿Qué falta después del fsync del archivo recién creado?

    Para un protocolo de publicación durable también importa la entrada del directorio y el orden de las operaciones. El laboratorio mide la sincronización del archivo y comprueba errores; el tema siguiente desarrolla las garantías y protocolos correspondientes.

    Para practicar

    1. Dibuja una línea temporal con dos modificaciones sobre una página y un envío intercalado. Identifica qué versión puede confirmar cada finalización.
    2. Consulta los seis parámetros de memoria virtual y convierte sus intervalos a segundos. Separa valores leídos, conversiones y predicciones que no puedes hacer.
    3. Ejecuta ambos patrones con nombres nuevos y 8 MiB. Comprueba el tamaño final, los bytes lógicos y el número de ráfagas.
    4. Repite una ejecución con pausa cero y otra con 20 ms. Compara write_s, fsync_s e intervalo total sin presentar solo la ventana más favorable.
    5. Alterna el orden de secuencial y reuso en cinco pares de ensayos pequeños. Conserva todos los CSV y compara resultados por ejecución.
    6. Elimina una fila de una copia del CSV y duplica otra en una segunda copia. Comprueba que el analizador las rechaza.
    7. Prueba MiB cero, 129 y una ruta ya existente. Verifica el fracaso y que un archivo existente conserve su contenido.
    8. Compara la serie del observador con las fotos del escritor. Explica por qué sus ceros temporales y su frecuencia no son idénticos.
    9. En el simulador, reduce el servicio a cero y describe cuántas páginas avanzan antes de detenerse el productor.
    10. Usa reuso, fondo 1 y servicio 1. Anota una versión en vuelo y comprueba qué ocurre al generar otra antes de completar todos los cambios.
    11. Solicita vaciado y da ticks sin productor hasta llegar a cero pendientes. Compara un corte en ese momento con otro mientras hay trabajo en vuelo.
    12. Redacta una conclusión de cinco líneas que incluya entorno, carga, ventanas medidas, observación y una limitación. Evita presentar una tasa de entrega lógica como velocidad física del disco.

    En el tema 15 estudiaremos sync, fsync y fdatasync: cómo pedir sincronización, comprobar sus resultados y elegir puntos de durabilidad según los datos y los metadatos de la aplicación.