Explicação detalhada do princípio e implementação da função de alocação de memória malloc

Qualquer pessoa que tenha usado ou aprendido C estará familiarizada com malloc. Todo mundo sabe que malloc pode alocar um espaço de memória contínuo e liberá-lo gratuitamente quando não for mais usado. No entanto, muitos programadores não estão familiarizados com o que está por trás do malloc, e muitos até consideram o malloc como uma chamada de sistema fornecida pelo sistema operacional ou uma palavra-chave C.

Na verdade, malloc é apenas uma função comum fornecida na biblioteca padrão C, e a ideia básica de implementação de malloc não é complicada e pode ser facilmente compreendida por qualquer programador que tenha algum conhecimento de C e do sistema operacional.

Este artigo descreve o mecanismo por trás do malloc implementando um malloc simples. É claro que, em comparação com as implementações existentes da biblioteca padrão C (como glibc), nossa implementação do malloc não é particularmente eficiente, mas essa implementação é muito mais simples do que a implementação real do malloc atual, por isso é fácil de entender. É importante ressaltar que esta implementação é consistente com a implementação real dos princípios básicos.

Este artigo apresentará primeiro alguns conhecimentos básicos necessários, como o gerenciamento de memória do processo do sistema operacional e chamadas de sistema relacionadas, e então implementará gradualmente um malloc simples. Por uma questão de simplicidade, este artigo considerará apenas a arquitetura x86_64 e o sistema operacional é Linux.

1 O que é Malloc

Antes de implementar o malloc, devemos primeiro definir o malloc de forma relativamente formal.

De acordo com a definição de função da biblioteca C padrão, malloc possui o seguinte protótipo:

void* malloc(size_t size);

A função a ser implementada por esta função é alocar um período contínuo de memória disponível no sistema. Os requisitos específicos são os seguintes:

  • O tamanho da memória alocada por malloc é pelo menos o número de bytes especificado pelo parâmetro size.
  • O valor de retorno de malloc é um ponteiro apontando para o endereço inicial de um segmento de memória disponível.
  • Os endereços alocados por malloc várias vezes não podem se sobrepor, a menos que o endereço alocado por malloc seja liberado.
  • malloc deve completar a alocação de memória e retornar o mais rápido possível ( o algoritmo de alocação de memória de NP-hard[1] não pode ser usado)
  • Ao implementar malloc, as funções de ajuste de tamanho de memória e liberação de memória (ou seja, realloc e free) devem ser implementadas ao mesmo tempo.

Para obter mais instruções sobre malloc, você pode digitar o seguinte comando na linha de comando para visualizar:

man malloc

2 Conhecimento preliminar

Antes de implementar o malloc, alguns conhecimentos relacionados à memória do sistema Linux precisam ser explicados.

2.1 Gerenciamento de memória Linux

2.1.1 Endereço de memória virtual e endereço de memória física

Por uma questão de simplicidade, os sistemas operacionais modernos geralmente usam tecnologia de endereço de memória virtual ao processar endereços de memória. Ou seja, no nível assembler (ou linguagem de máquina), quando endereços de memória estão envolvidos, são usados ​​endereços de memória virtual. Ao utilizar esta tecnologia, cada processo parece ter seus próprios 2N bytes de memória, onde N é o número de bits da máquina. Por exemplo, em uma CPU de 64 bits e em um sistema operacional de 64 bits, o espaço de endereço virtual de cada processo é de 264 bytes.

A principal função deste espaço de endereço virtual é simplificar a escrita de programas e facilitar o gerenciamento de isolamento da memória entre processos pelo sistema operacional. É improvável (e não pode ser usado) que um processo real tenha um espaço de memória tão grande. que pode ser usado Depende do tamanho da memória física.

Como os endereços virtuais são usados ​​no nível da linguagem de máquina, quando o programa de código de máquina real envolve operações de memória, o endereço virtual precisa ser convertido em um endereço de memória física de acordo com o contexto real do processo atual em execução, a fim de realizar a operação de dados reais da memória. Essa conversão geralmente é concluída por uma peça de hardware chamada MMU[2] (Memory Management Unit).

