1. Qué conserva la caché de páginas
En el tema 11 recorrimos archivos y enviamos su contenido a /dev/null. Una pregunta quedó abierta: ¿cuántos bytes tuvieron que llegar realmente desde el almacenamiento durante cada pasada? Read comunica bytes entregados al programa, pero no exige una lectura física nueva del dispositivo por cada llamada.
La caché de páginas es memoria gestionada por el núcleo que conserva contenido de archivos para atender accesos posteriores y organizar la E/S bufferizada. En el caso ordinario de un archivo regular, sus regiones en memoria se asocian al objeto del archivo y a sus índices lógicos, no al nombre de una variable ni a una apertura particular.
Dos procesos que abren el mismo objeto pueden aprovechar páginas comunes aunque tengan descriptores y posiciones independientes. Si uno termina, no es obligatorio expulsar de RAM todos los datos que utilizó: el núcleo puede conservarlos para otro acceso y recuperar ese espacio cuando resulte necesario.
Qué significa «página»
La unidad básica de memoria depende de la arquitectura y del núcleo. En muchos equipos Linux es de 4096 bytes, pero no debe suponerse para todo sistema. Puedes consultarla con getconf PAGESIZE o, desde C, sysconf(_SC_PAGESIZE). El núcleo también puede gestionar agrupaciones mayores mediante estructuras como folios.
El tamaño de página no es necesariamente el tamaño del sector, el bloque del sistema de archivos o el búfer de tu programa. Leer 100 bytes puede requerir disponer de una región de memoria mayor. El sistema de archivos decide cómo obtener y representar el contenido de esa región.
Una capa distinta de stdio
fgetc / fread
│ buffer de FILE* en el proceso, si existe
▼
read / pread
│ cache de paginas del archivo en el nucleo
▼
sistema de archivos y capa de E/S
│ caches y comportamiento del dispositivo o del servidor
▼
respaldo del contenido
Una llamada fgetc puede resolverse en el búfer de usuario sin invocar read. Una llamada read puede resolverse en la caché de páginas sin obtener de nuevo los datos del dispositivo. Desactivar un búfer de stdio no elimina el segundo nivel.
La documentación del núcleo ofrece una descripción de los conceptos de memoria y caché de páginas. Aquí trabajaremos con archivos regulares y E/S bufferizada; sistemas de red, acceso directo y otros tipos de memoria añaden condiciones que iremos distinguiendo.
2. El recorrido de una lectura
Imagina un archivo dividido lógicamente en páginas 0, 1, 2… y una petición que necesita parte de la página 3. El núcleo comprueba si dispone del contenido necesario en memoria y coordina su obtención cuando falta o todavía no está listo.
- Localizar la región lógica. Se utiliza el objeto abierto y la posición de la operación, sin volver a interpretar una ruta completa en cada read.
- Comprobar su estado en memoria. Si el contenido requerido está disponible, se puede atender desde RAM.
- Obtener lo que falte. El sistema de archivos y la capa de E/S realizan el trabajo necesario; puede haber espera y lectura anticipada de otras regiones.
- Entregar los bytes solicitados. Read copia a la región del proceso y devuelve la cantidad válida, con las reglas de parciales y errores ya estudiadas.
Un acierto de caché evita parte del recorrido de almacenamiento, pero no hace gratuita la llamada: siguen existiendo comprobaciones, movimiento de bytes y trabajo de CPU. Si solo una parte de la petición está disponible, la operación puede combinar distintas situaciones.
Lectura anticipada
Al detectar ciertos patrones, Linux puede traer páginas antes de que la aplicación las pida explícitamente. Si consultas residencia después de leer una región pequeña, podrías encontrar otras páginas presentes. Eso no demuestra que el programa haya solicitado cada una mediante su propia llamada.
En el tema 13 mediremos lectura en frío, en caliente y anticipación con más detalle. En este tema basta con reconocer que un read lógico y una transferencia de almacenamiento no tienen una correspondencia obligatoria de uno a uno.
Un hueco puede devolver ceros sin leer bloques de datos
En un archivo disperso, una región lógica no asignada se lee como ceros. El sistema puede producirlos sin recuperar un bloque físico lleno de ceros por cada región. Por eso una tasa de lectura elevada no demuestra por sí sola que la unidad haya suministrado a esa velocidad todos los bytes lógicos.
Una segunda pasada puede resultar más rápida, igual o incluso más lenta por otras condiciones. La idea de caché permite formular una explicación, pero hay que contrastarla con observaciones y con los límites de cada contador.
3. Páginas limpias, sucias y writeback
La E/S bufferizada también utiliza memoria al escribir. En el caso ordinario, los datos aceptados actualizan el contenido en caché y se marca que hace falta llevar cambios al respaldo. No se exige que cada write espere todo el recorrido físico antes de retornar.
| Estado conceptual | Qué significa | Qué no implica |
|---|---|---|
| Ausente. | El modelo de caché no conserva esa página en RAM. | Que el archivo no exista o haya perdido sus bytes. |
| Limpia. | No hay modificaciones pendientes de escritura en esa página según la contabilidad correspondiente. | Una garantía completa de persistencia de todo el archivo y sus metadatos. |
| Sucia, dirty. | Hay cambios en memoria que requieren escritura al respaldo. | Que el contenido esté corrupto o sea inválido para leer. |
| En writeback. | Se está realizando una escritura de contenido al respaldo. | Que nuevas escrituras de la aplicación hayan dejado de ser posibles. |
Un lector de la misma región puede observar la versión actualizada en caché aunque aún haya trabajo pendiente de respaldo. La caché no se limita a guardar copias de lectura: puede contener la versión más reciente que el sistema está usando.
No se descarta una página sucia como una limpia
Una página limpia puede recuperarse para otro uso si no hay condiciones que lo impidan, porque su contenido se puede volver a obtener. Descartar sin más la única versión modificada de una página sucia perdería esos cambios. La recuperación puede necesitar escribirlos o esperar una operación en curso.
Linux decide cuándo realizar writeback en segundo plano y cuándo presionar o ralentizar a escritores. Esas políticas se desarrollan en el tema 14. No hay un temporizador universal que permita afirmar que «todo write llega al dispositivo exactamente después de tantos segundos».
Una página puede modificarse de nuevo mientras se escribe
Si comenzó una escritura de la versión A y después la aplicación produce B, terminar el trabajo de A no demuestra que B ya esté respaldada. El sistema debe conservar que hay cambios nuevos pendientes. El simulador separará la versión tomada al iniciar writeback de la versión actual en memoria.
Fflush, close, writeback y fsync pertenecen a contratos distintos. No usaremos una página marcada limpia como prueba de que se publicaron durablemente su nombre, metadatos y todas las dependencias del archivo. La sincronización explícita se estudia en el tema 15.
4. Por qué memoria libre y disponible no son iguales
Un sistema puede aprovechar RAM que de otro modo quedaría sin uso para conservar datos de archivos. Que MemFree sea pequeño no demuestra por sí solo que toda la memoria esté comprometida de manera irrecuperable. Parte de lo usado puede reclamarse cuando aparezcan otras necesidades.
MemFree describe memoria actualmente libre. MemAvailable es una estimación de la que podría usarse para nuevas aplicaciones sin recurrir a intercambio, teniendo en cuenta memoria recuperable y reservas. No es simplemente el mismo número con otra etiqueta.
La estimación contempla que no todo es recuperable de inmediato y que el sistema necesita mantener recursos para funcionar. No debes sumar sin más todas las columnas visibles para reconstruir una cantidad de «RAM garantizada». Algunas categorías se relacionan o solapan y las reservas importan.
Caché no equivale automáticamente a fuga
Después de leer archivos, Cached puede crecer y permanecer alto aunque el lector haya terminado. Eso es compatible con un aprovechamiento normal de RAM compartida. Para investigar un problema hay que mirar disponibilidad, evolución, presión, actividad y el tipo de memoria, no solo que una categoría haya aumentado.
Tampoco significa que cualquier uso elevado sea inocuo. Si el conjunto de trabajo supera la memoria que puede sostenerse, hay recargas, recuperación y esperas. El rendimiento puede degradarse aunque la lógica de caché sea correcta.
Archivo y memoria anónima no son categorías intercambiables
Los datos de un archivo regular con respaldo, los arreglos privados de una aplicación, tmpfs y memoria compartida tienen relaciones y reglas distintas. El intercambio de memoria anónima no es simplemente «guardar cualquier caché de archivo en swap». Una página limpia de archivo puede volver a obtenerse de su respaldo; una modificación privada puede necesitar otro tratamiento.
Las herramientas globales resumen todo el sistema. En contenedores o máquinas virtuales, el ámbito observado y sus límites pueden no coincidir con toda la memoria del anfitrión. En WSL estás observando la vista de Linux, no un desglose completo de la RAM de Windows.
5. Observar free y /proc/meminfo
$ free -h
$ awk '/^(MemTotal|MemFree|MemAvailable|Buffers|Cached|Dirty|Writeback|SReclaimable|Shmem):/' /proc/meminfo
$ getconf PAGESIZE
4096
El valor de página es ilustrativo. Los importes de memoria dependen de tu equipo y cambian continuamente. Free presenta una síntesis cuyas fórmulas y formato pueden variar con la versión; /proc/meminfo expone categorías que permiten razonar sobre esa síntesis.
| Campo | Qué observar | Precaución |
|---|---|---|
MemTotal | RAM utilizable que informa el núcleo. | No tiene por qué coincidir con toda la memoria física instalada en el anfitrión. |
MemFree | Memoria actualmente sin usar. | No incluye por sí sola todo lo recuperable. |
MemAvailable | Estimación para nuevas cargas sin swap. | No es una promesa de disponibilidad instantánea para cualquier asignación. |
Cached | Memoria cacheada de archivos; su contabilidad también se relaciona con tmpfs y shmem. | No atribuir todo el campo a los bytes de un archivo concreto. |
Buffers | Contabilidad de búferes del núcleo para ciertos usos de bloques. | No es el arreglo que pasaste a read ni el búfer FILE*. |
Dirty | Memoria con cambios pendientes de escritura. | Es global; no identifica qué archivo tiene esos cambios. |
Writeback | Memoria en proceso de escritura. | Un cero en una instantánea no demuestra ausencia de toda actividad de escritura. |
SReclaimable | Parte del slab potencialmente recuperable. | No es simplemente contenido de archivos que pueda sumarse sin condiciones. |
Shmem | Memoria compartida y usos relacionados como tmpfs. | Ayuda a interpretar categorías; no es un importe extra independiente de todo lo demás. |
La documentación de /proc del núcleo describe estos campos. Los valores etiquetados kB en meminfo se interpretan en unidades de 1024 bytes; al dividir por 1024 obtenemos MiB. No mezcles esa convención con prefijos decimales de otros informes.
Una instantánea global no identifica al causante
Si Cached sube 10 MiB mientras lees un archivo de 10 MiB, la coincidencia no demuestra atribución exacta. Otros procesos pueden usar archivos, el sistema puede anticipar regiones o recuperar otras páginas y los metadatos tienen su propia contabilidad. Incluso un cambio neto cero puede esconder entradas y salidas simultáneas.
Para observar un archivo concreto necesitamos otra vista. Antes registraremos los campos globales de forma uniforme para no perder el contexto de las pruebas.
6. Un registro de memoria en Bash
Guarda foto-memoria.sh. El script solo lee contadores y presenta una etiqueta que indica cuándo se tomó la observación. No cambia configuraciones, no vacía cachés y no necesita permisos de administrador.
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
if (( $# > 1 )); then
printf 'Uso: %s [ETIQUETA]\n' "$0" >&2
exit 2
fi
printf '\nMemoria: %s\n' "${1:-instantanea}"
awk '
/^(MemTotal|MemFree|MemAvailable|Buffers|Cached|Dirty|Writeback|SReclaimable|Shmem):/ {
printf "%-15s %12s KiB %10.2f MiB\n", $1, $2, $2 / 1024
}
' /proc/meminfo
Es un informe de categorías, no una fórmula para sumarlas todas. La lectura de los contadores y el resto de operaciones del sistema no forman una transacción congelada. Las comparaciones deben conservar esa limitación temporal.
$ LAB12=$(mktemp -d "$HOME/tema12.XXXXXX")
$ cd "$LAB12"
$ bash -n foto-memoria.sh
$ bash foto-memoria.sh antes
$ dd if=/dev/zero of=datos.bin bs=1M count=16 status=none
$ printf 'CACHE\n' >>datos.bin
$ bash foto-memoria.sh despues-de-crear
Guarda el script en LAB12 antes de ejecutarlo y mantén la terminal para conservar esa variable. El archivo tiene 16.777.222 bytes. Acabamos de escribirlo: no hay razón para suponer que su primera lectura será fría. La propia creación puede haber dejado gran parte de su contenido en caché.
Se usan 16 MiB para mantener acotada la práctica. Un cambio global de ese tamaño puede ser poco visible en un sistema con mucha actividad y memoria; no hace falta crear archivos enormes para demostrar la distinción conceptual.
7. Observar un archivo con mincore
Mincore informa de residencia de páginas de un intervalo de memoria mapeado. Para observar un archivo regular, podemos crear un mapeo de lectura sin tocar sus datos y consultar el vector de residencia. El mapeo por sí solo, sin población solicitada ni accesos al contenido, no equivale a leer todo el archivo.
El programa residentes.c está orientado a Linux. Acepta archivos regulares de hasta 1 GiB que pertenezcan al usuario efectivo del proceso, para limitar la práctica a archivos propios y evitar interpretar información sobre objetos ajenos con políticas de visibilidad diferentes. No usa mincore como una interfaz de permisos o de durabilidad.
#define _DEFAULT_SOURCE
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "uso: %s ARCHIVO_PROPIO\n", argv[0]);
return 2;
}
int fd = open(argv[1], O_RDONLY | O_NONBLOCK | O_CLOEXEC);
if (fd == -1) {
perror("open");
return 1;
}
int estado = 1;
void *mapa = MAP_FAILED;
unsigned char *vector = NULL;
size_t longitud = 0;
struct stat s;
if (fstat(fd, &s) == -1) {
perror("fstat");
goto limpiar;
}
if (!S_ISREG(s.st_mode) || s.st_uid != geteuid() ||
s.st_size < 0 || (uintmax_t)s.st_size > 1073741824 ||
(uintmax_t)s.st_size > SIZE_MAX) {
fprintf(stderr, "se requiere un archivo propio regular de hasta 1 GiB\n");
goto limpiar;
}
long pagina = sysconf(_SC_PAGESIZE);
if (pagina <= 0) {
fprintf(stderr, "no se pudo obtener el tamano de pagina\n");
goto limpiar;
}
longitud = (size_t)s.st_size;
printf("archivo=%ju bytes pagina=%ld bytes\n", (uintmax_t)longitud, pagina);
if (longitud == 0) {
puts("paginas=0 residentes=0; archivo vacio, no se crea un mmap de longitud cero");
estado = 0;
goto limpiar;
}
size_t tam_pagina = (size_t)pagina;
size_t paginas = 1 + (longitud - 1) / tam_pagina;
vector = malloc(paginas);
if (vector == NULL) {
perror("malloc vector");
goto limpiar;
}
mapa = mmap(NULL, longitud, PROT_READ, MAP_SHARED, fd, 0);
if (mapa == MAP_FAILED) {
perror("mmap");
goto limpiar;
}
if (mincore(mapa, longitud, vector) == -1) {
perror("mincore");
goto limpiar;
}
size_t residentes = 0, cubiertos = 0;
for (size_t i = 0; i < paginas; i++) {
if (vector[i] & 1) {
residentes++;
size_t restantes = longitud - i * tam_pagina;
cubiertos += restantes < tam_pagina ? restantes : tam_pagina;
}
}
printf("paginas=%zu residentes=%zu porcentaje=%.2f\n",
paginas, residentes, 100.0 * (double)residentes / (double)paginas);
printf("bytes logicos cubiertos por paginas residentes=%zu\n", cubiertos);
printf("primeras paginas (R=residente, .=no residente): ");
size_t limite = paginas < 64 ? paginas : 64;
for (size_t i = 0; i < limite; i++)
putchar((vector[i] & 1) ? 'R' : '.');
if (paginas > limite)
printf(" ...");
putchar('\n');
estado = 0;
limpiar:
if (mapa != MAP_FAILED && munmap(mapa, longitud) == -1) {
perror("munmap");
estado = 1;
}
free(vector);
if (close(fd) == -1) {
perror("close");
estado = 1;
}
if (fflush(stdout) == EOF || ferror(stdout)) {
fprintf(stderr, "error al producir el informe\n");
estado = 1;
}
return estado;
}
Cómo leer el vector
Se reserva un byte por página del intervalo, redondeando hacia arriba la última región parcial. Solo se interpreta el bit menos significativo de cada byte. Los demás no se usan como banderas de página limpia o sucia. El manual de mincore(2) documenta la interfaz y advierte que la observación puede quedar desactualizada incluso al retornar.
Los bytes lógicos cubiertos no son un cálculo de memoria física exclusiva de este proceso. La última página puede incluir bytes fuera de la longitud y la memoria puede estar compartida. Un mapeo puede observar páginas presentes en caché aunque el proceso no haya tocado sus direcciones para incorporarlas a su propio conjunto residente.
Lo que mincore no responde
No informa de la velocidad del almacenamiento, no garantiza que la página siga presente al leerla y no indica por sí solo si sus datos están pendientes de respaldo. Tampoco proporciona una suma que pueda equipararse sin más al RSS del proceso o a Cached global.
El archivo debe mantenerse estable durante la práctica. Truncados o escrituras concurrentes pueden cambiar la relación entre tamaño, mapeo y residencia. Las restricciones y detalles del núcleo y del sistema de archivos también influyen en la información observable.
8. Laboratorio de residencia compartida
$ cd "$LAB12"
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 residentes.c -o residentes
$ ./residentes datos.bin
$ cat datos.bin >/dev/null
$ ./residentes datos.bin
$ bash foto-memoria.sh despues-de-leer
Las cantidades residentes pueden ser ya altas en la primera consulta porque la escritura creó contenido en memoria. Si las dos observaciones muestran todas las páginas, no hay un experimento fallido: has observado que el archivo estaba presente antes de la lectura. No declares que el primer cat recuperó del disco todo su contenido.
Si faltaban regiones y después aparecen, el cambio es compatible con que las lecturas las hayan traído o mantenido, bajo las condiciones del sistema. La observación no congela la memoria; otro trabajo puede recuperar páginas entre las dos consultas.
Dos nombres para el mismo contenido cacheado
$ ln datos.bin alias-datos.bin
$ stat -c 'dispositivo=%d inodo=%i nombre=%n' datos.bin alias-datos.bin
$ ./residentes alias-datos.bin
$ cat alias-datos.bin >/dev/null
$ ./residentes datos.bin
El enlace duro llega al mismo objeto. Sus aperturas pueden tener posiciones diferentes y aprovechar el mismo contenido cacheado. No necesitas que un proceso permanezca abierto para que otro pueda encontrar esas páginas.
Archivo vacío y última página parcial
$ touch vacio.dat
$ printf 'ABC' >pequeno.dat
$ ./residentes vacio.dat
$ ./residentes pequeno.dat
El vacío tiene cero páginas lógicas del archivo y no se intenta un mmap de longitud cero. El de tres bytes ocupa una región parcial de la primera página: si esta se informa residente, cubre tres bytes del archivo, no una propiedad exclusiva de 4096 bytes asignados a ese proceso.
9. Bytes leídos y E/S atribuida al proceso
Linux expone contabilidad de E/S por proceso bajo /proc/PID/io. Dos campos ayudan a formular preguntas diferentes:
| Campo | Pregunta aproximada | Límite |
|---|---|---|
rchar | ¿Cuántos bytes se contabilizaron en operaciones de lectura del proceso? | Puede incluir entrada de terminal, proc y lecturas atendidas desde caché. |
read_bytes | ¿Cuánta lectura de almacenamiento se atribuyó al proceso en la contabilidad correspondiente? | No es una cuenta universal de bytes físicos de toda la plataforma ni una medida directa de aciertos de caché. |
wchar | ¿Cuántos bytes se contabilizaron en operaciones de escritura? | No demuestra persistencia ni que esos bytes ya salieron del equipo. |
write_bytes | ¿Qué trabajo de escritura se atribuye en esa capa? | La atribución y el momento no se reducen a mirar si un write acaba de retornar. |
La anticipación puede traer más datos que el fragmento solicitado, y los sistemas de archivos tienen rutas distintas de atribución. Una resta de dos campos no se convierte por sí sola en una tasa exacta de cache hits. Un read_bytes pequeño con muchos bytes lógicos leídos es evidencia útil de un recorrido que no exigió esa misma cantidad de almacenamiento atribuido en la ventana.
Observar también introduce actividad
Leer /proc/self/io usa operaciones de lectura. Si el propio programa toma una muestra antes y otra después, la diferencia de rchar puede incluir el coste de esa instrumentación. No debe exigirse que delta_rchar sea exactamente igual al tamaño del archivo.
En un comando externo, /proc/self/io se refiere al proceso que lo está leyendo. Para observar la terminal desde una herramienta usarías el PID del shell, pero sus contadores no son una suma simple del trabajo de cualquier herramienta ya terminada. El programa siguiente tomará sus propias muestras en el proceso que realmente realiza la lectura.
10. Experimento: leer varias veces y comparar
Lecturas-cache.c hace de una a diez pasadas, tres por defecto. Vuelve a abrir el archivo regular en cada una, lee todos sus bytes con un búfer fijo y calcula una suma sencilla para mostrar que recorrió el contenido. Cronometra transferencia y cierre, y toma contadores de proc fuera de la ventana.
La suma módulo 2⁶⁴ no es una verificación criptográfica ni demuestra igualdad única de contenidos. Añade trabajo por byte y, por tanto, el tiempo no se compara directamente con el medidor del tema 11 como si ambos hicieran idéntica tarea. Sirve para contrastar pasadas de este mismo programa.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
struct contadores {
uintmax_t rchar;
uintmax_t read_bytes;
};
static int tomar_io(struct contadores *c)
{
FILE *f = fopen("/proc/self/io", "r");
if (f == NULL)
return -1;
char linea[256];
int campos = 0;
while (fgets(linea, sizeof linea, f) != NULL) {
uintmax_t valor;
if (sscanf(linea, "rchar: %ju", &valor) == 1) {
c->rchar = valor;
campos |= 1;
}
if (sscanf(linea, "read_bytes: %ju", &valor) == 1) {
c->read_bytes = valor;
campos |= 2;
}
}
int error = ferror(f);
if (fclose(f) == EOF)
error = 1;
return !error && campos == 3 ? 0 : -1;
}
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3) {
fprintf(stderr, "uso: %s ARCHIVO [PASADAS:1..10]\n", argv[0]);
return 2;
}
unsigned long pasadas = 3;
if (argc == 3) {
char *fin;
errno = 0;
pasadas = strtoul(argv[2], &fin, 10);
if (errno != 0 || fin == argv[2] || *fin != '\0' ||
argv[2][0] == '-' || pasadas < 1 || pasadas > 10) {
fprintf(stderr, "numero de pasadas invalido\n");
return 2;
}
}
unsigned char buf[65536];
int estado = 0;
puts("pasada,bytes,suma,segundos,delta_rchar,delta_read_bytes");
for (unsigned long vuelta = 1; vuelta <= pasadas; vuelta++) {
int fd = open(argv[1], O_RDONLY | O_NONBLOCK | O_CLOEXEC);
if (fd == -1) {
perror("open");
estado = 1;
break;
}
struct stat s;
if (fstat(fd, &s) == -1) {
perror("fstat");
estado = 1;
goto cerrar;
}
if (!S_ISREG(s.st_mode)) {
fprintf(stderr, "se requiere un archivo regular\n");
estado = 1;
goto cerrar;
}
struct contadores antes, despues;
int hay_antes = tomar_io(&antes) == 0;
struct timespec inicio, fin;
if (clock_gettime(CLOCK_MONOTONIC, &inicio) == -1) {
perror("reloj inicial");
estado = 1;
goto cerrar;
}
uintmax_t bytes = 0;
uint64_t suma = 0;
for (;;) {
ssize_t n = read(fd, buf, sizeof buf);
if (n == -1) {
if (errno == EINTR)
continue;
perror("read");
estado = 1;
goto cerrar;
}
if (n == 0)
break;
bytes += (uintmax_t)n;
for (size_t i = 0; i < (size_t)n; i++)
suma += buf[i];
}
int actual = fd;
fd = -1;
if (close(actual) == -1) {
perror("close");
estado = 1;
goto cerrar;
}
if (clock_gettime(CLOCK_MONOTONIC, &fin) == -1) {
perror("reloj final");
estado = 1;
goto cerrar;
}
double segundos = (double)(fin.tv_sec - inicio.tv_sec) +
(double)(fin.tv_nsec - inicio.tv_nsec) / 1e9;
if (segundos <= 0) {
fprintf(stderr, "intervalo no positivo\n");
estado = 1;
goto cerrar;
}
int hay_despues = tomar_io(&despues) == 0;
printf("%lu,%ju,%" PRIu64 ",%.9f,", vuelta, bytes, suma, segundos);
if (hay_antes && hay_despues && despues.rchar >= antes.rchar &&
despues.read_bytes >= antes.read_bytes) {
printf("%ju,%ju\n", despues.rchar - antes.rchar,
despues.read_bytes - antes.read_bytes);
} else {
puts("NA,NA");
}
cerrar:
if (fd != -1 && close(fd) == -1) {
perror("close de limpieza");
estado = 1;
}
if (estado != 0)
break;
}
if (fflush(stdout) == EOF || ferror(stdout)) {
fprintf(stderr, "error al guardar el informe\n");
estado = 1;
}
return estado;
}
Ejecutar y conservar el contexto
$ cd "$LAB12"
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 lecturas-cache.c -o lecturas-cache
$ bash foto-memoria.sh antes-de-las-pasadas
$ ./lecturas-cache datos.bin 3 >lecturas.csv
$ echo $?
0
$ cat lecturas.csv
$ ./residentes datos.bin
$ bash foto-memoria.sh despues-de-las-pasadas
Cada pasada debe informar 16.777.222 bytes y una suma de 350 para el archivo creado. Los tiempos y las diferencias de contadores se obtienen en tu sistema y no tienen valores universales. Si la lectura de /proc/self/io no está disponible, se muestran NA en esos campos sin fingir una medición de cero.
Guarda el informe en una ruta distinta del origen. Si falla una pasada, no se imprime una fila de éxito para ella y el programa devuelve 1; puede haber filas anteriores válidas. Los fallos de cierre también forman parte del resultado.
Qué evidencia combinar
Una residencia alta antes de leer, bytes lógicos constantes y un delta_read_bytes pequeño en las pasadas son observaciones compatibles con que el contenido se atienda principalmente desde caché. La suma y el conteo confirman el trabajo lógico del programa; la contabilidad de almacenamiento aporta otra vista del recorrido.
Si todos los tiempos son similares, eso puede ocurrir porque las páginas ya estaban presentes desde la creación o una lectura anterior. Si hay diferencias, considera también CPU, planificación, tamaño de entrada y actividad concurrente. No existe una regla de que la primera fila represente siempre un acceso físico completo.
11. Qué no prueban las observaciones
| Observación | Conclusión razonable | Conclusión que no basta para sostener |
|---|---|---|
| Cached creció. | Aumentó esa categoría global en la observación. | Todo el crecimiento pertenece a mi archivo. |
| Mincore muestra páginas residentes. | La consulta informa presencia en memoria para esas regiones. | Seguirán residentes y están todas limpias o durables. |
| Delta_rchar supera el tamaño. | Hubo más lecturas contabilizadas, incluida instrumentación posible. | El archivo necesariamente se leyó dos veces del dispositivo. |
| Delta_read_bytes vale cero. | No aumentó esa contabilidad atribuida en la ventana. | No hubo actividad física en ninguna capa del equipo. |
| La segunda pasada fue más rápida. | Mejoró el tiempo observado en esas condiciones. | La diferencia mide exactamente la latencia de un disco. |
| Dirty bajó. | Disminuyó la contabilidad global de ese estado. | Mi archivo y su directorio ya tienen una publicación durable completa. |
Residencia no equivale a RSS exclusivo
La caché de archivos es compartida. Un proceso puede provocar que una página se cargue y otro aprovecharla. Sumar residencia por archivo y RSS de cada proceso como si fueran memorias exclusivas puede duplicar cuentas. Las métricas tienen alcances y reglas diferentes.
Lectura bufferizada no cubre todos los tipos de acceso
O_DIRECT busca un recorrido diferente con restricciones de alineación y condiciones del sistema; se estudia en el tema 16. Los mapeos privados pueden producir copias de memoria al modificar, y los compartidos tienen sus propias reglas de coherencia. Nuestro mapeo observador es de lectura y no modifica contenido.
Tmpfs, archivos de red, sistemas con compresión y capas virtuales pueden cambiar la relación entre bytes lógicos y respaldo físico. La explicación de un archivo regular local ordinario no debe convertirse en una afirmación universal de todos los descriptores.
No necesitamos expulsar toda la caché para aprender la capa
Vaciar cachés globales modifica condiciones de otras cargas y requiere otro tipo de control experimental. Aquí observamos, repetimos sobre datos propios y documentamos lo encontrado. La comparación rigurosa de estados fríos y calientes se desarrollará con alcance explícito en el tema siguiente.
12. Recuperación de memoria y coherencia
La RAM de la caché compite con otras necesidades. El núcleo evalúa actividad y estados para decidir qué conservar y qué recuperar. Un modelo LRU simple ayuda a entender la utilidad de la localidad, pero no describe exactamente todos los algoritmos de Linux.
Localidad y conjunto de trabajo
Si se reutilizan unas pocas páginas que caben en memoria, después de las primeras cargas muchos accesos pueden ser aciertos. Si se recorre un conjunto mucho mayor y las páginas se expulsan antes de reutilizarlas, las recargas pueden dominar. La capacidad útil es compartida con otras cargas y no un cupo fijo garantizado por archivo.
La recuperación de una página limpia no borra el archivo. Su contenido podrá obtenerse otra vez cuando haga falta, con el coste que corresponda. Si está sucia o en escritura, el proceso de recuperación necesita respetar el estado de sus cambios.
Cerrar no significa expulsar
Close elimina una referencia del proceso; no ordena retirar de RAM todas las páginas del objeto. Fsync solicita garantías de sincronización, no una orden general de descartar caché. Puede seguir habiendo contenido limpio disponible para lecturas posteriores.
Por esa misma razón, ejecutar de nuevo un programa no implica comenzar con el sistema frío. La vida de la caché pertenece al núcleo y puede superar la de los procesos que la utilizaron.
Una versión compartida bajo el modelo ordinario
Cuando dos procesos leen y escriben mediante las operaciones bufferizadas ordinarias del mismo archivo local, no mantienen por ello dos copias de caché independientes elegidas por nombre. Sus aperturas tienen cursores propios, pero el contenido del objeto se comparte conforme a las reglas de las operaciones.
Eso no resuelve todas las carreras: una secuencia de varias escrituras puede observarse parcialmente y una aplicación necesita coordinar sus registros o versiones. Compartir páginas y publicar una actualización atómica de alto nivel son garantías diferentes.
13. Simulador de páginas compartidas y respaldo
El modelo tiene un archivo de ocho páginas lógicas, dos procesos que llegan al mismo objeto y una caché pequeña. Cada página muestra una versión v0 inicial. Las lecturas pueden cargarla; las escrituras de página completa producen una versión nueva en memoria. El respaldo solo cambia al completar el writeback simulado.
Compartir páginas y conservar cambios pendientes
| Página | Estado en caché | Versión RAM | Versión en writeback | Versión respaldo | Último uso |
|---|
Historial de operaciones
Usamos páginas didácticas de 4 KiB y reemplazo LRU solo entre páginas limpias que no estén en writeback. Linux real utiliza estructuras, políticas y restricciones más complejas. Si no hay espacio recuperable, el modelo detiene esa operación y pide completar escritura; un sistema real puede iniciar trabajo, esperar o regular la carga.
Una sobrescritura completa no necesita leer la versión anterior en este modelo; por eso no aumenta el contador de lecturas de respaldo. Las modificaciones parciales pueden necesitar otro recorrido. La versión capturada al iniciar writeback se conserva hasta terminarlo, aunque la aplicación vuelva a escribir.
En el modelo, «sucia» significa que la versión de RAM todavía difiere de la de respaldo, y puede contarse también entre las páginas en writeback. Estos contadores didácticos no reproducen exactamente los campos Dirty y Writeback de Linux. El respaldo tampoco modela cachés del dispositivo, errores, metadatos ni barreras de durabilidad: completar su writeback no representa una llamada real a fsync. Abrir y cerrar solo afecta referencias del modelo.
Secuencias para comprobar
- Lee la página 0 con P1 y después con P2. La segunda lectura puede ser un acierto de la misma caché, sin otra lectura de respaldo.
- Cierra P1 y conserva P2. La página no desaparece por el cierre. Vuelve a abrir P1 y repite la lectura.
- Sobrescribe la página 0. P2 lee la versión nueva de RAM aunque el respaldo siga en v0.
- Inicia writeback, vuelve a sobrescribir antes de completarlo y termina la escritura iniciada. El respaldo recibe la versión capturada, pero la página sigue sucia por la nueva modificación.
- Completa otro writeback de esa versión nueva. Ahora puede recuperarse como limpia sin perder la versión actual del modelo.
- Con capacidad dos, escribe páginas distintas hasta llenar de sucias. Leer otra página no puede descartar sus cambios sin más. Escribe al respaldo alguna y observa cómo cambia la posibilidad de recuperar espacio.
14. Resumen
- La caché de páginas conserva contenido de archivos en RAM gestionada por el núcleo y puede compartirse entre aperturas y procesos.
- Un read lógico no obliga a obtener de nuevo cada byte del dispositivo; puede combinar caché, anticipación y trabajo de E/S.
- Los búferes FILE* viven en otra capa y pueden existir además de la caché del núcleo.
- Una página sucia contiene cambios pendientes de respaldo; limpia, sucia y writeback son estados distintos de una garantía completa de durabilidad.
- MemFree y MemAvailable responden preguntas diferentes. Cached alto por sí solo no demuestra una fuga.
- Free y meminfo son observaciones globales; no atribuyen directamente un importe a un archivo particular.
- Mincore ofrece una observación de residencia de un intervalo mapeado, no un bloqueo de páginas ni un indicador de limpieza.
- Rchar y read_bytes tienen alcances distintos; la instrumentación y las capas del sistema limitan su interpretación.
- La primera pasada de un archivo recién creado no tiene por qué ser fría.
- Cerrar o terminar un proceso no ordena expulsar la caché; recuperar páginas limpias no borra el archivo.
- Compartir contenido cacheado no elimina las carreras ni reemplaza un protocolo de actualización de la aplicación.
15. Preguntas de repaso y práctica
Antes de desplegar las respuestas, identifica qué estado pertenece al proceso, al núcleo o al respaldo del archivo.
1. ¿La caché de páginas pertenece a cada FILE* por separado?
No. El FILE* tiene su propio estado de biblioteca, pero el contenido del archivo cacheado por el núcleo puede compartirse entre procesos y aperturas del mismo objeto.
2. ¿Read positivo prueba que hubo una lectura física nueva del dispositivo?
No. Confirma entrega de bytes al proceso. El contenido puede estar ya en caché y la capa de almacenamiento puede tener otros comportamientos.
3. ¿Una página de 4096 bytes es una constante universal de Linux?
No. Es habitual en muchas plataformas, pero se consulta en el sistema concreto. Tampoco equivale necesariamente al sector, bloque de asignación o búfer de aplicación.
4. ¿Por qué Cached puede seguir alto después de terminar cat?
Porque la vida de la caché no está limitada a ese proceso. El núcleo puede conservar páginas para accesos posteriores y recuperarlas cuando sea necesario.
5. ¿MemFree bajo y MemAvailable alto son necesariamente contradictorios?
No. Hay memoria utilizada que puede recuperarse bajo determinadas condiciones. Disponible es una estimación que considera más recursos y reservas que la memoria actualmente libre.
6. ¿El crecimiento global de Cached demuestra cuánto cacheó un archivo?
No. Intervienen otros procesos y categorías, entradas y salidas de páginas y el momento de observación. Para un archivo concreto hace falta una vista adicional y aun así hay límites temporales.
7. ¿Qué significa que una página esté sucia?
Que contiene cambios pendientes de escritura según ese estado del sistema. No significa que sus bytes sean inválidos ni que otros lectores no puedan usar la versión en memoria.
8. ¿Se puede recuperar una página sucia igual que una limpia?
No descartando su única versión actualizada. Debe respetarse el trabajo de respaldo pendiente o conservar los cambios por el mecanismo adecuado.
9. ¿Qué ocurre si se vuelve a modificar una página durante writeback?
Completar la escritura de la versión anterior no demuestra que la nueva ya esté respaldada. Debe conservarse que hay una modificación posterior pendiente.
10. ¿Mincore carga todo el archivo porque creamos un mmap?
No en la consulta del ejemplo, que no toca los datos ni solicita población. Mincore observa residencia; no es una orden de recorrer el contenido completo.
11. ¿Una página residente informada por mincore está necesariamente limpia?
No. El vector de residencia no comunica ese estado ni garantiza que la página permanezca en memoria después de la consulta.
12. ¿Por qué residentes.c trata especialmente un archivo vacío?
Porque no hay páginas lógicas de contenido que consultar y no se crea un mmap de longitud cero. Evita confundir un límite de la interfaz con una ausencia errónea de datos.
13. ¿Rchar debe aumentar exactamente en el tamaño del archivo en este lector?
No necesariamente. El programa también lee proc para observar sus contadores y puede haber otra actividad contabilizada. Los bytes que cuenta el bucle son la medida lógica específica de la transferencia.
14. ¿Un delta_read_bytes cero es una tasa de aciertos del 100 % demostrada para toda la plataforma?
No. Indica que no aumentó esa contabilidad atribuida en la ventana. No cubre por sí solo todas las capas físicas, tipos de archivo, anticipación y actividades del equipo.
15. ¿Por qué no llamamos fría a la primera pasada de datos.bin?
Porque la creación por escritura y otros accesos anteriores pueden haber dejado páginas en caché. Volver a abrir no las expulsa.
16. ¿Close o fsync ordenan expulsar todas las páginas cacheadas?
No. Close libera una referencia y fsync solicita sincronización con su contrato. El contenido limpio puede seguir en RAM para accesos posteriores.
17. ¿Recuperar una página limpia elimina bytes del archivo?
No. Retira su representación cacheada cuando corresponde; el contenido puede volver a obtenerse del respaldo. El archivo y sus nombres tienen otra vida útil.
18. ¿Dos enlaces duros al mismo archivo necesitan dos cachés independientes?
No por el hecho de tener nombres distintos. Llegan al mismo objeto y pueden aprovechar el mismo contenido cacheado, aunque sus aperturas mantengan posiciones independientes.
Para practicar
- Ejecuta foto-memoria antes y después de crear el archivo. Anota MemFree, MemAvailable y Cached sin intentar atribuir cada diferencia neta únicamente a esa creación.
- Compila residentes.c y ejecútalo antes y después de cat. Explica el resultado si ambas observaciones ya muestran residencia completa.
- Prueba el observador con un archivo vacío, uno de tres bytes y uno cuya longitud sea una página más un byte. Calcula cuántas páginas lógicas comprende cada uno usando el tamaño real consultado.
- Crea un enlace duro, compara dispositivo e inodo y observa residencia a través de ambos nombres. Distingue el nombre usado de la identidad del contenido.
- Ejecuta lecturas-cache con una, tres y cinco pasadas. Comprueba que bytes y suma permanecen constantes y guarda los tiempos sin elegir solo el mejor.
- Compara delta_rchar con el tamaño de datos.bin y explica por qué la propia consulta de proc puede contribuir a la diferencia.
- Repite las lecturas después de que el primer proceso terminó. Comprueba qué puede inferirse sobre caché conservada y qué no se garantiza para una carga futura.
- Usa residentes sobre un archivo disperso propio y compáralo con uno escrito de longitud similar. Separa bytes lógicos, asignación y presencia en memoria.
- En el simulador, lee una página con P1 y luego con P2. Cierra una referencia y observa que la residencia no desaparece.
- Llena la caché simulada de páginas sucias y pide otra. Inicia y completa writeback de una de ellas y explica por qué entonces puede recuperarse.
- Escribe, inicia writeback, vuelve a escribir y completa la primera operación. Compara las versiones RAM, capturada y de respaldo antes de dar la página por limpia.
En el tema 13 estudiaremos cómo diseñar comparaciones de lectura en frío y en caliente y cómo observar lectura anticipada y sugerencias al núcleo. Ya tienes las herramientas para distinguir la presencia real observada de una etiqueta supuesta y para interpretar los tiempos sin confundir caché con dispositivo.