1. Una implementación que evoluciona
La biblioteca GNU C, glibc, incluye una implementación de malloc derivada históricamente de ptmalloc y dlmalloc, con muchos cambios propios posteriores. Su organización interna no es parte del contrato de C ni una descripción universal de los asignadores de Linux.
El programa puede usar otra biblioteca o un asignador reemplazado. Antes de interpretar un experimento conviene registrar la biblioteca y la configuración efectivas:
$ getconf GNU_LIBC_VERSION
$ ldd --version
$ printenv GLIBC_TUNABLES
Un detalle importante al comparar materiales: los fastbins aparecen en numerosas versiones históricas, mientras que el código actual y el manual de glibc consultados para este tema ya describen su eliminación. También han cambiado los límites y capacidades de tcache. Por eso explicaremos las piezas estables del diseño y señalaremos las variantes, sin fijar una tabla de tamaños como si fuera permanente.
Los ejemplos requieren Linux con glibc. El que utiliza mallinfo2 requiere glibc 2.33 o posterior. Se compilan como C17 y emplean extensiones GNU de diagnóstico; una aplicación portable no debe depender de ellas para respetar la vida útil de sus objetos.
2. Chunks y metadatos
Chunk es la unidad interna con la que el asignador describe un bloque y su organización. El tamaño pedido por la aplicación, el tamaño interno del chunk y el tamaño de la región obtenida del sistema son cantidades diferentes.
Como en nuestro asignador, un puntero de usuario puede estar después de información administrativa. Pero glibc no utiliza nuestra cabecera de 32 bytes: aprovecha la alineación y comparte el significado de determinadas posiciones entre chunks vecinos.
[ información de frontera ][ tamaño y flags ][ datos de la aplicación ... ]
^
puntero de usuario
Cuando está libre, parte del área ya no utilizable por la aplicación
puede contener enlaces del asignador.
El campo de tamaño puede contener flags en bits bajos que la alineación deja disponibles. Entre los nombres internos habituales están:
| Indicador | Idea que representa | Distinción necesaria |
|---|---|---|
PREV_INUSE | Estado del chunk físico anterior para la consolidación | No es simplemente «este objeto de usuario está vivo» |
IS_MMAPPED | Chunk con tratamiento de mapeo independiente | No identifica todo bloque alojado en una región cuyo origen fue mmap |
NON_MAIN_ARENA | Relación con una arena distinta de la principal en los estados pertinentes | No es un identificador de hilo |
Un bloque pequeño de una arena secundaria puede residir en una región obtenida por mmap y, aun así, no ser un chunk independiente marcado como IS_MMAPPED. Es la diferencia del tema 45 entre respaldar una arena y dedicar un mapeo a una petición.
No leeremos estos campos restando una constante al puntero de malloc. Su disposición es privada, depende del entorno y puede incluir otros mecanismos de protección o etiquetado. Comprender el esquema no autoriza a acceder fuera del objeto solicitado.
3. Boundary tags y consolidación
El asignador del tema anterior encontraba vecinos mediante una lista libre ordenada por dirección. Los boundary tags aportan información en las fronteras de los chunks para localizar vecinos físicos y combinar huecos sin depender de recorrer toda esa lista.
Conceptualmente, el tamaño permite avanzar al siguiente chunk y la información del anterior libre permite retroceder cuando corresponde. Al consolidar se ajustan las fronteras y los enlaces de las estructuras donde estaban los huecos. No se desplazan los objetos vivos.
Sin embargo, un free no tiene por qué consolidar inmediatamente. Si el bloque se conserva en una caché rápida, puede seguir representándose como ocupado desde el punto de vista de las fronteras usadas para la consolidación. La vida útil para la aplicación ya terminó, aunque su tratamiento interno difiera del de un hueco consolidable.
Esta separación explica por qué «ya llamé a free» no implica «los vecinos ya forman un único hueco». Tampoco implica que haya desaparecido una página del mapa o que el sistema haya recuperado RAM.
4. Bins: organizar por tamaño
Un bin agrupa chunks libres o disponibles según las reglas de una estructura. Las arenas mantienen varias clases de contenedores para evitar una única búsqueda lineal sobre todos los huecos.
| Pieza | Función conceptual | Qué no debemos asumir |
|---|---|---|
| Small bins | Agrupar chunks de determinados tamaños pequeños; cada clase reúne el mismo tamaño interno | Que una petición de n bytes vaya al bin llamado n |
| Large bins | Organizar tamaños mayores y ayudar a encontrar un hueco adecuado | Que todo bloque grande se obtenga con mmap |
| Unsorted bin | Mantener ciertos chunks pendientes de reutilización o clasificación | Que todos los free deban pasar por él |
| Top chunk | Espacio especial del extremo de un heap de la arena | Que sea otro bin ordinario o un bloque vivo de usuario |
El unsorted bin permite dar oportunidades de reutilización antes de clasificar ciertos tamaños. Las rutas exactas importan: algunas versiones colocan determinados chunks pequeños directamente en su small bin y tcache puede interceptar otros. No debe memorizarse una cadena universal «free, unsorted, small bin».
El top chunk se trata por separado y puede dividirse para atender peticiones o ampliarse cuando hace falta más espacio. También participa en decisiones de recorte. Los mapeos independientes siguen una ruta diferente de los chunks normales de la arena.
El principio es parecido al de separar cajones por tamaño: se reducen búsquedas, pero se añaden metadatos, reglas de clasificación y decisiones sobre cuándo combinar y cuándo conservar huecos reutilizables.
5. Tcache: reutilización por hilo
Tcache es una caché asociada a un hilo que puede conservar un número limitado de chunks de tamaños elegibles. Permite atender ciertas asignaciones y liberaciones sin entrar por la ruta compartida de la arena.
Si un hilo libera un bloque elegible y hay sitio en la clase correspondiente, ese chunk puede quedar en su tcache. Una petición posterior compatible del mismo hilo puede recuperarlo con poco trabajo. En esas rutas se evita normalmente tomar el mutex de la arena, aunque la inicialización, los fallos de caché y otras tareas siguen necesitando trabajo adicional.
Los límites de tamaño, el número de clases y los chunks permitidos por clase dependen de la versión y la configuración. Versiones recientes también permiten ampliar tcache a tamaños mayores. En este tema usaremos clases pequeñas y fijaremos límites explícitos solo en experimentos controlados.
El costo es la retención distribuida: si cada hilo conserva huecos, la memoria reutilizable de un hilo no está necesariamente disponible de inmediato para atender la petición de otro. Con muchos hilos y varias clases, ese espacio puede ser apreciable aunque haya pocos objetos vivos de la aplicación.
Al terminar un hilo, glibc devuelve los chunks retenidos en su tcache al tratamiento de sus arenas correspondientes. Eso puede permitir consolidación, pero no promete devolver toda esa memoria al núcleo ni destruir la arena.
Un hilo puede liberar un bloque creado por otro, siempre que la aplicación transfiera la propiedad con sincronización correcta y nadie vuelva a usar el objeto liberado. El chunk conserva su relación con la arena de origen; si resulta elegible, puede entrar en la tcache del hilo que ejecuta free. «Arena de origen» y «caché del hilo» no son lo mismo.
6. Dónde encajan los fastbins históricos
En muchas versiones de glibc, los fastbins eran listas rápidas por tamaño asociadas a una arena para ciertos chunks pequeños. Se distinguían de tcache porque no eran una caché privada por hilo y de los bins consolidados porque posponían la fusión.
El asignador podía consolidarlos posteriormente en situaciones determinadas. Así, una liberación rápida y la creación de un hueco mayor podían ocurrir en momentos diferentes. Tcache y fastbins coexistieron en distintas versiones, por lo que no son dos nombres para la misma estructura.
El manual actual de opciones de glibc indica que glibc.malloc.mxfast ya no tiene efecto porque se eliminaron los fastbins. El código actual consultado tampoco los contiene. Si estudias una biblioteca anterior, sus trazas, campos estadísticos y rutas pueden seguir reflejándolos.
La conclusión práctica es registrar la versión antes de aplicar un diagrama. No se debe afirmar que un bloque pasó por fastbins únicamente por su tamaño ni interpretar todos los campos históricos de una API como estructuras que sigan activas.
7. Arenas y concurrencia
Una arena reúne estado del asignador, bins, información de sus heaps y sincronización. Permite administrar una parte del trabajo sin que todas las operaciones compartan obligatoriamente un único mutex global.
La arena principal suele estar relacionada con el heap que puede crecer mediante el break. Las secundarias pueden obtener sus heaps mediante mmap. Una arena puede administrar más de una región; no equivale a un solo mapeo ni a una página.
Hilo A: tcache A ----> arena 0 ----> heap principal
Hilo B: tcache B ----> arena 1 ----> uno o varios heaps
Hilo C: tcache C ----> arena 1
Además pueden existir chunks con mapeos independientes.
No hay necesariamente una arena por hilo. Una arena puede compartirse entre varios hilos; un hilo puede reutilizar una existente y la biblioteca puede crear otras dentro de los límites configurados. El número de CPU influye en políticas predeterminadas de ciertas versiones, pero no permite deducir el número exacto de arenas que tendrá cualquier ejecución.
Más arenas pueden reducir contención en rutas compartidas. También pueden dispersar el espacio libre entre varios heaps y aumentar memoria reservada o retenida. Limitar arenas puede reducir ese costo y, al mismo tiempo, aumentar la competencia por sus mutex.
La seguridad de malloc entre hilos protege el estado del asignador. No protege automáticamente los objetos de la aplicación: un hilo que accede a un bloque mientras otro lo libera sigue teniendo un error de vida útil.
8. Rutas de malloc y free
El siguiente recorrido sirve como orientación, no como orden exhaustivo de cada versión:
- Petición. Se comprueba el tamaño y se transforma en una representación interna alineada.
- Ruta de caché. Una entrada compatible de tcache puede resolver la petición sin buscar en los bins de una arena.
- Ruta de arena. Se obtiene acceso al estado pertinente y se estudian candidatos, bins, restos y top según las reglas de esa versión.
- Más respaldo. Si hace falta, se amplía un heap o se obtiene un mapeo; también puede fallar la petición.
- Liberación. Según el chunk y la política, puede retenerse en una caché, volver a una arena para su tratamiento o retirarse un mapeo independiente.
Por eso no hay una correspondencia uno a uno entre malloc y brk, ni entre free y munmap. Un bucle que repite malloc y free sobre un mismo tamaño puede ejercitar sobre todo una caché; no mide necesariamente la búsqueda en bins ni la obtención de memoria del núcleo.
El comportamiento depende de la secuencia completa: tamaños, orden de liberación, hilos participantes y configuración. El caso del tema 46 era deliberadamente más previsible porque eliminaba casi todas esas variables.
9. Observar tamaños sin leer cabeceras
malloc_usable_size es una extensión GNU de diagnóstico. Devuelve una capacidad utilizable que puede ser mayor que la petición, pero no es el tamaño total del chunk ni el costo completo de la región. El contrato de la aplicación debe seguir basándose en el tamaño solicitado.
#define _DEFAULT_SOURCE
#include <gnu/libc-version.h>
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const size_t pedidos[] = { 1, 16, 24, 25, 64, 257, 1024, 8192 };
printf("glibc %s\n", gnu_get_libc_version());
puts("pedido usable direccion");
for (size_t i = 0; i < sizeof pedidos / sizeof pedidos[0]; i++) {
size_t n = pedidos[i];
unsigned char *p = malloc(n);
if (p == NULL) { perror("malloc"); return 1; }
p[0] = 1;
p[n - 1] = 2;
printf("%6zu %6zu %p\n", n, malloc_usable_size(p), (void *) p);
free(p);
}
return 0;
}
$ gcc -std=c17 -Wall -Wextra -O2 tamanos_glibc.c -o tamanos_glibc
$ ./tamanos_glibc
glibc ...
pedido usable direccion
... una fila por petición ...
Dos peticiones diferentes pueden recibir capacidades iguales por el redondeo a clases. Eso no implica que se deba escribir hasta la capacidad diagnóstica: utilizar el exceso sin redimensionar el objeto no es una práctica soportada por esta interfaz. Tampoco se llama a malloc_usable_size después de free.
No calcules «cabecera = usable menos pedido»: esa resta mezcla redondeos y capacidad adicional, y omite otros costos. El programa no examina bytes anteriores al puntero ni deduce flags privados.
10. Estadísticas y ciclo de asignación
malloc_info(0, archivo) exporta XML con información de las arenas. mallinfo2 devuelve una estructura con campos de tipo size_t y evita el límite de enteros de la antigua mallinfo. Son instrumentos del asignador, no un inventario exacto de objetos vivos de la aplicación.
El siguiente programa toma cuatro fotografías: inicio, bloques vivos, bloques liberados y después de intentar recortar. Escribe cuatro archivos XML en el directorio actual. Usa glibc 2.33 o posterior.
#define _DEFAULT_SOURCE
#include <gnu/libc-version.h>
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#define CANTIDAD 256
static int informe(const char *ruta)
{
struct mallinfo2 m = mallinfo2();
printf("%s: arena=%zu; uordblks=%zu; fordblks=%zu; hblkhd=%zu\n",
ruta, m.arena, m.uordblks, m.fordblks, m.hblkhd);
FILE *f = fopen(ruta, "w");
if (f == NULL) { perror("fopen"); return 1; }
int error = malloc_info(0, f) != 0;
if (error) perror("malloc_info");
if (fclose(f) != 0) { perror("fclose"); error = 1; }
return error;
}
int main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("glibc %s\n", gnu_get_libc_version());
void *bloques[CANTIDAD] = { 0 };
void *grande = NULL;
int error = informe("estado_inicio.xml");
if (error) return 1;
for (size_t i = 0; i < CANTIDAD; i++) {
size_t n = i < CANTIDAD / 2 ? 256 : 8192;
bloques[i] = malloc(n);
if (bloques[i] == NULL) { perror("malloc"); error = 1; goto limpiar; }
volatile unsigned char *p = bloques[i];
p[0] = 1;
p[n - 1] = 2;
}
grande = malloc(4UL * 1024 * 1024);
if (grande == NULL) { perror("malloc grande"); error = 1; goto limpiar; }
volatile unsigned char *p = grande;
for (size_t i = 0; i < 4UL * 1024 * 1024; i += 4096)
p[i] = 3;
error |= informe("estado_vivos.xml");
limpiar:
free(grande);
grande = NULL;
for (size_t i = 0; i < CANTIDAD; i++) {
free(bloques[i]);
bloques[i] = NULL;
}
error |= informe("estado_libres.xml");
printf("malloc_trim devolvió %d\n", malloc_trim(0));
error |= informe("estado_trim.xml");
return error ? 1 : 0;
}
$ gcc -std=c17 -Wall -Wextra -O2 estado_glibc.c -o estado_glibc
$ ./estado_glibc
$ less estado_vivos.xml
$ diff -u estado_vivos.xml estado_libres.xml
| Campo mostrado | Lectura orientativa | Límite de la interpretación |
|---|---|---|
arena | Espacio contabilizado en heaps de arenas por la implementación | No es el número de arenas ni equivale necesariamente solo al break |
uordblks | Espacio contabilizado como en uso según las estructuras recorridas | No equivale a sumar los tamaños solicitados de los objetos vivos |
fordblks | Espacio contabilizado como libre por esas estructuras | No es necesariamente toda la memoria reutilizable de tcache |
hblkhd | Bytes de mapeos independientes contabilizados por malloc | No incluye cualquier mmap del proceso ni convierte todos los heaps secundarios en asignaciones independientes |
Las herramientas de diagnóstico también realizan E/S y pueden reservar memoria. Además, un recorrido por arenas no es una fotografía atómica del proceso entero si hay hilos asignando simultáneamente. El XML incluye una versión de formato y sus detalles pueden evolucionar.
Al interpretar mallinfo2 hay que revisar la implementación usada: los manuales de algunas versiones advierten de cobertura limitada de arenas, mientras que el código actual consultado recorre las arenas. Ninguna de esas variantes debe confundirse con un censo de tcache u objetos de usuario. Para el experimento concurrente usaremos malloc_info y puntos de espera explícitos.
La separación de 4096 bytes al tocar el bloque grande es una elección de la prueba, no una consulta del tamaño de página ni una garantía de RSS. El programa estudia estadísticas del asignador. malloc_trim intenta devolver memoria libre apropiada, pero no vacía por contrato todas las cachés de los hilos y su resultado cero no es un error convencional.
11. Un experimento con varios hilos
Crearemos cuatro trabajadores que reciben una señal de comienzo, reservan bloques y esperan conservándolos vivos. El hilo principal toma una fotografía XML, los deja liberar, espera a que terminen y toma otra. Los puntos de espera usan mutex y variables de condición; no dependen de sleep ni de suponer cuánto tarda un hilo.
#define _POSIX_C_SOURCE 200809L
#include <gnu/libc-version.h>
#include <malloc.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define HILOS 4
#define BLOQUES 1024
#define BYTES 2048
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cambio = PTHREAD_COND_INITIALIZER;
static int empezar, liberar, preparados;
struct resultado { size_t obtenidos; };
static void comprobar(int codigo, const char *operacion)
{
if (codigo != 0) {
fprintf(stderr, "%s: %s\n", operacion, strerror(codigo));
exit(EXIT_FAILURE);
}
}
static void *trabajador(void *arg)
{
struct resultado *r = arg;
void *bloques[BLOQUES] = { 0 };
comprobar(pthread_mutex_lock(&mutex), "lock inicio");
while (!empezar)
comprobar(pthread_cond_wait(&cambio, &mutex), "espera inicio");
comprobar(pthread_mutex_unlock(&mutex), "unlock inicio");
for (size_t i = 0; i < BLOQUES; i++) {
bloques[i] = malloc(BYTES);
if (bloques[i] == NULL) break;
volatile unsigned char *p = bloques[i];
p[0] = 1;
p[BYTES - 1] = 2;
r->obtenidos++;
}
/* También notifica si no pudo obtener todos los bloques. */
comprobar(pthread_mutex_lock(&mutex), "lock preparado");
preparados++;
comprobar(pthread_cond_broadcast(&cambio), "aviso preparado");
while (!liberar)
comprobar(pthread_cond_wait(&cambio, &mutex), "espera liberar");
comprobar(pthread_mutex_unlock(&mutex), "unlock preparado");
for (size_t i = 0; i < r->obtenidos; i++)
free(bloques[i]);
return NULL;
}
static int fotografia(const char *ruta)
{
FILE *f = fopen(ruta, "w");
if (f == NULL) { perror("fopen"); return 1; }
int error = malloc_info(0, f) != 0;
if (error) perror("malloc_info");
if (fclose(f) != 0) { perror("fclose"); error = 1; }
return error;
}
int main(void)
{
pthread_t hilos[HILOS];
struct resultado resultados[HILOS] = { 0 };
int creados = 0, error = 0;
printf("glibc %s\n", gnu_get_libc_version());
for (int i = 0; i < HILOS; i++) {
int rc = pthread_create(&hilos[i], NULL, trabajador, &resultados[i]);
if (rc != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(rc));
error = 1;
break;
}
creados++;
}
comprobar(pthread_mutex_lock(&mutex), "lock principal");
empezar = 1;
comprobar(pthread_cond_broadcast(&cambio), "comenzar");
while (preparados < creados)
comprobar(pthread_cond_wait(&cambio, &mutex), "espera preparados");
comprobar(pthread_mutex_unlock(&mutex), "unlock principal");
error |= fotografia("arenas_vivas.xml");
comprobar(pthread_mutex_lock(&mutex), "lock liberar");
liberar = 1;
comprobar(pthread_cond_broadcast(&cambio), "liberar trabajadores");
comprobar(pthread_mutex_unlock(&mutex), "unlock liberar");
for (int i = 0; i < creados; i++) {
comprobar(pthread_join(hilos[i], NULL), "pthread_join");
printf("Hilo %d: %zu bloques obtenidos\n", i, resultados[i].obtenidos);
if (resultados[i].obtenidos != BLOQUES) error = 1;
}
error |= fotografia("arenas_final.xml");
comprobar(pthread_cond_destroy(&cambio), "cond_destroy");
comprobar(pthread_mutex_destroy(&mutex), "mutex_destroy");
return error ? 1 : 0;
}
$ gcc -std=c17 -Wall -Wextra -O2 arenas_hilos.c -o arenas_hilos -pthread
$ ./arenas_hilos
$ grep '<heap nr=' arenas_vivas.xml
$ GLIBC_TUNABLES=glibc.malloc.arena_max=1 ./arenas_hilos
$ grep '<heap nr=' arenas_vivas.xml
Copia los resultados de una variante antes de ejecutar otra si quieres conservarlos. Los elementos <heap nr="..."> del XML representan las entradas por arena del informe; no equivalen al número de mapeos virtuales o de heaps físicos que pueda administrar cada arena.
Con el límite de una arena se restringe su cantidad, pero tcache no queda desactivada automáticamente. Tampoco tienen por qué desaparecer inmediatamente los heaps o arenas al terminar los hilos. Las pilas de los trabajadores y las estructuras de pthread son otro consumo del proceso que no debes atribuir a chunks de usuario.
Este programa permite observar estructura y retención. No es un benchmark de latencia: incluye creación de hilos, sincronización, E/S y diagnóstico. Que aparezcan más arenas no demuestra que una carga concreta vaya a ser más rápida.
12. Simulador de cachés y arenas
El modelo tiene dos hilos, una clase de tamaño y uno o dos pools de arena. Cada chunk representado ocupa 96 bytes del modelo y entrega 64 útiles; estos números no son una fórmula del chunk real de glibc. Puedes elegir qué hilo ejecuta malloc o free.
Simulador · tcache privada y reutilización en arena
Cambiar arenas o límite reinicia el estado. La demostración conserva esas opciones.
| Chunks vivos | Retenidos en tcache | Libres en arenas | Respaldo del modelo | Aciertos tcache | Pasos por arena |
|---|---|---|---|---|---|
| Caché o pool | Chunks disponibles |
|---|
| Chunk | Arena de origen | Estado actual |
|---|
La demostración hace que A reserve un chunk, lo libere, B reserve uno y A vuelva a pedir. Con tcache activa, el chunk retenido por A no atiende directamente la petición de B; la última petición de A sí puede recuperarlo. Con tcache desactivada y una sola arena, B puede reutilizar ese hueco del pool común.
También puedes reservar con A y liberar con B. El modelo supone que la aplicación ya transfirió el puntero y su propiedad de forma sincronizada: no representa una carrera. El chunk puede quedar en tcache B mientras conserva arena de origen 0.
No se modelan tamaños distintos, consolidación, bins específicos, fastbins históricos, refills por lotes, top, mapas del núcleo ni devolución de RAM. «Paso por arena» cuenta una operación que usa esa ruta en el modelo, no una medición de bloqueos reales. Hay un máximo didáctico de 32 chunks de respaldo y un historial de 40 eventos.
13. Configurar y medir con criterio
| Opción de glibc | Qué permite explorar | Posible costo |
|---|---|---|
glibc.malloc.arena_max | Límite de arenas | Reducirlo puede aumentar competencia en rutas compartidas |
glibc.malloc.tcache_count | Retención por clase; cero desactiva tcache | Menos retención puede aumentar trabajo sobre arenas |
glibc.malloc.tcache_max | Tamaños elegibles según la versión | Ampliarlo puede retener chunks mayores por hilo |
glibc.malloc.mmap_threshold | Política relacionada con mapeos independientes | Cambia reutilización, llamadas y devolución; no resuelve todos los casos |
$ GLIBC_TUNABLES=glibc.malloc.tcache_count=0 ./estado_glibc
$ GLIBC_TUNABLES=glibc.malloc.arena_max=1:glibc.malloc.tcache_count=0 ./arenas_hilos
$ strace -f -e trace=brk,mmap,munmap,madvise ./arenas_hilos
Las opciones se leen al iniciar y solo se aplican en los entornos y versiones que las soporten. Se muestran para experimentar, no como una recomendación global de rendimiento. Compara variantes en procesos nuevos y conserva los informes antes de que la siguiente ejecución los sobrescriba.
Un benchmark útil necesita controlar tamaños y distribución, vida útil, concurrencia, qué hilo libera y cuánto trabajo realiza entre asignaciones. Separa la medición de tiempo de los volcados XML y de strace. Registra variación entre ejecuciones, consumo de memoria y latencias, además del promedio.
No deduzcas el estado de tcache a partir de que dos direcciones impresas coincidan: la reutilización puede ocurrir por más de una ruta. Tampoco compares o desreferencies punteros liberados para averiguarlo. El ejemplo de tamaños imprime cada dirección mientras su objeto sigue vivo.
14. Qué no muestran las estadísticas
La memoria tiene varias contabilidades: bytes solicitados por objetos vivos, capacidad de chunks, espacio de arenas, mapeos, páginas residentes y estructuras de los hilos. Sus totales no son intercambiables. Tcache puede conservar chunks que ciertos informes siguen contabilizando como en uso desde el punto de vista de la arena, aunque la aplicación ya haya llamado a free.
Un valor elevado de uordblks no prueba por sí solo una fuga de objetos; un RSS que no baja tampoco. A la inversa, que el proceso conserve memoria para reutilizar no justifica perder las referencias a objetos que siguen vivos. Para investigar una fuga hace falta estudiar la propiedad y vida útil de las asignaciones.
Los informes pueden incluir consumo del arranque, stdio, pthread y la propia instrumentación. Los sanitizadores o un asignador precargado pueden cambiar la implementación que se está observando. Un resultado obtenido bajo ese entorno no debe presentarse como la ruta interna normal de glibc.
La biblioteca incorpora comprobaciones y mecanismos de endurecimiento para detectar o dificultar algunas corrupciones, pero no hacen válidos un desbordamiento, una doble liberación o un uso después de free. El código de aplicación debe seguir respetando los límites y las reglas de sincronización.
Para contrastar versiones y detalles, consulta las opciones de memoria de glibc, el código de malloc.c y arena.c en el espejo del proyecto, y los manuales de malloc_info, mallinfo2, malloc_usable_size y malloc_trim. Las ramas de desarrollo pueden diferir de la biblioteca instalada.
15. Resumen
- glibc combina varias estructuras para equilibrar reutilización, búsqueda, concurrencia y consumo.
- La petición, el tamaño interno del chunk y la región del sistema son magnitudes distintas.
- Los boundary tags ayudan a localizar y consolidar vecinos físicos cuando corresponde.
- Los bins agrupan chunks; el top y los mapeos independientes tienen tratamiento propio.
- Tcache puede resolver peticiones por hilo sin entrar en la ruta compartida, a costa de retener espacio.
- Los fastbins pertenecen a distintas versiones históricas y no deben suponerse presentes en toda glibc.
- Una arena no equivale a un hilo, un mmap ni un nodo NUMA.
- La transferencia de un bloque entre hilos necesita sincronización de la aplicación.
- malloc_info, mallinfo2 y malloc_usable_size son diagnóstico; no cambian el contrato del objeto.
- La versión, configuración, secuencia de operaciones e instrumentación forman parte de cualquier conclusión experimental.
16. Preguntas de repaso
Intenta responder antes de abrir cada explicación.
1. ¿La estructura interna de glibc forma parte del contrato de malloc?
No. El contrato define el comportamiento observable de la API; chunks, bins y políticas son decisiones de implementación que pueden cambiar.
2. ¿PREV_INUSE significa que este objeto de usuario está vivo?
No. Se refiere al chunk físico anterior para el tratamiento interno de fronteras y consolidación. Además, un bloque liberado a una caché puede no consolidarse inmediatamente.
3. ¿Todo bloque obtenido dentro de una región mmap tiene IS_MMAPPED?
No. Un heap de una arena puede estar respaldado por mmap y contener muchos chunks ordinarios. El indicador identifica el tratamiento de un chunk independiente.
4. ¿Todos los free van al unsorted bin?
No. Tcache, mapeos independientes y rutas específicas de bins pueden intervenir. El recorrido exacto depende de la versión y el estado.
5. ¿Por qué tcache puede aumentar la retención?
Porque distribuye chunks reutilizables entre cachés de hilos. El espacio retenido en una de ellas no se ofrece directamente a cada petición de los demás.
6. ¿Tcache y fastbins son lo mismo?
No. Tcache se asocia al hilo. Los fastbins históricos se asociaban a arenas y diferían la consolidación. Su presencia depende de la versión.
7. ¿Una aplicación con ocho hilos tendrá ocho arenas?
No necesariamente. La biblioteca puede reutilizar arenas, compartirlas o crear otras dentro de sus límites. Hay que observar la ejecución.
8. ¿arena_max=1 desactiva tcache?
No. Limita arenas. La política de tcache es un control distinto; para explorar su desactivación se puede usar tcache_count=0 en un entorno que lo soporte.
9. ¿Puede un hilo liberar un bloque creado por otro?
Sí, si la propiedad se transfiere de forma sincronizada y el bloque solo se libera una vez. Que malloc sea seguro entre hilos no resuelve esa transferencia por sí mismo.
10. ¿usable menos pedido permite calcular el tamaño de la cabecera?
No. La diferencia refleja capacidad adicional y redondeo. No describe todo el chunk ni la región y no autoriza a leer antes del puntero.
11. ¿malloc_info informa todos los objetos vivos y todas las tcache?
No. Describe estadísticas del asignador y sus arenas según el formato y la implementación. No es un inventario exacto de objetos ni un volcado completo de las cachés por hilo.
12. ¿Terminar un hilo garantiza que su memoria vuelva al núcleo?
No. Su tcache puede devolverse al tratamiento de las arenas, pero la memoria puede conservarse para reutilización y las arenas pueden seguir existiendo.
Para practicar
- Registra tu versión de glibc y ejecuta tamanos_glibc. Identifica peticiones diferentes con capacidad diagnóstica igual sin escribir fuera de lo solicitado.
- Ejecuta estado_glibc y compara los cuatro XML. Distingue el costo de los objetos vivos de las estadísticas después de free.
- Repite el programa en un proceso nuevo con tcache_count=0. Conserva los resultados y explica por qué las diferencias no permiten deducir directamente una fuga.
- Ejecuta arenas_hilos con la configuración predeterminada y con arena_max=1. Cuenta las entradas del informe sin asumir una por trabajador.
- Compara arenas_vivas.xml y arenas_final.xml. Relaciona la diferencia de fase con la terminación y limpieza de los trabajadores.
- En el simulador, compara una arena con tcache desactivada, una con tcache activa y dos con tcache activa. Explica qué hilo puede recuperar cada chunk.
- Reserva con A, libera con B y vuelve a reservar con B. Observa que la caché que lo retiene y la arena de origen son atributos distintos.
- Diseña un benchmark separado de las fotografías XML que mantenga el mismo trabajo útil y mida variación, tiempo y consumo.
- Busca en la documentación de tu versión si hay fastbins y cuáles son los límites de tcache. Explica qué cambiarías en un diagrama de una versión diferente.