← Gestión de Memoria Bloque 9 · Intercambio y mapeo de memoria · Tema 41 de 52

Tema 41

Archivos mapeados con mmap

Normalmente un programa lee un archivo con read y escribe con write. mmap propone otra interfaz: vincula una región del espacio de direcciones con el archivo y permite recorrerlo como un arreglo. El núcleo carga las páginas cuando se tocan, comparte con la E/S tradicional la misma caché y puede escribir los cambios de vuelta. En este tema aprenderás la llamada completa, las diferencias entre MAP_SHARED y MAP_PRIVATE, cuándo usar msync, por qué aparecen fallos de página y cómo evitar los errores más peligrosos.

1. Del archivo al espacio de direcciones

Un archivo es una secuencia de bytes persistente. Con la interfaz clásica, el programa reserva un búfer, llama a read para copiar bytes hasta él, los procesa y, si los modifica, llama a write. Con un archivo mapeado, el núcleo reserva un intervalo de direcciones virtuales y establece que el byte situado en una dirección de ese intervalo corresponde a un byte del archivo.

Archivo mapeado en memoria es una asociación entre una región del espacio de direcciones virtuales y una región de un archivo. El programa accede mediante cargas y almacenes normales; el sistema operativo trae y escribe las páginas necesarias.

«Mapeado en memoria» no significa que el archivo completo se copie inmediatamente a la RAM. mmap crea la asociación, pero las páginas se incorporan bajo demanda. Si se mapea un archivo de 4 GiB y solo se consulta su primera página, normalmente solo esa página necesita llegar a memoria. Es la misma paginación por demanda estudiada en los temas 29 y 30, aplicada a un archivo.

Supón una página de 4096 bytes y un mapeo que comienza en el byte 0. La expresión mapa[9000] apunta al byte 9000 del archivo, que pertenece a la tercera página. Si esa página todavía no está presente, la CPU provoca un fallo de página; el núcleo la obtiene de la caché de páginas o del dispositivo y reanuda la misma instrucción.

InterfazCómo se accedeQuién mueve los datos
read / writeLlamadas al sistema sobre un descriptor y un búferEl núcleo copia entre la caché de páginas y el búfer del proceso
mmapCargas y almacenes: mapa[i], memcpy, punterosEl gestor de memoria conecta páginas de la caché con el espacio virtual

2. La interfaz mmap

La función se declara en <sys/mman.h>:

prototipo POSIX
void *mmap(void *addr, size_t length, int prot,
           int flags, int fd, off_t offset);
ArgumentoSignificado habitual para un archivo
addrNULL: se deja al núcleo elegir una dirección libre. Es la opción normal y portable.
lengthCantidad de bytes que estarán disponibles desde la dirección devuelta. Debe ser mayor que cero.
protPermisos: PROT_READ, PROT_WRITE, PROT_EXEC o PROT_NONE, combinados con |.
flagsTipo de mapeo. Para este tema, exactamente uno de MAP_SHARED o MAP_PRIVATE.
fdDescriptor de un archivo abierto con permisos compatibles.
offsetPrimer byte del archivo asociado al mapeo; debe ser múltiplo del tamaño de página.

El resultado no se comprueba contra NULL. Ante un error, mmap devuelve el valor especial MAP_FAILED, equivalente conceptualmente a (void *) -1, y deja la causa en errno. Al terminar se elimina la asociación con munmap(direccion, longitud).

esqueleto mínimo
void *p = mmap(NULL, longitud, PROT_READ,
               MAP_PRIVATE, fd, 0);
if (p == MAP_FAILED) {
    perror("mmap");
    return 1;
}

/* usar p[0] ... p[longitud - 1] */

if (munmap(p, longitud) == -1)
    perror("munmap");

close(fd) y munmap cumplen funciones distintas. Una vez creado el mapeo, se puede cerrar el descriptor sin invalidarlo: el núcleo conserva la referencia interna al archivo. En cambio, cerrar el descriptor no elimina el mapeo; este existe hasta munmap o hasta que termina el proceso.

3. Leer un archivo como un arreglo

