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

Tema 16

O_DIRECT y la E/S sin caché

La caché de páginas resulta útil para muchas cargas, pero algunas aplicaciones administran su propia memoria de datos y necesitan estudiar otro recorrido de E/S. O_DIRECT permite solicitar E/S directa, con requisitos que dependen del archivo y del sistema de archivos. Aprenderemos a consultar esos requisitos, reservar memoria alineada y manejar lecturas parciales sin romper la siguiente operación. Dos programas en C permitirán observar solicitudes alineadas y comparar recorridos de lectura; un simulador mostrará la diferencia entre alinear una petición y aprovechar una caché.

1. Qué cambia con O_DIRECT

Los temas 12 a 15 estudiaron residencia, anticipación, páginas sucias y sincronización. Ahora cambiaremos una decisión del recorrido: cómo participa la caché de páginas en las transferencias de datos de un archivo regular. Seguiremos usando descriptores y llamadas como pread(); no accederemos a dispositivos de bloques ni necesitaremos privilegios.

E/S directa: modalidad que solicita transferir datos entre almacenamiento y búferes de la aplicación evitando el recorrido ordinario por la caché de páginas para esos datos. En Linux se solicita con la bandera O_DIRECT. Su soporte y sus restricciones deben comprobarse en el entorno y el archivo utilizados.

recorridos conceptuales · datos de un archivo regular
lectura bufferizada:
    almacenamiento → caché de páginas → búfer de la aplicación
                            ▲
                    una repetición puede aprovecharla

lectura directa solicitada:
    almacenamiento ──────────────────→ búfer alineado de la aplicación

En ambos casos siguen interviniendo el sistema de archivos y el kernel.
Las cachés del dispositivo o del servidor no desaparecen por esta bandera.

La documentación de E/S directa de iomap define ese recorrido y describe trabajo adicional de coherencia con páginas existentes. Evitar la caché para una transferencia no equivale a eliminar toda interacción con ella ni todas las capas inferiores.

No hay una promesa universal de “cero copias” en toda implementación, de menor latencia o de ausencia de cachés. Tampoco desaparecen las estructuras de nombres, inodos, asignación de bloques y metadatos. El programa conserva la responsabilidad de gestionar los bytes recibidos y comprobar los resultados.

Una ventaja posible aparece cuando la aplicación mantiene su propio conjunto de páginas: puede evitar almacenar otra copia útil de los mismos datos en la caché del kernel. Una desventaja aparece si no tiene ese mecanismo y vuelve a pedir los mismos datos: puede perder la reutilización que obtenía de la caché de páginas.

“Sin caché” necesita un alcance.

El título se refiere al recorrido de datos respecto de la caché de páginas. No afirma que el SSD, la controladora, el anfitrión de una VM o un servidor remoto carezcan de cachés.

2. Caché, espera y durabilidad

En el tema 15 elegimos cuándo confirmar datos y metadatos. Esa decisión sigue existiendo. Una transferencia puede utilizar E/S directa y necesitar igualmente un protocolo de sincronización para cumplir la promesa de durabilidad de la aplicación.

DecisiónPreguntaMecanismo relacionado
Participación de la caché¿Se solicita el recorrido directo para los datos?O_DIRECT
Punto de espera¿Cuándo espera el programa la operación que lanzó?Llamadas síncronas o una interfaz asíncrona con su finalización.
Confirmación de almacenamiento¿Qué datos y metadatos deben quedar confirmados?fsync, fdatasync, O_SYNC, O_DSYNC, según el protocolo.
Publicación¿Qué nombre debe sobrevivir a la caída?Operaciones del directorio y su confirmación.

O_DIRECT no sustituye las obligaciones de O_SYNC. El manual de open(2) distingue estas banderas y advierte que los requisitos y el tratamiento de peticiones desalineadas varían: pueden fallar o utilizar un recorrido bufferizado.

fragmento · decisiones que se combinan según el requisito
/* Lectura directa solicitada. No modifica el archivo. */
int fd = open(ruta, O_RDONLY | O_CLOEXEC | O_DIRECT);

/* En un diseño de escritura, el recorrido y la confirmación se eligen aparte:
   O_DIRECT | O_SYNC, o puntos explícitos de sincronización según el protocolo.
   Una publicación de nombre sigue necesitando atender el directorio. */

También se debe separar “directa” de “asíncrona”. Los programas del tema llaman a pread() y esperan su retorno antes de lanzar la siguiente operación. Su profundidad de operaciones es uno. Un motor que mantiene varias operaciones en vuelo constituye otro diseño y debe administrar el tiempo de vida de todos sus búferes.

Que una lectura directa vuelva a ser rápida tampoco demuestra que vino del medio físico: puede haber cachés inferiores, datos no asignados que se resuelven sin leer bloques o particularidades del sistema de archivos. El benchmark informará tiempos de las llamadas y de su procesamiento, sin convertirlos en una lectura exclusiva del dispositivo.

3. Las tres alineaciones de una operación

Una petición tiene dirección de memoria, longitud y posición dentro del archivo. Alinear solo una de esas cantidades no alinea automáticamente las demás. Para razonar usaremos M como requisito de memoria y A como requisito de offset y longitud de la lectura.

condiciones · requisitos del archivo, no constantes universales
dirección_del_búfer % M == 0
offset_del_archivo % A == 0
longitud_solicitada % A == 0

Ejemplo didáctico: M = 4096 y A = 512

dirección = 65536, offset = 1024, longitud = 4096 → cumple las tres
dirección = 65537, offset = 1024, longitud = 4096 → falla memoria
dirección = 65536, offset = 1025, longitud = 4096 → falla offset
dirección = 65536, offset = 1024, longitud = 4095 → falla longitud
CantidadCómo se controlaError frecuente
DirecciónReservar memoria con la alineación adecuada.Suponer que cualquier dirección de malloc sirve.
LongitudElegir una cantidad compatible con el requisito de E/S.Usar el tamaño del dato lógico sin comprobar el múltiplo.
OffsetUsar posiciones compatibles, por ejemplo con pread.Avanzar después de una lectura parcial sin revisar la siguiente petición.

Una página de memoria de 4096 bytes, un bloque lógico del dispositivo y un bloque del sistema de archivos no son conceptos intercambiables. getconf PAGESIZE informa una propiedad de la memoria virtual; no descubre por sí solo el requisito de E/S directa de un archivo.

