El address space del proceso y la memoria virtual
Cada nota hasta acá imprimió direcciones como si tu programa fuera dueño de toda la memoria.
No lo es — y sin embargo la ilusión es hermética. Cada proceso recibe su propio address
space virtual privado: un rango plano desde 0 hacia arriba que parece RAM dedicada pero
es una ficción que el OS y la MMU (memory management unit) de la CPU mantienen sobre la
memoria física compartida. La dirección 0x1040 en tu proceso y 0x1040 en otro son bytes
distintos de la RAM física — o uno puede estar en RAM y el otro swapeado a disco. Esta
indirección compra tres cosas a la vez: aislamiento (un proceso no puede ver ni corromper
la memoria de otro), la ilusión de espacio contiguo, y más address space del que tenés
RAM. Esta nota es la primera mirada; el mecanismo completo es un tema de kernel que el
proyecto del OS va a construir.
El reset: un puntero es una dirección virtual, no una ubicación física. Dos procesos pueden tener el mismo valor de puntero y referirse a memoria completamente distinta. El mapa plano del address space de las notas anteriores es por-proceso y enteramente virtual.
La ilusión: virtual vs físico
Los programas solo usan direcciones virtuales. En cada acceso a memoria, la MMU traduce la dirección virtual a una dirección física en RAM, usando tablas por-proceso que el kernel mantiene. Como el mapeo es por-proceso:
- El aislamiento es automático. El proceso A simplemente no tiene mapeo para las páginas del proceso B, así que no puede nombrarlas. Un puntero salvaje crashea tu propio proceso, no el sistema.
- El layout puede ser simple. Cada proceso ve el mapa limpio text/data/heap/…/stack de la nota de stack-vs-heap, arrancando cerca de 0, sin importar dónde cayeron físicamente sus páginas ni cuán fragmentada esté la RAM.
- Lo virtual puede exceder lo físico. Las páginas poco usadas viven en disco (swap) y se traen bajo demanda, así que el total de memoria virtual entre procesos puede exceder la RAM instalada.
Páginas: la unidad de mapeo
La memoria virtual no se mapea byte por byte — esa tabla sería enorme. La memoria se divide en páginas de tamaño fijo (4 KB en x86-64 y Apple Silicon; existen "huge pages" más grandes). Las page tables del kernel mapean cada página virtual a un frame físico, y la MMU las recorre en cada acceso, cacheando las traducciones recientes en el TLB (translation lookaside buffer) para que el caso común sea rápido.
- Una dirección virtual se parte en un número de página (cuál página) y un offset (dónde dentro de la página). Solo se traduce el número de página; el offset pasa tal cual.
- Una referencia a una página que no está en RAM dispara un page fault — la CPU trapea al kernel, que trae la página (de disco, o la rellena con ceros si es fresca) y reanuda tu instrucción como si nada.
- Cada página lleva permisos (read/write/execute). Escribir una página read-only (como el código) o una página sin mapear es lo que produce un segmentation fault.
Mirá el aislamiento: la misma dirección, dos valores
fork() duplica un proceso. El padre y el hijo después tienen la misma dirección virtual
para un global — pero escribirla en el hijo no afecta al padre, porque cada uno tiene su
propia página física (copiada al escribir). Compilá y corré:
// vmem.c — una dirección virtual, dos valores independientes entre procesos.
// gcc -O0 -Wall -Wextra vmem.c -o vmem && ./vmem
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int shared_global = 100; // misma dirección virtual en padre e hijo
int main(void) {
printf("page size: %ld bytes\n", sysconf(_SC_PAGESIZE));
printf("&shared_global = %p (value %d)\n\n",
(void*)&shared_global, shared_global);
fflush(stdout); // flush antes del fork, o el hijo hereda nuestro buffer
pid_t pid = fork();
if (pid == 0) { // hijo: misma dirección, su propia página física (COW)
shared_global = 999;
printf("[child pid=%d] &g = %p value = %d\n",
getpid(), (void*)&shared_global, shared_global);
return 0;
}
wait(NULL); // padre: la escritura del hijo no lo tocó
printf("[parent pid=%d] &g = %p value = %d\n",
getpid(), (void*)&shared_global, shared_global);
return 0;
}
page size: 4096 bytes
&shared_global = 0x105960000 (value 100)
[child pid=68695] &g = 0x105960000 value = 999
[parent pid=68694] &g = 0x105960000 value = 100
El mismo puntero 0x105960000 en los dos procesos, y sin embargo el hijo lee 999 y el
padre sigue leyendo 100. Eso es memoria virtual: la dirección es un índice en el mapa
privado de cada proceso, no una ubicación física. El copy-on-write que abarata al fork
— las páginas se comparten read-only hasta que un lado escribe, y ahí se duplican de forma
transparente — es la misma maquinaria de paginación en acción.
Dónde conecta (y qué queda diferido)
Esto es el cimiento debajo de buena parte del atlas, pero el mecanismo profundo es trabajo de kernel:
mmaple pide al kernel que agregue mapeos a tu address space directamente — memoria anónima (lo quemallocusa por debajo para pedidos grandes) o un archivo mapeado en memoria. Eso es un tema de systems programming.- Las page tables, el TLB, el manejo de page faults y la política de paginación se construyen desde cero en el proyecto del OS — cuando escribas el código de paginación, esta ilusión se vuelve algo que vos implementás.
- Las caches están físicamente entre la MMU y la RAM, así que la memoria virtual y la jerarquía de memoria interactúan: un TLB miss y un cache miss son frenos distintos en el mismo camino de acceso.
Modos de falla y trade-offs
- Segfault = acceso virtual inválido. Desreferenciar NULL, un puntero colgado, o pasarte de un array toca una página sin mapear o con permiso equivocado; la MMU trapea y el kernel mata al proceso. Es el hardware protegiendo a otros procesos de tu bug.
- La traducción tiene un costo. Cada acceso se traduce; un TLB miss fuerza un recorrido de la page table. La mala localidad thrashea el TLB igual que thrashea las caches — otra razón por la que los patrones de acceso importan.
- El swapping (thrashing) es un precipicio. Cuando el working set excede la RAM, el kernel pagina a disco constantemente y el throughput colapsa — el mundo de los nanosegundos cayendo a milisegundos, como en la nota de la jerarquía.
- Las direcciones no son estables ni comparables entre procesos. Una dirección virtual tiene sentido solo dentro de su proceso; nunca compartas punteros crudos entre procesos (usá shared memory con su propio mapeo). El ASLR además las randomiza en cada corrida.
En la práctica
- Pensá "mis punteros son direcciones virtuales privadas". Ese solo reencuadre explica el aislamiento, por qué los segfaults son locales, y por qué los punteros de dos procesos no se relacionan.
- Razoná la memoria en páginas. La asignación, la protección (
mprotect), el mapeo de archivos y el comportamiento ante faults son todos a granularidad de página; la página de 4 KB es el quantum real de memoria del OS. - Un segfault es un diagnóstico, no un misterio. Significa "nombraste una dirección virtual que no tenías permitido tocar" — casi siempre un puntero malo; agarrá ASan/el debugger.
- Esto cierra el modelo de máquina. Ahora tenés CPU, registros, la jerarquía de memoria, representación de datos, cómo el fuente se vuelve ejecución, y el address space donde viven esos bytes — el sustrato sobre el que la capa de C construye a continuación.
Conecta con: Modelo de Máquina · Stack vs heap · La jerarquía de memoria · Cómo el fuente se vuelve ejecución · Systems Programming · OS desde Cero
Fuentes
- Arpaci-Dusseau — Operating Systems: Three Easy Pieces, "Virtualizing Memory" — el tratamiento gratuito más claro de address spaces, paginación, TLBs y page faults; la columna de esta nota. https://pages.cs.wisc.edu/~remzi/OSTEP/
- Bryant & O'Hallaron — Computer Systems: A Programmer's Perspective (CS:APP), cap. 9 — la memoria virtual desde la vista del programador, con la mecánica de x86-64. https://csapp.cs.cmu.edu/
- Ulrich Drepper — What Every Programmer Should Know About Memory — cómo la memoria virtual, el TLB y las caches interactúan en hardware real. https://people.freebsd.org/~lstewart/articles/cpumemory.pdf
- Intel 64 and IA-32 Architectures Software Developer's Manual, Vol. 3A — la especificación autoritativa de paginación y MMU para x86-64. https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
- man pages —
mmap(2),fork(2),mprotect(2)— los syscalls que manipulan el address space de un proceso. https://man7.org/linux/man-pages/man2/mmap.2.html