2.1.2 Composição de páginas e endereços

Nos sistemas operacionais modernos, nem a memória virtual nem a memória física são gerenciadas em unidades de bytes, mas em unidades de páginas. Uma página de memória é o termo geral para um endereço de memória contínua de tamanho fixo. Especificamente no Linux, o tamanho típico da página de memória é 4096Byte (4K).

Assim, o endereço da memória pode ser dividido em número de página e deslocamento dentro da página. Tomando como exemplo uma máquina de 64 bits, memória física 4G e tamanho de página 4K, a composição do endereço de memória virtual e do endereço de memória física é a seguinte:

A parte superior é o endereço da memória virtual e a parte inferior é o endereço da memória física. Como o tamanho da página é 4K, o deslocamento dentro da página é representado pelos 12 bits inferiores e o endereço alto restante representa o número da página.

A unidade de mapeamento MMU não são bytes, mas páginas.Este mapeamento é implementado consultando uma tabela de páginas de estrutura de dados residente na memória [3] . Hoje em dia, o mapeamento específico de endereços de memória de computadores é relativamente complexo, para agilizar o processo, são introduzidas uma série de caches e otimizações, como TLB [4] e outros mecanismos.

Abaixo está um diagrama esquemático simplificado da tradução de endereços de memória. Embora simplificado, o princípio básico é consistente com a situação real dos computadores modernos.

2.1.3 Páginas de memória e páginas de disco

Sabemos que a memória é geralmente considerada um cache de disco. Às vezes, quando o MMU está funcionando, ele descobrirá que a tabela de páginas indica que uma determinada página de memória não está na memória física. Neste momento, uma exceção de falha de página (Falha de página ) será acionado. Neste momento, o sistema irá O local correspondente no disco carrega a página do disco na memória e, em seguida, executa novamente a instrução de máquina que falhou devido à falha de página. Em relação a esta parte, por poder ser considerada transparente para a implementação do malloc, não entrarei em detalhes.

Por fim, anexei um processo encontrado na Wikipedia que está mais alinhado com a tradução do endereço real para sua referência. Esta imagem adiciona o processo de TLB e exceções de páginas ausentes.

2.2 Gerenciamento de memória em nível de processo Linux

2.2.1 Disposição da memória

Agora que entendemos a relação entre a memória virtual e a memória física e o mecanismo de mapeamento relacionado, vamos dar uma olhada em como a memória é organizada dentro de um processo.

Veja o sistema Linux de 64 bits como exemplo. Teoricamente, o espaço disponível para endereços de memória de 64 bits é 0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF. Este é um espaço bastante grande e o Linux na verdade usa apenas uma pequena parte dele (256T).

De acordo com os documentos relacionados ao kernel Linux [6] , o sistema operacional Linux de 64 bits usa apenas os 47 bits inferiores e os 17 bits superiores para expansão (só podem ser todos 0 ou 1). Portanto, os endereços reais usados ​​são os espaços 0x000000000000000 ~ 0x00007FFFFFFFFFFFF e 0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF, onde o primeiro é o Espaço do Usuário e o último é o Espaço do Kernel. O diagrama é o seguinte:

Para os usuários, o principal espaço de preocupação é o Espaço do Usuário. Depois de ampliar o Espaço do Usuário, você pode ver que ele está dividido principalmente nas seguintes seções:

  • Código: Esta é a parte de endereço mais baixa de todo o espaço do usuário, que armazena instruções (ou seja, o código de máquina executável compilado pelo programa)
  • Dados: As variáveis ​​globais inicializadas são armazenadas aqui.
  • BSS: Variáveis ​​globais não inicializadas são armazenadas aqui.
  • Heap: Heap, este é o foco deste artigo. O heap cresce de endereços baixos para endereços altos. As chamadas de sistema relacionadas ao brk, que serão discutidas posteriormente, alocam memória a partir daqui.
  • Área de Mapeamento: Esta é a área relacionada à chamada do sistema mmap. A maioria das implementações práticas de malloc considerarão a alocação de áreas de memória maiores via mmap, e este artigo não discute esta situação. Esta área cresce de endereços altos para endereços baixos
  • Pilha: Esta é a área da pilha, crescendo do endereço alto para o endereço baixo.

