2009-03-13

Gestor de memória, parte II

Agora que já temos uma ideia do que temos de fazer podemos começar a escrever o nosso gestor de memória.

Makefile

Vamos dividir o trabalho em três modulos:

  1. mymalloc.o: um módulo com as funções mymalloc e myfree
  2. dict.o: para testar vamos usar uma versão modificada do código do primeiro guião, usando mymalloc e myfree no lugar de malloc e free
  3. prog.o: um módulo com a função main, exactamente igual à do primeiro guião

Para compilar isto usamos este Makefile:

OBJECT_FILES=prog.o mymalloc.o dict.o
CFLAGS=-g

prog : $(OBJECT_FILES)
$(CC) $(CFLAGS) -o $@ $(OBJECT_FILES)

.c.o :
$(CC) $(CFLAGS) -c -o $@ $<;

dict.o : dict.h
mymalloc.o : mymalloc.h

Este foi escrito tirando partido de algumas funcionalidades avançadas do make. Num outro post posso falar sobre isso. Não se preocupem se não perceberem. Se quiserem podem tentar escrever o vosso com base no do primeiro guião. Não se esqueçam da esquisitice do make em relação a tabs e espaços.

Preparação

Os cabeçalhos de mymalloc e myfree ficam em mymalloc.h:

#include <stddef.h>

void* mymalloc(size_t size);

void myfree(void* ptr);

O tipo de dados size_t, que está definido na biblioteca stddef.h é o tipo de dados que se deve usar para representar tamanhos de dados. Geralmente é um inteiro, mas sempre com o tamanho certo para cada máquina. A função malloc recebe um parâmetro deste tipo, e consequentemente também a função mymalloc.

As implementações ficam em mymalloc.c:

#include "mymalloc.h"

#include <unistd.h>

// (1) Outras declarações...

void* mymalloc(size_t size)
{
// (2) ...
}

void myfree(void* ptr)
{
// (3) ...
}

Antes de implementar mymalloc ou myfree, temos de definir a estrutura de dados para armazenar a informação dos blocos:

// (1) Outras declarações
typedef struct blockInfo
{
size_t length;
int isFree;
} BlockInfo;

Como vimos anteriormente, a primeira coisa a fazer na função mymalloc é procurar um bloco livre com tamanho suficiente. Para começar essa pesquisa precisamos do endereço do primeiro bloco.

// (2) ...
static BlockInfo* firstBlock;

Este endereço vai ser obtido da primeira vez que a função mymalloc seja chamada e nunca mais vai ser alterado porque o primeiro bloco vai estar sempre no início da heap. Podemos obtê-lo assim:

// (2)...
static int isInitialized = 0;

if(!isInitialized)
{
isInitialized = 1;
firstBlock = (BlockInfo*)sbrk(0);
}

Agora sim, podemos escrever as funções mymalloc e myfree.

mymalloc

Como já vimos, a primeira coisa a fazer em mymalloc é percorrer a heap à procura de um bloco com tamanho suficiente. Mas temos de saber quando devemos parar, caso não encontremos um. Para isso temos de saber onde termina a heap. Mais uma vez, chamamos a função sbrk com o parâmetro 0.

void* end = sbrk(0);

Agora percorremos a heap:

    BlockInfo* block = firstBlock;
int found = 0;
while(block != end && !found)
{
if(block->length >= size)
{
found = 1;
}
block += sizeof(BlockInfo) + block->length; // Errado!
}

Parece tudo bem, mas a última linha tem um erro. Como block é um apontador para BlockInfo, quando somámos uma quantidade n, block avança não n bytes, mas sim n vezes o tamanho de um BlockInfo. Se o bloco tiver 100 bytes, vamos avançar (100+8)*8 = 864 bytes, porque um BlockInfo ocupa oito bytes.

Uma solução é dividir o avanço pelo tamanho de um BlockInfo. No entanto, esta solução cria outro problema. Nem todos os números são divisíveis pelo tamanho de BlockInfo (oito, neste caso). Temos então de alinhar todos os blocos a oito bytes, ou melhor, ao tamanho de BlockInfo, antes de percorrermos a heap:

    if(size % sizeof(BlockInfo) != 0)
{
size += sizeof(BlockInfo) – size % sizeof(BlockInfo);
}
BlockInfo* block = firstBlock;
int found = 0;
while(block != end && !found)
{
if(block->isFree && block->length >= size)
{
found = 1;
}
block += (sizeof(BlockInfo) + block->length) / sizeof(BlockInfo);
// ou block += 1 + block->length / sizeof(BlockInfo);
}

Depois de percorrermos a heap:

  • encontrámos um bloco com tamanho suficiente, ou
  • chegámos ao fim da heap sem encontrar um bloco com tamanho suficiente.