El campo st_blksize tampoco debe presentarse como una respuesta universal a estas tres condiciones. En el laboratorio consultaremos una interfaz específica y separaremos el requisito conocido de los límites elegidos por el ejemplo.

Base alineada y subregión

Si una dirección p cumple M, p + 1 puede dejar de cumplirlo. Una reserva correcta no garantiza que todos los punteros derivados sirvan. Lo mismo ocurre al dividir un búfer para procesar registros o al continuar una transferencia incompleta.

Las comprobaciones con el operador módulo ayudan a detectar errores antes de llamar al kernel. No demuestran que el montaje soporte la operación ni que todas las demás condiciones del archivo se satisfagan. Son una parte del contrato de entrada, no una certificación completa de la ruta física.

4. Consultar requisitos con statx

statx() permite pedir información extendida sobre un archivo. Con STATX_DIOALIGN se solicitan las restricciones de memoria y de offset/longitud para E/S directa. Hay que comprobar tanto el retorno de la llamada como la máscara de campos realmente entregados.

ElementoLectura correcta
stx_maskIndica qué información fue entregada; no basta que la llamada termine con éxito.
stx_dio_mem_alignRequisito de alineación de los búferes.
stx_dio_offset_alignRequisito de offset y longitud de los segmentos de E/S.
STATX_DIO_READ_ALIGNExtensión para consultar un requisito específico de lecturas, cuando esté disponible.

La extensión de lectura puede informar una restricción menos exigente que la general. Si no se informa una alternativa no nula, se conserva la general. Consulta la disponibilidad y semántica de estos campos en statx(2).

Tres respuestas distintas

  1. Requisitos informados y no nulos. Se usan para construir las peticiones y se comprueban las operaciones reales.
  2. Campo no informado. La información es desconocida por esta interfaz. No equivale a “sin restricciones” ni a “O_DIRECT no existe”.
  3. Campo informado con requisitos nulos. La respuesta señala ausencia de soporte de E/S directa para ese archivo.

También hay dos versiones que importan: las cabeceras con las que compilaste y el kernel que atiende la llamada. Una macro disponible en las cabeceras no garantiza que el kernel y el sistema de archivos entreguen ese campo. El código usa compilación condicional y comprueba la respuesta efectiva.

Consultaremos el descriptor ya abierto mediante AT_EMPTY_PATH. Así la consulta y las lecturas se refieren al mismo objeto abierto, sin volver a resolver una ruta para obtener los requisitos. La ruta podría cambiar de nombre sin cambiar el objeto del descriptor; eso no autoriza a modificar su contenido durante el ensayo.

Decisión del laboratorio.

Si no dispone de información utilizable, el ejemplo termina con estado 77. Ese número es una convención de estos programas para “ensayo no disponible bajo sus condiciones”. No es un código de error definido por O_DIRECT ni una conclusión universal de incompatibilidad.

5. Reservar y conservar memoria alineada

posix_memalign() reserva un bloque cuya dirección cumple la alineación solicitada. La alineación de esa función debe ser potencia de dos y múltiplo del tamaño de un puntero; su valor de retorno es un código de error, no el estilo -1 con errno. La memoria se libera con free(). Estas condiciones se describen en posix_memalign(3).

fragmento · un valor ilustrativo, no descubrimiento del requisito
void *reserva = NULL;
size_t alineacion = 4096; /* En el programa completo viene de la consulta. */
int error = posix_memalign(&reserva, alineacion, 65536);
if (error != 0) {
    fprintf(stderr, "posix_memalign: %s\n", strerror(error));
} else {
    /* Usar solo dentro de los 65536 bytes reservados. */
    free(reserva);
}

Si el requisito de memoria informado es menor que sizeof(void *) y compatible con una potencia de dos, se puede pedir una alineación mayor que satisfaga ambos. Si el requisito no puede representarse con la estrategia de reserva del ejemplo, se comunica ese límite; no se redondea a ciegas y se declara que el archivo es incompatible.

La longitud de la reserva y la alineación de su base son propiedades diferentes. Una reserva de 65536 bytes puede tener base inadecuada; una base perfectamente alineada puede acompañar una longitud no admitida para la operación. El programa debe mantener ambas condiciones.

Tiempo de vida

Un búfer no puede liberarse ni reutilizarse mientras la operación que lo emplea siga en curso. En nuestros ejemplos cada llamada termina antes de procesar y reutilizar la memoria. En un diseño asíncrono se necesitan estados de pertenencia del búfer hasta recibir su finalización.

Los procesos de este laboratorio no llaman a fork() ni lanzan hilos durante sus lecturas. Si se amplía el diseño, deben revisarse las restricciones documentadas de combinar E/S directa en curso con fork y memoria privada. Que el padre y el hijo tengan direcciones virtuales similares no constituye una garantía de seguridad de la transferencia.

6. Lecturas cortas, EOF y escrituras

Una lectura exitosa puede devolver menos bytes que los pedidos y cero indica EOF. Una lectura con pread() no mueve el offset compartido del descriptor. Las interfaces y sus resultados se detallan en read(2) y pread(2).

Por qué no basta copiar un bucle genérico

ejemplo · continuación de una transferencia
petición inicial: base alineada, offset=0, longitud=4096
resultado parcial: 1536 bytes

continuación ingenua:
    dirección = base + 1536
    offset = 1536
    longitud = 2560

Si M=4096 y A=512:
    offset y longitud cumplen A, pero la dirección nueva no cumple M.

Alternativa para lecturas del comparador:
    reutilizar la base alineada para la petición siguiente,
    procesar antes los bytes recibidos y verificar el nuevo offset.

El comparador procesa cada fragmento recibido y vuelve a usar la base de la reserva. Si una lectura corta deja el siguiente offset desalineado para modo directo, detiene el ensayo con un diagnóstico. No presenta una lectura incompleta como si hubiera transferido todo el archivo.

Antes de lanzar una petición se valida su longitud solicitada. El número retornado cerca de EOF puede ser menor; no se deduce que ese retorno deba tener el mismo tamaño que se pidió. Para simplificar la comparación, el programa exige que el tamaño total del archivo sea múltiplo del requisito de lectura y maneja un último bloque menor que la transferencia habitual cuando corresponde.

Datos lógicos de tamaño arbitrario

Una aplicación puede necesitar leer veinte bytes en una posición no alineada. Puede diseñar una lectura alineada que abarque la región y después extraer los veinte bytes. Eso introduce trabajo adicional y condiciones de concurrencia. No significa que deba cambiar el archivo ni rellenarlo para que una lectura lógica tenga una longitud válida.

