1. Dos niveles: asignador y núcleo
Un asignador de memoria es una pieza de software que entrega bloques a la aplicación y administra su reutilización. Para disponer de espacio puede obtener regiones del sistema operativo, pero un bloque entregado por malloc no tiene por qué corresponder a una región del núcleo.
La aplicación pide un tamaño y recibe un puntero. El asignador se ocupa de alinear el bloque, recordar información administrativa, encontrar espacio disponible y coordinar accesos concurrentes si su implementación lo necesita. El núcleo, por su parte, administra regiones virtuales, permisos y páginas.
| Nivel | Unidad habitual | Operaciones de ejemplo | Responsabilidad |
|---|---|---|---|
| Aplicación | Objeto o arreglo | malloc, calloc, realloc, free | Conocer la vida útil y respetar los límites |
| Asignador | Bloques y regiones administradas | Reutilizar, dividir, fusionar, ampliar | Entregar bloques válidos con poco costo y desperdicio |
| Núcleo | Intervalos virtuales y páginas | brk, mmap, munmap, madvise | Gestionar el espacio virtual y su respaldo |
Si una región tiene 64 KiB, el asignador puede atender muchas peticiones pequeñas dentro de ella sin volver al núcleo. Al liberar un bloque, puede guardarlo para otra petición. Esta intermediación amortiza el costo de las llamadas al sistema y evita exigir una página completa por cada objeto pequeño.
En este tema observaremos Linux y el asignador de glibc. La especificación de malloc no exige usar brk, sbrk o mmap. Otros sistemas y bibliotecas pueden organizar la memoria de otra manera.
2. El program break y el heap tradicional
El program break es el límite lógico superior de la región de datos que puede crecer mediante brk. Al aumentarlo se amplía ese espacio; al reducirlo se retira su extremo superior. En los mapas de Linux esa región suele aparecer con la etiqueta [heap].
Antes: [ datos / BSS ][ espacio administrado por el heap ] | break
Después: [ datos / BSS ][ espacio administrado por el heap ][ nuevo ] | break
Dentro del espacio administrado:
[ bloque A ocupado ][ bloque B libre ][ bloque C ocupado ][ extremo libre ]
No se trata de un mapa a escala ni de una disposición universal: ASLR, las regiones de carga y la arquitectura influyen. La propiedad relevante es que el break es un límite. No permite liberar un hueco interior haciendo que el espacio posterior siga perteneciendo al mismo intervalo de crecimiento.
Si B está libre y C sigue ocupado por encima, no podemos bajar el break hasta el comienzo de B sin retirar también C. B puede reutilizarse para futuros bloques. Si, en cambio, queda espacio libre al final, el asignador puede evaluar un recorte de ese extremo.
La página es una unidad del núcleo y el break se expresa como dirección, con granularidad lógica de bytes. Pequeños movimientos dentro de una misma página no implican necesariamente mapear o desmapear otra página completa. Y obtener direcciones adicionales no obliga a materializar inmediatamente un marco físico privado por cada página.
3. brk y sbrk: interfaces y resultados
int brk(void *direccion);
void *sbrk(intptr_t incremento);
| Operación | Significado | Éxito | Error |
|---|---|---|---|
brk(direccion) | Solicitar un límite absoluto | 0 | -1, con errno |
sbrk(incremento) | Mover el límite por una cantidad con signo | El break anterior | (void *) -1, con errno |
sbrk(0) | Consultar el break sin ampliarlo | El break actual | Se comprueba el mismo indicador de error |
sbrk(4096) devuelve el inicio del espacio agregado si tiene éxito; no devuelve el nuevo extremo superior. No se compara su resultado con NULL. Un incremento negativo solicita reducir, pero no debe usarse sin saber quién posee el intervalo que se pretende retirar.
sbrk es una interfaz de biblioteca construida sobre la gestión del break; en Linux no es una llamada al sistema independiente. Además, el wrapper de glibc para brk adapta el resultado de la llamada del núcleo: la interfaz de biblioteca devuelve cero o menos uno, mientras que la llamada Linux sin ese wrapper tiene otra convención de retorno. Los ejemplos usan la biblioteca, no syscall directamente.
Son interfaces históricas y no forman parte del C estándar ni del POSIX moderno. Aquí se utilizan para comprender el mecanismo. En código de aplicación normal se prefiere la familia malloc o un mapeo propio cuando la granularidad y el control de páginas lo justifican.
4. Un experimento aislado con sbrk
El break es un recurso compartido por el proceso, no una reserva privada para cada biblioteca o hilo. Si se mueve manualmente mientras malloc también lo administra, se puede retirar memoria que el asignador cree válida o desordenar su estado. Por eso el siguiente programa es independiente, tiene un solo hilo y no llama a funciones de asignación ni de E/S entre la ampliación exitosa y el recorte.
Se agregan dos páginas, se escribe dentro del intervalo y se restaura el límite que devolvió sbrk. Solo después se imprime el resultado. No debe incorporarse esta secuencia a un programa que ya utiliza el heap con otros componentes.
#define _DEFAULT_SOURCE
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
long n = sysconf(_SC_PAGESIZE);
if (n <= 0 || (uintmax_t) n > (uintmax_t) INTPTR_MAX / 2) {
fprintf(stderr, "Tamaño de página no utilizable\n");
return 1;
}
intptr_t incremento = (intptr_t) n * 2;
void *anterior = sbrk(incremento);
if (anterior == (void *) -1) {
perror("sbrk"); return 1;
}
/* No printf, malloc ni otras operaciones que puedan usar el heap aquí. */
uintptr_t direccion = (uintptr_t) anterior;
volatile unsigned char *p = anterior;
p[0] = 17;
p[(size_t) incremento - 1] = 29;
unsigned suma = (unsigned) p[0] + p[(size_t) incremento - 1];
if (brk(anterior) == -1) {
perror("brk al restaurar"); return 1;
}
/* No volver a acceder a p ni al intervalo que acabamos de retirar. */
printf("Inicio agregado: 0x%" PRIxPTR "; bytes: %" PRIdPTR "; suma: %u\n",
direccion, incremento, suma);
puts("El break se restauró antes de imprimir.");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -O2 experimento_sbrk.c -o experimento_sbrk
$ ./experimento_sbrk
Inicio agregado: 0x55...; bytes: 8192; suma: 46
El break se restauró antes de imprimir.
La dirección cambia entre ejecuciones. La conversión a uintptr_t permite conservar un valor numérico para mostrarlo después de retirar el intervalo; no se vuelve a utilizar el puntero a los bytes retirados. volatile conserva los accesos del experimento frente a optimizaciones, pero no resuelve la concurrencia ni hace segura la mezcla con otros gestores del heap.
5. Regiones independientes con mmap
Como vimos en el tema 42, un mmap anónimo privado crea una región virtual con bytes iniciales en cero. No necesita mover el program break. La región puede retirarse con munmap sin que deba ser el extremo del heap tradicional.
El siguiente programa pide 10 000 bytes útiles y calcula explícitamente el tamaño redondeado a página. Conserva ambos tamaños: uno define los límites de sus datos y el otro describe la reserva que liberará.
#define _DEFAULT_SOURCE
#include <stdint.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
int main(void)
{
long n = sysconf(_SC_PAGESIZE);
if (n <= 0) {
fprintf(stderr, "Tamaño de página no disponible\n"); return 1;
}
size_t pagina = (size_t) n;
size_t utiles = 10000;
if (utiles > SIZE_MAX - (pagina - 1)) {
fprintf(stderr, "Desbordamiento al redondear\n"); return 1;
}
size_t reserva = ((utiles + pagina - 1) / pagina) * pagina;
unsigned char *p = mmap(NULL, reserva, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED) { perror("mmap"); return 1; }
for (size_t i = 0; i < utiles; i++)
p[i] = (unsigned char) (i % 251);
printf("Útiles: %zu; reserva: %zu; base: %p; último: %u\n",
utiles, reserva, (void *) p, (unsigned) p[utiles - 1]);
if (munmap(p, reserva) == -1) { perror("munmap"); return 1; }
return 0;
}
$ gcc -std=c17 -Wall -Wextra -O2 region_mmap.c -o region_mmap
$ ./region_mmap
Útiles: 10000; reserva: 12288; base: 0x7f...; último: 210
La diferencia es de 2288 bytes en ese entorno. No se debe acceder más allá del tamaño útil de un objeto por el hecho de que la reserva ocupe páginas completas. En un asignador real también hay alineación, metadatos y quizá varios bloques dentro de una región, por lo que el costo total no se deduce solo de este ejemplo.
Si un malloc se atendió internamente mediante un mapeo, su puntero puede apuntar después de una cabecera. La aplicación no conoce necesariamente la base y el tamaño que el asignador utiliza. Debe llamar a free, no intentar adivinar un munmap.
6. Comparar los dos mecanismos
| Aspecto | Espacio obtenido moviendo el break | Mapeo anónimo independiente |
|---|---|---|
| Organización virtual | Amplía o reduce el extremo de una región | Obtiene un intervalo elegido por el núcleo |
| Liberación por el mecanismo | Bajar el break retira el extremo superior | munmap puede retirar una región sin depender del break |
| Bloques pequeños | Puede proporcionar espacio para muchos bloques | También puede respaldar una región subdividida en muchos bloques |
| Asignación grande independiente | Puede dificultar recortar si quedan bloques por encima | Puede retirarse por separado cuando se libera |
| Costo por objeto | Se amortiza al pedir espacio por lotes | Un mapeo por objeto pequeño agregaría llamadas, páginas y administración |
| Memoria física | Normalmente materializada bajo demanda | Normalmente materializada bajo demanda |
mmap no implica necesariamente «un objeto grande». Un asignador puede obtener una región grande mediante mmap y subdividirla en objetos pequeños. En glibc, además de los mapeos independientes para ciertas peticiones, hay regiones de arenas que pueden obtenerse mediante mmap. El tema 47 profundizará en esa organización.
El límite de recortar por el extremo se refiere al mecanismo del break. El núcleo también permite descartar respaldo físico de páginas adecuadas mediante otras interfaces sin retirar sus direcciones virtuales. Esta diferencia será importante al interpretar el RSS.
7. Cuándo malloc necesita memoria del sistema
Una petición puede resolverse sin una llamada nueva al núcleo. Conceptualmente, el asignador puede seguir un camino como este, aunque los detalles varían:
- Ajustar el tamaño. Considera alineación, información administrativa y límites aritméticos.
- Buscar espacio reutilizable. Puede usar un bloque liberado, una caché o espacio libre de una región existente.
- Obtener más espacio si hace falta. Amplía una región o crea otra según sus políticas.
- Entregar el bloque. El puntero de usuario puede ser distinto de la base de la región que obtuvo del núcleo.
En glibc existe un umbral relacionado con las asignaciones independientes mediante mmap. Es habitual encontrar 128 KiB como valor inicial documentado, pero no es una ley de malloc ni una garantía de ruta para cada petición. La disponibilidad de bloques, los límites de mapeos, las arenas, la versión y las opciones de configuración también influyen.
El umbral puede ajustarse dinámicamente con el historial de asignación. Ciertas opciones fijadas explícitamente desactivan ese ajuste. Incluso peticiones pequeñas pueden apoyarse en regiones obtenidas por mmap: hay que distinguir el origen de una arena de un mapeo dedicado a un bloque.
$ GLIBC_TUNABLES=glibc.malloc.mmap_threshold=262144 ./observar_malloc
Este comando modifica una política del asignador en esa ejecución; no cambia la máquina globalmente. No se propone como optimización universal. Una medición debe registrar la versión y la configuración, y comprobar la ruta efectiva en lugar de inferirla solo a partir del tamaño.
8. Observar malloc desde C
Este programa pide bloques de 64 bytes, 4096 bytes y 4 MiB. Muestra el break en distintas etapas y toca los bloques para que la prueba incluya accesos reales. En el modo pausa espera Enter en cada etapa para permitir inspeccionar los mapas.
Aquí sbrk(0) solo consulta: no movemos manualmente el break ni intentamos restaurarlo. Las operaciones de biblioteca y el arranque también pueden asignar memoria. Para reducir un efecto frecuente de la salida, se configura stdout sin búfer antes de observar.
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void etapa(const char *nombre, int pausa)
{
void *limite = sbrk(0);
if (limite == (void *) -1)
perror("sbrk(0)");
else
printf("%s: break = %p\n", nombre, limite);
if (pausa) {
puts("Pulsa Enter para continuar.");
getchar();
}
}
static void tocar(void *direccion, size_t tam, size_t pagina)
{
volatile unsigned char *p = direccion;
for (size_t i = 0; i < tam; i += pagina)
p[i] = 1;
p[tam - 1] = 2;
}
int main(int argc, char *argv[])
{
if (argc > 2 || (argc == 2 && strcmp(argv[1], "pausa") != 0)) {
fprintf(stderr, "Uso: %s [pausa]\n", argv[0]); return 1;
}
setvbuf(stdout, NULL, _IONBF, 0);
long n = sysconf(_SC_PAGESIZE);
if (n <= 0) {
fprintf(stderr, "Tamaño de página no disponible\n"); return 1;
}
size_t pagina = (size_t) n;
int pausa = argc == 2;
printf("PID: %ld\n", (long) getpid());
etapa("Inicio", pausa);
void *a = malloc(64);
void *b = malloc(4096);
void *c = malloc(4UL * 1024 * 1024);
if (a == NULL || b == NULL || c == NULL) {
fprintf(stderr, "No se pudieron obtener los tres bloques\n");
free(c); free(b); free(a); return 1;
}
tocar(a, 64, pagina);
tocar(b, 4096, pagina);
tocar(c, 4UL * 1024 * 1024, pagina);
printf("a = %p; b = %p; c = %p\n", a, b, c);
etapa("Tres bloques vivos", pausa);
free(b);
b = NULL;
etapa("Liberado el bloque de 4096 bytes", pausa);
free(c);
c = NULL;
etapa("Liberado el bloque de 4 MiB", pausa);
free(a);
a = NULL;
etapa("Todos los bloques liberados", pausa);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -O2 observar_malloc.c -o observar_malloc
$ ./observar_malloc pausa
PID: 4217
Inicio: break = 0x55...
Pulsa Enter para continuar.
... direcciones y etapas de esta ejecución ...
No hay una lista universal de direcciones o de movimientos del break que debas obtener. Si el bloque grande se atiende con mmap, puede desaparecer su mapeo al liberarlo aunque el break no cambie. Si había espacio adecuado ya disponible, alguna petición puede resolverse sin ampliar nada.
Los punteros se muestran mientras los bloques están vivos. Después de free se dejan en NULL y no se consultan sus bytes. Conservar el número de una dirección para estudiar una traza no autoriza a seguir accediendo al objeto liberado.
9. Interpretar strace y los mapas
$ strace -e trace=brk,mmap,munmap,madvise ./observar_malloc
... llamadas de arranque y carga de bibliotecas ...
brk(NULL) = 0x55...
brk(0x55...) = 0x55...
mmap(NULL, 4198400, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f...
... etapas del programa ...
munmap(0x7f..., 4198400) = 0
El extracto es ilustrativo: tamaños, orden y número de llamadas dependen de la ejecución. El tamaño mapeado puede superar los 4 MiB pedidos por alineación y metadatos. Además, el cargador utiliza mmap para bibliotecas y otras regiones, así que no todo mmap observado corresponde a uno de nuestros bloques.
No verás una llamada Linux llamada sbrk: en este contexto es una interfaz de biblioteca. La consulta de glibc puede utilizar información que mantiene sobre el break y no generar una llamada nueva en cada consulta. Tampoco verás necesariamente una llamada al sistema por cada malloc o free.
$ cat /proc/4217/maps
$ less /proc/4217/smaps
$ pmap -x 4217
Compara las direcciones de los bloques vivos con los intervalos de maps. La etiqueta [heap] ayuda a identificar el espacio del break. Un bloque puede estar en una región anónima sin esa etiqueta. Que dos bloques estén en una misma región no los convierte en un único objeto C.
El número de líneas del mapa tampoco es el número de bloques ni de llamadas a mmap: regiones compatibles pueden fusionarse y otros cambios pueden dividirlas. Para relacionar eventos, combina las etapas del programa con la traza y con los intervalos observados.
10. Qué significa devolver memoria
Hay tres hechos diferentes que suelen mezclarse al hablar de «liberar memoria»:
- Terminar la vida del bloque. Después de free, la aplicación ya no puede usarlo.
- Conservar espacio reutilizable. El asignador puede mantener la región para atender nuevas peticiones sin volver al núcleo.
- Retirar direcciones o respaldo físico. Puede desmapear una región, recortar un extremo o descartar páginas físicas adecuadas conservando las direcciones.
Un bloque dedicado obtenido mediante mmap puede devolverse con munmap cuando se libera. Un hueco interior del heap no permite bajar el break si hay bloques vivos por encima, pero puede reutilizarse. El asignador también puede conservar un extremo libre para evitar alternar constantemente entre ampliar y reducir.
En glibc, malloc_trim permite intentar devolver memoria libre al sistema. Es una extensión GNU, no parte de C. Su resultado es 1 si se liberó memoria y 0 si no se pudo liberar ninguna; cero no se interpreta como el error convencional de una llamada al sistema.
#include <malloc.h>
int devuelto = malloc_trim(0);
printf("malloc_trim: %d\n", devuelto);
La operación puede utilizar recorte o madvise para páginas libres apropiadas. No garantiza devolver una cantidad solicitada ni significa que todos los bloques libres desaparezcan. Tampoco arregla pérdidas de memoria: no puede liberar los objetos que siguen asignados porque la aplicación olvidó sus punteros.
El RSS cuenta páginas residentes y el tamaño virtual cuenta intervalos. Puede bajar el RSS sin bajar el break si se descarta respaldo, o puede aumentar la cantidad de bloques libres internos sin que ninguna de esas métricas baje de inmediato. Un RSS que no vuelve al valor inicial después de free no prueba, por sí solo, una fuga.
11. Simulador de regiones y bloques
Este modelo utiliza un heap que crece por lotes de 64 KiB y un umbral configurable para mapeos dedicados. Cada petición se redondea a 4 KiB para que las cuentas sean visibles. Las peticiones por debajo del umbral buscan primero un hueco y, si hace falta, amplían el heap; las otras obtienen un mapeo independiente.
Simulador · pedir al núcleo y reutilizar bloques
| Útil vivo | Heap reservado | Libre en heap | Mapeos reservados | Total reservado |
|---|---|---|---|---|
Bloques del heap, ordenados de menor a mayor dirección relativa:
| Inicio (KiB) | Tamaño (KiB) | Estado |
|---|
Mapeos dedicados:
| Bloque | Útil (KiB) | Reserva (KiB) |
|---|
Historial, últimos 40 eventos:
La demostración pide cuatro bloques de 16 KiB, libera el segundo y trata de recortar. El heap conserva 64 KiB porque el extremo sigue ocupado. Libera después los bloques 4 y 3 y vuelve a recortar: los huecos contiguos del final pueden retirarse y el heap queda en 16 KiB.
Para comprobar la reutilización, después de la demostración pide 8 KiB: entra en el hueco interior sin ampliar el heap. Para comparar, pide 512 KiB con el umbral de 256: se crea un mapeo dedicado que se retira al liberar ese bloque.
Las reglas son deliberadamente simples. El modelo no reproduce glibc: omite cabeceras, cachés, arenas múltiples, umbral dinámico, páginas físicas, madvise y concurrencia. El recorte solo ocurre al pulsar su botón; el umbral se aplica a peticiones futuras. No debes interpretar sus cifras como RSS ni como una predicción de llamadas reales.
12. Errores, límites y contrato de la API
| API o situación | Qué comprobar o respetar |
|---|---|
| malloc y calloc | Comprobar NULL ante un fallo y no suponer bytes inicializados en malloc |
| sbrk | Comparar con (void *) -1 y no mezclar modificaciones manuales con el asignador |
| mmap | Comparar con MAP_FAILED y utilizar longitud válida |
| free | Entregar el puntero válido de la familia malloc o NULL, sin doble liberación |
| munmap | Conocer la base y longitud de una región propia; no usarlo para liberar un malloc |
| Multiplicaciones y redondeos | Detectar desbordamientos antes de pedir memoria insuficiente |
| Acceso después de liberar | Es inválido aunque la dirección todavía aparezca mapeada y conserve los bytes |
Que una reserva tenga éxito no garantiza que cualquier acceso futuro pueda materializarse bajo toda condición de presión de memoria. Las políticas de overcommit, los límites de recursos y los controles del entorno influyen. La distinción entre espacio virtual y RAM utilizada sigue siendo la del bloque anterior.
Un bloque que sale de malloc tampoco queda inicializado porque el núcleo entregue páginas anónimas a cero. El asignador puede reutilizar bytes de un objeto anterior. El contrato de inicialización a cero corresponde a calloc, no a malloc.
Al escribir un asignador propio habrá que preservar la alineación requerida, mantener sus metadatos fuera del alcance del usuario y comprobar errores del mecanismo que lo respalda. Una demostración de mover el break no es todavía un malloc utilizable.
13. El paso hacia un asignador propio
Ahora podemos separar dos preguntas. La primera es de dónde sale una región: de un mapeo, de ampliar el espacio del break o de otro mecanismo de la plataforma. La segunda es cómo se administran los bloques dentro de ella: búsqueda, división, fusión y seguimiento de los tamaños.
En el tema 46 construiremos un asignador con lista libre. Esa capa debe saber qué bloques están ocupados, qué huecos pueden reutilizarse y cuándo conviene pedir otra región. La interfaz que use para obtener espacio no sustituye esas decisiones.
Después estudiaremos glibc con más detalle y volveremos a la fragmentación y las mediciones. La idea que conviene conservar es que malloc gestiona objetos a una escala distinta de la paginación, aunque dependa de ella para obtener respaldo.
Referencias para contrastar las interfaces y las políticas: brk y sbrk, mmap y munmap, malloc, mallopt, opciones del asignador de glibc y malloc_trim.
14. Resumen
- malloc administra bloques; el núcleo administra regiones virtuales y páginas. No hay necesariamente una llamada al sistema por petición.
- brk cambia un límite absoluto. sbrk recibe un incremento y devuelve el break anterior; sbrk es una interfaz de biblioteca.
- El espacio del break se recorta por el extremo: un hueco interior puede reutilizarse sin permitir reducir ese límite.
- mmap permite obtener regiones anónimas independientes del break y munmap retirarlas cuando corresponde.
- Un asignador puede subdividir una región obtenida por cualquiera de esos mecanismos.
- Los umbrales y las rutas de glibc dependen de su configuración, estado y versión; no forman parte del contrato de C.
- free termina la vida del objeto, pero puede conservar su espacio para reutilizarlo.
- Retirar un intervalo virtual y devolver respaldo físico son operaciones distintas; el RSS no cuenta bloques libres internos.
- Se debe liberar cada reserva mediante su API: free para la familia malloc y munmap para un mapeo propio.
- Las trazas, los mapas y las etapas del programa permiten comprobar lo que ocurrió sin inferirlo solo a partir del tamaño.
15. Preguntas de repaso
Responde antes de abrir cada explicación.
1. ¿malloc(100) debe realizar una llamada al sistema?
No. Puede atenderse con un bloque libre o espacio disponible en una región que el asignador ya administra.
2. ¿Qué devuelve sbrk cuando aumenta el break?
El break anterior, que corresponde al inicio del espacio agregado. Ante un error devuelve (void *) -1, no NULL.
3. ¿Por qué strace no muestra una llamada sbrk?
Porque sbrk es una interfaz de biblioteca. En Linux, las operaciones del break utilizan la llamada brk; además, una consulta puede resolverse con el estado que mantiene glibc.
4. ¿Por qué no puede bajarse el break para quitar solo un bloque interior?
Porque es el límite superior de un intervalo. Bajarlo hasta el bloque interior también retiraría el espacio situado por encima, que puede contener bloques vivos.
5. ¿Todo mmap corresponde a un único malloc grande?
No. El cargador y otros componentes también usan mmap. Además, un mapeo puede respaldar una arena que se subdivide en muchos bloques pequeños.
6. ¿Un umbral de 128 KiB es una regla universal?
No. Es un valor inicial habitual documentado para una política de glibc. Puede ajustarse y no define el comportamiento de todas las implementaciones ni de cada petición.
7. ¿Puede usarse munmap para liberar un puntero devuelto por malloc?
No. La aplicación debe usar free. El asignador conoce la base, el tamaño y los metadatos de la reserva subyacente.
8. ¿Por qué free puede no reducir el RSS?
Porque el asignador puede conservar páginas residentes con bloques reutilizables. Además, hay metadatos, cachés y otros objetos que pueden mantener páginas en uso.
9. ¿malloc debe entregar ceros porque mmap anónimo los proporciona?
No. Puede reutilizar un bloque con datos anteriores. Para obtener inicialización a cero se utiliza calloc.
10. ¿malloc_trim arregla una fuga de memoria?
No. Intenta devolver memoria que el asignador ya considera libre. No libera objetos que siguen asignados y cuyos punteros se perdieron.
11. ¿Se puede restaurar manualmente el break después de hacer varios malloc?
No de forma general. Se puede retirar memoria que el asignador y otras bibliotecas siguen considerando suya. El experimento con sbrk es aislado y no es una técnica para limpiar el heap de una aplicación.
Para practicar
- Compila los tres programas y registra el tamaño de página del sistema. Verifica que el ejemplo de sbrk restaura el límite antes de imprimir.
- Ejecuta region_mmap bajo strace e identifica su mapeo entre los que genera el arranque del proceso.
- En observar_malloc, compara las direcciones de los bloques con maps durante la pausa. Clasifica lo observado en esa ejecución sin convertirlo en una regla universal.
- Cuenta las llamadas brk y mmap al aumentar la cantidad de peticiones pequeñas. Explica por qué no necesitas obtener una llamada por cada malloc.
- Compara free del bloque pequeño y del grande mediante la traza. Distingue reutilización interna, cambio del break y munmap.
- En una copia del programa para glibc, agrega malloc_trim(0) después de liberar y compara break, RSS y llamadas. Explica también un resultado cero.
- Usa el simulador para llenar 64 KiB con cuatro bloques, liberar uno interior y reutilizarlo con un bloque menor. Comprueba que la reserva no crece.
- Libera los bloques del extremo del simulador y recorta. Calcula cuánto puede retirarse sin afectar al primer bloque vivo.
- Prueba una configuración explícita de mmap_threshold en una ejecución nueva y registra la versión de glibc, los tamaños y la traza obtenida.