Se encontrámos um bloco podemos marcá-lo como usado e devolver um apontador para o espaço livre desse bloco.

    if(found)
{
block->isFree = 0;
// O espaço do utilizador fica exactamente um BlockInfo à frente
return block + 1;
}

Caso contrário, temos de alocar espaço para um bloco novo, e para a respectiva estrutura de controlo com sbrk:

    else
{
BlockInfo* newBlock = (BlockInfo*)sbrk(sizeof(BlockInfo) + size);
if(newBlock < 0)
{
// Não há memória suficiente!
return NULL;
}
newBlock->length = size;
newBlock->isFree = 0;
// O espaço do utilizador fica exactamente um BlockInfo à frente
return newBlock + 1;
}

myfree

Para libertar um bloco, o utilizador passa à função myfree um apontador para o espaço usado por ele. Para o marcar como livre temos de aceder à estrutura de controlo que está antes desse bloco:

myfree 

Para isso temos de usar aritmética de apontadores. O que nós queremos é andar um BlockInfo para trás. Mas ptr é um apontador do tipo void*. Temos então de fazer um cast para BlockInfo*. Depois disso já podemos recuar um, simplesmente subtraindo 1 ao apontador.

    BlockInfo* usedBlock = ((BlockInfo*)ptr) - 1;

Agora podemos marcar este bloco como livre:

    usedBlock->isFree = 1;

Notas finais

Se agora compilarmos o programa podemos ver que não há erros de compilação:

$ make

E se corrermos o programa podemos confirmar que o dicionário do primeiro guião ainda funciona:

$ ./prog

* Put 42
42: foo
--------
* Put 17
17: spam
42: foo
--------
* Put 23
17: spam
23: bar
42: foo
--------
* Get 1: (null)
--------
* Get 42: foo
--------
* Get 17: spam
--------
* Get 23: bar
--------
* Del 1
17: spam
23: bar
42: foo
--------
* Del 23
17: spam
42: foo
--------
* Del 42
17: spam
--------
* Del 17
--------

O código completo encontram no sítio do costume.

15 commentários:

Anónimo disse...

Olá!

Essas solução do exercício foi feita por ti?

Martinho Fernandes disse...

Sim fui eu que fiz.

Nota: Tinha uns pequenos erros mas já os corrigi.
E também já fiz upload do código, que me tinha esquecido.

Anónimo disse...

Como é que consegues fazer isso sozinho?
Apoias-te em algum livro? SO está a passar-me ao lado...

Martinho Fernandes disse...

Eu já tive a cadeira no ano passado. Mas não apareci ao exame e chumbei :(.

Também já mexo em C há uns dez anos, e a prática traz a perfeição...

Anónimo disse...

Boas!

Antes de mais nada bom trabalho com o blog, pelo menos a mim, tem-me ajudado bastante x]

Tenho só um problema, não estou a conseguir compilar o teu código. O makefile não me dava, mas corrigi os espaços pelos tabs e deu um erro no mymalloc.c "mymalloc.c.12 : elemento inicializador não é constante".

Alguma ideia?

Obrigado

Anónimo disse...

Continua a insistir nisto!

Ajuda muito preciosa para gente como eu, que tá inscrito pela primeira na cadeira.

Obrigado.

Anónimo disse...

Desde já o meu obrigado por te dares a este trabalho, vai-me dar imenso jeito nestas árduas aulas de SO.

Mas...

É mesmo necessário o uso de um BlockInfo para resolver este problema? Não pode ser feito um mymalloc/myfree sem o uso do mesmo?

Por exemplo, se eu usar o mymalloc para armazenar seja o que for, por um exemplo uma estrutura que ocupa 10bytes, o mymalloc feito aqui vai sempre reservar 10bytes para a minha estrutura + 4bytes (por norma) para o inteiro isFree na estrutura BlockInfo. Ou seja, por cada chamada que eu faça com o mymalloc vou usar sempre sizeof(TIPO_DE_DADOS) + 4bytes...

Isto é suposto? O malloc original do stdlib funciona exactamente assim?

E outro pormenor, normalmente os professores não gostam muito de variáveis globais e aquele firstBlock estático deixa-me um bocado desconfortável. Será esta a melhor solução ou haverá outra forma sem usar variáveis globais?

Atenção que isto não é nenhuma critica, é mesmo um pedido de ajuda. :)

Martinho Fernandes disse...

@Nazgulled: Sim, é necessário o uso de uma estrutura de controlo para controlar os blocos. O malloc do stdlib também faz isso (não exactamente igual, mas semelhante). Se os blocos tivessem tamanho fixo podiamos usar um bitmap, mas como não é o caso temos de guardar todos os comprimentos.