Abaixo focamos principalmente nas operações da área Heap. Os alunos interessados ​​em todo o arranjo de memória do Linux podem consultar outros materiais.

2.2.2 Modelo de memória heap

De modo geral, a memória solicitada pelo malloc é alocada principalmente na área Heap (este artigo não considera a aplicação de grandes blocos de memória por meio do mmap).

Como sabemos pelo exposto, o espaço de endereço da memória virtual enfrentado pelo processo só pode ser realmente usado se for mapeado para o endereço da memória física por página. Devido às limitações da capacidade de armazenamento físico, é impossível que todo o espaço de memória virtual do heap seja mapeado para a memória física real. O gerenciamento de heap do Linux é o seguinte:

O Linux mantém um ponteiro de interrupção, que aponta para um endereço no espaço heap. O espaço de endereço do endereço inicial do heap até o break é mapeado e pode ser acessado pelo processo; e do break para cima, é um espaço de endereço não mapeado. Se esse espaço for acessado, o programa reportará um erro.

2.2.3 freio e freio

Como sabemos acima, para aumentar o tamanho real de heap disponível de um processo, você precisa mover o ponteiro de interrupção para um endereço mais alto. O Linux opera o ponteiro de interrupção por meio das chamadas de sistema brk e sbrk. Os protótipos das duas chamadas de sistema são os seguintes:

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

brik define o ponteiro de quebra diretamente para um endereço, enquanto sbrk move break da posição atual pelo incremento especificado por increment. brk retorna 0 quando executado com sucesso, caso contrário retorna -1 e define errno como ENOMEM; quando sbrk é bem-sucedido, ele retorna o endereço apontado antes do break ser movido, caso contrário retorna (void *)-1.

Um pequeno truque é que se você definir o incremento como 0, poderá obter o endereço do intervalo atual.

Outra coisa a notar é que, como o Linux mapeia a memória por página, se o break estiver definido para não ser alinhado pelo tamanho da página, o sistema irá realmente mapear uma página completa no final, então o espaço de memória real mapeado é maior que o break A área apontada para é maior. Mas usar o endereço após o intervalo é perigoso (embora talvez haja uma pequena área de endereço de memória livre após o intervalo).

2.2.4 Limites de recursos e rlimit

Os recursos alocados pelo sistema para cada processo não são ilimitados, incluindo espaço de memória mapeável, portanto cada processo possui um rlimit que representa o limite superior de recursos disponíveis para o processo atual.

Esse limite pode ser obtido por meio da chamada de sistema getrlimit. O código a seguir obtém o rlimit do espaço de memória virtual do processo atual:

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);
}

onde rlimit é uma estrutura:

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

Cada recurso possui limites flexíveis e rígidos, e rlimit pode ser definido condicionalmente por meio de setrlimit. O limite rígido serve como limite superior do limite flexível.Os processos não privilegiados só podem definir limites flexíveis e não podem exceder o limite rígido.

 Information Direct: rota de aprendizado da tecnologia de código-fonte do kernel Linux + tutorial em vídeo do código-fonte do kernel

Learning Express: Código-fonte do kernel Linux Ajuste de memória Sistema de arquivos Gerenciamento de processos Driver de dispositivo/pilha de protocolo de rede

3 Implementar malloc

3.1 Implementação do brinquedo

Antes de começarmos oficialmente a discutir a implementação do malloc, podemos usar o conhecimento acima para implementar um malloc de brinquedo real simples, mas quase impossível de usar, que deve ser usado como uma revisão do conhecimento acima:

