1. Qué significa frío y caliente
En el tema 12 observamos contenido de archivos que permanecía en la caché de páginas después de terminar el proceso que lo utilizó. Ahora queremos comparar accesos bajo estados distintos, sin confundir una etiqueta elegida por el programa con una condición demostrada.
En este tema, una lectura fría respecto a la caché de páginas comienza sin residencia observada de las páginas del intervalo estudiado. Una lectura caliente observada comienza con ellas residentes. También existe residencia parcial. Estas descripciones se refieren a esa capa y a una instantánea, no a todas las cachés de la plataforma.
Incluso sin páginas presentes en esa consulta, el dispositivo, el servidor o el anfitrión de una máquina virtual pueden conservar datos. Por tanto, «frío respecto a la caché de Linux» no equivale automáticamente a «cada byte se obtuvo de la superficie física de un disco».
Primera pasada no es sinónimo de fría
Escribir el archivo para preparar una prueba puede dejar sus páginas en RAM. También pudo leerlo otro proceso. Volver a abrirlo o lanzar otro ejecutable no expulsa la caché. Una prueba que solo llama primera a una fila no ha medido todavía su estado inicial.
En el otro extremo, una segunda pasada no garantiza residencia completa: el archivo puede ser mayor que la memoria útil o competir con otras cargas. Puede ser caliente, parcialmente caliente o volver a exigir muchas cargas.
El intervalo importa
Un archivo puede tener el prefijo presente y el resto ausente. Si solo vas a leer una cabecera, su estado relevante no se resume necesariamente por el porcentaje del archivo completo. Los experimentos mostrarán tanto conteos como un mapa inicial de páginas para no ocultar la distribución.
Mincore aporta una observación temporal, no un bloqueo de residencia. El estado puede cambiar entre la consulta y la operación. Usaremos esa evidencia con sus límites, sin convertirla en una garantía de que nada más ocurrió en el equipo.
2. Diseñar una comparación verificable
Comparar tiempos requiere conservar el trabajo lógico: mismo archivo estable, misma cantidad de bytes, mismo algoritmo de lectura y mismo tamaño de petición. La variable que estudiaremos es la preparación o sugerencia aplicada y el estado observado de caché.
- Crear un archivo propio y acotado. Evita datos compartidos con otras cargas y documenta que su creación ya pudo calentarlo.
- Separar preparación y lectura. Si se sincroniza, descarta o anticipa, explica si ese tiempo está dentro o fuera de la comparación.
- Observar residencia. Registra total de páginas y presentes antes y después; no impongas la etiqueta frío a un estado distinto.
- Leer y comprobar el trabajo. Cuenta los bytes realmente recibidos y conserva errores y una suma de control didáctica.
- Repetir sin mezclar grupos incompatibles. Compara muestras que tengan preparación y residencia inicial comparables.
No eliminaremos cachés globales ni usaremos sudo. Daremos sugerencias para el archivo de laboratorio. El alcance menor facilita razonar sobre el experimento y evita presentar como condición local una modificación del sistema entero.
Qué no se mantiene fijo automáticamente
La carga de CPU, el planificador, la frecuencia, otras lecturas y las decisiones del núcleo siguen variando. Además, el estado de anticipación de una apertura puede evolucionar entre pasadas. El programa registra condiciones observables, pero no congela toda la máquina.
La suma de bytes que usaremos no es criptográfica ni una prueba única de igualdad de contenido. Sirve para detectar diferencias evidentes entre pasadas de los datos conocidos. La hipótesis de archivo estable sigue siendo parte del contrato.
3. La interfaz posix_fadvise
#define _POSIX_C_SOURCE 200809L
#include <fcntl.h>
int posix_fadvise(int fd, off_t offset, off_t len, int advice);
El descriptor ya debe estar abierto. Offset y len describen una región lógica; len igual a cero expresa desde offset hasta el final. La función comunica una expectativa o necesidad de acceso, no entrega bytes a un búfer del programa ni cambia por sí misma su posición actual.
Posix_fadvise devuelve 0 si acepta la llamada o un código de error positivo. Ese código se interpreta con strerror. No se maneja como una función que siempre devuelve -1 y deja el diagnóstico en errno.
int codigo = posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
if (codigo != 0) {
fprintf(stderr, "posix_fadvise: %s\n", strerror(codigo));
}
| Consejo | Intención | Precaución |
|---|---|---|
POSIX_FADV_NORMAL | Patrón ordinario, sin una preferencia especial declarada. | No expulsa ni precarga automáticamente todo el archivo. |
POSIX_FADV_SEQUENTIAL | Se espera recorrido secuencial. | Linux puede ampliar su anticipación; no cambia el orden de tus lecturas. |
POSIX_FADV_RANDOM | No se espera localidad secuencial útil. | En el recorrido bufferizado ordinario de Linux desactiva la anticipación automática de esa apertura; no borra datos ya cacheados. |
POSIX_FADV_WILLNEED | Se necesitará la región próximamente. | Puede iniciar preparación sin esperar a que toda la región esté lista. |
POSIX_FADV_DONTNEED | No se espera reutilizar ahora esa región cacheada. | El descarte es una sugerencia con condiciones y debe observarse. |
POSIX_FADV_NOREUSE | Indicar poco interés en reutilización. | Su efecto depende del núcleo y la implementación; no lo usaremos como mecanismo de descarte. |
En Linux, NORMAL, SEQUENTIAL y RANDOM modifican aspectos del comportamiento de la descripción de archivo abierta, no una política privada de cada nombre. Los duplicados que comparten esa apertura pueden compartir ese estado. Las páginas cacheadas, en cambio, pueden ser aprovechadas por otras aperturas del mismo objeto.
El manual de posix_fadvise(2) describe las sugerencias y sus efectos en Linux. Un retorno correcto no demuestra que el sistema aplicó exactamente la cantidad de trabajo que imaginaba la aplicación.
4. DONTNEED, páginas sucias y alcance
DONTNEED intenta liberar páginas cacheadas de la región. No borra el archivo ni altera sus bytes. Si después se necesitan datos ausentes, habrá que obtenerlos de nuevo por el recorrido correspondiente.
Las páginas sucias no se descartan perdiendo cambios
Los datos pendientes de respaldo no se eliminan como si fueran copias limpias prescindibles. Una práctica que acaba de escribir un archivo y pide DONTNEED puede conservar páginas por ese motivo. Los experimentos sincronizarán primero el archivo propio, fuera de la ventana de lectura, y comprobarán el resultado de la operación.
Eso no significa que fsync vacíe la caché: hace otro trabajo. Sincronizar y sugerir descarte son pasos diferentes. Tampoco una sincronización inicial impide que otro escritor vuelva a ensuciar páginas; el archivo de laboratorio no debe modificarse durante la prueba.
Alineación y páginas parciales
Las peticiones de descarte de páginas cubiertas solo parcialmente pueden conservar esas páginas para no eliminar una región que no se pidió completa. Alinear rangos a los límites de página ayuda a interpretar ensayos parciales. El final del archivo y las reglas del sistema tienen matices: observa, no deduzcas un conteo exacto solo de una suma de bytes.
Para el ensayo de archivo completo usamos offset 0 y len 0. El tamaño real de página se consulta. En experimentos con ventanas, el redondeo debe formar parte del diseño y no realizarse inadvertidamente más allá de un intervalo que no se pretendía afectar.
No existe una exclusividad de caché por proceso
Una sugerencia sobre un archivo puede afectar contenido compartido con otros lectores. Por eso el programa exige un archivo propio de práctica, estable y pequeño, y la documentación pide que no sea utilizado por otras tareas. La propiedad del archivo no demuestra por sí sola que nadie más lo haya abierto.
Otro acceso puede volver a traer páginas inmediatamente después. Mapeos, referencias activas, el sistema de archivos o políticas del entorno también pueden limitar el descarte. Una consulta de residencia posterior es más informativa que nombrar la siguiente lectura como fría por haber ejecutado la sugerencia.
5. Una cabecera para observar residencia
Los dos programas compartirán cache-observada.h. La función foto_cache crea un mapeo de lectura, consulta mincore y lo elimina sin tocar los datos. Devuelve páginas totales, residentes y un mapa de las primeras 64. El llamador valida tipo, tamaño y propiedad del archivo.
#ifndef CACHE_OBSERVADA_H
#define CACHE_OBSERVADA_H
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdint.h>
#include <inttypes.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct foto {
size_t paginas;
size_t residentes;
char mapa[65];
};
static int foto_cache(int fd, size_t longitud, struct foto *f)
{
f->paginas = f->residentes = 0;
f->mapa[0] = '\0';
if (longitud == 0)
return 0;
long valor = sysconf(_SC_PAGESIZE);
if (valor <= 0) {
errno = EINVAL;
return -1;
}
size_t pagina = (size_t)valor;
f->paginas = 1 + (longitud - 1) / pagina;
unsigned char *vec = malloc(f->paginas);
if (vec == NULL)
return -1;
void *m = mmap(NULL, longitud, PROT_READ, MAP_SHARED, fd, 0);
if (m == MAP_FAILED) {
int codigo = errno;
free(vec);
errno = codigo;
return -1;
}
int resultado = mincore(m, longitud, vec);
int codigo = resultado == -1 ? errno : 0;
if (resultado == 0) {
size_t limite = f->paginas < 64 ? f->paginas : 64;
for (size_t i = 0; i < f->paginas; i++) {
if (vec[i] & 1)
f->residentes++;
if (i < limite)
f->mapa[i] = (vec[i] & 1) ? 'R' : '.';
}
f->mapa[limite] = '\0';
}
if (munmap(m, longitud) == -1 && resultado == 0) {
codigo = errno;
resultado = -1;
}
free(vec);
if (resultado == -1)
errno = codigo;
return resultado;
}
static int archivo_propio(int fd, size_t *longitud)
{
struct stat s;
if (fstat(fd, &s) == -1)
return -1;
if (!S_ISREG(s.st_mode) || s.st_uid != geteuid() || s.st_size < 0 ||
(uintmax_t)s.st_size > 268435456 || (uintmax_t)s.st_size > SIZE_MAX) {
errno = EINVAL;
return -1;
}
*longitud = (size_t)s.st_size;
return 0;
}
static double intervalo(const struct timespec *a, const struct timespec *b)
{
return (double)(b->tv_sec - a->tv_sec) +
(double)(b->tv_nsec - a->tv_nsec) / 1e9;
}
static int aconsejar(int fd, int consejo)
{
int codigo = posix_fadvise(fd, 0, 0, consejo);
if (codigo != 0) {
fprintf(stderr, "posix_fadvise: %s\n", strerror(codigo));
return -1;
}
return 0;
}
#endif
Guarda la cabecera junto a los programas. Las macros que habilitan las interfaces se definirán al principio de cada archivo C, antes de incluirla. El límite de 256 MiB es deliberado para mantener acotada la práctica; no representa el máximo de Linux.
La función no conserva un mapeo entre la sugerencia y la lectura, ni bloquea páginas. Como vimos en el tema 12, mincore(2) ofrece una observación que puede quedar desactualizada. El mapa es una muestra del principio, no una descripción completa si el archivo tiene más de 64 páginas.
6. Experimento: sugerir descarte y repetir
Frio-caliente.c recibe archivo, consejo de acceso y rondas. Abre el archivo propio con lectura y escritura para realizar una sincronización inicial explícita, sin modificar su contenido lógico. Después aplica el consejo elegido y, por ronda, realiza una lectura posterior a DONTNEED y una repetición inmediata sin ese descarte.
Las etiquetas del CSV son post-dontneed y repetida, no «fría garantizada» y «caliente garantizada». La residencia antes de cada lectura permite interpretar qué ocurrió. Normal, secuencial y sin-anticipar mantienen el mismo recorrido secuencial de bytes; el último solicita POSIX_FADV_RANDOM, no genera posiciones aleatorias.
#define _GNU_SOURCE
#define _FILE_OFFSET_BITS 64
#include "cache-observada.h"
static int leer_todo(int fd, uintmax_t *bytes, uint64_t *suma, double *tiempo)
{
unsigned char buf[65536];
if (lseek(fd, 0, SEEK_SET) == (off_t)-1)
return -1;
struct timespec a, b;
if (clock_gettime(CLOCK_MONOTONIC, &a) == -1)
return -1;
*bytes = 0;
*suma = 0;
for (;;) {
ssize_t n = read(fd, buf, sizeof buf);
if (n == -1) {
if (errno == EINTR)
continue;
return -1;
}
if (n == 0)
break;
*bytes += (uintmax_t)n;
for (size_t i = 0; i < (size_t)n; i++)
*suma += buf[i];
}
if (clock_gettime(CLOCK_MONOTONIC, &b) == -1)
return -1;
*tiempo = intervalo(&a, &b);
if (*tiempo <= 0) {
errno = EIO;
return -1;
}
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 4) {
fprintf(stderr, "uso: %s ARCHIVO normal|secuencial|sin-anticipar RONDAS:1..10\n", argv[0]);
return 2;
}
int consejo;
if (strcmp(argv[2], "normal") == 0)
consejo = POSIX_FADV_NORMAL;
else if (strcmp(argv[2], "secuencial") == 0)
consejo = POSIX_FADV_SEQUENTIAL;
else if (strcmp(argv[2], "sin-anticipar") == 0)
consejo = POSIX_FADV_RANDOM;
else {
fprintf(stderr, "consejo desconocido\n");
return 2;
}
char *fin;
errno = 0;
unsigned long rondas = strtoul(argv[3], &fin, 10);
if (errno || fin == argv[3] || *fin || argv[3][0] == '-' || rondas < 1 || rondas > 10) {
fprintf(stderr, "rondas invalidas\n");
return 2;
}
int fd = open(argv[1], O_RDWR | O_NONBLOCK | O_CLOEXEC);
if (fd == -1) {
perror("open archivo propio");
return 1;
}
int estado = 1;
size_t longitud;
if (archivo_propio(fd, &longitud) == -1) {
perror("validar archivo propio de hasta 256 MiB");
goto cerrar;
}
if (fsync(fd) == -1) {
perror("fsync preparatorio");
goto cerrar;
}
if (aconsejar(fd, consejo) == -1)
goto cerrar;
puts("ronda,consejo,condicion,bytes,suma,paginas,residentes_antes,residentes_despues,preparacion_s,lectura_s");
for (unsigned long ronda = 1; ronda <= rondas; ronda++) {
for (int repetida = 0; repetida < 2; repetida++) {
double preparacion = 0;
if (!repetida) {
struct timespec a, b;
if (clock_gettime(CLOCK_MONOTONIC, &a) == -1) {
perror("reloj preparacion");
goto cerrar;
}
if (aconsejar(fd, POSIX_FADV_DONTNEED) == -1)
goto cerrar;
if (clock_gettime(CLOCK_MONOTONIC, &b) == -1) {
perror("reloj preparacion final");
goto cerrar;
}
preparacion = intervalo(&a, &b);
}
struct foto antes, despues;
if (foto_cache(fd, longitud, &antes) == -1) {
perror("residencia antes");
goto cerrar;
}
uintmax_t bytes;
uint64_t suma;
double lectura;
if (leer_todo(fd, &bytes, &suma, &lectura) == -1) {
perror("lectura medida");
goto cerrar;
}
if (bytes != longitud) {
fprintf(stderr, "la longitud leida cambio durante el ensayo\n");
goto cerrar;
}
if (foto_cache(fd, longitud, &despues) == -1) {
perror("residencia despues");
goto cerrar;
}
printf("%lu,%s,%s,%ju,%" PRIu64 ",%zu,%zu,%zu,%.9f,%.9f\n",
ronda, argv[2], repetida ? "repetida" : "post-dontneed",
bytes, suma, antes.paginas, antes.residentes,
despues.residentes, preparacion, lectura);
}
}
estado = 0;
cerrar:
if (close(fd) == -1) {
perror("close");
estado = 1;
}
if (fflush(stdout) == EOF || ferror(stdout)) {
fprintf(stderr, "error al producir CSV\n");
estado = 1;
}
return estado;
}
Qué entra en cada tiempo
Preparacion_s mide la llamada DONTNEED de esa fila; no incluye la sincronización inicial ni las consultas de residencia. Lectura_s mide el bucle que obtiene bytes y calcula su suma, con el posicionamiento anterior fuera de la ventana. Open, fsync, mincore, impresión y close tampoco forman parte de ese intervalo de lectura.
Sumar preparación y lectura sigue sin representar todo el tiempo de extremo a extremo porque hay instrumentación y preparación inicial excluidas. La separación permite estudiar componentes; no debe usarse para presentar la parte omitida como si fuera gratuita.
El búfer, la CPU y el estado dinámico de anticipación pueden calentarse durante las primeras vueltas. Las sugerencias no reinician toda la historia de la máquina. El programa comprueba errores y cantidades, no pretende que las dos condiciones solo difieran en una variable física aislada.
7. Laboratorio de lecturas observadas
Usa Bash en Linux o WSL y una carpeta nueva. Guarda cache-observada.h y frio-caliente.c antes de compilar. El archivo debe ser tuyo, no compartirse con otras tareas y permitir lectura y escritura para la preparación. No se cambian sus bytes ni se vacían cachés globales.
$ LAB13=$(mktemp -d "$HOME/tema13.XXXXXX")
$ cd "$LAB13"
$ dd if=/dev/zero of=datos.bin bs=1M count=8 status=none
$ printf 'CACHE\n' >>datos.bin
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 frio-caliente.c -o frio-caliente
$ ./frio-caliente datos.bin normal 3 >normal.csv
$ echo $?
0
$ cat normal.csv
El archivo tiene 8.388.614 bytes y suma didáctica 350. Se esperan dos filas por ronda. Los tiempos y la residencia se observan en tu equipo; no hay un valor universal ni se exige que el descarte consiga cero páginas.
| Residencia antes | Interpretación de esa instantánea | Qué hacer con la muestra |
|---|---|---|
| Cero de un total positivo. | Ausencia observada en esa caché del intervalo completo. | Puede agruparse como fría observada respecto a esta capa. |
| Entre cero y el total. | Residencia parcial. | No mezclar automáticamente con ausencia completa. |
| Igual al total positivo. | Residencia completa observada. | Puede agruparse como caliente observada, sin garantía futura. |
| Total cero. | Archivo sin páginas de contenido. | No extraer una conclusión de frío/caliente útil sobre datos inexistentes. |
Comparar consejos sin cambiar el recorrido de la aplicación
$ ./frio-caliente datos.bin secuencial 3 >secuencial.csv
$ ./frio-caliente datos.bin sin-anticipar 3 >sin-anticipar.csv
Sin-anticipar permite estudiar qué ocurre al declarar RANDOM mientras la aplicación sigue avanzando en bloques. No representa una carga aleatoria real. Una comparación de patrones necesitaría cambiar también el orden de offsets y controlar las regiones visitadas.
Que una sugerencia no produzca una mejora visible puede ser coherente: la entrada ya estaba residente, el archivo es pequeño, la capa de almacenamiento no domina o la política no tiene el efecto esperado en ese formato. Guarda la evidencia antes de elegir una explicación.
8. Repetir y agrupar por estado real observado
El script ensayo-cache.sh hace cinco series y alterna el orden de los tres consejos. Cada invocación produce una pareja post-dontneed/repetida. Una serie se guarda como campo adicional, sin ocultar errores del programa detrás de awk.
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
if (( $# != 1 )); then
printf 'Uso: %s ARCHIVO_PROPIO\n' "$0" >&2
exit 2
fi
printf 'serie,ronda,consejo,condicion,bytes,suma,paginas,residentes_antes,residentes_despues,preparacion_s,lectura_s\n'
for serie in 1 2 3 4 5; do
if (( serie % 2 == 1 )); then
consejos=(normal secuencial sin-anticipar)
else
consejos=(sin-anticipar secuencial normal)
fi
for consejo in "${consejos[@]}"; do
./frio-caliente "$1" "$consejo" 1 |
awk -v serie="$serie" 'NR > 1 { print serie "," $0 }'
done
done
Para resumir, separaremos consejo, condición y residencia inicial observada. No calcularemos un cociente frío/caliente a partir de grupos que quizá nunca alcanzaron ausencia o residencia completa.
import csv
import math
import statistics
import sys
if len(sys.argv) != 2:
raise SystemExit("uso: python3 resumir-cache.py ENSAYO.csv")
grupos = {}
vistas = set()
contenidos = set()
consejos = {"normal", "secuencial", "sin-anticipar"}
condiciones = {"post-dontneed", "repetida"}
try:
with open(sys.argv[1], newline="", encoding="utf-8") as archivo:
for fila in csv.DictReader(archivo):
serie = int(fila["serie"])
ronda = int(fila["ronda"])
consejo, condicion = fila["consejo"], fila["condicion"]
paginas = int(fila["paginas"])
antes = int(fila["residentes_antes"])
despues = int(fila["residentes_despues"])
cantidad, suma = int(fila["bytes"]), int(fila["suma"])
tiempo, preparacion = float(fila["lectura_s"]), float(fila["preparacion_s"])
clave = (serie, consejo, condicion)
if (serie not in range(1, 6) or ronda != 1 or consejo not in consejos
or condicion not in condiciones or clave in vistas
or paginas < 0 or not 0 <= antes <= paginas
or not 0 <= despues <= paginas or cantidad < 0 or suma < 0
or not math.isfinite(tiempo) or tiempo <= 0
or not math.isfinite(preparacion) or preparacion < 0):
raise ValueError("fila invalida o duplicada")
vistas.add(clave)
contenidos.add((cantidad, suma, paginas))
estado = "sin-datos" if paginas == 0 else "ausente" if antes == 0 else "completa" if antes == paginas else "parcial"
grupos.setdefault((consejo, condicion, estado), []).append(tiempo)
esperadas = {(s, c, p) for s in range(1, 6) for c in consejos for p in condiciones}
if vistas != esperadas or len(contenidos) != 1:
raise ValueError("faltan muestras o difieren los datos logicos")
except (OSError, ValueError, KeyError, TypeError) as error:
raise SystemExit(f"CSV no valido o incompleto: {error}")
print("consejo condicion residencia_antes n mediana_ms minimo_ms maximo_ms")
for clave, tiempos in sorted(grupos.items()):
print(*clave, len(tiempos), f"{statistics.median(tiempos)*1000:.3f}",
f"{min(tiempos)*1000:.3f}", f"{max(tiempos)*1000:.3f}")
$ bash -n ensayo-cache.sh
$ bash ensayo-cache.sh datos.bin >ensayo.csv
$ echo $?
0
$ python3 resumir-cache.py ensayo.csv
El CSV completo tiene treinta filas de datos. Un grupo puede tener menos de cinco porque algunas series tuvieron otro estado de residencia. Ese hecho no se oculta promediando todo bajo la misma palabra.
La clasificación se refiere al archivo completo en la instantánea. La misma cantidad de páginas parciales puede distribuirse en regiones diferentes; el resumen no conserva esa geometría. Para investigar anticipación de un prefijo usaremos también mapas de páginas.
9. Anticipación automática y explícita
La anticipación automática intenta deducir qué regiones se necesitarán a partir del acceso observado y de las políticas de la apertura. Puede reducir esperas si trae datos antes de que el programa los pida. Si adivina mal, genera trabajo y ocupa memoria que quizá no se aproveche.
El tamaño de petición no es el tamaño de anticipación
Un read de un byte puede desencadenar obtención de regiones mayores. Un read de 64 KiB tampoco obliga a que la unidad física, el formato o la ventana de readahead coincidan exactamente con 64 KiB. Los contadores de llamadas y la residencia observada responden preguntas distintas.
WILLNEED: anunciar un intervalo próximo
WILLNEED puede iniciar obtención de una región y retornar sin esperar a que esté completamente disponible. El sistema puede limitar cuánto prepara. Consultar inmediatamente residencia puede encontrar parte todavía ausente o en proceso de llegada; una lectura posterior puede tener que esperar.
Puede servir cuando la aplicación conoce su siguiente región y tiene trabajo útil que realizar entre la sugerencia y el consumo. Insertar un sleep solo para hacer que la siguiente lectura parezca rápida no elimina el tiempo de espera de la tarea completa.
Readahead: una interfaz específica de Linux
#define _GNU_SOURCE
#include <fcntl.h>
ssize_t readahead(int fd, off_t offset, size_t count);
if (readahead(fd, 0, ventana) == -1) {
perror("readahead");
}
Readahead devuelve 0 si la llamada tiene éxito o -1 con errno si falla; no devuelve la cantidad de bytes ya precargados. No cambia el cursor y no solicita datos después del final. Aunque busca iniciar trabajo anticipado, puede bloquear en tareas como obtener metadatos, por lo que no debe prometerse latencia nula.
El manual de readahead(2) describe esos efectos y límites. La comparación del programa siguiente conserva las diferencias de contrato entre readahead y posix_fadvise en sus comprobaciones.
10. Experimento: una sonda de readahead
Sonda-readahead.c muestra tres fotos: después de sugerir descarte, después de la preparación seleccionada y después de leer un byte del principio mediante pread. Se consulta el archivo completo, pero el mapa visible muestra solo sus primeras 64 páginas.
Los modos normal y random comparan el consejo para la anticipación automática sin precarga explícita. Willneed y readahead solicitan una ventana inicial de hasta el número de páginas indicado. Cada ejecución sincroniza y sugiere descarte antes de observar, y puede encontrar residencia conservada igualmente.
#define _GNU_SOURCE
#define _FILE_OFFSET_BITS 64
#include "cache-observada.h"
static int mostrar(int fd, size_t longitud, const char *etapa)
{
struct foto f;
if (foto_cache(fd, longitud, &f) == -1)
return -1;
printf("%s: residentes=%zu/%zu mapa=%s\n",
etapa, f.residentes, f.paginas, f.mapa);
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 4 || (strcmp(argv[2], "normal") != 0 &&
strcmp(argv[2], "random") != 0 && strcmp(argv[2], "willneed") != 0 &&
strcmp(argv[2], "readahead") != 0)) {
fprintf(stderr, "uso: %s ARCHIVO normal|random|willneed|readahead PAGINAS:1..1024\n", argv[0]);
return 2;
}
char *fin;
errno = 0;
unsigned long paginas = strtoul(argv[3], &fin, 10);
if (errno || fin == argv[3] || *fin || argv[3][0] == '-' || paginas < 1 || paginas > 1024) {
fprintf(stderr, "numero de paginas invalido\n");
return 2;
}
int fd = open(argv[1], O_RDWR | O_NONBLOCK | O_CLOEXEC);
if (fd == -1) {
perror("open archivo propio");
return 1;
}
int estado = 1;
size_t longitud;
if (archivo_propio(fd, &longitud) == -1) {
perror("validar archivo propio de hasta 256 MiB");
goto cerrar;
}
long pagina = sysconf(_SC_PAGESIZE);
if (pagina <= 0 || paginas > SIZE_MAX / (size_t)pagina) {
fprintf(stderr, "tamano de ventana no representable\n");
goto cerrar;
}
size_t ventana = (size_t)paginas * (size_t)pagina;
if (ventana > longitud)
ventana = longitud;
if (fsync(fd) == -1) {
perror("fsync preparatorio");
goto cerrar;
}
int consejo = strcmp(argv[2], "random") == 0 ? POSIX_FADV_RANDOM : POSIX_FADV_NORMAL;
if (aconsejar(fd, consejo) == -1 || aconsejar(fd, POSIX_FADV_DONTNEED) == -1)
goto cerrar;
if (mostrar(fd, longitud, "tras DONTNEED") == -1) {
perror("foto inicial");
goto cerrar;
}
struct timespec a, b, c, d;
if (clock_gettime(CLOCK_MONOTONIC, &a) == -1) {
perror("reloj inicial");
goto cerrar;
}
if (ventana > 0 && strcmp(argv[2], "willneed") == 0) {
int codigo = posix_fadvise(fd, 0, (off_t)ventana, POSIX_FADV_WILLNEED);
if (codigo != 0) {
fprintf(stderr, "WILLNEED: %s\n", strerror(codigo));
goto cerrar;
}
} else if (ventana > 0 && strcmp(argv[2], "readahead") == 0) {
if (readahead(fd, 0, ventana) == -1) {
perror("readahead");
goto cerrar;
}
}
if (clock_gettime(CLOCK_MONOTONIC, &b) == -1) {
perror("reloj tras preparacion");
goto cerrar;
}
if (mostrar(fd, longitud, "tras preparacion") == -1) {
perror("foto tras preparacion");
goto cerrar;
}
if (clock_gettime(CLOCK_MONOTONIC, &c) == -1) {
perror("reloj sonda");
goto cerrar;
}
unsigned char dato;
ssize_t n;
do {
n = pread(fd, &dato, 1, 0);
} while (n == -1 && errno == EINTR);
if (n == -1) {
perror("pread sonda");
goto cerrar;
}
if (clock_gettime(CLOCK_MONOTONIC, &d) == -1) {
perror("reloj tras sonda");
goto cerrar;
}
printf("sonda: %zd bytes", n);
if (n == 1)
printf(" valor=%02x", (unsigned int)dato);
putchar('\n');
if (mostrar(fd, longitud, "tras sonda") == -1) {
perror("foto tras sonda");
goto cerrar;
}
printf("ventana solicitada=%zu bytes preparacion_s=%.9f sonda_s=%.9f\n",
ventana, intervalo(&a, &b), intervalo(&c, &d));
printf("intervalo conjunto con observacion intermedia_s=%.9f\n", intervalo(&a, &d));
estado = 0;
cerrar:
if (close(fd) == -1) {
perror("close");
estado = 1;
}
if (fflush(stdout) == EOF || ferror(stdout)) {
fprintf(stderr, "error al producir informe\n");
estado = 1;
}
return estado;
}
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 sonda-readahead.c -o sonda-readahead
$ ./sonda-readahead datos.bin normal 32
$ ./sonda-readahead datos.bin random 32
$ ./sonda-readahead datos.bin willneed 32
$ ./sonda-readahead datos.bin readahead 32
Con páginas de 4096 bytes, 32 páginas expresan hasta 128 KiB desde el comienzo. El programa recorta la ventana al tamaño del archivo y no solicita trabajo después del final. En un archivo vacío omite la precarga de longitud cero, y la sonda informa EOF sin inventar un byte.
Si el mapa pasa de puntos a R antes de consumir cada byte, se observó residencia que la aplicación no obtuvo mediante una lectura propia de cada página. Si en modo normal aparecen páginas después de la sonda de un byte, es compatible con anticipación automática. Random no elimina páginas que ya estaban presentes y la implementación o el formato puede limitar el contraste.
Las fotos intermedias y la impresión también dejan transcurrir tiempo mientras una preparación puede estar en curso. El informe conjunto incluye esa observación intermedia y no es una lectura pura de archivo completo. Esta sonda se usa para investigar comportamiento, no para declarar un ganador por la mínima cifra aislada.
11. Dónde quedó el coste de preparar
La anticipación intenta hacer trabajo antes de necesitar su resultado y, cuando sea posible, solaparlo con otra actividad útil. No vuelve inexistentes las transferencias. Una lectura muy breve después de una espera larga puede ser una mejora de latencia en ese punto y no una mejora del tiempo completo.
tiempo de lectura posterior:
preparar → esperar / trabajar → [ leer ]
tiempo de extremo a extremo:
[ preparar → esperar / trabajar → leer ]
Si durante la preparación el programa realiza cálculo que igualmente necesitaba, un solapamiento útil puede reducir esperas totales. Si solo añade un retardo para calentar la caché, el usuario sigue pagando ese retardo. Un benchmark debe decir qué ventana está mostrando.
Anticipar demasiado puede estorbar
Si se precarga una región que no se usará, se consume ancho de banda y capacidad de caché. Las páginas útiles de otra carga o de otra región pueden ser expulsadas. Un patrón aleatorio o una carga con poco reúso puede necesitar una política diferente de un lector secuencial sostenido.
El coste también depende de la distancia temporal: pedir demasiado tarde no evita la espera; pedir demasiado pronto puede permitir que los datos se desalojen antes de usarlos. No existe un número de páginas anticipadas universalmente óptimo a partir del tamaño de sector o de una sola medición.
No confundir consejo con programación de E/S completa
WILLNEED y readahead no entregan una promesa de completado por página que la aplicación pueda tratar como un futuro garantizado. Para control de solicitudes y terminación hay otras interfaces, con sus propios contratos. Aquí investigamos sugerencias y presencia observada.
12. Interpretar resultados y evitar falsas garantías
| Observación | Qué permite decir | Qué falta para otra afirmación |
|---|---|---|
| DONTNEED devuelve 0. | La llamada se aceptó. | Consultar residencia para saber qué quedó presente. |
| Residencia completa tras WILLNEED. | La foto encontró presentes esas páginas. | No garantiza su permanencia ni un tiempo total mejor. |
| Residencia parcial tras una sonda. | Parte del archivo está presente. | El mapa o un análisis por región para saber dónde. |
| Lectura posterior más rápida. | Disminuyó esa ventana en esas condiciones. | Incluir preparación y trabajo útil para comparar tareas completas. |
| RANDOM no mejora la prueba. | Ese consejo no produjo una mejora medida allí. | No concluye que sea inútil en todos los patrones. |
| Archivo sin páginas presentes. | Ausencia observada en esa caché. | No prueba ausencia en el dispositivo, servidor o anfitrión. |
Los sistemas de archivos pueden ignorar o aplicar parcialmente consejos, y los accesos compartidos pueden cambiar la residencia entre pasos. En WSL, contenedores o máquinas virtuales aparecen otras capas; no describas una prueba local como un control completo del almacenamiento físico.
Separa además la capa del consejo: posix_fadvise se aplica a una apertura y regiones de archivo; madvise y posix_madvise trabajan sobre regiones de memoria mapeada y no son reemplazos textuales. Sus efectos y contratos deben leerse de forma independiente.
Qué conclusión práctica conservar
Una buena comparación conserva las condiciones y los fallos, clasifica por evidencia y reconoce incertidumbre. Si no lograste muestras ausentes, puedes estudiar lecturas con residencia parcial o completa, pero no renombrarlas para sostener una diferencia frío/caliente que no observaste.
Los capítulos siguientes tratarán escrituras sucias y sincronización. No mezcles «se pudo descartar una página limpia» con «se garantizó una actualización durable del archivo y de su nombre».
13. Simulador de anticipación y uso de caché
El modelo tiene un archivo de doce páginas y una caché pequeña. La aplicación sigue un patrón elegido. El consejo modifica solo las solicitudes automáticas futuras: normal anticipa una página al reconocer continuidad; secuencial pide tres siguientes; sin automática no pide vecinas. Son reglas didácticas, no el algoritmo real de Linux.
Solicitar no equivale a tener listo
| Página | Estado | Anticipada aún sin usar | Lecturas de aplicación | Último uso en modelo |
|---|
Historial de operaciones
Una página solicitada por anticipación todavía no cuenta como acierto residente. Si se demanda mientras está pendiente, el modelo completa esa misma transferencia y registra espera por preparación pendiente, sin duplicar la solicitud. Si ya llegó y se usa, registra anticipación aprovechada.
Las transferencias de respaldo se cuentan al iniciarse, incluidas las anticipadas. No se convierten en tiempo real ni se afirma un número de operaciones físicas de disco. Completar una anticipación puede desplazar una página con la regla LRU del modelo y puede desperdiciar una página anticipada que nadie consumió.
Todas las páginas son limpias; DONTNEED acepta el descarte de las residentes de esa ventana en el modelo, sin borrar datos del archivo. Las solicitudes que ya estaban pendientes no se cancelan aquí y pueden volver a poblar la región. En Linux real, el resultado de una sugerencia se observa y no se deduce de esta regla didáctica. Cambiar el consejo no borra caché ni solicitudes anteriores.
Secuencias para comprobar
- Pide WILLNEED de 0 a 3. Observa que son pendientes, no residentes. Lee 0 de inmediato y comprueba que hubo que esperar esa preparación.
- Reinicia, pide la misma ventana y completa sus cuatro solicitudes antes de leer. Con capacidad cuatro, la demanda de esas páginas puede encontrar residencia ya lista.
- Usa saltos con consejo secuencial y completa anticipaciones. Observa si se cargan vecinas que no son las siguientes que pedirá la aplicación.
- Con el patrón de reúso y sin anticipación automática, compara capacidad dos y cuatro. El conjunto de trabajo y el reemplazo importan además del consejo.
- Después de llenar una ventana, sugiere DONTNEED y repítela. El archivo permanece, pero la caché del modelo cambió.
- Cambia el consejo de secuencial a sin automática. Las solicitudes ya iniciadas siguen visibles; el cambio afecta a las futuras decisiones.
14. Resumen
- Frío y caliente deben referirse a una capa, un intervalo y un estado observado, no solo al número de pasada.
- Posix_fadvise devuelve un código de error directamente y no cambia la posición como lseek.
- SEQUENTIAL y RANDOM expresan expectativas; no reordenan las lecturas de la aplicación ni borran caché existente.
- DONTNEED intenta descartar contenido cacheado bajo condiciones; no elimina el archivo ni puede perder cambios sucios sin más.
- Sincronizar y sugerir descarte son operaciones distintas, y su tiempo no debe desaparecer de la descripción del experimento.
- Mincore permite clasificar residencia inicial ausente, parcial o completa con límites temporales.
- WILLNEED y readahead solicitan preparación; un retorno correcto no garantiza que toda la ventana ya esté lista.
- Readahead es específica de Linux, no mueve el cursor y su retorno no es una cantidad de bytes precargados.
- Una lectura posterior rápida puede esconder trabajo anterior si solo se mide la última ventana.
- La anticipación útil depende del patrón, distancia temporal, capacidad de caché y competencia de recursos.
- Los resultados se agrupan por condiciones comprobadas y no se extrapolan a todas las cachés del equipo.
15. Preguntas de repaso y práctica
Antes de desplegar cada respuesta, separa petición, preparación, residencia y consumo de datos.
1. ¿La primera lectura de un archivo recién escrito es necesariamente fría?
No. La creación puede haber dejado páginas en RAM y otros procesos también pueden haber accedido. Hace falta observar el estado y definir qué capa se estudia.
2. ¿Qué demuestra que DONTNEED devuelva 0?
Que la llamada se aceptó, no que el archivo completo haya quedado ausente. El descarte tiene condiciones y el contenido puede volver a cargarse; se consulta residencia después.
3. ¿Cómo se informa un error de posix_fadvise?
Mediante un código positivo devuelto por la función. Se interpreta con strerror(codigo), sin depender de un errno antiguo ni esperar siempre -1.
4. ¿DONTNEED elimina el archivo o sus bytes?
No. Expresa poco interés en conservar contenido cacheado de una región. El objeto y sus datos siguen teniendo su respaldo y pueden volver a obtenerse.
5. ¿Por qué se sincroniza antes del intento de descarte del laboratorio?
Porque páginas sucias pueden conservarse para no perder cambios pendientes. La sincronización preparatoria se comprueba y se mantiene fuera de la ventana de lectura, declarando ese alcance.
6. ¿Una página cubierta solo parcialmente debe desaparecer necesariamente?
No. Las reglas de descarte pueden conservar páginas parciales. La alineación y el final del archivo deben contemplarse y el resultado debe observarse.
7. ¿Sin-anticipar hace lecturas en posiciones aleatorias?
No en nuestro programa. Solicita POSIX_FADV_RANDOM, pero el bucle sigue recorriendo los bytes secuencialmente. Cambiar un consejo no cambia el algoritmo de la aplicación.
8. ¿RANDOM borra las páginas que otro proceso ya cargó?
No por su función de desactivar anticipación automática. Las páginas residentes pertenecen a una caché compartida y no desaparecen solo por cambiar esa expectativa.
9. ¿Qué implica WILLNEED correcto respecto a la disponibilidad inmediata?
Puede iniciar preparación, pero no garantiza que toda la región haya terminado de llegar. Una lectura posterior puede encontrar parte residente y tener que esperar por otra.
10. ¿Readahead devuelve el número de bytes ya cargados?
No. Devuelve 0 ante éxito de la llamada o -1 con errno ante error. No es un contador de precarga completada y no modifica la posición actual.
11. ¿Readahead garantiza no bloquear en ningún trabajo?
No. Puede necesitar trabajo de metadatos u otras condiciones que impliquen espera. No debe tratarse como una operación de latencia siempre nula.
12. ¿Una foto sin páginas presentes prueba que el SSD no tiene datos en su propia caché?
No. La observación pertenece a la caché de páginas del intervalo mapeado y no controla todos los niveles de almacenamiento o virtualización.
13. ¿Por qué agrupar muestras por residencia antes de comparar tiempos?
Porque las etiquetas de preparación pueden producir estados diferentes. Mezclar ausencia, residencia parcial y completa puede atribuir al consejo una diferencia causada por el estado inicial.
14. ¿Mincore después de WILLNEED es una observación sin efecto temporal?
No. Consultar, mapear e imprimir consume tiempo mientras puede avanzar trabajo de preparación. La sonda declara esa instrumentación y no la presenta como una lectura pura.
15. ¿Una lectura rápida después de esperar demuestra una tarea completa más rápida?
No necesariamente. Hay que incluir el coste de preparación y espera, y distinguir si hubo otro trabajo útil solapado o solo se desplazó el tiempo fuera del cronómetro.
16. ¿Por qué anticipar muchas páginas puede empeorar una carga?
Puede iniciar transferencias que no se usarán y ocupar caché desplazando datos útiles. La ventana adecuada depende del patrón, capacidad y momento de consumo.
17. ¿Dos consultas con el mismo porcentaje parcial garantizan el mismo estado útil?
No. Pueden tener presentes páginas distintas. La distribución respecto a la próxima región demandada importa, no solo la suma.
18. ¿Cambiar el consejo reinicia toda la historia de caché y anticipación?
No. Puede modificar decisiones futuras de una apertura, pero no implica borrar residencia, cancelar todo trabajo iniciado o congelar las otras cargas del sistema.
Para practicar
- Ejecuta frio-caliente en los tres consejos y clasifica cada fila por residencia inicial, sin llamar fría a una muestra que tenía páginas presentes.
- Completa ensayo-cache.sh y comprueba sus treinta filas. Usa resumir-cache.py y explica por qué puede haber más grupos que combinaciones de consejo y condición.
- Elimina una fila de una copia del CSV y duplica otra en una segunda copia. Verifica que el analizador detecta el conjunto incompleto o repetido.
- Prueba los programas con un archivo vacío, uno menor que una página y uno con última página parcial. Distingue límites de residencia y resultados de EOF.
- Ejecuta la sonda normal y random con ventanas de 1, 8 y 32 páginas. Explica qué parámetro solo se usa para precarga explícita y qué patrón mantiene la lectura real.
- Compara las fotos tras WILLNEED y readahead antes de la sonda de un byte. No supongas residencia completa por el éxito de la llamada.
- Anota preparación_s, sonda_s y el intervalo conjunto con observación. Explica por qué no representan ventanas idénticas ni una lectura de todo el archivo.
- Revisa las comprobaciones de error de posix_fadvise y readahead. Identifica por qué intercambiar sus estilos produciría diagnósticos equivocados.
- En el simulador pide una ventana y consúmela sin completar antes la anticipación. Compara esperas y solicitudes con el caso en que se completó previamente.
- Combina patrón de saltos, capacidad pequeña y consejo secuencial. Observa transferencias que no se aprovechan y compáralas con sin automática.
- Diseña una tarea que tenga cálculo útil entre anunciar una región y leerla. Define qué tiempo medirías para demostrar un solapamiento beneficioso sin esconder preparación.
En el tema 14 estudiaremos escritura diferida y páginas sucias: cómo se acumula el trabajo pendiente, cuándo intervienen los hilos de writeback y por qué un escritor puede terminar esperando aunque use E/S bufferizada.