FreeBSD VM system - vmspace, vm_map y vm_map_entry
Como parte de comprender como funciona la administracion de memoria virtual en FreeBSD a nivel de codigo, y como una preface a las “shadow chains” y “vm_object”, veremos como en el kernel de FreeBSD se implementaron y funcionan las estructuras vmspace, vm_map y vm_map_entry.
In computing, virtual memory is a memory management technique that is implemented using both hardware and software. It maps memory addresses used by a program, called virtual addresses, into physical addresses in computer memory but it lacks code details
En si, este proceso de traduccion de direcciones virtuales a fisicas se realiza con el MMU (Memory Management Unit). Como sabras, en los sistemas operativos modernos cada proceso tiene su propio espacio de direcciones virtuales (VAS), que forma parte de un mecanismo del espacio de usuario para aislar procesos entre si y garantizar que un proceso externo no pueda (si se puede) realizar operaciones de lectura y escritura sobre otro proceso, FreeBSD no es la excepcion, incluso, en el caso de FreeBSD pueden ser mapeados diferentes fuente de datos (objetos) como archivos o piezas anonimas y privadas de la swap y este ultimo punto es una de las diferencias entre FreeBSD y otros OS, ya que este caso, la RAM no se asigna permanentemente a un proceso u objeto, si no que aqui funciona como una cache para las paginas (vm_page) de los objetos virtuales (vm_object) lo que permite reutilizar la memoria y reducir el tiempo de acceso a datos que son usados con frecuencia, y en escencia un manejo mas eficiente de la memoria fisica.
Por parte del espacio de kernel y espacio de usuario, se usan las mismas estructuras que son; vmspace, vm_map, vm_map_entry, vm_object (tambien shadow) y vm_page, estas son la clave de como se estructura la memoria virtual en FreeBSD.
vmspace
Es una estructura de alto nivel que integra componentes dependientes e independientes del hardware para gestionar y llevar un seguimiento del VAS de un proceso. Esta es la estructura base del VAS de cada proceso. Cuando se menciona que “integra componentes independientes”, esto quiere decir que no dependen de las caracteristicas fisicas del CPU o memoria fisica, si no que son una abstraccion del diseño del OS, por ejemplo “vm_map” que veremos mas adelante.
Se define de la siguiente forma en el archivo vm_map.h en la linea 298:
struct vmspace {
struct vm_map vm_map; /* VM address map */
struct shmmap_state *vm_shm; /* SYS5 shared memory private data XXX */
segsz_t vm_swrss; /* resident set size before last swap */
segsz_t vm_tsize; /* text size (pages) XXX */
segsz_t vm_dsize; /* data size (pages) XXX */
segsz_t vm_ssize; /* stack size (pages) */
caddr_t vm_taddr; /* (c) user virtual address of text */
caddr_t vm_daddr; /* (c) user virtual address of data */
caddr_t vm_maxsaddr; /* user VA at max stack growth */
vm_offset_t vm_stacktop; /* top of the stack, may not be page-aligned */
vm_offset_t vm_shp_base; /* shared page address */
u_int vm_refcnt; /* number of references */
/*
* Keep the PMAP last, so that CPU-specific variations of that
* structure on a single architecture don't result in offset
* variations of the machine-independent fields in the vmspace.
*/
struct pmap vm_pmap; /* private physical map */
};
De momento solo nos centraremos en el campo struct vm_map vm_map; y struct pmap vm_pmap;, el primero contiene el mapeo completo de las direcciones virtuales del proceso, otra forma de explicarlo es que contiene todo el rango usable de direcciones virtuales para ese proceso.
¿Como se inicializa vmspace?
Basicamente se inicializa en el contexto de la creacion de un nuevo proceso con fork(), sin embargo, cuando un proceso hijo es creado a partir de un proceso padre, esto se hace mediante la funcion vmspace_fork(), que permite copiar la informacion del VAS y virtual map del padre al hijo. Se declara en la linea 4344 del archivo vm_map.c:
struct vmspace *
vmspace_fork(struct vmspace *vm1, vm_ooffset_t *fork_charge)
{
struct vmspace *vm2;
vm_map_t new_map, old_map;
vm_map_entry_t new_entry, old_entry;
vm_object_t object;
int error, locked __diagused;
vm_inherit_t inh;
old_map = &vm1->vm_map;
/* Copy immutable fields of vm1 to vm2. */
vm2 = vmspace_alloc(vm_map_min(old_map), vm_map_max(old_map),
pmap_pinit);
if (vm2 == NULL)
return (NULL);
vm2->vm_taddr = vm1->vm_taddr;
vm2->vm_daddr = vm1->vm_daddr;
vm2->vm_maxsaddr = vm1->vm_maxsaddr;
vm2->vm_stacktop = vm1->vm_stacktop;
vm2->vm_shp_base = vm1->vm_shp_base;
vm_map_lock(old_map);
if (old_map->busy)
vm_map_wait_busy(old_map);
new_map = &vm2->vm_map;
locked = vm_map_trylock(new_map); /* trylock to silence WITNESS */
KASSERT(locked, ("vmspace_fork: lock failed"));
error = pmap_vmspace_copy(new_map->pmap, old_map->pmap);
if (error != 0) {
sx_xunlock(&old_map->lock);
sx_xunlock(&new_map->lock);
vm_map_process_deferred();
vmspace_free(vm2);
return (NULL);
}
new_map->anon_loc = old_map->anon_loc;
new_map->flags |= old_map->flags & (MAP_ASLR | MAP_ASLR_IGNSTART |
MAP_ASLR_STACK | MAP_WXORX);
VM_MAP_ENTRY_FOREACH(old_entry, old_map) {
if ((old_entry->eflags & MAP_ENTRY_IS_SUB_MAP) != 0)
panic("vm_map_fork: encountered a submap");
inh = old_entry->inheritance;
if ((old_entry->eflags & MAP_ENTRY_GUARD) != 0 &&
inh != VM_INHERIT_NONE)
inh = VM_INHERIT_COPY;
switch (inh) {
case VM_INHERIT_NONE:
break;
case VM_INHERIT_SHARE:
/*
* Clone the entry, creating the shared object if
* necessary.
*/
object = old_entry->object.vm_object;
if (object == NULL) {
vm_map_entry_back(old_entry);
object = old_entry->object.vm_object;
}
/*
* Add the reference before calling vm_object_shadow
* to insure that a shadow object is created.
*/
vm_object_reference(object);
if (old_entry->eflags & MAP_ENTRY_NEEDS_COPY) {
vm_object_shadow(&old_entry->object.vm_object,
&old_entry->offset,
old_entry->end - old_entry->start,
old_entry->cred,
/* Transfer the second reference too. */
true);
old_entry->eflags &= ~MAP_ENTRY_NEEDS_COPY;
old_entry->cred = NULL;
/*
* As in vm_map_merged_neighbor_dispose(),
* the vnode lock will not be acquired in
* this call to vm_object_deallocate().
*/
vm_object_deallocate(object);
object = old_entry->object.vm_object;
} else {
VM_OBJECT_WLOCK(object);
vm_object_clear_flag(object, OBJ_ONEMAPPING);
if (old_entry->cred != NULL) {
KASSERT(object->cred == NULL,
("vmspace_fork both cred"));
object->cred = old_entry->cred;
object->charge = old_entry->end -
old_entry->start;
old_entry->cred = NULL;
}
/*
* Assert the correct state of the vnode
* v_writecount while the object is locked, to
* not relock it later for the assertion
* correctness.
*/
if (old_entry->eflags & MAP_ENTRY_WRITECNT &&
object->type == OBJT_VNODE) {
KASSERT(((struct vnode *)object->
handle)->v_writecount > 0,
("vmspace_fork: v_writecount %p",
object));
KASSERT(object->un_pager.vnp.
writemappings > 0,
("vmspace_fork: vnp.writecount %p",
object));
}
VM_OBJECT_WUNLOCK(object);
}
/*
* Clone the entry, referencing the shared object.
*/
new_entry = vm_map_entry_create(new_map);
*new_entry = *old_entry;
new_entry->eflags &= ~(MAP_ENTRY_USER_WIRED |
MAP_ENTRY_IN_TRANSITION);
new_entry->wiring_thread = NULL;
new_entry->wired_count = 0;
if (new_entry->eflags & MAP_ENTRY_WRITECNT) {
vm_pager_update_writecount(object,
new_entry->start, new_entry->end);
}
vm_map_entry_set_vnode_text(new_entry, true);
/*
* Insert the entry into the new map -- we know we're
* inserting at the end of the new map.
*/
vm_map_entry_link(new_map, new_entry);
vmspace_map_entry_forked(vm1, vm2, new_entry);
/*
* Update the physical map
*/
pmap_copy(new_map->pmap, old_map->pmap,
new_entry->start,
(old_entry->end - old_entry->start),
old_entry->start);
break;
case VM_INHERIT_COPY:
/*
* Clone the entry and link into the map.
*/
new_entry = vm_map_entry_create(new_map);
*new_entry = *old_entry;
/*
* Copied entry is COW over the old object.
*/
new_entry->eflags &= ~(MAP_ENTRY_USER_WIRED |
MAP_ENTRY_IN_TRANSITION | MAP_ENTRY_WRITECNT);
new_entry->wiring_thread = NULL;
new_entry->wired_count = 0;
new_entry->object.vm_object = NULL;
new_entry->cred = NULL;
vm_map_entry_link(new_map, new_entry);
vmspace_map_entry_forked(vm1, vm2, new_entry);
vm_map_copy_entry(old_map, new_map, old_entry,
new_entry, fork_charge);
vm_map_entry_set_vnode_text(new_entry, true);
break;
case VM_INHERIT_ZERO:
/*
* Create a new anonymous mapping entry modelled from
* the old one.
*/
new_entry = vm_map_entry_create(new_map);
memset(new_entry, 0, sizeof(*new_entry));
new_entry->start = old_entry->start;
new_entry->end = old_entry->end;
new_entry->eflags = old_entry->eflags &
~(MAP_ENTRY_USER_WIRED | MAP_ENTRY_IN_TRANSITION |
MAP_ENTRY_WRITECNT | MAP_ENTRY_VN_EXEC |
MAP_ENTRY_SPLIT_BOUNDARY_MASK);
new_entry->protection = old_entry->protection;
new_entry->max_protection = old_entry->max_protection;
new_entry->inheritance = VM_INHERIT_ZERO;
vm_map_entry_link(new_map, new_entry);
vmspace_map_entry_forked(vm1, vm2, new_entry);
new_entry->cred = curthread->td_ucred;
crhold(new_entry->cred);
*fork_charge += (new_entry->end - new_entry->start);
break;
}
}
/*
* Use inlined vm_map_unlock() to postpone handling the deferred
* map entries, which cannot be done until both old_map and
* new_map locks are released.
*/
sx_xunlock(&old_map->lock);
sx_xunlock(&new_map->lock);
vm_map_process_deferred();
return (vm2);
}
Como puedes ver es un monton, que en resumen, como habia dicho antes “permite copiar la informacion del VAS y virtual map del padre al hijo”, que en el codigo las variables vm1 y vm2 son las que contiene la informacion del VAS del padre e hijo respectivamente. Lo primero que se hace es crear un nuevo vmspace para el hijo
vm2 = vmspace_alloc(vm_map_min(old_map), vm_map_max(old_map), pmap_pinit);
La funcion vmspace_alloc de define como
vmspace_alloc(vm_offset_t min, vm_offset_t max, pmap_pinit_t pinit)
{
struct vmspace *vm;
vm = uma_zalloc(vmspace_zone, M_WAITOK);
KASSERT(vm->vm_map.pmap == NULL, ("vm_map.pmap must be NULL"));
if (!pinit(vmspace_pmap(vm))) {
uma_zfree(vmspace_zone, vm);
return (NULL);
}
CTR1(KTR_VM, "vmspace_alloc: %p", vm);
_vm_map_init(&vm->vm_map, vmspace_pmap(vm), min, max);
refcount_init(&vm->vm_refcnt, 1);
vm->vm_shm = NULL;
vm->vm_swrss = 0;
vm->vm_tsize = 0;
vm->vm_dsize = 0;
vm->vm_ssize = 0;
vm->vm_taddr = 0;
vm->vm_daddr = 0;
vm->vm_maxsaddr = 0;
return (vm);
}
En esta creacion del nuevo vmspace, tambien se incluye la inicializacion de los vm_map y pmap para el proceso hijo usando las funciones _vm_map_init. Tambien me gustaria destacar la funcion uma_zalloc que asigna memoria de un conjunto de objetos prealocados como parte de UMA (Universal Memory Allocator).
Asi mismo me gustaria recalcar la lineas
KASSERT(vm->vm_map.pmap == NULL, ("vm_map.pmap must be NULL"));
Que usando la macro KASSERT se verifica si que el pmap del vm_map se haya inicializado correctamente, mas concreto que no sea NULL, esto es importante por que uma_zalloc asigna memoria desde vmspace_zone, y cuando uma_zalloc devuelva la estructura que apunta a la memoria, debe estar limpia, que en la practica se puede llenar con ceros, de lo contrario indica que el vmspace esta corrupto.
¿Que es lo que se copia del VAS al proceso hijo exactamente?
Eso lo podemos ver en las siguientes lineas
vm2->vm_taddr = vm1->vm_taddr;
vm2->vm_daddr = vm1->vm_daddr;
vm2->vm_maxsaddr = vm1->vm_maxsaddr;
vm2->vm_stacktop = vm1->vm_stacktop;
vm2->vm_shp_base = vm1->vm_shp_base;
vm_map_lock(old_map);
- vm_taddr: Direccion base del segmento de texto
- vm_daddr: Direccion base del segmento de datos
- vm_maxsaddr: Direccion del limite inferior de la pila
- vm_stacktop: Direccion inicial de la pila
- vm_shp_base: Direccion base de la memoria compartida
¿Que es lo que se copia del vm_map al proceso hijo?
Esto lo podemos ver dentro de las lineas
VM_MAP_ENTRY_FOREACH(old_entry, old_map) {
...
}
Como su nombre lo indica, permite recorrer todas las entrada del PMAP, y dependiendo del tipo de herencia del proceso padre al hijo, sera la accion,
- VM_INHERIT_SHARE: Se crea una referencia compartida a las mismas páginas de memoria fisica
- VM_INHERIT_COPY: Esto va relacionado con el anterior, en caso de que alguno de los dos procesos modifica las paginas de memoria fisica, se crea una copia privada (aislada del otro proceso) para el proceso que las modifico, esto se hace mediante COW (Copy-On-Write)
- VM_INHERIT_ZERO: Crea una nueva region de memoria anonima vacia, con anonima se refiere a que no esta asociado a ningun descriptor de archivos y en general a ningun recurso del sistema de archivos
Sabiendo todo esto ya podemos pasar al vm_map
vm_map
Describe el VAS de un proceso de manera independiente del hardware, que como mencione antes “contiene el mapeo completo de las direcciones virtuales del proceso”. En si vm_map apunta a una lista enlazada ordenada de estructuras vm_map_entry y a un arbol de busqueda binario como se oberva en la imagen