Para una escritura parcial, la situación exige más cuidado: si la aplicación decide leer, modificar y reescribir una unidad mayor, debe preservar los bytes vecinos y coordinar otros escritores. Una técnica de relleno o de truncado también afecta tamaño, errores y durabilidad. Los programas de este tema son de lectura para estudiar las restricciones sin implementar una actualización general de bloques compartidos.

Reintentar no corrige la geometría.

EINTR permite repetir la llamada con sus parámetros en los ejemplos. Un EINVAL por parámetros incompatibles no se arregla repitiendo exactamente la misma petición indefinidamente.

7. Coherencia, concurrencia y ciclo de vida

Si existen páginas del mismo archivo en caché, la E/S directa puede necesitar trabajo de coherencia antes de completar una operación. Evitar que una lectura ordinaria use la caché no elimina los efectos de modificaciones pendientes sobre ese objeto.

No conviene improvisar una mezcla concurrente de escrituras directas, escrituras bufferizadas y mapeos del mismo archivo. Además de razonar sobre coherencia, hay que medir el trabajo que aparece al mantenerla. Un diseño consistente reduce el número de recorridos que deben coordinarse.

El laboratorio prepara un archivo, entrega y sincroniza sus datos y termina esa preparación antes de medir. Después ejecuta lectores, uno por vez, sin modificar el contenido ni mantener un mapeo del archivo. Comparar secuencialmente modos de lectura sobre una entrada estable es una decisión acotada; no demuestra el comportamiento de escritores concurrentes.

SituaciónQué debe controlar la aplicación
Búfer reutilizadoQue terminó la operación anterior y que la base conserva su alineación.
Archivo con otro escritorCoordinación y definición del estado que se quiere leer.
Temporal o nombre reemplazadoLa identidad del objeto abierto y el protocolo de publicación.
Varias operaciones en vueloBúferes, offsets, finalizaciones y estados de error individuales.
Lecturas repetidasQué caché, si alguna, espera reutilizar y qué memoria administra.

El comparador revisará tamaño y marcas del archivo antes y después. Esa comprobación detecta cambios observables, pero no crea una instantánea ni reemplaza la condición de entrada estable: un escritor no coordinado podría invalidar las conclusiones incluso si una comprobación limitada no lo detecta.

Los programas rechazan enlaces simbólicos en la ruta final y archivos que no sean regulares. El propósito es mantener claro el objeto del ensayo; no interpretar un dispositivo, una tubería o un directorio como si fuese el archivo de datos preparado.

8. Utilidades compartidas del laboratorio

Los dos programas usan dio-comun.h. Se compilan con cabeceras de Linux/glibc que permitan declarar statx; la consulta opcional específica de lecturas se habilita solo cuando su macro existe. No se inventa una alineación de respaldo si falta información.

El tamaño del archivo se limita a 64 MiB y la transferencia a 1 MiB. Se busca un bloque múltiplo tanto de 64 KiB como del requisito de E/S, usando el mínimo común múltiplo. La memoria se reserva con un requisito independiente. Estos límites son decisiones del ejemplo para acotar la prueba.

dio-comun.h · consulta del descriptor, límites y reserva alineada
#ifndef DIO_COMUN_H
#define DIO_COMUN_H
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

enum { LIMITE_ARCHIVO = 64 * 1024 * 1024, LIMITE_BLOQUE = 1024 * 1024 };
struct requisitos { size_t memoria, io, reserva, bloque; };

static int cerrar_dio(int *fd)
{
    int viejo = *fd;
    *fd = -1;
    return viejo == -1 ? 0 : close(viejo);
}

static int archivo_dio(int fd, struct stat *st)
{
    if (fstat(fd, st) == -1) { perror("fstat"); return 1; }
    if (!S_ISREG(st->st_mode) || st->st_size <= 0 ||
        st->st_size > LIMITE_ARCHIVO) {
        fprintf(stderr, "Se exige archivo regular de 1 byte a 64 MiB\n");
        return 1;
    }
    return 0;
}

static inline size_t mcd_dio(size_t a, size_t b)
{
    while (b) { size_t r = a % b; a = b; b = r; }
    return a;
}

static int consultar_dio(int fd, struct requisitos *r)
{
#ifdef STATX_DIOALIGN
    struct statx sx;
    unsigned int mascara = STATX_DIOALIGN;
#ifdef STATX_DIO_READ_ALIGN
    mascara |= STATX_DIO_READ_ALIGN;
#endif
    memset(&sx, 0, sizeof sx);
    if (statx(fd, "", AT_EMPTY_PATH, mascara, &sx) == -1) {
        int error = errno;
        fprintf(stderr, "statx: %s\n", strerror(error));
        return error == ENOSYS || error == EINVAL || error == EOPNOTSUPP ? 77 : 1;
    }
    if (!(sx.stx_mask & STATX_DIOALIGN)) {
        fprintf(stderr, "Alineación no informada: el ejemplo no la supone\n");
        return 77;
    }
    if (sx.stx_dio_mem_align == 0 && sx.stx_dio_offset_align == 0) {
        fprintf(stderr, "La respuesta informa E/S directa no soportada en este archivo\n");
        return 77;
    }
    if (!sx.stx_dio_mem_align || !sx.stx_dio_offset_align) {
        fprintf(stderr, "Requisitos incompletos o inconsistentes\n"); return 1;
    }
    r->memoria = sx.stx_dio_mem_align;
    r->io = sx.stx_dio_offset_align;
#ifdef STATX_DIO_READ_ALIGN
    if ((sx.stx_mask & STATX_DIO_READ_ALIGN) && sx.stx_dio_read_offset_align)
        r->io = sx.stx_dio_read_offset_align;
#endif
    if (r->memoria > LIMITE_BLOQUE || r->io > LIMITE_BLOQUE ||
        (r->memoria & (r->memoria - 1)) != 0) {
        fprintf(stderr, "Requisitos fuera de la estrategia acotada de este ejemplo\n");
        return 77;
    }
    r->reserva = r->memoria < sizeof(void *) ? sizeof(void *) : r->memoria;
    size_t factor = 65536 / mcd_dio(65536, r->io);
    if (r->io > LIMITE_BLOQUE / factor) {
        fprintf(stderr, "El bloque común supera 1 MiB\n"); return 77;
    }
    r->bloque = factor * r->io;
    return 0;
#else
    (void)fd; (void)r;
    fprintf(stderr, "Las cabeceras no ofrecen STATX_DIOALIGN\n");
    return 77;
#endif
}