El siguiente programa cuenta líneas sin llamar a read. Primero abre el archivo, averigua su tamaño con fstat, lo mapea como privado y de solo lectura, y finalmente recorre los bytes.

contar_mmap.c
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Uso: %s archivo\n", argv[0]);
        return 1;
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    struct stat st;
    if (fstat(fd, &st) == -1) {
        perror("fstat");
        close(fd);
        return 1;
    }

    /* POSIX no permite mmap con longitud cero. */
    if (st.st_size == 0) {
        printf("0 líneas (archivo vacío)\n");
        close(fd);
        return 0;
    }

    const unsigned char *datos = mmap(NULL, (size_t) st.st_size,
        PROT_READ, MAP_PRIVATE, fd, 0);
    if (datos == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }
    close(fd);  /* el mapeo sigue siendo válido */

    size_t lineas = 0;
    for (off_t i = 0; i < st.st_size; i++)
        if (datos[i] == '\n')
            lineas++;

    if (datos[st.st_size - 1] != '\n')
        lineas++;  /* última línea sin salto final */

    printf("%zu líneas\n", lineas);
    if (munmap((void *) datos, (size_t) st.st_size) == -1) {
        perror("munmap");
        return 1;
    }
    return 0;
}
terminal · compilar y probar
$ gcc -std=c17 -D_POSIX_C_SOURCE=200809L -Wall -Wextra -O2 -o contar_mmap contar_mmap.c
$ ./contar_mmap /etc/passwd
48 líneas

El puntero es const porque el mapeo solo tiene PROT_READ. Intentar escribir por ese puntero —forzando una conversión de tipo— no modifica el permiso real: la MMU detecta la escritura sobre una página de solo lectura y el proceso recibe SIGSEGV.

4. Qué sucede al tocar una página

Crear el mapeo solo instala una región de memoria virtual (VMA en Linux) con el archivo, el desplazamiento y los permisos. El camino normal del primer acceso es:

  1. La CPU intenta leer una dirección del mapeo. Su entrada todavía no está presente en la tabla de páginas y se produce un fallo.
  2. El núcleo localiza la VMA. Comprueba que la dirección pertenece al mapeo y que el acceso respeta PROT_READ o PROT_WRITE.
  3. Busca la página del archivo en la caché de páginas. Si ya está allí —quizá porque otro proceso la leyó— no hace falta ir al dispositivo.
  4. Si no está, inicia la lectura. El proceso espera mientras el sistema trae el bloque del archivo. Este es un fallo mayor; si la página ya estaba en caché, es menor (tema 30).
  5. Instala la traducción y reanuda. La dirección virtual queda asociada al marco que contiene esa página y la instrucción se repite sin que el programa deba intervenir.

La caché de páginas es el punto de encuentro entre mmap y read. No hay una «copia de mmap» y otra «copia de read» del mismo byte: las dos interfaces usan las páginas que el núcleo mantiene para el archivo. Por eso leer primero con una interfaz puede calentar la caché para la otra.

Las páginas limpias del mapeo son fáciles de recuperar bajo presión: el núcleo puede descartar el marco porque el archivo conserva la copia. Si se acceden otra vez, las relee. Esta es precisamente la diferencia con las páginas anónimas que debían ir al swap en el tema 40.

5. MAP_SHARED y MAP_PRIVATE

Los permisos indican qué operaciones acepta la CPU. El tipo de mapeo indica qué significa una escritura:

TipoUna escritura modifica…Otros procesosArchivo
MAP_SHAREDLa página compartida de la cachéPueden ver el cambio si mapean la misma regiónEl cambio puede escribirse al archivo
MAP_PRIVATEUna copia privada creada al escribirNo ven esa copiaNo se modifica

MAP_PRIVATE usa copy-on-write (tema 31). Al principio puede compartir con la caché el mismo marco de solo lectura. La primera escritura provoca un fallo de protección; el núcleo copia la página y hace que solo este proceso vea la nueva versión. Las páginas no modificadas siguen compartiéndose.

