Explicación detallada del principio y la implementación de la función de asignación de memoria malloc

Cualquiera que haya usado o aprendido C estará familiarizado con malloc. Todo el mundo sabe que malloc puede asignar un espacio de memoria continuo y liberarlo cuando ya no se utiliza. Sin embargo, muchos programadores no están familiarizados con lo que hay detrás de malloc, y muchos incluso consideran que malloc es una llamada al sistema proporcionada por el sistema operativo o una palabra clave en C.

De hecho, malloc es solo una función común proporcionada en la biblioteca estándar de C, y la idea básica de implementar malloc no es complicada y cualquier programador que tenga algún conocimiento de C y el sistema operativo puede entenderla fácilmente.

Este artículo describe el mecanismo detrás de malloc mediante la implementación de un malloc simple. Por supuesto, en comparación con las implementaciones de bibliotecas estándar de C existentes (como glibc), nuestra implementación de malloc no es particularmente eficiente, pero esta implementación es mucho más simple que la implementación real actual de malloc, por lo que es fácil de entender. Es importante destacar que esta implementación es consistente con la implementación real en principios básicos.

Este artículo primero presentará algunos conocimientos básicos necesarios, como la gestión de la memoria del proceso por parte del sistema operativo y las llamadas al sistema relacionadas, y luego implementará gradualmente un malloc simple. En aras de la simplicidad, este artículo solo considerará la arquitectura x86_64 y el sistema operativo es Linux.

1 ¿Qué es malloc?

Antes de implementar malloc, primero debemos definir malloc de manera relativamente formal.

Según la definición de función de biblioteca C estándar, malloc tiene el siguiente prototipo:

void* malloc(size_t size);

La función a implementar por esta función es asignar un período continuo de memoria disponible en el sistema, los requisitos específicos son los siguientes:

  • El tamaño de memoria asignado por malloc es al menos el número de bytes especificado por el parámetro de tamaño.
  • El valor de retorno de malloc es un puntero que apunta a la dirección inicial de un segmento de memoria disponible.
  • Las direcciones asignadas por malloc varias veces no pueden superponerse a menos que se libere la dirección asignada por malloc.
  • malloc debe completar la asignación de memoria y regresar lo antes posible ( el algoritmo de asignación de memoria de NP-hard[1] no se puede utilizar)
  • Al implementar malloc, las funciones de ajuste del tamaño de la memoria y liberación de memoria (es decir, realloc y free) deben implementarse al mismo tiempo.

Para obtener más instrucciones sobre malloc, puede escribir el siguiente comando en la línea de comando para verlo:

man malloc

2 Conocimientos preliminares

Antes de implementar malloc, es necesario explicar algunos conocimientos relacionados con la memoria del sistema Linux.

2.1 gestión de memoria de Linux

2.1.1 Dirección de memoria virtual y dirección de memoria física

En aras de la simplicidad, los sistemas operativos modernos generalmente utilizan tecnología de direcciones de memoria virtual al procesar direcciones de memoria. Es decir, a nivel de ensamblador (o lenguaje de máquina), cuando están involucradas direcciones de memoria, se utilizan direcciones de memoria virtual. Cuando se utiliza esta tecnología, cada proceso parece tener sus propios 2N bytes de memoria, donde N es el número de bits de la máquina. Por ejemplo, en una CPU de 64 bits y un sistema operativo de 64 bits, el espacio de direcciones virtuales de cada proceso es de 264 bytes.

La función principal de este espacio de direcciones virtuales es simplificar la escritura de programas y facilitar la gestión de aislamiento de la memoria entre procesos por parte del sistema operativo. Es poco probable (y no se puede utilizar) que un proceso real tenga un espacio de memoria tan grande. La memoria real que se puede utilizar Depende del tamaño de la memoria física.

Dado que las direcciones virtuales se utilizan a nivel de lenguaje de máquina, cuando el programa de código de máquina real implica operaciones de memoria, la dirección virtual debe convertirse en una dirección de memoria física de acuerdo con el contexto real del proceso actual en ejecución para poder realizar la operación de datos de memoria reales. Esta conversión generalmente se completa con una pieza de hardware llamada MMU[2] (Unidad de administración de memoria).

2.1.2 Composición de páginas y direcciones

En los sistemas operativos modernos, ni la memoria virtual ni la memoria física se gestionan en unidades de bytes, sino en unidades de páginas. Una página de memoria es el término general para una dirección de memoria continua de tamaño fijo. Específicamente en Linux, el tamaño típico de la página de memoria es 4096 Byte (4K).