static int reservar_dio(const struct requisitos *r, unsigned char **buffer)
{
    void *p = NULL;
    int error = posix_memalign(&p, r->reserva, r->bloque + 1);
    if (error) { fprintf(stderr, "posix_memalign: %s\n", strerror(error)); return 1; }
    memset(p, 0, r->bloque + 1);
    *buffer = p;
    return 0;
}

static int abrir_dio(const char *ruta, int directa, int *fd)
{
    *fd = open(ruta, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK |
                     (directa ? O_DIRECT : 0));
    if (*fd == -1) {
        int error = errno;
        fprintf(stderr, "open: %s\n", strerror(error));
        return directa && (error == EINVAL || error == EOPNOTSUPP) ? 77 : 1;
    }
    return 0;
}
#endif

La reserva contiene un byte adicional porque la sonda probará buffer + 1 con la longitud normal. Así la petición desalineada sigue dentro de memoria válida; estudiar la alineación no requiere provocar un acceso fuera de la reserva.

La apertura utiliza O_NONBLOCK para que una ruta que resulte ser una tubería no deje el ejemplo esperando antes de poder comprobar el tipo. No se utiliza esa bandera como promesa de latencia de un archivo regular. También se comprueban los cierres, sin reintentar el mismo número de descriptor.

Si se compila con cabeceras anteriores a STATX_DIOALIGN, la función de consulta informa la limitación. El ejemplo necesita además las declaraciones de statx y las banderas utilizadas; un entorno de desarrollo mucho más antiguo debe actualizarse o adaptar explícitamente la interfaz.

9. Sonda de solicitudes alineadas y desalineadas

sonda-directa.c abre el archivo con O_DIRECT, consulta los requisitos y realiza cuatro lecturas: una conforme y tres que alteran una dimensión. Informa qué condición calculada se viola y qué resultado devolvió la llamada. No predice que todo sistema de archivos deba rechazar cada sonda.

sonda-directa.c · observar resultados sin escribir en el archivo
#define _GNU_SOURCE
#define _FILE_OFFSET_BITS 64
#include "dio-comun.h"

int main(int argc, char **argv)
{
    int fd = -1, estado = 1, rc;
    struct stat st;
    struct requisitos r;
    unsigned char *buffer = NULL;
    const char *casos[] = {"conforme", "direccion_mas_uno", "longitud_menos_uno", "offset_uno"};
    if (argc != 2) { fprintf(stderr, "Uso: %s ARCHIVO\n", argv[0]); return 1; }
    estado = abrir_dio(argv[1], 1, &fd);
    if (estado) goto salir;
    estado = archivo_dio(fd, &st);
    if (estado) goto salir;
    estado = consultar_dio(fd, &r);
    if (estado) goto salir;
    if (st.st_size < (off_t)r.bloque + 1) {
        fprintf(stderr, "La sonda necesita al menos bloque+1 bytes de archivo\n");
        estado = 1; goto salir;
    }
    if (reservar_dio(&r, &buffer)) { estado = 1; goto salir; }
    fprintf(stderr, "mem_align=%zu io_align_lectura=%zu bloque=%zu\n",
            r.memoria, r.io, r.bloque);
    if (puts("caso,viola_memoria,viola_offset,viola_longitud,bytes,errno") == EOF) {
        estado = 1; goto salir;
    }
    estado = 0;
    for (int i = 0; i < 4; ++i) {
        unsigned char *p = buffer + (i == 1 ? 1 : 0);
        size_t n = r.bloque - (i == 2 ? 1 : 0);
        off_t off = i == 3 ? 1 : 0;
        int vm = (uintptr_t)p % r.memoria != 0;
        int vo = (size_t)off % r.io != 0;
        int vl = n % r.io != 0;
        ssize_t k;
        do { k = pread(fd, p, n, off); } while (k == -1 && errno == EINTR);
        int error = k == -1 ? errno : 0;
        if (printf("%s,%d,%d,%d,%lld,%d\n", casos[i], vm, vo, vl,
                   (long long)k, error) < 0) estado = 1;
        if ((i == 0 && k != (ssize_t)n) || (k == -1 && error != EINVAL))
            estado = 1;
    }
    if (fflush(stdout) == EOF) estado = 1;
salir:
    free(buffer);
    rc = cerrar_dio(&fd);
    if (rc == -1) { perror("close"); estado = 1; }
    return estado;
}

Cómo leer la sonda

La fila conforme debe transferir el bloque pedido en este archivo estable y suficientemente grande. Una fila desalineada puede devolver -1 con EINVAL; ese resultado esperado no hace fracasar toda la sonda. Un error de otra clase o un fracaso de la fila conforme sí se conserva como fracaso del ensayo.

Si una fila con una condición violada devuelve bytes, la observación es “se aceptó esta petición”. No es una demostración de que los requisitos no existen en otros archivos ni de que la transferencia mantuvo una ruta directa. Algunas implementaciones pueden resolver casos mediante un recorrido alternativo.

Si el requisito fuera uno, sumar uno a la dirección o al offset no violaría esa dimensión; por eso las columnas calculan la violación en lugar de etiquetar el caso como incorrecto de antemano. Los nombres describen la transformación que hizo el programa, no una sentencia universal sobre su validez.

La sonda no usa un valor numérico fijo para EINVAL ni espera un texto de error localizado. Registra el entero de errno devuelto por el entorno, junto con el número de bytes; un retorno exitoso se informa con error cero.

10. Comparador de lecturas en C

comparar-directa.c realiza de uno a cuatro recorridos secuenciales del mismo archivo abierto. En cada proceso se elige buffered o direct. Ambos usan la misma regla para el tamaño de transferencia y una reserva alineada para evitar introducir dos tamaños de bloque diferentes en la comparación.

El comparador requiere requisitos informados también en el modo bufferizado: esa es una condición de su comparación común, no una obligación general de leer un archivo bufferizado. Si no puede construir el mismo ensayo, informa estado 77 sin adivinar una geometría.

comparar-directa.c · llamadas cronometradas y procesamiento separado
#define _GNU_SOURCE
#define _FILE_OFFSET_BITS 64
#include <inttypes.h>
#include <time.h>
#include "dio-comun.h"

