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.
| Interfaz | Cómo se accede | Quién mueve los datos |
|---|---|---|
read / write | Llamadas al sistema sobre un descriptor y un búfer | El núcleo copia entre la caché de páginas y el búfer del proceso |
mmap | Cargas y almacenes: mapa[i], memcpy, punteros | El 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>:
void *mmap(void *addr, size_t length, int prot,
int flags, int fd, off_t offset);
| Argumento | Significado habitual para un archivo |
|---|---|
addr | NULL: se deja al núcleo elegir una dirección libre. Es la opción normal y portable. |
length | Cantidad de bytes que estarán disponibles desde la dirección devuelta. Debe ser mayor que cero. |
prot | Permisos: PROT_READ, PROT_WRITE, PROT_EXEC o PROT_NONE, combinados con |. |
flags | Tipo de mapeo. Para este tema, exactamente uno de MAP_SHARED o MAP_PRIVATE. |
fd | Descriptor de un archivo abierto con permisos compatibles. |
offset | Primer 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).
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.
#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;
}
$ 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:
- 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.
- El núcleo localiza la VMA. Comprueba que la dirección pertenece al mapeo y que el acceso respeta
PROT_READoPROT_WRITE. - 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.
- 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).
- 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:
| Tipo | Una escritura modifica… | Otros procesos | Archivo |
|---|---|---|---|
MAP_SHARED | La página compartida de la caché | Pueden ver el cambio si mapean la misma región | El cambio puede escribirse al archivo |
MAP_PRIVATE | Una copia privada creada al escribir | No ven esa copia | No 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:
#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:
MS_SYNCespera a que termine la escritura de las páginas sucias del intervalo.MS_ASYNCsolicita una actualización asíncrona y retorna sin esperar.MS_INVALIDATEpide invalidar copias incompatibles en otros mapeos; se combina con uno de los anteriores.
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:
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
| Criterio | mmap | read / write |
|---|---|---|
| Interfaz | Punteros y accesos de memoria | Descriptor, búfer y llamadas explícitas |
| Acceso aleatorio | Natural: p[posicion] | Requiere pread o cambiar la posición |
| Copias | Evita copiar cada byte a un búfer de usuario separado | Normalmente copia desde/hacia el búfer del proceso |
| Costo de entrada | Fallos de página, fuera del flujo explícito del programa | Costo visible en cada llamada |
| Manejo de errores tardíos | Puede llegar como señal (SIGBUS) | La llamada devuelve -1 y errno |
| Archivos que cambian de tamaño | Requiere mucho cuidado y coordinación | Más sencillo para flujos que crecen |
| Flujo secuencial | Claro, pero no necesariamente más rápido | Un 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
- Comprobar contra
NULL. Es incorrecto: el error esMAP_FAILED. - Mapear un archivo vacío. La longitud cero no es válida. Hay que tratar ese caso antes o agrandar el archivo.
- Confundir permisos. Un mapeo compartido escribible exige un descriptor abierto de forma compatible, normalmente con
O_RDWR. - Pasar un offset no alineado.
mmapfalla conEINVAL; se usa el esquema base + delta de la sección 7. - Acceder después de
munmap. El puntero queda colgante. Una lectura posterior normalmente termina enSIGSEGV. - Truncar mientras está mapeado. Si otro proceso reduce el archivo y se toca una página que ya no tiene respaldo, puede llegar
SIGBUS. - Esperar que
MAP_PRIVATEguarde cambios. Sus escrituras son copias privadas;msyncno las convierte en cambios del archivo. - Usar el mapeo como sincronización. La visibilidad compartida no evita carreras ni hace atómicas estructuras de varios bytes.
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álidos | Aciertos | Fallos de página | Accesos 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:
$ 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.
$ /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
mmapasocia bytes de un archivo con una región virtual; no copia todo el archivo al crearla.- Las páginas llegan bajo demanda mediante fallos de página y se comparten con
read/writea través de la caché de páginas. MAP_SHAREDpermite que las escrituras lleguen al archivo;MAP_PRIVATEcrea copias privadas al escribir.PROT_READ,PROT_WRITEyPROT_EXECson permisos reales que aplica la MMU.- El error de
mmapesMAP_FAILED;munmapelimina la región y cerrar el descriptor no lo hace. msync(..., MS_SYNC)espera la escritura de páginas sucias compartidas, pero visibilidad y durabilidad son conceptos distintos.- El offset debe estar alineado; se mapea desde una base inferior y se suma un delta para comenzar en cualquier byte.
mmapes especialmente cómodo para acceso aleatorio y estructuras dentro de archivos;read/writesigue siendo mejor para muchos flujos simples.- Reducir un archivo mientras está mapeado o acceder más allá de su respaldo puede provocar
SIGBUS.
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
- Compila
contar_mmap.cy comprueba su resultado contrawc -l. Explica la diferencia si el archivo no termina en salto de línea. - Ejecuta dos veces el contador sobre un archivo grande con
/usr/bin/time -v. Compara los fallos mayores: ¿qué efecto tuvo la caché? - Cambia
MAP_PRIVATEporMAP_SHAREDen una copia del programa de mayúsculas y prueba qué ocurre en cada caso. - Usa el simulador con accesos
0, 4095, 4096, 8191, 8192y explica por qué aparecen tres fallos con páginas de 4096 bytes. - Modifica un programa para llamar a
close(fd)justo después demmapy comprueba que el recorrido sigue funcionando. - 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. - Mapea una ventana de 100 bytes que empiece en el offset 5000 aplicando base + delta.
- Consulta
/proc/PID/mapsmientras un programa conserva abierto un mapeo y localiza su intervalo, permisos y offset.