Esto convierte a MAP_PRIVATE en una herramienta cómoda para analizar o transformar una vista temporal del archivo. Pero hay una advertencia: no debe entenderse como una fotografía estable frente a cambios externos. Si otro proceso modifica el archivo mientras está mapeado, la interacción con un mapeo privado puede resultar confusa. Cuando la consistencia importa, hacen falta coordinación y un protocolo de bloqueo.

Con MAP_SHARED, dos procesos pueden comunicarse mapeando el mismo archivo. Compartir bytes no aporta sincronización: si ambos actualizan una estructura a la vez, siguen siendo necesarios mutex, semáforos o atomics, y un formato diseñado para concurrencia. La memoria compartida resuelve dónde viven los datos; no decide quién puede cambiarlos.

6. Modificar y sincronizar

Para cambiar el archivo se abre con lectura y escritura, se usa PROT_READ | PROT_WRITE y MAP_SHARED. Este ejemplo convierte letras ASCII minúsculas a mayúsculas:

mayusculas_mmap.c
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Uso: %s archivo\n", argv[0]);
        return 1;
    }

    int fd = open(argv[1], O_RDWR);
    if (fd == -1) { perror("open"); return 1; }

    struct stat st;
    if (fstat(fd, &st) == -1) {
        perror("fstat"); close(fd); return 1;
    }
    if (st.st_size == 0) { close(fd); return 0; }

    unsigned char *p = mmap(NULL, (size_t) st.st_size,
        PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED) {
        perror("mmap"); close(fd); return 1;
    }
    close(fd);

    for (off_t i = 0; i < st.st_size; i++)
        if (p[i] >= 'a' && p[i] <= 'z')
            p[i] = (unsigned char) (p[i] - 'a' + 'A');

    if (msync(p, (size_t) st.st_size, MS_SYNC) == -1)
        perror("msync");
    if (munmap(p, (size_t) st.st_size) == -1) {
        perror("munmap");
        return 1;
    }
    return 0;
}

Una escritura de CPU marca la página como sucia. Linux la escribirá al archivo en segundo plano, aunque el proceso no llame inmediatamente a msync. La llamada sirve para pedir una sincronización explícita:

msync trata el contenido mapeado. Si un programa necesita garantías estrictas de persistencia después de una caída, también debe considerar los metadatos y el protocolo del sistema de archivos, y usar las operaciones de sincronización apropiadas sobre el descriptor. La visibilidad en RAM y la durabilidad física no son la misma propiedad.

Mapear no agranda el archivo. Si se necesita una salida de 1 MiB, primero debe establecerse ese tamaño con ftruncate(fd, 1024 * 1024) y solo entonces mapear y escribir. Acceder a páginas más allá del final real puede provocar SIGBUS.

7. Desplazamientos y tamaños

El argumento offset debe estar alineado al tamaño de página. Si se quiere mapear a partir del byte 10 000 y la página mide 4096, no se puede pasar 10 000 directamente. Se redondea hacia abajo y se agrega la diferencia al puntero:

mapear una ventana cuyo inicio lógico no está alineado
long pagina = sysconf(_SC_PAGESIZE);
off_t inicio = 10000;
size_t cantidad = 5000;

off_t base = inicio - (inicio % pagina);  /* 8192 */
size_t delta = (size_t) (inicio - base);   /* 1808 */
size_t longitud = delta + cantidad;       /* 6808 */

unsigned char *mapa = mmap(NULL, longitud, PROT_READ,
    MAP_PRIVATE, fd, base);
if (mapa == MAP_FAILED) { perror("mmap"); return 1; }

unsigned char *datos = mapa + delta;
/* datos[0] corresponde al byte 10000 del archivo. */

munmap(mapa, longitud);  /* se usa la dirección original */

El mapeo puede terminar en medio de una página. En la última página parcial, los bytes que quedan después del final del archivo se leen como cero, pero no se escriben al archivo. Acceder a una página completa situada más allá del final provoca SIGBUS. Por eso el límite del bucle debe ser el tamaño lógico del archivo y no la longitud redondeada de la región.