Por lo tanto, la dirección de la memoria se puede dividir en número de página y desplazarse dentro de la página. Tomando como ejemplo una máquina de 64 bits, memoria física 4G y tamaño de página 4K, la composición de la dirección de memoria virtual y la dirección de memoria física es la siguiente:

La parte superior es la dirección de la memoria virtual y la parte inferior es la dirección de la memoria física. Dado que el tamaño de la página es 4K, el desplazamiento dentro de la página está representado por los 12 bits inferiores y la dirección alta restante representa el número de página.

La unidad de mapeo de MMU no son bytes, sino páginas. Este mapeo se implementa buscando una tabla de páginas de estructura de datos residente en memoria [3] . Hoy en día, el mapeo de direcciones de memoria específicas de las computadoras es relativamente complejo, para acelerar el proceso se introducen una serie de cachés y optimizaciones, como TLB [4] y otros mecanismos.

A continuación se proporciona un diagrama esquemático simplificado de la traducción de direcciones de memoria. Aunque está simplificado, el principio básico es consistente con la situación real de las computadoras modernas.

2.1.3 Páginas de memoria y páginas de disco

Sabemos que la memoria generalmente se considera un caché de disco. A veces, cuando la MMU está funcionando, encontrará que la tabla de páginas indica que una determinada página de memoria no está en la memoria física. En este momento, se genera una excepción de falla de página (falla de página). ) se activará. En este momento, el sistema La ubicación correspondiente en el disco cargará la página del disco en la memoria y luego volverá a ejecutar la instrucción de la máquina que falló debido a la falla de la página. Con respecto a esta parte, debido a que puede considerarse transparente para la implementación de malloc, no entraré en detalles.

Finalmente, adjunto un proceso que se encuentra en Wikipedia que está más en línea con la traducción de direcciones reales para su referencia. Esta imagen agrega el proceso de TLB y las excepciones de páginas faltantes.

2.2 Gestión de memoria a nivel de proceso de Linux

2.2.1 Disposición de la memoria

Ahora que entendemos la relación entre la memoria virtual y la memoria física y el mecanismo de mapeo relacionado, echemos un vistazo a cómo se organiza la memoria dentro de un proceso.

Tomemos como ejemplo el sistema Linux de 64 bits. En teoría, el espacio disponible para direcciones de memoria de 64 bits es 0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFFFF, que es un espacio bastante grande y Linux en realidad solo usa una pequeña parte (256T).

Según los documentos relacionados con el kernel de Linux [6] , el sistema operativo Linux de 64 bits solo usa los 47 bits inferiores y los 17 bits superiores para la expansión (solo pueden ser todos 0 o todos 1). Por lo tanto, las direcciones reales utilizadas son los espacios 0x0000000000000000 ~ 0x00007FFFFFFFFFFFF y 0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFFFF, donde el primero es el espacio del usuario y el segundo es el espacio del kernel. El diagrama es el siguiente:

Para los usuarios, el principal espacio de preocupación es el Espacio de Usuario. Después de ampliar el Espacio de usuario, podrá ver que está dividido principalmente en las siguientes secciones:

  • Código: esta es la parte de dirección más baja de todo el espacio de usuario, que almacena instrucciones (es decir, el código de máquina ejecutable compilado por el programa)
  • Datos: Las variables globales inicializadas se almacenan aquí.
  • BSS: aquí se almacenan las variables globales no inicializadas.
  • Montón: Montón, este es el enfoque de este artículo. El montón crece desde direcciones bajas hasta direcciones altas. Las llamadas al sistema relacionadas con brk que se analizarán más adelante asignan memoria desde aquí.
  • Área de mapeo: esta es el área relacionada con la llamada al sistema mmap. La mayoría de las implementaciones prácticas de malloc considerarán la asignación de áreas de memoria más grandes a través de mmap, y este artículo no analiza esta situación. Esta área crece desde direcciones altas hasta direcciones bajas.
  • Pila: esta es el área de la pila, que crece desde la dirección alta hasta la dirección baja.

A continuación nos centramos principalmente en las operaciones del área Heap. Los estudiantes que estén interesados ​​en la disposición completa de la memoria de Linux pueden consultar otros materiales.

2.2.2 Modelo de memoria dinámica

