1. Qué significa completar el proyecto
Retomamos el asignador con lista libre del tema 46: una región, primer ajuste, división de restos útiles y fusión inmediata. Ahora agregamos un contrato verificable, estadísticas, calloc y una batería que comprueba tanto la estructura como los bytes entregados.
Una prueba del asignador debe verificar una propiedad observable y dejar evidencia de que se conserva. Que no haya una caída del proceso no demuestra que los bloques sean distintos, estén alineados o mantengan sus datos.
La capacidad seguirá siendo fija y el uso será de un solo hilo. Eso permite estudiar la corrección sin añadir todavía varias regiones, cachés o bloqueos. «Completo» significa que todas las operaciones del contrato definido están implementadas y probadas; no que este archivo sustituya a libc.
Los archivos se guardan en un directorio de práctica y no reemplazan los de los temas anteriores. Tampoco se redefinen los símbolos malloc y free: la biblioteca sigue administrando su propia memoria para printf y otras funciones.
2. Contrato y organización de archivos
| Operación | Contrato del proyecto |
|---|---|
| mi_init(bytes) | Una región mmap, redondeada a página; 0 si funciona, -1 con errno si falla; rechaza otra inicialización activa |
| mi_malloc(n) | Puntero alineado o NULL; n igual a cero devuelve NULL sin contar un fallo |
| mi_calloc(cantidad, tamaño) | Comprueba el producto y pone a cero exactamente los bytes pedidos; producto cero devuelve NULL |
| mi_free(p) | Devuelve 0 al liberar un bloque vivo o NULL; devuelve -1 con EINVAL si no encuentra un comienzo válido ocupado |
| mi_check() | Devuelve 1 si el estado satisface las invariantes y 0 si detecta una inconsistencia |
| mi_stats(salida) | Obtiene estadísticas solo de una región activa y coherente; devuelve 0 o -1 |
| mi_destroy() | Retira la región si no hay bloques vivos; con objetos pendientes devuelve -1 y EBUSY |
La devolución de estado de mi_free es una decisión de esta API. El free estándar devuelve void y no permite pasarle punteros interiores, ajenos o ya liberados. No se deben trasladar las pruebas negativas de este proyecto al free de libc.
La búsqueda por igualdad de puntero evita leer una supuesta cabecera antes de un puntero ajeno. Aun así, no detecta referencias obsoletas cuando una dirección vuelve a alojar un objeto nuevo: esa dirección puede coincidir con un comienzo ocupado válido. El usuario sigue siendo responsable de la propiedad y de no conservar referencias después de liberar.
Utilizaremos asignador.h, asignador.c y pruebas.c. Los tres bloques de código siguientes contienen todo lo necesario para compilar en Linux con C17.
3. Cabeceras, lista y estadísticas
La cabecera conserva tamaño total, tamaño solicitado, enlace libre y estado. Una unión con max_align_t mantiene la alineación. Las solicitudes se redondean a A = _Alignof(max_align_t), y solo se divide si el resto alcanza para una cabecera H y una unidad A de datos.
Guardar el tamaño solicitado permite distinguir relleno de datos útiles. Las estadísticas se definen sobre la capacidad administrada, no sobre RSS ni sobre todas las asignaciones del proceso:
capacidad = pedidos_vivos + relleno_ocupado + libre_util + cabeceras
libre_util: suma de (tamaño total - H) de los bloques libres.
mayor_libre: mayor capacidad útil de un único bloque libre.
cabeceras: número de bloques físicos × H.
relleno: suma de (tamaño total - H - pedido) de los ocupados.
Al fusionar desaparecen cabeceras intermedias y crece el espacio útil de un hueco. Por eso la capacidad útil libre final es capacidad - H, no la suma que había antes de fusionar.
El pico de pedidos cuenta el máximo de bytes solicitados simultáneamente vivos. Las asignaciones exitosas, fallos positivos y liberaciones aceptadas se acumulan desde la última inicialización correcta. La petición cero y mi_free(NULL) no incrementan esos contadores. Las estadísticas dejan de estar disponibles tras destruir la región.
4. El verificador de invariantes
El verificador recorre los bloques físicos y contrasta la lista libre. Antes de avanzar comprueba que la cabecera que va a leer cabe en la región, que el tamaño es válido y que no se sale del intervalo.
- Todos los tamaños y comienzos respetan A y cada bloque tiene al menos H + A bytes.
- Los bloques cubren exactamente la capacidad sin huecos de administración ni superposiciones.
- Los libres tienen pedido cero y aparecen una sola vez en orden físico en la lista.
- No hay libres contiguos sin fusionar, ni ocupados enlazados como libres.
- El número y la suma de pedidos de los ocupados coinciden con los contadores.
No se desreferencia un enlace de lista sin haberlo contrastado con una cabecera física válida: se compara el puntero esperado con el bloque que ya estamos recorriendo. Un enlace erróneo, extra o cíclico debe hacer fallar esa correspondencia.
Esto es una ayuda de diagnóstico, no una barrera de seguridad frente a corrupción arbitraria o escrituras concurrentes. Las operaciones normales presuponen que el estado no fue alterado fuera del contrato, y la batería llama a mi_check después de cada cambio.
5. Archivo de interfaz
#ifndef ASIGNADOR_H
#define ASIGNADOR_H
#include <stddef.h>
struct MiStats {
size_t capacidad, mapeado, cabecera, alineacion;
size_t vivos, pedidos, pico, libre_util, mayor_libre;
size_t cabeceras, relleno;
unsigned long long exitos, fallos, liberaciones;
};
int mi_init(size_t bytes);
void *mi_malloc(size_t n);
void *mi_calloc(size_t cantidad, size_t tam);
int mi_free(void *p);
int mi_check(void);
int mi_stats(struct MiStats *salida);
int mi_destroy(void);
#endif
El tamaño de la cabecera y la alineación se publican como diagnóstico, para que las pruebas calculen límites sin fijar 32 y 16 como constantes de una ABI. La aplicación no obtiene permiso para leer cabeceras por conocer esos valores.
6. Implementación del asignador
Las diferencias principales con el tema 46 son el tamaño solicitado por bloque, la región configurable, los retornos comprobables, el recorrido para localizar un puntero de usuario y el verificador completo.
#define _DEFAULT_SOURCE
#include "asignador.h"
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
typedef union Bloque Bloque;
union Bloque {
struct { size_t total, pedido; Bloque *sig; int libre; } v;
max_align_t alineacion;
};
static unsigned char *base;
static Bloque *lista;
static size_t capacidad, mapeado, vivos, pedidos, pico;
static unsigned long long exitos, fallos, liberaciones;
static size_t offset(const Bloque *b)
{
return (size_t) ((const unsigned char *) b - base);
}
static void *fallar(int codigo)
{
fallos++;
errno = codigo;
return NULL;
}
int mi_init(size_t bytes)
{
if (base != NULL) { errno = EBUSY; return -1; }
long consulta = sysconf(_SC_PAGESIZE);
if (bytes == 0 || consulta <= 0) { errno = EINVAL; return -1; }
size_t pagina = (size_t) consulta, a = _Alignof(max_align_t);
if (bytes > SIZE_MAX - (pagina - 1)) { errno = ENOMEM; return -1; }
size_t mapa = ((bytes + pagina - 1) / pagina) * pagina;
if (mapa > (size_t) PTRDIFF_MAX) { errno = ENOMEM; return -1; }
size_t cap = (mapa / a) * a;
if (cap < sizeof(Bloque) + a) { errno = EINVAL; return -1; }
void *p = mmap(NULL, mapa, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED) return -1;
if ((uintptr_t) p % a) { munmap(p, mapa); errno = EINVAL; return -1; }
base = p; mapeado = mapa; capacidad = cap;
vivos = pedidos = pico = 0;
exitos = fallos = liberaciones = 0;
lista = (Bloque *) base;
lista->v.total = capacidad; lista->v.pedido = 0;
lista->v.sig = NULL; lista->v.libre = 1;
return 0;
}
void *mi_malloc(size_t n)
{
if (n == 0) return NULL;
if (base == NULL) return fallar(EINVAL);
size_t a = _Alignof(max_align_t), h = sizeof(Bloque);
if (n > SIZE_MAX - h) return fallar(ENOMEM);
size_t necesario = n + h;
if (necesario > SIZE_MAX - (a - 1)) return fallar(ENOMEM);
necesario = ((necesario + a - 1) / a) * a;
Bloque **enlace = &lista;
while (*enlace != NULL && (*enlace)->v.total < necesario)
enlace = &(*enlace)->v.sig;
if (*enlace == NULL) return fallar(ENOMEM);
Bloque *b = *enlace;
size_t resto = b->v.total - necesario;
if (resto >= h + a) {
Bloque *r = (Bloque *) (base + offset(b) + necesario);
r->v.total = resto; r->v.pedido = 0;
r->v.sig = b->v.sig; r->v.libre = 1;
*enlace = r;
b->v.total = necesario;
} else *enlace = b->v.sig;
b->v.pedido = n; b->v.sig = NULL; b->v.libre = 0;
vivos++; pedidos += n; exitos++;
if (pedidos > pico) pico = pedidos;
return (unsigned char *) b + h;
}
void *mi_calloc(size_t cantidad, size_t tam)
{
if (cantidad == 0 || tam == 0) return NULL;
if (tam > SIZE_MAX / cantidad) return fallar(ENOMEM);
size_t n = cantidad * tam;
void *p = mi_malloc(n);
if (p != NULL) memset(p, 0, n);
return p;
}
int mi_free(void *p)
{
if (p == NULL) return 0;
if (base == NULL) { errno = EINVAL; return -1; }
Bloque *b = NULL;
for (size_t pos = 0; pos < capacidad;) {
Bloque *c = (Bloque *) (base + pos);
if ((void *) (base + pos + sizeof(Bloque)) == p) { b = c; break; }
pos += c->v.total;
}
if (b == NULL || b->v.libre) { errno = EINVAL; return -1; }
Bloque *antes = NULL, *despues = lista;
while (despues != NULL && offset(despues) < offset(b)) {
antes = despues; despues = despues->v.sig;
}
pedidos -= b->v.pedido; vivos--; liberaciones++;
b->v.pedido = 0; b->v.libre = 1; b->v.sig = despues;
if (antes != NULL) antes->v.sig = b; else lista = b;
if (despues != NULL && offset(b) + b->v.total == offset(despues)) {
b->v.total += despues->v.total; b->v.sig = despues->v.sig;
}
if (antes != NULL && offset(antes) + antes->v.total == offset(b)) {
antes->v.total += b->v.total; antes->v.sig = b->v.sig;
}
return 0;
}
int mi_check(void)
{
if (base == NULL)
return capacidad == 0 && mapeado == 0 && lista == NULL && vivos == 0 && pedidos == 0;
size_t a = _Alignof(max_align_t), h = sizeof(Bloque);
if ((uintptr_t) base % a || capacidad % a || capacidad > mapeado || capacidad < h + a)
return 0;
size_t pos = 0, cuenta = 0, suma = 0;
int previo_libre = 0;
Bloque *esperado = lista;
while (pos < capacidad) {
if (pos % a || capacidad - pos < h) return 0;
Bloque *b = (Bloque *) (base + pos);
size_t t = b->v.total;
if (t < h + a || t % a || t > capacidad - pos ||
(b->v.libre != 0 && b->v.libre != 1)) return 0;
if (b->v.libre) {
if (previo_libre || b->v.pedido != 0 || esperado != b) return 0;
esperado = b->v.sig;
} else {
if (b->v.pedido == 0 || b->v.pedido > t - h || b->v.sig != NULL) return 0;
cuenta++; suma += b->v.pedido;
}
previo_libre = b->v.libre;
pos += t;
}
return pos == capacidad && esperado == NULL && cuenta == vivos && suma == pedidos;
}
int mi_stats(struct MiStats *s)
{
if (s == NULL || base == NULL) { errno = EINVAL; return -1; }
if (!mi_check()) { errno = EFAULT; return -1; }
*s = (struct MiStats) { 0 };
s->capacidad = capacidad; s->mapeado = mapeado;
s->cabecera = sizeof(Bloque); s->alineacion = _Alignof(max_align_t);
s->vivos = vivos; s->pedidos = pedidos; s->pico = pico;
s->exitos = exitos; s->fallos = fallos; s->liberaciones = liberaciones;
for (size_t pos = 0; pos < capacidad;) {
Bloque *b = (Bloque *) (base + pos);
size_t util = b->v.total - sizeof(Bloque);
s->cabeceras += sizeof(Bloque);
if (b->v.libre) {
s->libre_util += util;
if (util > s->mayor_libre) s->mayor_libre = util;
} else s->relleno += util - b->v.pedido;
pos += b->v.total;
}
return 0;
}
int mi_destroy(void)
{
if (base == NULL) return 0;
if (vivos != 0) { errno = EBUSY; return -1; }
if (munmap(base, mapeado) == -1) return -1;
base = NULL; lista = NULL;
capacidad = mapeado = vivos = pedidos = 0;
return 0;
}
mi_free recorre todos los bloques físicos para comprobar la igualdad con un comienzo de datos ocupado. No resta H al puntero recibido ni lo desreferencia. Eso agrega costo respecto de la versión del tema 46, y no ofrece una garantía frente a corrupción previa de las propias cabeceras.
Los desbordamientos se comprueban antes de sumar cabeceras, redondear o multiplicar. El mapeo se limita a PTRDIFF_MAX para que los desplazamientos internos no excedan el rango utilizado en diferencias de punteros. Si falla una reserva, la estructura y los objetos vivos deben permanecer intactos, aunque aumente el contador de fallos.
7. Casos deterministas y fallos esperados
Las pruebas pequeñas tienen una expectativa concreta y permiten localizar el problema antes del estrés:
| Caso | Propiedad que comprueba |
|---|---|
| Petición cero y producto cero | Contrato explícito, sin reserva ni falso fallo |
| SIZE_MAX y producto desbordado | Rechazo sin convertir la petición en un bloque pequeño |
| Hueco interior reutilizado | Primer ajuste, división y conservación de vecinos |
| Resto A frente a resto H + A | Absorción de resto inútil y división mínima válida |
| Dos huecos separados | Fallo por fragmentación sin modificar objetos vivos |
| Agotamiento con objetos pequeños | NULL esperado, sin bucles ni pérdidas de bloques |
| Liberación final | Un único hueco de capacidad útil capacidad - H |
| Puntero ajeno o interior | Rechazo de esta API didáctica sin leer memoria del candidato |
Un fallo de asignación previsto no hace fallar la batería. Lo que debe fallar es la comprobación si el asignador devuelve un resultado incompatible o altera el estado que debía conservar.
Las pruebas mantienen datos con patrones distintos y verifican los bytes pedidos. No inspeccionan relleno ni utilizan bytes de un objeto liberado. Las comparaciones de direcciones para verificar reutilización usan un valor numérico guardado con fines de diagnóstico, sin conservar acceso a la vida anterior del objeto.
8. Cómo generar estrés reproducible
Usaremos 64 slots externos que representan referencias de la aplicación. Cada operación elige uno: si está vivo, se libera; si está vacío, se intenta una reserva de 1 a 8192 bytes, a veces mediante calloc.
Un generador de 32 bits con estado y semilla conocidos produce las decisiones. No se usa la hora ni un resultado dependiente del planificador. La semilla y el número de paso se incluyen en los errores para poder repetir la secuencia.
- Verificar los patrones de todos los objetos vivos antes de cambiar el estado.
- Realizar una operación y comprobar su resultado según el contrato.
- Si se reservó, comprobar alineación y, para calloc, los ceros; después escribir el patrón.
- Verificar invariantes, identidad de estadísticas y todos los objetos vivos otra vez.
- Al terminar, liberar los slots y comprobar fusión completa.
Las comprobaciones por byte pueden ser mucho más costosas que la operación de asignación. Es una prueba de corrección, no un benchmark. Para medir velocidad habría que separar o desactivar esa instrumentación y diseñar otra carga.
9. Batería completa en C
El macro CHECK permanece activo con NDEBUG. Las llamadas con efectos no dependen de assert, de modo que una compilación de pruebas no elimina operaciones accidentalmente.
#include "asignador.h"
#include <ctype.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static uint32_t semilla = 12345, estado;
static size_t paso;
#define CHECK(x) do { if (!(x)) { \
fprintf(stderr, "Fallo: %s; línea %d; semilla %lu; paso %zu\n", \
#x, __LINE__, (unsigned long) semilla, paso); \
exit(EXIT_FAILURE); \
} } while (0)
static struct MiStats foto(void)
{
struct MiStats s;
CHECK(mi_check());
CHECK(mi_stats(&s) == 0);
CHECK(s.pedidos + s.relleno + s.libre_util + s.cabeceras == s.capacidad);
return s;
}
static void vacio(void)
{
struct MiStats s = foto();
CHECK(s.vivos == 0 && s.pedidos == 0);
CHECK(s.cabeceras == s.cabecera && s.libre_util == s.capacidad - s.cabecera);
}
static void deterministas(void)
{
CHECK(mi_check() && mi_free(NULL) == 0);
CHECK(mi_malloc(1) == NULL && errno == EINVAL);
CHECK(mi_init(0) == -1 && errno == EINVAL);
CHECK(mi_init(64UL * 1024) == 0);
CHECK(mi_init(64UL * 1024) == -1 && errno == EBUSY);
struct MiStats s = foto();
size_t cap = s.capacidad, h = s.cabecera, a = s.alineacion;
CHECK(mi_malloc(0) == NULL && mi_calloc(0, 5) == NULL);
CHECK(foto().fallos == 0);
CHECK(mi_malloc(SIZE_MAX) == NULL && errno == ENOMEM);
CHECK(mi_calloc(SIZE_MAX, 2) == NULL && errno == ENOMEM);
vacio();
unsigned char *p = mi_malloc(4 * a), *q = mi_malloc(8 * a), *r = mi_malloc(4 * a);
CHECK(p && q && r);
CHECK((uintptr_t) p % a == 0 && (uintptr_t) q % a == 0 && (uintptr_t) r % a == 0);
memset(p, 0xA1, 4 * a); memset(r, 0xC3, 4 * a);
uintptr_t lugar = (uintptr_t) q;
CHECK(mi_destroy() == -1 && errno == EBUSY);
int ajeno = 0;
CHECK(mi_free(&ajeno) == -1 && errno == EINVAL);
CHECK(mi_free(p + 1) == -1 && errno == EINVAL);
CHECK(mi_free(q) == 0); q = NULL;
q = mi_calloc(1, 3 * a);
CHECK(q != NULL && (uintptr_t) q == lugar);
for (size_t i = 0; i < 3 * a; i++) CHECK(q[i] == 0);
for (size_t i = 0; i < 4 * a; i++) CHECK(p[i] == 0xA1 && r[i] == 0xC3);
CHECK(mi_free(q) == 0);
CHECK(mi_free(q) == -1 && errno == EINVAL); q = NULL;
CHECK(mi_free(p) == 0); p = NULL;
CHECK(mi_free(r) == 0); r = NULL;
vacio();
p = mi_malloc(cap - h - a);
CHECK(p != NULL && foto().libre_util == 0);
CHECK(foto().cabeceras == h);
CHECK(mi_free(p) == 0); p = NULL;
p = mi_malloc(cap - 2 * h - a);
CHECK(p != NULL && foto().libre_util == a);
CHECK(foto().cabeceras == 2 * h);
CHECK(mi_free(p) == 0); p = NULL;
vacio();
void *cuatro[4];
CHECK(cap % (4 * a) == 0);
for (size_t i = 0; i < 4; i++) { cuatro[i] = mi_malloc(cap / 4 - h); CHECK(cuatro[i]); }
CHECK(mi_free(cuatro[0]) == 0); cuatro[0] = NULL;
CHECK(mi_free(cuatro[2]) == 0); cuatro[2] = NULL;
s = foto();
CHECK(s.libre_util > cap / 3 && s.mayor_libre < cap / 3);
CHECK(mi_malloc(cap / 3) == NULL && errno == ENOMEM);
struct MiStats despues = foto();
CHECK(despues.pedidos == s.pedidos && despues.libre_util == s.libre_util);
CHECK(despues.fallos == s.fallos + 1);
CHECK(mi_free(cuatro[1]) == 0); cuatro[1] = NULL;
CHECK(mi_free(cuatro[3]) == 0); cuatro[3] = NULL;
vacio();
void *pequenos[4096];
size_t cantidad = 0;
while ((p = mi_malloc(1)) != NULL) {
CHECK(cantidad < 4096); pequenos[cantidad++] = p;
}
CHECK(errno == ENOMEM && cantidad > 0);
(void) foto();
for (size_t i = 0; i < cantidad; i += 2) CHECK(mi_free(pequenos[i]) == 0);
(void) foto();
for (size_t i = 1; i < cantidad; i += 2) CHECK(mi_free(pequenos[i]) == 0);
vacio();
CHECK(mi_destroy() == 0 && mi_check());
CHECK(mi_destroy() == 0);
}
static uint32_t aleatorio(void)
{
estado = estado * UINT32_C(1664525) + UINT32_C(1013904223);
return estado;
}
struct Slot { unsigned char *p; size_t n; unsigned char patron; };
static void comprobar_datos(const struct Slot slots[], size_t cantidad)
{
size_t vivos = 0, suma = 0;
struct MiStats s = foto();
for (size_t i = 0; i < cantidad; i++) if (slots[i].p != NULL) {
vivos++; suma += slots[i].n;
CHECK((uintptr_t) slots[i].p % s.alineacion == 0);
for (size_t j = 0; j < slots[i].n; j++) CHECK(slots[i].p[j] == slots[i].patron);
for (size_t j = 0; j < i; j++) if (slots[j].p != NULL) {
uintptr_t a = (uintptr_t) slots[i].p, b = (uintptr_t) slots[j].p;
CHECK(a < b ? b - a >= slots[i].n : a - b >= slots[j].n);
}
}
CHECK(s.vivos == vivos && s.pedidos == suma);
}
static void estres(size_t pasos)
{
CHECK(mi_init(64UL * 1024) == 0);
struct Slot slots[64] = { 0 };
estado = semilla;
for (paso = 1; paso <= pasos; paso++) {
comprobar_datos(slots, 64);
size_t i = (aleatorio() >> 16) % 64;
if (slots[i].p != NULL) {
CHECK(mi_free(slots[i].p) == 0); slots[i].p = NULL;
} else {
size_t n = 1 + (aleatorio() >> 8) % 8192;
int ceros = (aleatorio() & 3) == 0;
unsigned char *p = ceros ? mi_calloc(1, n) : mi_malloc(n);
if (p != NULL) {
if (ceros) for (size_t j = 0; j < n; j++) CHECK(p[j] == 0);
unsigned char patron = (unsigned char) (aleatorio() >> 24);
memset(p, patron, n);
slots[i] = (struct Slot) { p, n, patron };
} else CHECK(errno == ENOMEM);
}
comprobar_datos(slots, 64);
}
struct MiStats s = foto();
printf("Estrés: semilla=%lu; pasos=%zu; exitos=%llu; fallos=%llu; pico=%zu B\n",
(unsigned long) semilla, pasos, s.exitos, s.fallos, s.pico);
for (size_t i = 0; i < 64; i++) { CHECK(mi_free(slots[i].p) == 0); slots[i].p = NULL; }
vacio();
CHECK(mi_destroy() == 0 && mi_check());
}
static int numero(const char *texto, unsigned long maximo, unsigned long *n)
{
if (!*texto) return 0;
for (const char *p = texto; *p; p++) if (!isdigit((unsigned char) *p)) return 0;
char *fin; errno = 0;
*n = strtoul(texto, &fin, 10);
return errno == 0 && *fin == '\0' && *n <= maximo;
}
int main(int argc, char *argv[])
{
unsigned long sem = 12345, pasos = 5000;
if (argc > 3 || (argc > 1 && !numero(argv[1], UINT32_MAX, &sem)) ||
(argc > 2 && (!numero(argv[2], 100000, &pasos) || pasos == 0))) {
fprintf(stderr, "Uso: %s [semilla_32_bits [pasos_1_a_100000]]\n", argv[0]); return 1;
}
semilla = (uint32_t) sem;
deterministas();
puts("Casos deterministas correctos.");
estres((size_t) pasos);
puts("Datos, invariantes, estadísticas y fusión final correctos.");
return 0;
}
Las comprobaciones de no superposición convierten direcciones a uintptr_t para diagnóstico dentro del mapeo de Linux. No se usan para desreferenciar objetos liberados ni pretenden definir orden portable entre objetos de cualquier plataforma.
El banco de slots está fuera de la región probada y lleva su propio tamaño y patrón. Así, no confía en el campo pedido de la cabecera para decidir cuántos bytes verificar. El verificador estructural y los datos externos se complementan.
10. Compilar, ejecutar e interpretar resultados
$ gcc -std=c17 -Wall -Wextra -Wpedantic -O2 asignador.c pruebas.c -o pruebas
$ ./pruebas
Casos deterministas correctos.
Estrés: semilla=12345; pasos=5000; exitos=...; fallos=...; pico=... B
Datos, invariantes, estadísticas y fusión final correctos.
$ ./pruebas 0 10000
$ ./pruebas 4294967295 10000
Los contadores dependen de la semilla, longitud y tamaños de la plataforma. Un número de fallos positivo es normal en una región pequeña con demanda variable. Lo que debe ser estable al repetir en el mismo entorno es la secuencia y el resultado.
Si una comprobación falla, conserva la semilla y el paso. Repite con menos pasos hasta mantener el prefijo que reproduce el problema. Después reduce la secuencia a un caso determinista que explique la causa; añadir más semillas sin entender el fallo no lo corrige.
En plataformas donde la página base o la alineación cambien, puede ser necesario adaptar el supuesto de cuatro cuartos alineados de la prueba de fragmentación. La implementación publica las magnitudes para poder comprobar ese supuesto en vez de ocultarlo.
11. Laboratorio de pruebas interactivo
Este banco usa una región de 4096 bytes, cabecera de 32, alineación de 16 y 16 slots externos. Las peticiones van de 1 a 512 bytes. Comprueba partición, fusión, no superposición, estadísticas y patrones tras cada operación. Es un modelo del método, con parámetros distintos de la batería C.
Laboratorio · estrés reproducible y conservación de datos
| Objetos vivos | Pedidos vivos | Libre útil | Mayor hueco | Cabeceras | Relleno | Éxitos | Fallos previstos | Pico útil |
|---|---|---|---|---|---|---|---|---|
| Cabecera, offset | Total | Pedido | Estado |
|---|
Últimos 30 eventos:
Cambiar los campos no modifica la ejecución activa hasta pulsar Iniciar pruebas. La semilla permite repetir ese banco con sus propios parámetros; no se promete que sus contadores coincidan con C, que usa otra capacidad y otras peticiones.
Para probar el detector, avanza hasta que exista algún objeto y pulsa la alteración intencional. Solo se cambia un byte del arreglo del modelo, no memoria de otras aplicaciones. En la siguiente comprobación debe detectarse el patrón incorrecto y detenerse el banco con semilla, paso y slot.
La limpieza finaliza la sesión, incluso si no se ejecutaron todos los pasos, libera los objetos y comprueba una sola cabecera con 4064 bytes útiles. Después de una prueba negativa no se permite borrar la evidencia mediante esa limpieza: hay que iniciar otra sesión.
12. Sanitizadores y límites de la instrumentación
$ gcc -std=c17 -Wall -Wextra -Wpedantic -O1 -g -fsanitize=undefined -fno-sanitize-recover=all asignador.c pruebas.c -o pruebas_ubsan
$ ./pruebas_ubsan 12345 5000
$ gcc -std=c17 -Wall -Wextra -Wpedantic -O2 -DNDEBUG asignador.c pruebas.c -o pruebas_ndebug
$ ./pruebas_ndebug 12345 5000
UndefinedBehaviorSanitizer puede ayudar a detectar accesos desalineados y otras operaciones instrumentables. No demuestra ausencia de todos los errores. AddressSanitizer tampoco conoce automáticamente cada subasignación dentro de nuestro mmap: sin integración adicional, un acceso que cruza un bloque puede permanecer dentro del mismo mapeo.
Las invariantes descubren daños de estructura y los patrones detectan alteraciones de bytes vivos. No detectan necesariamente una lectura indebida que no cambia contenido, un acceso a relleno que nadie inspecciona ni una referencia obsoleta que coincida con una nueva reserva válida.
Para completar un informe registra compilador, opciones, tamaño de cabecera, alineación, página base, semillas y cantidad de pasos. Mantén separadas las pruebas de corrección, el perfil de memoria del tema 49 y los benchmarks de velocidad.
Los contratos subyacentes pueden contrastarse en los manuales de mmap y la familia malloc, y las opciones de instrumentación en la documentación de GCC. Nuestra API tiene las diferencias explícitas de este proyecto.
13. Criterios de aceptación
- Los tres archivos compilan con las advertencias indicadas sin depender de inclusiones del tema 46.
- Los casos deterministas cubren alineación, división mínima, fusión, reutilización, fragmentación y agotamiento.
- Los fallos esperados conservan estructura y datos vivos, y las estadísticas distinguen éxito y rechazo.
- Los patrones y la no superposición se comprueban antes y después de las operaciones del estrés.
- La misma semilla reproduce la ejecución en el mismo entorno.
- La limpieza termina en un único hueco y la destrucción se rechaza si quedan objetos vivos.
- Las comprobaciones no desaparecen al definir NDEBUG.
No es suficiente ejecutar una sola semilla grande. Casos pequeños con expectativas exactas y secuencias más largas encuentran clases de problemas diferentes. Tampoco basta aumentar un contador de pruebas: importa qué propiedad podría detectar cada una.
14. Extensiones y límites del diseño
Esta implementación no tiene realloc, alineaciones extendidas, varias regiones, devolución parcial ni concurrencia. Buscar un puntero en mi_free recorre los bloques físicos; encontrar un hueco e insertar en la lista son recorridos lineales. El diagnóstico añade todavía más trabajo.
Como siguiente paso puedes incorporar una sola ampliación y escribir primero su contrato y sus pruebas:
- Realloc. Conservar el prefijo, no perder el bloque original en un fallo y definir tamaño cero.
- Varias regiones. Registrar bases y tamaños, no fusionar reservas diferentes y retirar solo regiones enteramente libres.
- Canarios o zonas de guarda. Reservar espacio adicional y definir cuándo se verifica, sin atribuirles detección universal.
- Concurrencia. Proteger todas las transiciones relacionadas y probar transferencias de propiedad entre hilos.
Un identificador con generación o una interfaz de handles puede ayudar con referencias obsoletas, pero cambia la forma de uso: un puntero sin información adicional no permite distinguir siempre dos vidas sucesivas en la misma dirección.
El curso termina con dos proyectos sobre capas diferentes: el tema 50 mantiene páginas y traducciones; este administra objetos dentro de una región. La interacción entre ambas capas explica gran parte de lo observado al medir memoria real.
15. Resumen
- El contrato define qué tamaños, errores y operaciones se deben probar.
- La alineación y los desbordamientos se resuelven antes de entregar almacenamiento.
- La lista libre y el mapa físico deben describir el mismo conjunto de huecos.
- Guardar el tamaño solicitado permite separar datos útiles, relleno y cabeceras.
- Un fallo de reserva no debe alterar los objetos vivos ni la estructura, aunque aumente el contador.
- Las invariantes y los patrones externos detectan problemas diferentes y se complementan.
- La semilla y el paso permiten reproducir el estrés y reducirlo a un caso explicable.
- La limpieza debe recuperar toda la región como un único bloque libre.
- Un resultado correcto bajo sanitizador no convierte el asignador en seguro frente a cualquier acceso inválido.
- Corrección, consumo y rendimiento requieren pruebas y métricas diferentes.
16. Preguntas de repaso y desafíos
Intenta responder antes de abrir cada explicación.
1. ¿Por qué se necesitan patrones además de mi_check?
La estructura puede seguir pareciendo válida mientras una operación escribe sobre los datos de otro objeto. El patrón externo verifica contenido y no depende del pedido guardado en la cabecera.
2. ¿Por qué guardar el tamaño pedido si ya existe el tamaño total?
Para distinguir datos vivos de capacidad adicional y conservar una contabilidad útil. El tamaño total también incluye cabecera y relleno.
3. ¿Un fallo de asignación hace fallar automáticamente la batería?
No. El agotamiento o la fragmentación pueden producir NULL de forma correcta. La prueba verifica el error esperado y que lo demás permanezca intacto.
4. ¿Qué evita la búsqueda por igualdad en mi_free?
Evita inferir y leer una cabecera antes de cualquier puntero recibido. Solo acepta un comienzo de datos ocupado que encuentre en el recorrido físico válido.
5. ¿Eso detecta siempre un puntero obsoleto?
No. Si la dirección ya fue reutilizada para otro objeto vivo, un puntero sin generación puede coincidir con el comienzo válido de esa nueva vida.
6. ¿Por qué CHECK no se escribe como assert?
Porque las comprobaciones de esta batería deben ejecutarse incluso con NDEBUG. Además, no se deben eliminar llamadas con efectos al cambiar opciones de compilación.
7. ¿Qué recupera la fusión además de unir capacidades?
Los bytes de las cabeceras intermedias dejan de ser metadatos independientes y pasan al espacio útil del bloque combinado.
8. ¿La misma semilla produce contadores idénticos en cualquier plataforma?
No necesariamente. El generador es reproducible, pero alineación, cabeceras y capacidad redondeada pueden cambiar las decisiones de reserva. Hay que registrar el entorno.
9. ¿Por qué no se mide velocidad con este estrés?
Porque recorre repetidamente objetos y estadísticas para comprobar corrección. Ese trabajo puede dominar sobre malloc y free.
10. ¿Se pueden pasar estos bloques a free de libc?
No. Pertenecen a mi_malloc y se liberan con su API. Las bibliotecas no comparten automáticamente cabeceras ni propiedad.
Para practicar
- Compila los tres archivos y ejecuta varias semillas, incluyendo cero y UINT32_MAX.
- Agrega los seis órdenes de liberación de tres bloques y comprueba fusión completa después de cada caso.
- Prueba init con SIZE_MAX y peticiones cercanas a los límites de redondeo sin realizar accesos a resultados NULL.
- Amplía las pruebas de fallos para contrastar todas las magnitudes estructurales, excluyendo los contadores que sí deben cambiar.
- En el laboratorio, repite semilla y pasos y compara estadísticas. Luego activa una alteración intencional y observa el diagnóstico.
- Introduce en una copia un error de fusión y determina qué caso pequeño lo encuentra antes que el estrés.
- Documenta qué errores de lectura o referencia obsoleta no detectan los patrones y las invariantes actuales.
- Diseña realloc y sus pruebas de crecimiento, reducción, conservación y fallo antes de implementarlo.