În această temă veţi implementa un alocator simplu de memorie, similar sistemului malloc/free.
Responsabil: Relu Dragan
Autor inițial: Stefan Bucur
Deadline hard: 12.12.2017 23:55
În urma realizării acestei teme, studentul va fi capabil:
Mihai s-a oferit să o ajute pe sora sa, Ilinca, să înțeleagă cum funcționează alocarea de memorie dinamică. Primul impuls a fost să îi arate codul sursa al funcțiilor de alocare din glibc
. Când a văzut însă cum arată codul pentru malloc(), s-a lăsat păgubaş considerându-l prea greu de urmărit. Astfel, Mihai s-a decis să proiecteze singur un sistem de alocare de memorie, simplu şi pe înţelesul tuturor.
Programul vostru va trebui să realizeze o simulare a unui sistem de alocare de memorie. Programul va primi la intrare comenzi de alocare, alterare, afişare şi eliberare de memorie, şi va trebui să furnizaţi la ieşire rezultatele fiecărei comenzi. Nu veţi înlocui sistemul standard malloc()
şi free()
, ci vă veţi baza pe el, alocând la început un bloc mare de memorie, şi apoi presupunând că acela reprezintă toată “memoria” voastră, pe care trebuie s-o gestionaţi.
Un alocator de memorie poate fi descris, în termenii cei mai simpli, în felul următor:
arenă
. De exemplu, sistemul de alocare cu malloc()
are în gestiune heap
-ul programului vostru, care este un segment special de memorie special rezervat pentru alocările dinamice.disjuncte
), pentru că altfel datele modificate într-un bloc vor altera şi datele din celălalt bloc.
O problemă pe care o are orice alocator de memorie este cum este ţinută evidenţa blocurilor alocate, a porţiunilor libere şi a dimensiunilor acestora
. Pentru această problemă există în general două soluţii:
pe care voi o veţi implementa în această temă
, foloseşte arena pentru a stoca informaţii despre blocurile alocate. Preţul plătit este faptul că arena nu va fi disponibilă în totalitate utilizatorilor, pentru că va conţine, pe lângă date, şi informaţiile de gestiune, însă avantajul este că nu are nevoie de memorie suplimentară şi este în general mai rapidă decât prima variantă.Există mai multe metode prin care se poate ţine evidenţa blocurilor alocate în arenă, în funcţie de performanţele dorite. Voi va trebui să implementaţi un mecanim destul de simplu, care va fi prezentat în secţiunea următoare. Deşi nu este extrem de performant, se va descurca destul de bine pe dimensiuni moderate ale arenei (de ordinul MB).
În continuare vom considera arena ca pe o succesiune (vector) de N
octeţi (tipul de date unsigned char
). Fiecare octet poate fi accesat prin indexul său (de la 0
la N-1
). Vom considera că un index este un întreg fara semn pe 32 de biţi (tipul de date uint32_t
din libraria stdint.h
). De asemenea, va fi nevoie câteodată să considerăm 4 octeţi succesivi din arenă ca reprezentând valoarea unui index. În această situaţie, vom considera că acel index este reprezentat în format 'little-endian
' (revedeţi exercitiile de la laboratorul de pointeri pentru mai multe detalii şi citiţi acest articol mult mai descriptiv), şi astfel vom putea face cast de la un pointer de tip unsigned char *
la unul de tip uint32_t*
, pentru a accesa valoarea indexului, stocată în arenă.
Figura de mai jos ilustrează structura detaliată a arenei, în decursul execuţiei programului:
Se poate observa că fiecare bloc alocat de memorie (marcat cu un chenar îngroşat) constă din două secţiuni:
de gestiune
, este reprezentată de 12 octeţi (3 * sizeof(uint32_t)
) împărţiţi în 3 întregi (a câte 4 octeţi fiecare). Cei trei întregi reprezintă următoarele:0
.0
.datele efective
ale utilizatorului. Secţiunea are lungimea în octeţi egală cu dimensiunea datelor cerută de utilizator la apelul funcţiei de alocare. Indicele returnat de alocator la o nouă alocare reprezintă începutul acestei secţiuni din noul bloc, şi 'nu
' începutul primei secţiuni, întrucât partea de gestiune a memoriei trebuie să fie complet transparentă pentru utilizator.
int32_t
, sau int
daca pe arhitectura voastra sizeof(int)
este 4. Si aceste tipuri de date se incadreaza in restrictiile temei (testele vor fi de asa natura incat indecsii vostrii ar trebui sa se incadreze in [0 .. 2 ^ 31 - 1]
).
Indicele de start
reprezinta indicele primului bloc (cel mai “din stânga”) din arenă. Acesta trebuie stocat de voi intr-o variabila separata. Dacă arena nu conţine niciun bloc (de exemplu, imediat după iniţializare), acest indice este 0
.
Indicele de start
marchează începutul lanţului de blocuri
din arenă: din acest indice putem ajunge la începutul primului bloc, apoi folosind secţiunea de gestiune a primului bloc putem găsi începutul celui de-al doilea bloc, şi asa mai departe, până când ajungem la blocul care are indexul blocului următor 0 (este ultimul bloc din arenă). În acest mod putem traversa toate blocurile din arenă, şi de asemenea să identificăm spaţiile libere din arenă, care reprezintă spaţiile dintre două blocuri succesive.
Primul bloc din arena trebuie sa aiba octetii indexului blocului anterior setati pe 0. Ultimul bloc din arena trebuie sa aiba octetii indexului blocului urmator setati pe 0.
Este de remarcat faptul că lanţul poate fi parcurs în ambele sensuri: dintr-un bloc putem ajunge atât la vecinul din dreapta, cât şi la cel din stânga.
De asemenea, atunci când este alocat un bloc nou sau este eliberat unul vechi, 'lanţul de blocuri trebuie modificat
'. Astfel, la alocarea unui nou bloc de memorie, trebuie să ţineţi cont de următoarele:
0
în locul vecinului care lipseşte.La eliberarea unui bloc, trebuie modificate secţiunile de gestiune a vecinilor într-o manieră similară ca la adăugare.
Programul vostru va trebui să implementeze o serie de operaţii de lucru cu arena, care vor fi lansate în urma comenzilor pe care le primeşte la intrare. Fiecare comandă va fi dată pe câte o linie, şi rezultatele vor trebui afişate pe loc. Secţiunea următoare prezintă sintaxa comenzilor posibile şi formatul de afişare al rezultatelor.
“Introduceţi comanda: ”
).
Fiecare comanda trebuie afisata pe cate o linie separata, exact cum este citita de la input. Programul vostru va trebui să accepte următoarele comenzi la intrare:
INITIALIZE <N>
N
octeţi. Prin iniţializare se înţelege alocarea dinamică a memoriei necesare stocării arenei, setarea fiecărui octet pe 0, şi iniţializarea lanţului de blocuri (setarea indicelui de start pe 0
).FINALIZE
DUMP
\t
) , urmat de 16 octeţi, afişati separaţi printr-un spaţiu şi în format hexazecimal, cu 2 cifre hexa majuscule fiecare. Între cel de-al 8-lea şi cel de-al 9-lea octet se va afişa un spaţiu suplimentar.printf(”%02X”)
si printf(”%08X”)
pentru afisare.ALLOC <SIZE>
SIZE
octeţi de memorie din arenă, unde SIZE
e o valoare strict pozitiva. Ea va trebui să găsească o zonă liberă suficient de mare (care să încapă SIZE
octeţi + secţiunea de gestiune), şi să rezerve un bloc 'la începutul
' zonei (nu în mijloc, nu la sfârşit). Va trebui folosită prima zonă liberă validă, într-o căutare de la stânga la dreapta.0
dacă nu a fost găsită nici o zonă liberă suficient de mare în arenă. Atenţie: Va trebui să afişaţi indexul secţiunii de date din noul bloc, şi nu al secţiunii de gestiune.FREE <INDEX>
INDEX
în arenă. Practic, INDEX
va fi o valoare care a fost întoarsă în prealabil de o comandă
'ALLOC
'. În urma execuţiei acestei comenzi, octetii vechiului bloc (octeti gestiune + octeti date) vor fi setati pe 0, iar spaţiul de arenă ocupat va redeveni disponibil pentru alocări ulterioare. FILL <INDEX> <SIZE> <VALUE>
SIZE
octeţi din arenă la valoarea VALUE
, cuprinsă între 0 şi 255 inclusiv, si va modifica octetii blocurilor începând cu blocul cu indexul INDEX
. Atenție, această comandă NU modifica octeți de gestiune, ci doar octeți de date. Se vor seta octeti de date pana cand s-au setat SIZE
octeti sau pana cand s-au parcurs toate blocurile de dupa blocul INDEX
si s-au setat toti octetii de date ale acestora. Cu alte cuvinte, NR_OCTETI_SETATI = min(SIZE, SIZE(block(INDEX)) + SIZE(next(block(INDEX))) + SIZE(next(next(block(INDEX)))) + … )
.
În această secţiune sunt ilustrate câteva exemple de rulare a programului, pentru a înţelege mai bine modul în care programul vostru trebuie să se comporte. Fiecare exemplu este urmat apoi de câteva explicaţii. }
Observaţii:
DUMP
, pe prima linie, octetii 0-3 sunt 0 (reprezinta indexul blocului urmator), octetii 4-7 sunt 0 (reprezinta indexul blocului precedent), iar octetii 8 - 11 sunt '02 00 00 00
' (reprezinta valoarea intreaga 2 pe 4 octeti in format little endian).'DUMP
', octetii 0-3 reprezinta indexul zonei de gestiune a blocului 2, 0E 00 00 00
reprezinta 14. Octetii 44-47 sunt 0 pentru ca blocul 4 este ultimul in arena, iar octetii 48-51 reprezinta indexul zonei de gestiune pentru blocul 3: 1C 00 00 00
adica 28.'0x0
', '0x10
', '0x20
' si respectiv '0x40
'.'FREE 12
' toti cei 16 octeti rezervati pentru blocul 1 sunt setati pe 0. Din zona de gestiune pentru blocul 2 ar fi trebuit sa se modifice octetii pentru indexul anterior, insa in acest caz ei raman 0 pentru ca blocul 2 devine primul bloc din arena si trebuie sa aiba octetii indexului anterior setati pe 0.'ALLOC 4
' de dupa 'FREE 12
', se aloca un bloc de dimensiune 4 la indexul 12, iar memoria ajunge in aceeasi stare ca la primul 'DUMP
'.'FILL 12 100 255
' scrie '0xFF
' peste toti octetii de date din blocuri (incepand cu primul). Observati ca dimensiunea data ca parametru 100 este mai mare ca suma dimensiunilor tuturor blocurilor in prealabil alocate pana in acest moment (4 + 4 + 20 + 4 = 32). Prin urmare, doar 32 de octeti vor fi scrisi. Insa, in figura observam doar 28 de octeti '0xFF
' si 4 octeti '0x7F
'. Acest lucru este datorat celei de-a doua comenzi 'FILL 76 4 127
' care suprascrie octetii 76-79, initial setati cu valoarea '0xFF
'.
Se va acorda un bonus de 150/1150 puncte
(pe lângă faimă şi respect :) ), dacă se vor implementa, pe lângă comenzile standard prezentate mai devreme, şi următoarele comenzi:
ALLOCALIGNED <SIZE> <ALIGN>
ALLOC
, cu excepţia faptului că indexul returnat va trebui să fie aliniat la ALIGN
octeţi, unde ALIGN
este o putere a lui 2 (poate fi 1, 2, 4, 8, etc.). Un index INDEX
este aliniat la ALIGN
octeţi dacă INDEX % ALIGN == 0
.REALLOC <INDEX> <SIZE>
INDEX
într-un nou spaţiu de memorie de dimensiune SIZE
şi va afişa indexul secţiunii de date a noului bloc alocat. De asemenea, va copia datele aflate în vechiul bloc în noul bloc. Daca SIZE
este mai mic decât dimensiunea originală, vor fi copiaţi numai SIZE
octeţi (va avea loc o trunchiere
).SIZE
este prea mare si alocarea unei noi zone de memorie de dimensiune SIZE
nu este posibila, se realizeaza doar eliberarea memoriei blocului cu indexul de date INDEX
si se intoarce 0.Atenţie:
Pentru găsirea unei zone de memorie libere va trebui să reluaţi procedura de căutare de la stânga la dreapta. Nu este valid să verificaţi că în locul curent există deja spaţiu pentru expansiune / micşorare.Se doreste realocarea primului bloc la o dimensiune de 20. Spatiul gasit in arena este aflat in continuarea blocului 2, mai exact octetii 46-77.
0 < ARENA_SIZE ⇐ 2 ^ 16
0 < NUMAR_COMENZI < 5000
Timp de executie per test: 10 sec
Puteti descarca arhiva cu testele, checker-ul si scheletul de aici:
./allocator
).checker.sh
care poate fi rulat cu urmatoarele optiuni:./checker.sh alocator
./checker.sh alocator basic
sau ./checker.sh alocator advanced
sau ./checker.sh alocator random
sau ./checker.sh alocator bonus
../checker.sh alocator basic 3
sau de exemplu ./checker.sh bonus 10
.diff output/test10.out
ref/test10.ref
.valgrind –version
sudo apt-get install valgrind
valgrind –leak-check=full ./alocator < input/random/test5.in
skel/
care realizeaza doar parsarea comenzilor.în directorul rădăcină
doar următoarele:.c
şi eventual .h
).Makefile
care să conţină regulile build
şi clean
. Regula build
va compila programul într-un executabil cu numele alocator
. Regula clean
va şterge executabilul şi eventual toate binarele intermediare (fişiere obiect) generate de voi.README
care să conţină prezentarea implementării alese de voi. Dacă aţi implementat şi bonusul, menţionaţi acest lucru în README. 900 puncte
- testele automate din arhiva de testare.100 puncte
- explicaţiile din README şi aspectul codului sursă.150 puncte
- implementarea bonusului.Mențiuni suplimentare:
1) De ce imi trec testele pe checkerul local dar nu si pe vmchecker?
R: Cel mai probabil este o problema de undefined behaviour. Aveti grija sa nu faceti citiri din zone de memorie neinitializate, sa nu faceti apeluri recursive care pot conduce la un STACK OVERFLOW
pe masina virtuala (dimensiunea stivei pe vmchecker este undeva la 8 MB), sa nu cititi/scrieti dintr-o zona de memorie pe care ati apelat anterior free
.
2) Am primit SEGMENTATION FAULT… Ce fac mai departe?
R: Incercati sa depistati linia de cod ce genereaza seg fault. Puteti pune printf-uri in cod cu '\n' la finalul oricarui mesaj, altfel mesajul va fi suprascris de mesajul de eroare “Segmentation fault” si nu se va afisa la stdout. O alta modalitate este folosirea debugger-ului gdb
.
user@machine> gdb ./alocator
gdb> run < input/test0.in
gdb> backtrace