struct muestra {
    uint64_t bytes, suma;
    unsigned long llamadas;
    double io_s, total_s;
};

static int pases_validos(const char *s)
{
    return strlen(s) == 1 && s[0] >= '1' && s[0] <= '4' ? s[0] - '0' : 0;
}

static double reloj_dio(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 / 1e9;
}

static int sin_cambios(const struct stat *a, const struct stat *b)
{
    return a->st_size == b->st_size &&
        a->st_mtim.tv_sec == b->st_mtim.tv_sec &&
        a->st_mtim.tv_nsec == b->st_mtim.tv_nsec &&
        a->st_ctim.tv_sec == b->st_ctim.tv_sec &&
        a->st_ctim.tv_nsec == b->st_ctim.tv_nsec;
}

int main(int argc, char **argv)
{
    int fd = -1, estado = 1, directa, pases;
    struct stat inicial, final;
    struct requisitos r;
    struct muestra m[4] = {{0}};
    unsigned char *buffer = NULL;
    if (argc != 4 || !(pases = pases_validos(argv[3])) ||
        (strcmp(argv[2], "buffered") && strcmp(argv[2], "direct"))) {
        fprintf(stderr, "Uso: %s ARCHIVO buffered|direct pases(1..4)\n", argv[0]);
        return 1;
    }
    directa = strcmp(argv[2], "direct") == 0;
    estado = abrir_dio(argv[1], directa, &fd);
    if (estado) goto salir;
    estado = archivo_dio(fd, &inicial);
    if (estado) goto salir;
    estado = consultar_dio(fd, &r);
    if (estado) goto salir;
    if ((uint64_t)inicial.st_size % r.io != 0) {
        fprintf(stderr, "Este comparador exige tamaño múltiplo de io_align\n");
        estado = 77; goto salir;
    }
    if (reservar_dio(&r, &buffer)) { estado = 1; goto salir; }
    estado = 1;
    for (int p = 0; p < pases; ++p) {
        off_t pos = 0;
        double inicio = reloj_dio();
        while (pos < inicial.st_size) {
            size_t n = (uint64_t)(inicial.st_size - pos) < r.bloque ?
                       (size_t)(inicial.st_size - pos) : r.bloque;
            double t = reloj_dio();
            ssize_t k = pread(fd, buffer, n, pos);
            int error = k == -1 ? errno : 0;
            m[p].io_s += reloj_dio() - t;
            ++m[p].llamadas;
            if (k == -1 && error == EINTR) continue;
            if (k == -1) {
                fprintf(stderr, "pread: %s\n", strerror(error)); goto salir;
            }
            if (k == 0) { fprintf(stderr, "EOF antes del tamaño observado\n"); goto salir; }
            for (ssize_t i = 0; i < k; ++i) m[p].suma += buffer[i];
            m[p].bytes += (uint64_t)k;
            pos += k;
            if (directa && pos < inicial.st_size && (uint64_t)pos % r.io != 0) {
                fprintf(stderr, "Lectura corta deja el siguiente offset desalineado\n");
                goto salir;
            }
        }
        m[p].total_s = reloj_dio() - inicio;
    }
    if (fstat(fd, &final) == -1) { perror("fstat final"); goto salir; }
    if (!sin_cambios(&inicial, &final)) {
        fprintf(stderr, "Cambió el archivo durante el ensayo\n"); goto salir;
    }
    if (cerrar_dio(&fd) == -1) { perror("close"); goto salir; }
    if (puts("modo,pase,bytes,llamadas,checksum,io_s,total_s,bloque,mem_align,io_align") == EOF)
        goto salir;
    for (int p = 0; p < pases; ++p)
        if (printf("%s,%d,%" PRIu64 ",%lu,%" PRIu64 ",%.9f,%.9f,%zu,%zu,%zu\n",
                   argv[2], p + 1, m[p].bytes, m[p].llamadas, m[p].suma,
                   m[p].io_s, m[p].total_s, r.bloque, r.memoria, r.io) < 0)
            goto salir;
    if (fflush(stdout) == EOF) { perror("CSV"); goto salir; }
    estado = 0;
salir:
    free(buffer);
    if (cerrar_dio(&fd) == -1) { perror("close en limpieza"); estado = 1; }
    return estado;
}

Ventanas y comprobaciones

io_s suma las ventanas alrededor de cada llamada pread, incluidas las interrupciones y esperas observadas. total_s cubre el recorrido completo, con el cálculo de la suma de bytes y el control del bucle. La diferencia no es una medida exacta de consumo de CPU; es tiempo transcurrido fuera de las ventanas de llamada.

La apertura, consulta, reserva e inicialización de memoria quedan fuera del cronómetro. La memoria se toca antes para no presentar su inicialización como trabajo exclusivo de la primera lectura. Tampoco se incluye imprimir CSV: las muestras se almacenan y se emiten después de verificar el archivo y cerrar el descriptor.

checksum es una suma de bytes de 64 bits, útil como comprobación sencilla de que se procesó contenido equivalente. Tiene colisiones y no es una función criptográfica ni una prueba suficiente frente a un escritor concurrente. El analizador comprueba que todos los recorridos de la serie coincidan.

No hay expulsión de páginas, sugerencias de lectura ni intentos de enfriar cachés. “Pase 1” significa primer recorrido de ese proceso, no lectura fría. La preparación puede haber dejado todo el archivo en caché. Una segunda lectura bufferizada puede reutilizarlo; una directa puede beneficiarse de cachés inferiores y de otras condiciones del entorno.

11. Laboratorio reproducible y análisis

Preparación de una entrada estable

En una ubicación local con espacio disponible, crea un directorio nuevo y guarda allí los archivos del tema. Usaremos un archivo de 8 MiB, creado de forma exclusiva y con datos aleatorios. Se termina su entrega y sincronización antes de lanzar los lectores. Los datos no deben modificarse mientras dure el experimento.

terminal · directorio propio, entorno y preparación acotada
$ LAB16=$(mktemp -d ./tema16.XXXXXX)
$ cd "$LAB16"
Guarda aquí los archivos de código antes de compilar y ejecutar.
$ uname -r
$ findmnt -T .
$ getconf PAGESIZE
$ df -h .
$ python3 -c 'import os; f=open("datos-8m.bin","xb"); [f.write(os.urandom(65536)) for _ in range(128)]; f.flush(); os.fsync(f.fileno()); f.close()'
$ gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror sonda-directa.c -o sonda-directa
$ gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror comparar-directa.c -o comparar-directa