En términos generales, la memoria solicitada por malloc se asigna principalmente desde el área del montón (este artículo no considera la solicitud de grandes bloques de memoria a través de mmap).

Como sabemos por lo anterior, el espacio de direcciones de memoria virtual al que se enfrenta el proceso solo se puede utilizar realmente si se asigna a la dirección de memoria física por página. Debido a las limitaciones de la capacidad de almacenamiento físico, es imposible asignar todo el espacio de memoria virtual del montón a la memoria física real. La gestión del montón de Linux es la siguiente:

Linux mantiene un puntero de interrupción, que apunta a una dirección en el espacio del montón. El espacio de direcciones desde la dirección inicial del montón hasta la ruptura está asignado y el proceso puede acceder a él, y desde la ruptura hacia arriba, es un espacio de direcciones no asignado. Si se accede a este espacio, el programa informará un error.

2.2.3 freno y freno

Como sabemos por lo anterior, para aumentar el tamaño de almacenamiento dinámico disponible real de un proceso, es necesario mover el puntero de interrupción a una dirección más alta. Linux opera el puntero de interrupción a través de las llamadas al sistema brk y sbrk. Los prototipos de las dos llamadas al sistema son los siguientes:

int brk(void *addr);
void *sbrk(intptr_t increment);

brk establece el puntero de interrupción directamente a una dirección, mientras que sbrk mueve la interrupción desde la posición actual en el incremento especificado por incremento. brk devuelve 0 cuando se ejecuta correctamente; de ​​lo contrario, devuelve -1 y establece errno en ENOMEM; cuando sbrk tiene éxito, devuelve la dirección a la que apuntaba antes de que se moviera la interrupción; de lo contrario, devuelve (void *)-1.

Un pequeño truco es que si establece el incremento en 0, puede obtener la dirección de la interrupción actual.

Otra cosa a tener en cuenta es que, dado que Linux asigna memoria por página, si se configura la interrupción para que no esté alineada por el tamaño de la página, el sistema en realidad asignará una página completa al final, por lo que el espacio de memoria asignado real es mayor que el área señalada por la interrupción. a es más grande. Pero usar la dirección después de la interrupción es peligroso (aunque tal vez haya una pequeña área de dirección de memoria libre después de la interrupción).

2.2.4 Límites de recursos y rlimit

Los recursos asignados por el sistema a cada proceso no son ilimitados, incluido el espacio de memoria asignable, por lo que cada proceso tiene un rlimit que representa el límite superior de recursos disponibles para el proceso actual.

Este límite se puede obtener mediante la llamada al sistema getrlimit. El siguiente código obtiene el rlimit del espacio de memoria virtual del proceso actual:

int main() {
struct rlimit *limit = (struct rlimit *)malloc(sizeof(struct rlimit));
getrlimit(RLIMIT_AS, limit);
printf("soft limit: %ld, hard limit: %ld\n", limit->rlim_cur, limit->rlim_max);
}

donde rlimit es una estructura:

struct rlimit {
rlim_t rlim_cur; /* Soft limit */
rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};

Cada recurso tiene límites suaves y límites estrictos, y rlimit se puede establecer condicionalmente a través de setrlimit. El límite estricto sirve como el límite superior del límite flexible. Los procesos sin privilegios solo pueden establecer límites flexibles y no pueden exceder el límite estricto.

 Information Direct: ruta de aprendizaje de tecnología del código fuente del kernel de Linux + video tutorial sobre el código fuente del kernel

Learning Express: Código fuente del kernel de Linux Ajuste de memoria Sistema de archivos Gestión de procesos Controlador de dispositivo/Pila de protocolo de red

3 Implementar malloc

3.1 Implementación de juguetes

Antes de comenzar a discutir oficialmente la implementación de malloc, podemos usar el conocimiento anterior para implementar un malloc de juguete real simple pero casi imposible de usar, que debe usarse como una revisión del conocimiento anterior:

/* 一个玩具malloc */
#include <sys/types.h>
#include <unistd.h>
void *malloc(size_t size)
{
void *p;
p = sbrk(0);
if (sbrk(size) == (void *)-1)
return NULL;
return p;
}

Este malloc aumenta el número de bytes especificados por tamaño según la interrupción actual cada vez y devuelve la dirección de la interrupción anterior. Este malloc carece de registros de la memoria asignada y es inconveniente para la liberación de memoria, por lo que no se puede utilizar en escenarios reales.

3.2 Implementación formal

Analicemos seriamente la implementación de malloc.