/* 一个玩具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 o número de bytes especificados por tamanho com base na quebra atual a cada vez e retorna o endereço da quebra anterior. Este malloc não possui registros da memória alocada e é inconveniente para liberação de memória, portanto não pode ser usado em cenários reais.

3.2 Implementação formal

Vamos discutir seriamente a implementação do malloc.

3.2.1 Estrutura de dados

Primeiro precisamos determinar a estrutura de dados usada. Uma solução simples e viável é organizar o espaço de memória heap na forma de blocos. Cada bloco é composto por uma metaárea e uma área de dados. A metaárea registra as metainformações do bloco de dados (tamanho da área de dados, bandeira livre bit, ponteiro, etc.) ), a área de dados é a área de memória real alocada e o endereço do primeiro byte da área de dados é o endereço retornado por malloc.

Você pode definir um bloco com a seguinte estrutura:

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 */
};

Como consideramos apenas máquinas de 64 bits, por conveniência, preenchemos um int no final da estrutura para que o comprimento da própria estrutura seja um múltiplo de 8 para alinhamento de memória. O diagrama esquemático é o seguinte:

3.2.2 Encontre o bloco apropriado

Agora considere como encontrar o bloco apropriado na cadeia de blocos. De modo geral, existem dois algoritmos de pesquisa:

  • Primeiro ajuste : comece do zero e use o primeiro bloco cujo tamanho da área de dados seja maior que o tamanho necessário, o chamado bloco alocado desta vez.
  • Melhor ajuste : comece do início, percorra todos os blocos e use o bloco com o tamanho da área de dados maior que o tamanho e a menor diferença conforme o bloco alocado desta vez.

Ambos os métodos têm seus próprios méritos: o melhor ajuste tem maior uso de memória (maior carga útil), enquanto o primeiro ajuste tem melhor eficiência operacional. Aqui usamos o primeiro algoritmo de 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 inicia em frist_block, encontra o primeiro bloco que atende aos requisitos e retorna o endereço inicial do bloco. Se não for encontrado, retorna NULL.

Aqui,um ponteiro chamado last será atualizado durante a travessia.Este ponteiro sempre aponta para o bloco atualmente percorrido. Isto é usado para abrir um novo bloco se um bloco adequado não puder ser encontrado, o que será usado na próxima seção.

3.2.3 Abra um novo bloco

Se os blocos existentes não atenderem aos requisitos de tamanho, um novo bloco deverá ser aberto no final da lista vinculada. A chave aqui é como criar uma estrutura usando apenas 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 Bloco dividido

O primeiro ajuste tem uma falha fatal, ou seja, um tamanho pequeno pode ocupar um bloco grande. Neste momento, para aumentar a carga útil, ele deve ser dividido em um novo bloco quando a área de dados restante for grande o suficiente., a representação é o seguinte:

Código de implementação:

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 Implementação do malloc

Com o código acima, podemos usá-los para integrá-los em um malloc simples, mas inicialmente utilizável. Observe que primeiro precisamos definir o head first_block da lista de blocos e inicializá-lo como NULL; além disso, precisamos que o espaço restante seja pelo menos BLOCK_SIZE + 8 antes de realizar a operação de divisão.

Como queremos que a área de dados alocada por malloc seja alinhada por 8 bytes, quando o tamanho não for múltiplo de 8, precisamos ajustar o tamanho para o menor múltiplo de 8 que seja maior que o tamanho:

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 Implementação de calloc

Com malloc, existem apenas duas etapas para implementar calloc:

  1. malloc um pedaço de memória
  2. Defina o conteúdo da área de dados como 0

Como nossa área de dados está alinhada em 8 bytes, para melhorar a eficiência, podemos definir 0 em grupos de 8 bytes em vez de defini-los um por um. Podemos conseguir isso criando um novo ponteiro size_t e forçando a área de memória a ser do 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 Implementação de gratuidade

A implementação do free não é tão simples quanto parece. Aqui temos que resolver duas questões principais:

  1. Como verificar se o endereço de entrada é um endereço válido, ou seja, é de fato o primeiro endereço da área de dados alocado através do malloc.
  2. Como corrigir problemas de fragmentação

Em primeiro lugar, precisamos de garantir que o endereço gratuito de entrada é válido.Esta validade inclui dois aspectos:

  • O endereço deve estar dentro da área alocada pelo malloc antes, ou seja, dentro do intervalo do first_block e do ponteiro de quebra atual
  • Este endereço foi de fato alocado anteriormente através do nosso próprio malloc