Para archivos enormes no siempre conviene mantener una única región gigantesca, aunque el acceso sea bajo demanda. En procesos de 64 bits suele haber espacio virtual de sobra, pero mapear ventanas permite limitar el número de páginas activas, simplificar el procesamiento por bloques y funcionar también en espacios de direcciones más pequeños.

8. mmap frente a read y write

Criteriommapread / write
InterfazPunteros y accesos de memoriaDescriptor, búfer y llamadas explícitas
Acceso aleatorioNatural: p[posicion]Requiere pread o cambiar la posición
CopiasEvita copiar cada byte a un búfer de usuario separadoNormalmente copia desde/hacia el búfer del proceso
Costo de entradaFallos de página, fuera del flujo explícito del programaCosto visible en cada llamada
Manejo de errores tardíosPuede llegar como señal (SIGBUS)La llamada devuelve -1 y errno
Archivos que cambian de tamañoRequiere mucho cuidado y coordinaciónMás sencillo para flujos que crecen
Flujo secuencialClaro, pero no necesariamente más rápidoUn búfer grande suele ser simple y muy eficiente

mmap no «salta» la caché de páginas ni vuelve gratis la E/S. La primera visita a una página que no está en RAM sigue esperando al almacenamiento. Su ventaja principal es la interfaz y evitar una copia adicional entre la caché del núcleo y un búfer de usuario; a cambio, los fallos ocurren implícitamente y los errores son más difíciles de contener.

Es una buena opción para bases de datos de solo lectura, índices, archivos con accesos dispersos, ejecutables y bibliotecas compartidas. read/write suele ser preferible para sockets y tuberías (que no son archivos regulares mapeables), flujos secuenciales simples, archivos cuyo tamaño cambia mientras se procesan o aplicaciones que necesitan recibir todos los errores como valores de retorno.

El rendimiento se mide, no se adivina. Para recorridos secuenciales, el núcleo aplica lectura anticipada con ambas interfaces. madvise permite dar pistas como MADV_SEQUENTIAL, MADV_RANDOM o MADV_WILLNEED, pero son consejos: no cambian la corrección del programa ni garantizan un resultado.

9. Errores y casos límite

Una distinción útil: SIGSEGV suele indicar que la dirección no pertenece a una región válida o viola sus permisos; SIGBUS, en este contexto, que la dirección pertenece a la región pero no puede respaldarse con una parte válida del archivo. No es una regla universal, pero ayuda mucho al depurar.

10. Simulador de acceso bajo demanda

El simulador representa un archivo dividido en páginas y un mapeo privado de solo lectura. Escribe una lista de desplazamientos: cada número es un byte que el proceso consulta. El primer acceso a una página produce un fallo; las visitas posteriores son aciertos mientras permanezca residente. Puedes vaciar los marcos para representar presión de memoria y repetir la secuencia.

Simulador · páginas de archivo bajo demanda

MAPEO

TRAZA DE ACCESOS

Accesos válidosAciertosFallos de páginaAccesos fuera del archivo

Prueba 0, 4095, 4096: los dos primeros bytes están en la página 0, mientras que 4096 inaugura la página 1. Después repite exactamente la misma lista sin vaciar marcos: todos serán aciertos. «Vaciar marcos» conserva el mapeo virtual, pero descarta sus páginas residentes; es lo que puede hacer el núcleo con páginas limpias cuando necesita RAM.

11. Observarlo en Linux

Mientras un proceso está detenido o espera entrada, /proc/PID/maps permite ver sus regiones. Una línea de un archivo mapeado incluye el intervalo virtual, permisos, desplazamiento dentro del archivo y ruta:

terminal · localizar un archivo mapeado
$ grep datos.bin /proc/4217/maps
7f59a8a00000-7f59a8a10000 r--p 00000000 08:02 1837421 /tmp/datos.bin

r--p significa lectura, sin escritura ni ejecución, y mapeo privado; un mapeo compartido aparece con s al final. El tamaño es la resta de las direcciones hexadecimal inicial y final. /proc/PID/smaps agrega estadísticas por región, entre ellas Rss, Shared_Clean, Private_Dirty y el tamaño de página.