3.2.1 Estructura de datos

Primero necesitamos determinar la estructura de datos utilizada. Una solución simple y factible es organizar el espacio de memoria del montón en forma de bloques. Cada bloque se compone de una metaárea y un área de datos. La metaárea registra la metainformación del bloque de datos (tamaño del área de datos, bandera libre bit, puntero, etc.)), el área de datos es el área de memoria asignada real y la dirección del primer byte del área de datos es la dirección devuelta por malloc.

Puede definir un bloque con la siguiente estructura:

typedef struct s_block *t_block;
struct s_block {
  size_t size; /* 数据区大小 */
  t_block next; /* 指向下个块的指针 */
  int free; /* 是否是空闲块 */
  int padding; /* 填充4字节,保证meta块长度为8的倍数 */
  char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};

Dado que solo consideramos máquinas de 64 bits, por conveniencia, completamos un int al final de la estructura para que la longitud de la estructura misma sea múltiplo de 8 para la alineación de la memoria. El diagrama esquemático es el siguiente:

3.2.2 Encuentra el bloque apropiado

Ahora considere cómo encontrar el bloque apropiado en la cadena de bloques. En términos generales, existen dos algoritmos de búsqueda:

  • Primer ajuste : comience desde cero y use el primer bloque cuyo tamaño del área de datos sea mayor que el tamaño requerido, el llamado bloque asignado esta vez.
  • Mejor ajuste : comience desde el principio, recorra todos los bloques y use el bloque con el tamaño del área de datos mayor que el tamaño y la diferencia más pequeña como el bloque asignado esta vez.

Ambos métodos tienen sus propios méritos: el mejor ajuste tiene un mayor uso de memoria (mayor carga útil), mientras que el primer ajuste tiene una mejor eficiencia operativa. Aquí utilizamos el algoritmo del primer ajuste.

/* First fit */
t_block find_block(t_block *last, size_t size) {
  t_block b = first_block;
  while(b && !(b->free && b->size >= size)) {
     *last = b;
     b = b->next;
    }
  return b;
}

find_block comienza desde frist_block, encuentra el primer bloque que cumple con los requisitos y devuelve la dirección inicial del bloque. Si no se encuentra, devuelve NULL.

Aquí, durante el recorrido se actualizará un puntero llamado último, que siempre apunta al bloque atravesado actualmente. Esto se usa para abrir un nuevo bloque si no se puede encontrar un bloque adecuado, que se usará en la siguiente sección.

3.2.3 Abrir un nuevo bloque

Si los bloques existentes no pueden cumplir con los requisitos de tamaño, se debe abrir un nuevo bloque al final de la lista vinculada. La clave aquí es cómo crear una estructura usando sólo sbrk:

#define BLOCK_SIZE 24 /* 由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 */
 
t_block extend_heap(t_block last, size_t s) {
t_block b;
b = sbrk(0);
if(sbrk(BLOCK_SIZE + s) == (void *)-1)
return NULL;
b->size = s;
b->next = NULL;
if(last)
last->next = b;
b->free = 0;
return b;
}

3.2.4 Bloque dividido

El primer ajuste tiene un defecto fatal, es decir, un tamaño pequeño puede ocupar un bloque grande. En este momento, para aumentar la carga útil, debe dividirse en un nuevo bloque cuando el área de datos restante sea lo suficientemente grande. , la representación es como sigue:

Código de implementación:

void split_block(t_block b, size_t s) {
t_block new;
new = b->data + s;
new->size = b->size - s - BLOCK_SIZE ;
new->next = b->next;
new->free = 1;
b->size = s;
b->next = new;
}

3.2.5 Implementación de malloc

Con el código anterior, podemos usarlos para integrarlos en un malloc simple pero inicialmente utilizable. Tenga en cuenta que primero debemos definir el encabezado first_block de la lista de bloques e inicializarlo en NULL; además, necesitamos que el espacio restante sea al menos BLOCK_SIZE + 8 antes de realizar la operación de división.

Como queremos que el área de datos asignada por malloc esté alineada en 8 bytes, cuando el tamaño no es múltiplo de 8, debemos ajustar el tamaño al múltiplo más pequeño de 8 que sea mayor que el tamaño:

size_t align8(size_t s) {
if(s & 0x7 == 0)
return s;
return ((s >> 3) + 1) << 3;
}

#define BLOCK_SIZE 24
void *first_block=NULL;
 