A função malloc de certeza que usa umas optimizações para minimizar este overhead, mas isso torna as coisas ainda mais complicadas.

Quanto às variáveis globais... Eu também não gosto, mas neste caso é um mal necessário. As funções malloc e free necessitam de manter estado entre elas. Ambas têm side-effects (alocação de memória). Não vejo outra forma de resolver o problema que não seja passar isso como parâmetro, mas isso não é o que se pretende. Como aqui estávamos a replicar funcionalidade existente, não há volta a dar... :(

Martinho Fernandes disse...

Peço imensa desculpa mas o código deste guião tinha dois erros. O Makefile tinha espaços em vez de tabs (ARRRGGHHH) e em mymalloc.c tinha uma inicialização feita num sítio ilegal, como um anónimo apontou. Já corrigi isso. Se encontrarem mais erros, digam.

Martinho Fernandes disse...

@Nazgulled: O problema que mencionaste do overhead pode ser reduzido para os blocos livres com recurso a fusão (exercício 2). Se tivermos três blocos contíguos de 12 bytes estamos a usar 3*(12+8) = 60 bytes, mas só contamos 36 como livres. Se os fundirmos ficamos com 60-8 = 52 bytes livres.

Os blocos usados não podem ser fundidos, porque não nos "pertencem", pertencem ao utilizador.

Anónimo disse...

Boas, desde já um muito obrigado por este blog. Está a ser uma ajuda preciosa:)

Bem, estive a ler tudo o que disseste e finalmente consegui compreender o que era suposto fazer:D

Tenho é uma questão, falam-me constantemente em "desfragmentação" para não ficarem "furos" na heap e segundo o que percebi como estás a fazer isso não acontece, certo?

Desculpa a pergunta. E como já alguém disse em cima, isto não são criticas são apenas dúvidas e pedidos de ajuda.

Andre Silva disse...

Boas! Não percebi uma coisa... Usas o sbrk(0), para saber onde começa a Heap e depois usas novamente o sbrk(0), para saber onde acaba . . . Nao consegui perceber isso :S

Martinho Fernandes disse...

@Andre Silva: O sbrk(0) diz onde acaba a heap. Mas no início a heap acaba exactamente onde começa.

@Anónimos (sobre os furos): A fragmentação da heap ocorre na mesma. É muito difícil de eliminar a fragmentação e não é pedido neste exercício (ainda bem). A desfragmentação uma operação pesada e na maior parte das vezes impossível porque não podemos mover blocos conforme queremos: o utilizador pode ter (e de certeza que tem) apontadores para esses blocos que teriam todos de ser actualizados. Isso geralmente só se faz em garbage collectors. Fora do âmbito do exercício (mais uma vez, ainda bem).

José Miguel disse...

Boas. Em primeiro lugar, parabéns pelo teu excelente trabalho com o blog. Está mesmo muito bom.

Mas neste post em particular, há uma coisa que me intriga, e que penso que deve haver uma solução melhor.

O meu professor, nas aulas práticas, disse que era completamente descabido invocar a função sbrk apenas com o tamanho que o utilizador pediu.

Refiro-me a esta linha:
BlockInfo* newBlock = (BlockInfo*)sbrk(sizeof(BlockInfo) + size);

Estás apenas a alocar espaço para o tamanho da estrutura de controlo e o tamanho. Devia-se, de alguma maneira, invocar muito mais que isso, para, da próxima vez que o utilizador quiser espaço, não termos que aumentar a heap. Sbrk requere muito "esforço" do processador (Não sei quantos ciclos de relógio porque tem que entrar em modo protegido, bla bla bla).

O professor disse, portanto para, quando tivermos que alocar, alocarmos logo uma boa quantidade de espaço.
Agora pergunto-me se, com esta pequena optimização, o código sofreria muitas alterações...

Se por acaso decidires passar por cá e alterar o código, avisa-me, por favor. : )
kurt_miguel_cobain@hotmail.com

Mais uma vez, parabéns pelo excelente blog. Não faria melhor nem nada que se pareça.
Cumprimentos.

Martinho Fernandes disse...

@José: tens toda a razão. sbrk() é uma operação dispendiosa. Na altura não liguei muito a esse pormenor porque achei que as optimizações que iria fazer na segunda parte fossem suficientes.

No entanto, não é muito difícil de alterar o código para funcionar como disseste. Basta guardar numa variável global :( quanto desse espaço extra ainda temos. Quando acabar, fazemos mais um sbrk().

Quanto à quantidade de espaço extra a aumentar, uma solução simples e bastante razoável é aumentar sempre uma quantidade mínima, assim:

if(size < MINIMUM) size = MINIMUM;

Enviar um comentário