La generación no presupone que los ceros o un patrón repetido tengan un tratamiento representativo en un montaje con compresión. El archivo preparado sigue siendo una carga pequeña. Su sincronización previa establece una frontera de preparación; no constituye una publicación durable de su nombre ni una operación incluida en el tiempo de lectura.

Primero averiguar si el ensayo está disponible

terminal · sonda y conservación del resultado del programa
$ RC16=0; ./sonda-directa datos-8m.bin > sonda.csv 2> sonda.log || RC16=$?; cat sonda.log; printf 'estado_sonda=%s\n' "$RC16"
$ cat sonda.csv

Si el estado es cero, examina la fila conforme y las tres transformaciones. Si es 77, conserva el diagnóstico: puede faltar soporte informado, una consulta utilizable o una condición necesaria para este ejemplo. Si es uno, revisa el error de apertura, tipo, lectura, cierre o salida. No interpretes un CSV vacío como un resultado de tiempo cero.

En WSL, una ruta sobre un montaje de Windows puede entregar una respuesta distinta de una ruta del sistema de archivos Linux. En un contenedor, el sistema de archivos visible también importa. Registrar kernel y montaje permite describir el resultado sin convertirlo en una afirmación sobre todas las máquinas.

Barrido de tres rondas y dos pases

Guarda el siguiente archivo como barrido-directa.sh. Se invoca con Bash; no necesita permisos de ejecución. Se alterna qué modo empieza cada ronda. Si un proceso devuelve 77, se detiene el barrido con ese mismo estado en lugar de completar la serie usando otro modo silenciosamente.

barrido-directa.sh · seis procesos, doce recorridos de lectura
#!/usr/bin/env bash
set -euo pipefail
for ronda in 1 2 3; do
    if (( ronda % 2 )); then modos=(buffered direct); else modos=(direct buffered); fi
    for modo in "${modos[@]}"; do
        if ./comparar-directa datos-8m.bin "$modo" 2 > "r${ronda}-${modo}.csv"; then
            :
        else
            estado=$?
            printf 'Ensayo detenido: ronda=%s modo=%s estado=%s\n' "$ronda" "$modo" "$estado" >&2
            exit "$estado"
        fi
    done
done
terminal · ejecutar el barrido y consultar su estado
$ BARRIDO16=0; bash barrido-directa.sh || BARRIDO16=$?; printf 'estado_barrido=%s\n' "$BARRIDO16"

Continúa con el análisis de una serie completa cuando el barrido termine con estado cero. Las seis salidas deben tener una cabecera y dos filas. Son doce recorridos de 8 MiB, 96 MiB lógicos leídos; esa cantidad no afirma que el dispositivo haya transferido 96 MiB físicos.

analizar-directa.py · integridad del conjunto y resumen por modo y pase
import csv
import math
from pathlib import Path
import statistics
import sys

CAMPOS = ("modo", "pase", "bytes", "llamadas", "checksum", "io_s", "total_s",
          "bloque", "mem_align", "io_align")
MODOS = ("buffered", "direct")

def analizar(carpeta):
    esperados = {f"r{r}-{m}.csv" for r in range(1, 4) for m in MODOS}
    if {p.name for p in carpeta.glob("r*-*.csv")} != esperados:
        raise ValueError("se necesitan los seis CSV de r1..r3 y ambos modos")
    grupos = {(m, p): [] for m in MODOS for p in (1, 2)}
    contenido, geometria = set(), set()
    for ronda in range(1, 4):
        for modo in MODOS:
            ruta = carpeta / f"r{ronda}-{modo}.csv"
            with ruta.open(newline="", encoding="utf-8") as archivo:
                lector = csv.DictReader(archivo)
                if tuple(lector.fieldnames or ()) != CAMPOS:
                    raise ValueError(f"{ruta.name}: cabecera incorrecta")
                filas = list(lector)
            if len(filas) != 2:
                raise ValueError(f"{ruta.name}: se esperaban dos pases")
            for pase, f in enumerate(filas, 1):
                if set(f) != set(CAMPOS) or f["modo"] != modo or int(f["pase"]) != pase:
                    raise ValueError(f"{ruta.name}: modo, columnas o pase incorrectos")
                b, n, suma, bloque, mem, al = (int(f[k]) for k in
                    ("bytes", "llamadas", "checksum", "bloque", "mem_align", "io_align"))
                if not 0 < b <= 64 * 1048576 or not 0 < bloque <= 1048576 or mem <= 0 or al <= 0:
                    raise ValueError(f"{ruta.name}: tamaño o alineación inválidos")
                if b % al or bloque % al or n < (b + bloque - 1) // bloque or not 0 <= suma <= 255 * b:
                    raise ValueError(f"{ruta.name}: contadores incompatibles")
                io, total = float(f["io_s"]), float(f["total_s"])
                if not math.isfinite(io) or not math.isfinite(total) or io < 0 or total <= 0 or io > total + 1e-6:
                    raise ValueError(f"{ruta.name}: ventanas de tiempo inválidas")
                contenido.add((b, suma))
                geometria.add((bloque, mem, al))
                grupos[(modo, pase)].append((io, total, b / 1048576 / total, n))
    if len(contenido) != 1 or len(geometria) != 1:
        raise ValueError("cambian el tamaño, checksum o requisitos entre muestras")
    print("12 recorridos completos; mismo contenido observado y misma geometría")
    print("modo       pase   mediana io ms   mediana total ms   mediana MiB/s total   llamadas")
    for (modo, pase), filas in grupos.items():
        med = lambda i: statistics.median(f[i] for f in filas)
        print(f"{modo:10} {pase:4} {med(0)*1000:15.4f} {med(1)*1000:18.4f} "
              f"{med(2):21.2f} {min(f[3] for f in filas)}..{max(f[3] for f in filas)}")
    print("Pase 1 no significa frío; pase 2 no identifica qué caché respondió.")
    print("Tres muestras por grupo describen este ensayo, no una garantía de rendimiento.")

if len(sys.argv) != 2:
    sys.exit("Uso: python3 analizar-directa.py DIRECTORIO")
try:
    analizar(Path(sys.argv[1]))
except (OSError, ValueError, TypeError, KeyError, csv.Error) as error:
    sys.exit(f"Serie inválida: {error}")
terminal · analizar únicamente una serie completa
$ python3 analizar-directa.py .
$ python3 -c 'from pathlib import Path; b=Path("datos-8m.bin").read_bytes(); assert len(b)==8*1048576; print("bytes=",len(b),"checksum=",sum(b))'