/* other functions... */
 
void *malloc(size_t size) {
t_block b, last;
size_t s;
/* 对齐地址 */
s = align8(size);
if(first_block) {
/* 查找合适的block */
last = first_block;
b = find_block(&last, s);
if(b) {
/* 如果可以,则分裂 */
if ((b->size - s) >= ( BLOCK_SIZE + 8))
split_block(b, s);
b->free = 0;
} else {
/* 没有合适的block,开辟一个新的 */
b = extend_heap(last, s);
if(!b)
return NULL;
}
} else {
b = extend_heap(NULL, s);
if(!b)
return NULL;
first_block = b;
}
return b->data;
}

3.2.6 Implementación de calloc

Con malloc, solo hay dos pasos para implementar calloc:

  1. malloc un pedazo de memoria
  2. Establezca el contenido del área de datos en 0

Dado que nuestra área de datos está alineada por 8 bytes, para mejorar la eficiencia, podemos configurar 0 en grupos de 8 bytes en lugar de configurarlos uno por uno. Podemos lograr esto creando un nuevo puntero size_t y forzando que el área de memoria sea del tipo size_t.

void *calloc(size_t number, size_t size) {
size_t *new;
size_t s8, i;
new = malloc(number * size);
if(new) {
s8 = align8(number * size) >> 3;
for(i = 0; i < s8; i++)
new[i] = 0;
}
return new;
}

3.2.7 Implementación gratuita

La implementación de lo gratuito no es tan sencilla como parece, aquí tenemos que resolver dos cuestiones clave:

  1. Cómo verificar que la dirección entrante es una dirección válida, es decir, de hecho es la primera dirección del área de datos asignada a través de malloc.
  2. Cómo solucionar problemas de fragmentación

En primer lugar debemos asegurarnos de que la dirección libre entrante sea válida, esta validez incluye dos aspectos:

  • La dirección debe estar dentro del área asignada por malloc antes, es decir, dentro del rango de first_block y el puntero de interrupción actual.
  • De hecho, esta dirección fue asignada previamente a través de nuestro propio malloc.

El primer problema es más fácil de resolver: basta con comparar las direcciones. La clave es el segundo problema.

Aquí hay dos soluciones: una es enterrar un campo de número mágico en la estructura. Antes de liberarlo, verifique si el valor de una posición específica es el número mágico que configuramos usando un desplazamiento relativo. El otro método es agregar un puntero mágico. a la estructura. Este puntero apunta al primer byte del área de datos (es decir, la dirección pasada cuando está libre es legal). Verificamos si el puntero mágico apunta a la dirección señalada por el parámetro antes de liberar. Aquí usamos la segunda opción:

Primero agregamos un puntero mágico a la estructura (y modificamos BLOCK_SIZE al mismo tiempo):

typedef struct s_block *t_block;
struct s_block {
size_t size; /* 数据区大小 */
t_block next; /* 指向下个块的指针 */
int free; /* 是否是空闲块 */
int padding; /* 填充4字节,保证meta块长度为8的倍数 */
void *ptr; /* Magic pointer,指向data */
char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};

Luego definimos una función que verifica la validez de la dirección:

t_block get_block(void *p) {
char *tmp;
tmp = p;
return (p = tmp -= BLOCK_SIZE);
}
 
int valid_addr(void *p) {
if(first_block) {
if(p > first_block && p < sbrk(0)) {
return p == (get_block(p))->ptr;
}
}
return 0;
}

Después de múltiples mallocs y liberaciones, todo el grupo de memoria puede producir muchos bloques fragmentados. Estos bloques son muy pequeños y a menudo no se pueden usar. Incluso puede haber muchos fragmentos conectados entre sí. Aunque se pueden cumplir los requisitos generales de malloc, se dividen en múltiples bloques pequeños Bloqueo y no caben, esto es un problema de fragmentación.

Una solución simple es que al liberar un bloque, si se descubre que su bloque adyacente también está libre, fusionar el bloque con el bloque adyacente. Para cumplir con esta implementación, es necesario cambiar s_block a una lista doblemente enlazada.

La estructura de bloques modificada es la siguiente:

typedef struct s_block *t_block;
struct s_block {
size_t size; /* 数据区大小 */
t_block prev; /* 指向上个块的指针 */
t_block next; /* 指向下个块的指针 */
int free; /* 是否是空闲块 */
int padding; /* 填充4字节,保证meta块长度为8的倍数 */
void *ptr; /* Magic pointer,指向data */
char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};