terminal · fallos de página y llamadas relacionadas
$ /usr/bin/time -v ./contar_mmap archivo-grande.txt
Minor (reclaiming a frame) page faults: 812
Major (requiring I/O) page faults: 3
$ strace -e trace=openat,mmap,munmap,close ./contar_mmap archivo.txt
openat(AT_FDCWD, "archivo.txt", O_RDONLY) = 3
mmap(NULL, 32768, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f...
close(3)                                = 0
munmap(0x7f..., 32768)                  = 0

strace muestra la creación del mapeo, pero no una llamada por cada página. Los fallos de página son excepciones atendidas dentro del núcleo, no llamadas al sistema iniciadas por el programa. Para ver su cantidad son más apropiados time -v, perf stat -e page-faults,minor-faults,major-faults o los campos minflt y majflt de /proc/PID/stat.

12. Resumen

13. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿mmap carga todo el archivo en RAM al ser llamado?

No. Crea una región virtual asociada al archivo. Las páginas suelen llegar bajo demanda cuando el proceso las toca.

2. ¿Por qué el resultado se compara con MAP_FAILED y no con NULL?

Porque la interfaz define MAP_FAILED como valor de error. NULL no es el indicador especificado para esta llamada.

3. ¿Qué diferencia fundamental hay entre MAP_SHARED y MAP_PRIVATE?

Las escrituras compartidas modifican páginas de la caché que pueden llegar al archivo y ser vistas por otros mapeos. Las privadas activan copy-on-write y no modifican el archivo.

4. ¿Por qué se puede cerrar fd inmediatamente después de mmap?

Porque el núcleo mantiene la referencia necesaria al objeto mapeado. El descriptor ya no se necesita para conservar la región.

5. ¿Qué relación existe entre mmap y la caché de páginas?

Las páginas de un archivo mapeado son las de la caché de páginas, la misma que usa la E/S con read y write. Esto permite compartir datos y evitar una copia adicional a un búfer separado.

6. ¿Cuándo aparece un fallo mayor al recorrer un mapeo?

Cuando la página requerida no está en memoria y el núcleo debe obtenerla del dispositivo. Si ya está en la caché pero falta instalar la traducción, el fallo es menor.

7. ¿Para qué sirve msync con MS_SYNC?

Para solicitar que se escriban las páginas sucias del intervalo compartido y esperar a que la operación termine. No hace persistentes los cambios de un mapeo privado.

8. ¿Cómo se mapea a partir de un offset no alineado?

Se redondea el offset hacia abajo a un múltiplo del tamaño de página, se aumenta la longitud con la diferencia y se suma ese delta al puntero devuelto.

9. ¿Por qué puede aparecer SIGBUS si otro proceso trunca el archivo?

Porque una dirección sigue perteneciendo a la región virtual, pero la página correspondiente ya no tiene bytes del archivo que puedan respaldarla.

Para practicar

  1. Compila contar_mmap.c y comprueba su resultado contra wc -l. Explica la diferencia si el archivo no termina en salto de línea.
  2. Ejecuta dos veces el contador sobre un archivo grande con /usr/bin/time -v. Compara los fallos mayores: ¿qué efecto tuvo la caché?
  3. Cambia MAP_PRIVATE por MAP_SHARED en una copia del programa de mayúsculas y prueba qué ocurre en cada caso.
  4. Usa el simulador con accesos 0, 4095, 4096, 8191, 8192 y explica por qué aparecen tres fallos con páginas de 4096 bytes.
  5. Modifica un programa para llamar a close(fd) justo después de mmap y comprueba que el recorrido sigue funcionando.
  6. Escribe un programa que cree un archivo de 1 MiB con ftruncate, lo mapee y guarde en cada página el número de página.
  7. Mapea una ventana de 100 bytes que empiece en el offset 5000 aplicando base + delta.
  8. Consulta /proc/PID/maps mientras un programa conserva abierto un mapeo y localiza su intervalo, permisos y offset.