Se declara como
struct vm_map {
struct vm_map_entry header; /* List of entries */
union {
struct sx lock; /* Lock for map data */
struct mtx system_mtx;
};
int nentries; /* Number of entries */
vm_size_t size; /* virtual size */
u_int timestamp; /* Version number */
u_int flags; /* flags for this vm_map */
vm_map_entry_t root; /* Root of a binary search tree */
pmap_t pmap; /* (c) Physical map */
vm_offset_t anon_loc;
int busy;
#ifdef DIAGNOSTIC
int nupdates;
#endif
};
El primer miembro (struct vm_map_entry header;) apunta a la cabeza de la lista de enlazada de vm_map_entry, donde cada nodo de la lista representa un rango de direcciones contiguas, lo que permite relizar busquedas entre las diferentes regiones de memoria virtual. Otro miembro importante del struct es vm_size_t size;, que contiene el tamaño total en bytes del VAS
¿Donde se inicializa?
Mediante la funcion _vm_map_init:
static void _vm_map_init(vm_map_t map, pmap_t pmap, vm_offset_t min, vm_offset_t max)
{
map->header.eflags = MAP_ENTRY_HEADER;
map->pmap = pmap;
map->header.end = min;
map->header.start = max;
map->flags = 0;
map->header.left = map->header.right = &map->header;
map->root = NULL;
map->timestamp = 0;
map->busy = 0;
map->anon_loc = 0;
#ifdef DIAGNOSTIC
map->nupdates = 0;
#endif
}
Tiene como objetivo configurar los valores iniciales de un vm_map y pmap, aqui quiero destacar la flag MAP_ENTRY_HEADER, que es una macro declarada como
#define MAP_ENTRY_HEADER 0x00080000
La cual indica tracked writeable mapping, eso quiere decir que el vm_map esta siendo rastreado por el kernel para detectar accesos y modificaciones.
vm_map_entry
Representa el rango de direcciones virtuales contiguas, donde cada entrada de vm_map_entry apunta a una cadena de objetos en memoria (vm_object), que describe el origen de los datos mepeados en rango de direcciones indicado, ese rango se define en el miembro vm_offset_t start; y vm_offset_t end; de vm_map_entry. El miembro mas relevante de la estructura es el union union vm_map_object object;, que es tal cual la abstraccion del objeto que se esta mapenado en el rango de direcciones virtuales, este puede ser cualquier fuente de datos, podria ser un archivo, ejecutable, objeto swap si se esta mapeando memoria anonima, incluso un dispositivo que esta mapeando al frame buffer. Otro miembro que es importante de la estructura es vm_ooffset_t offset, que indica el desplazamiento desde donde se empezo a mapeo de las direcciones virtuales. Una mejor representacion se observa en la siguiente imagen

Observa como dos vm_map_entry apuntan al mismo vm_object pero con diferente offset, esto toma sentido por que si dos procesos estan compartiendo memoria, esos dos procesos pueden hacer referencia al mismo offset, lo que permite saber que si un proceso cambia algo, el otro puede ver los cambios.
Eso ha sido todo, proximamente veremos las estructuras faltantes.
Referencias
- https://www.leidinger.net/FreeBSD/dox/vm/html/index.html
- https://docs-archive.freebsd.org/doc/6.2-RELEASE/usr/share/doc/en/articles/vm-design/vm-objects.html
- The Design and Implementation of the FreeBSD Operating System (2nd Edition)