El método de fusión es el siguiente:

t_block fusion(t_block b) {
  if (b->next && b->next->free) {
  b->size += BLOCK_SIZE + b->next->size;
  b->next = b->next->next;
  if(b->next)
  b->next->prev = b;
  }
  return b;
}

Con el método anterior, la idea de implementación de free es relativamente clara: primero verifique la legalidad de la dirección del parámetro, si es ilegal, no haga nada; de lo contrario, marque el free de este bloque como 1 y combínelo con Los siguientes bloques, si es posible, se fusionan.

Si el bloque actual es el último bloque, retroceda el puntero de interrupción para liberar la memoria del proceso. Si el bloque actual es el último bloque, retroceda el puntero de interrupción y establezca first_block en NULL. La implementación es la siguiente:

void free(void *p) {
  t_block b;
  if(valid_addr(p)) {
   b = get_block(p);
  b->free = 1;
  if(b->prev && b->prev->free)
  b = fusion(b->prev);
  if(b->next)
    fusion(b);
  else {
   if(b->prev)
     b->prev->prev = NULL;
   else
    first_block = NULL;
   brk(b);
  }
 }
}

3.2.8 Implementación de reasignación

Para implementar la realloc, primero necesitamos implementar un método de copia de memoria. Al igual que calloc, para mayor eficiencia, copiamos en unidades de 8 bytes:

void copy_block(t_block src, t_block dst) {
size_t *sdata, *ddata;
size_t i;
sdata = src->ptr;
ddata = dst->ptr;
for(i = 0; (i * 8) < src->size && (i * 8) < dst->size; i++)
ddata[i] = sdata[i];
}

Luego comenzamos a implementar realloc. Un método simple (pero ineficiente) es asignar mal una sección de memoria y luego copiar los datos allí. Pero podemos hacerlo de forma más eficiente, concretamente podemos considerar los siguientes aspectos:

  • Si el área de datos del bloque actual es mayor o igual que el tamaño requerido por la reasignación, no se realizará ninguna operación.
  • Si el nuevo tamaño se vuelve más pequeño, considere dividirlo
  • Si el área de datos del bloque actual no puede cumplir con el tamaño, pero su bloque posterior está libre y puede cumplir con el tamaño después de fusionarlo, considere fusionarlo.

La siguiente es la implementación de realloc:

void *realloc(void *p, size_t size) {
size_t s;
t_block b, new;
void *newp;
if (!p)
/* 根据标准库文档,当p传入NULL时,相当于调用malloc */
return malloc(size);
if(valid_addr(p)) {
s = align8(size);
b = get_block(p);
if(b->size >= s) {
if(b->size - s >= (BLOCK_SIZE + 8))
split_block(b,s);
} else {
/* 看是否可进行合并 */
if(b->next && b->next->free
&& (b->size + BLOCK_SIZE + b->next->size) >= s) {
fusion(b);
if(b->size - s >= (BLOCK_SIZE + 8))
split_block(b, s);
} else {
/* 新malloc */
newp = malloc (s);
if (!newp)
return NULL;
new = get_block(newp);
copy_block(b, new);
free(p);
return(newp);
}
}
return (p);
}
return NULL;
}

3.3 Problemas heredados y optimizaciones

Lo anterior es una implementación de malloc relativamente simple, pero inicialmente utilizable. Todavía quedan muchos puntos de optimización posibles, como por ejemplo:

  • Compatible con sistemas de 32 y 64 bits
  • Al asignar bloques de memoria más grandes, considere usar mmap en lugar de sbrk, que suele ser más eficiente
  • Puede considerar mantener varias listas vinculadas en lugar de una sola. El tamaño del bloque en cada lista vinculada está dentro de un rango, como lista vinculada de 8 bytes, lista vinculada de 16 bytes, lista vinculada de 24-32 bytes, etc. En este momento, la asignación se puede realizar a la lista vinculada correspondiente según el tamaño, lo que puede reducir efectivamente la fragmentación y mejorar la velocidad de consulta del bloque.
  • Puede considerar almacenar solo bloques libres en la lista vinculada en lugar de bloques asignados, lo que puede reducir la cantidad de búsquedas de bloques y mejorar la eficiencia.

Autor original: Aprenda integrado juntos

Supongo que te gusta

Origin blog.csdn.net/youzhangjing_/article/details/132762132
Recomendado
Clasificación