Preguntas que sí permite investigar

  1. Geometría real informada. Anota los requisitos de memoria y lectura, el bloque elegido y las respuestas de la sonda.
  2. Repetición. Compara los pases del mismo modo sin llamarlos automáticamente frío y caliente.
  3. Trabajo dentro y fuera de pread. Separa io_s de total_s. Ambos contienen tiempo transcurrido y pueden incluir demoras de planificación.
  4. Contenido equivalente. Comprueba bytes y suma, y conserva la condición de archivo estable.
  5. Alcance del resultado. Describe una lectura secuencial pequeña y con una operación en vuelo, no el rendimiento de un motor completo de base de datos.

El comparador no observa residencia con mincore ni traza el bloque físico; el éxito del modo solicitado y sus tiempos no certifican toda la ruta interna. No incluiremos tablas de velocidades inventadas: la tabla válida es la que genere tu ejecución junto con el contexto.

Para repetir, conserva la serie anterior en su propio directorio y prepara otro ensayo. No mezcles muestras de montajes, geometrías, contenidos o condiciones diferentes en una única mediana. Si falta una parte de la serie, el analizador la rechaza en lugar de presentar una comparación parcial como completa.

12. Cuándo evaluarlo y cómo decidir

Motores que administran sus propias páginas

Una base de datos puede mantener un conjunto de páginas, elegir cuáles retener y coordinar su escritura mediante su propio protocolo. En ese contexto tiene sentido estudiar si otra copia de datos en la caché de páginas aporta valor o compite por memoria. Pero tener una caché propia tampoco elimina los costos de leer, reemplazar, sincronizar y recuperar.

Como caso concreto, la documentación de innodb_flush_method de MySQL describe modalidades que combinan O_DIRECT en archivos de datos con llamadas de sincronización. Es un ejemplo de que gestionar el recorrido y confirmar persistencia siguen siendo decisiones relacionadas pero distintas.

Esto no implica que toda base de datos use la misma modalidad ni que convenga cambiar la configuración de un servidor a partir de un microbenchmark de ocho MiB. Hay que considerar carga, versión, motor, distribución del almacenamiento, memoria y requisitos de recuperación de esa instalación.

SituaciónQué investigarQué no concluir automáticamente
Aplicación con caché propiaUso de memoria, duplicación de datos y política de reemplazo.Que eliminar otra caché siempre mejora todo el sistema.
Muchas relecturas pequeñas sin caché propiaCuánto trabajo reutiliza actualmente el kernel.Que ir al recorrido directo abarata cada relectura.
Transferencia secuencial grandeTamaño de bloques, anticipación, memoria y servicio sostenido.Que una lectura directa síncrona pequeña representa toda la carga.
Acceso con múltiples operaciones en vueloProfundidad, colas, pertenencia de búferes y latencia de finalización.Que añadir O_DIRECT crea concurrencia por sí solo.
Entorno remoto o virtualCapas de caché y garantías de cada participante.Que la bandera del cliente describe el almacenamiento físico completo.

Un proceso de decisión útil

  1. Especifica la carga. Tamaños, posiciones, proporción lectura/escritura, reutilización y concurrencia.
  2. Especifica el compromiso. Qué datos se confirman y cuándo responde la aplicación.
  3. Comprueba soporte. Consulta por archivo y construye una política explícita si la modalidad no está disponible.
  4. Mide extremos comparables. Preparación separada, contenido equivalente y ventanas completas.
  5. Valida el conjunto. Considera memoria, CPU, colas y latencias además de una tasa de bytes.

Una aplicación de producción puede ofrecer un modo bufferizado alternativo cuando el usuario lo elige o cuando una política explícita lo permite. El problema sería etiquetar ese resultado como directo sin informar el cambio. La alternativa forma parte de la interfaz y debe quedar registrada en los diagnósticos y mediciones.

Un resultado negativo también enseña.

Que el ensayo se omita por falta de información o que el modo directo resulte más lento son resultados válidos. La herramienta sirve para evaluar condiciones concretas, no para confirmar una ventaja obligatoria de una bandera.

13. Simulador de alineación y caché

El archivo del modelo contiene 32768 bytes, ocho páginas de 4096. La caché del kernel puede retener cuatro páginas con reemplazo LRU y comienza vacía. Una lectura bufferizada puede reutilizarlas; una lectura directa ideal no las utiliza ni incorpora otras. No hay anticipación, caché de dispositivo ni caché propia de aplicación.

Peticiones, requisitos y reutilización

Los parámetros se leen en cada petición. Cambiar requisitos representa otro escenario del modelo; para compararlo desde el inicio usa Reiniciar.