O primeiro problema é mais fácil de resolver. Basta comparar os endereços. A chave é o segundo problema.

Existem duas soluções aqui: uma é enterrar um campo de número mágico na estrutura. Antes de liberar, verifique se o valor de uma posição específica é o número mágico que definimos usando um deslocamento relativo. O outro método é adicionar um ponteiro mágico para a estrutura. Este ponteiro aponta para o primeiro byte da área de dados (ou seja, o endereço passado quando free é legal). Verificamos se o ponteiro mágico aponta para o endereço apontado pelo parâmetro antes de liberar. Aqui usamos a segunda opção:

Primeiro adicionamos um ponteiro mágico à estrutura (e modificamos BLOCK_SIZE ao mesmo tempo):

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 */
};

Então definimos uma função que verifica a validade do endereço:

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;
}

Após vários mallocs e liberações, todo o pool de memória pode produzir muitos blocos fragmentados. Esses blocos são muito pequenos e muitas vezes não podem ser usados. Pode até haver muitos fragmentos conectados entre si. Embora os requisitos gerais do malloc possam ser atendidos, eles são divididos em vários blocos pequenos. Bloqueado e incapaz de caber, este é um problema de fragmentação.

Uma solução simples é que ao liberar um bloco, se for constatado que seu bloco adjacente também está livre, fundir o bloco com o bloco adjacente. Para atender a esta implementação, s_block precisa ser alterado para uma lista duplamente vinculada.

A estrutura do bloco modificada é a seguinte:

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 */
};

O método de mesclagem é o seguinte:

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;
}

Com o método acima, a ideia de implementação do free é relativamente clara: primeiro verifique a legalidade do endereço do parâmetro, se for ilegal, não faça nada; caso contrário, marque o free deste bloco como 1 e combine-o com o seguinte, se possível, os blocos são mesclados.

Se o bloco atual for o último bloco, reverta o ponteiro de interrupção para liberar a memória do processo. Se o bloco atual for o último bloco, reverta o ponteiro de interrupção e defina first_block como NULL. A implementação é a seguinte:

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 Implementação de realocação

Para implementar realloc, primeiro precisamos implementar um método de cópia de memória. Assim como calloc, para maior eficiência, copiamos em 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];
}

Então começamos a implementar realloc. Um método simples (mas ineficiente) é alocar uma seção de memória e depois copiar os dados para lá. Mas podemos fazê-lo de forma mais eficiente, considerando especificamente os seguintes aspectos:

  • Se a área de dados do bloco atual for maior ou igual ao tamanho exigido pelo realloc, nenhuma operação será executada.
  • Se o novo tamanho ficar menor, considere dividir
  • Se a área de dados do bloco atual não puder atingir o tamanho, mas seu bloco subsequente estiver livre e puder atingir o tamanho após a mesclagem, considere mesclá-lo.

A seguir está a implementação do 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 legados e otimizações

A descrição acima é uma implementação de malloc relativamente simples, mas inicialmente utilizável. Ainda existem muitos pontos de otimização possíveis restantes, como:

  • Compatível com sistemas de 32 e 64 bits
  • Ao alocar blocos maiores de memória, considere usar mmap em vez de sbrk, que geralmente é mais eficiente
  • Você pode considerar manter várias listas vinculadas em vez de uma única. O tamanho do bloco em cada lista vinculada está dentro de um intervalo, como lista vinculada de 8 bytes, lista vinculada de 16 bytes, lista vinculada de 24 a 32 bytes, etc. Neste momento, a alocação pode ser feita na lista vinculada correspondente de acordo com o tamanho, o que pode efetivamente reduzir a fragmentação e melhorar a velocidade de consulta do bloco.
  • Você pode considerar armazenar apenas blocos livres na lista vinculada em vez de blocos alocados, o que pode reduzir o número de pesquisas de bloco e melhorar a eficiência.

Autor original: Aprenda integrados juntos

Acho que você gosta

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