estado · páginas servidas y bytes lógicos, sin tiempos físicos
PáginaRango del archivoCaché del modelo
Historial de peticiones

    Recorridos propuestos

    1. Repetición bufferizada. Con los valores iniciales, lee dos veces. La segunda petición puede utilizar la página residente.
    2. Repetición directa. Reinicia, selecciona directa y repite dos veces una solicitud conforme. El modelo vuelve a servirla por el recorrido directo sin incorporarla a su caché.
    3. Una dimensión por vez. Usa directa y cambia desplazamiento a 1; luego restáuralo y cambia offset a 1; por último restaura offset y usa longitud 4095. Identifica el requisito violado.
    4. Respuesta alternativa. Conserva una petición desalineada y selecciona resolver bufferizada. El historial registra ese recorrido en lugar de contarlo como directo.
    5. Información ausente. Selecciona requisitos desconocidos. El modelo no inventa una geometría para ejecutar directa; una lectura bufferizada ordinaria sigue siendo otro caso.
    6. Capacidad. Lee bufferizado una página por vez en offsets 0, 4096, 8192, 12288 y 16384. La quinta incorporación desplaza una página por la capacidad de cuatro.

    Alcance del modelo: cada página que necesita servicio se cuenta como una unidad didáctica, no como una operación física del disco. Una petición puede tocar varias páginas y devolver solo los bytes anteriores al EOF del archivo. Las condiciones se verifican sobre la longitud solicitada, antes de ese recorte.

    El caso de información desconocida aplica una política conservadora del simulador. El caso de petición desalineada usa la respuesta seleccionada; no intenta descubrir cómo actuaría tu sistema. Las lecturas directas no cambian las páginas residentes del modelo. El botón de vaciar solo modifica estructuras de JavaScript, sin ejecutar drop_caches ni ninguna operación del equipo.

    14. Resumen

    15. Preguntas de repaso y práctica

    1. ¿Qué caché se intenta evitar con O_DIRECT?

    El recorrido ordinario de los datos por la caché de páginas del kernel. No elimina por sí solo cachés del dispositivo, del servidor o del anfitrión, ni las estructuras de metadatos del sistema de archivos.

    2. ¿O_DIRECT garantiza durabilidad?

    No sustituye el protocolo de sincronización de datos, metadatos y nombres. Las operaciones y banderas del tema 15 siguen respondiendo a la necesidad de confirmación.

    3. ¿Usar directa crea operaciones asíncronas?

    No. El comparador llama a pread y espera antes de la siguiente solicitud. Mantener varias operaciones en vuelo requiere otra interfaz y gestionar sus búferes y finalizaciones.

    4. ¿Qué tres cantidades hay que alinear?

    Dirección de memoria, longitud solicitada y offset. Los requisitos de memoria y de E/S pueden ser distintos y se consultan para el archivo utilizado.

    5. ¿Una base alineada garantiza que base+1 sirve?

    No. Si el requisito de memoria es mayor que uno, sumar un byte a una base conforme rompe ese múltiplo. También hay que permanecer dentro de la reserva válida.

    6. ¿PAGESIZE y st_blksize descubren toda la geometría?

    No. Describen otras propiedades y no reemplazan una consulta específica de restricciones de E/S directa. Suponer una constante puede funcionar en un equipo y fallar en otro.

    7. ¿Basta que statx devuelva cero?

    No. Hay que comprobar que la máscara entregada contiene la información solicitada y revisar sus valores. Una llamada exitosa puede no informar ese campo.

    8. ¿Un campo ausente significa que O_DIRECT no está soportado?

    No. Significa que esa interfaz no entregó la información. El ejemplo conserva la incertidumbre y omite su ensayo; no deduce una geometría ni una incompatibilidad universal.

    9. ¿Para qué existe una consulta específica de lectura?

    Para poder informar una restricción de offset y longitud de lectura distinta de la general cuando el entorno la ofrece. El requisito de alineación de memoria se trata por separado.

    10. ¿Cómo se comprueba posix_memalign?

    Mediante su código de retorno: cero indica éxito y un valor de error explica el fracaso. No se interpreta como el esquema -1 con errno de las llamadas al sistema. La reserva se libera con free.

    11. ¿Qué problema tiene continuar en buffer+k?

    El nuevo puntero puede no satisfacer el requisito de memoria. También pueden dejar de cumplir offset o longitud. El comparador procesa el fragmento, reutiliza la base y verifica que la siguiente posición directa sea válida.

    12. ¿Un resultado menor que lo pedido es automáticamente error?

    No. Las lecturas pueden ser parciales. El programa debe procesar lo recibido y decidir cómo continuar; en este ensayo un EOF anterior al tamaño observado se trata como fracaso de la comparación.

    13. ¿Una petición desalineada siempre devuelve EINVAL?

    No hay una respuesta universal para todos los sistemas de archivos y versiones. Puede fallar o resolverse mediante un recorrido alternativo. La sonda informa qué ocurrió sin presentar aceptación como prueba de una ruta directa.

    14. ¿Qué significa estado 77 en los programas?

    Es una convención del laboratorio para indicar que no se puede realizar el ensayo bajo sus condiciones, por ejemplo por información ausente o límites del ejemplo. No es un errno especial de O_DIRECT.

    15. ¿Qué distingue io_s de total_s?

    io_s suma ventanas alrededor de pread. total_s incluye el recorrido, el procesamiento del contenido y el control. La diferencia no mide CPU de forma exacta ni revela por sí sola qué capa generó una espera.

    16. ¿El primer pase es necesariamente frío?

    No. La preparación y otros accesos pueden haber dejado datos en caché. Tampoco una repetición directa elimina la posibilidad de reutilización en cachés inferiores.

    17. ¿Por qué un motor con caché propia puede evaluarlo?

    Para estudiar duplicación de datos, uso de memoria y control de su conjunto de páginas. Aun así debe gestionar servicio, coherencia, sincronización y recuperación; no hay una mejora obligatoria para todas las cargas.

    18. ¿Qué debe informarse si se cambia a modo bufferizado?

    El cambio de modalidad y su motivo. Puede ser una política válida, pero una medición resuelta por una alternativa no debe etiquetarse como si el programa hubiera mantenido su solicitud directa sin cambios.

    Para practicar

    1. Con M=4096 y A=512, construye tres solicitudes que violen exactamente una condición cada una y otra que cumpla las tres.
    2. Consulta la geometría con la sonda y conserva kernel, montaje y diagnóstico. Distingue información ausente de una petición rechazada.
    3. Ejecuta los doce recorridos del barrido. Compara modo y pase sin llamar fría a la primera lectura.
    4. Compara la suma independiente del archivo con la columna checksum. Explica por qué su igualdad no es una prueba criptográfica.
    5. En una copia de la serie, elimina un archivo, duplica una fila y cambia un pase. Comprueba que el analizador rechaza esas variantes.
    6. Prueba los programas con una ruta inexistente, un directorio, una tubería y un archivo vacío. Verifica que no se presentan como mediciones válidas.
    7. Crea un archivo propio con longitud múltiplo de A pero menor que un bloque completo. Estudia qué admite el comparador y qué condición adicional impone la sonda.
    8. Dibuja una lectura parcial cuyo offset nuevo siga alineado y otra cuyo offset no lo esté. Explica por qué reutilizar la base resuelve solo una dimensión.
    9. En el simulador, repite una página bufferizada y luego directa. Compara unidades de servicio sin convertirlas en milisegundos de un SSD.
    10. En el simulador, fuerza desalineación y alterna las dos respuestas del modelo. Revisa qué modalidad queda registrada.
    11. Describe cómo cambiaría el comparador para mantener cuatro operaciones en vuelo. Enumera estados y recursos antes de elegir una interfaz asíncrona.
    12. Propón una evaluación para una aplicación con caché propia. Incluye memoria total, reutilización, ventanas medidas y garantías de confirmación.

    Con este tema cerramos el bloque dedicado a la caché de disco. En el tema 17 comenzaremos la anatomía interna del sistema de archivos: volumen, superbloque, mapas de bits, inodos y bloques de datos.