Segregated Free Lists

Responsabili

  • Data publicării: 18.03.2024 17:00:00
  • Deadline HARD: 07.04.2024 23:55:00

Actualizări

  • Eliminare mesaj de eroare INVALID_COMMAND: 22.03.2024
  • Publicare checker: 22.03.2024
  • Update checker: 26.03.2024
  • Mențiune privind obligativitatea eliminării blocurilor la alocare din vectorul de liste: 27.03.2024

Obiective

  • Aprofundarea cunoștințelor în utilizarea limbajului C.
  • Implementarea și utilizarea structurii de date listă dublu înlănțuită.
  • Familiarizarea cu implementarea unei structuri de date generice.

Introducere

Studenții de anul întâi au aflat de la colegii mai mari de o materie interesantă pe care urmează să o aibă în anul 2, faimoasa Observarea Sistemelor de Operare (cu acronimul OSO). Fiind curioși, au început deja investigația, și pe lângă meme-uri, debate-uri și reminder-uri ale deadline-urilor temelor (“Rețineți termenul. Este fix și strict. Nu mai puteți submite teme după acest termen. Notați-l bine și țineți-vă de el.”), au găsit și concepte inedite, precum alocarea de memorie în cadrul bibliotecilor.

Bineînțeles, studenții sunt niște ființe deloc leneșe, așa că s-au apucat deja de a doua temă (prima era prea simplă), un alocator de memorie, pe care vi-l arată și vouă, extaziați. Scopul vostru este să le mai micșorați avântul și să implementați împreună cu frații voștri de suferință această funcționalitate a unei biblioteci, folosind Segregated Free Lists. Pe parcurs, nu veți avea nevoie de cunoștințe de Sisteme de Operare, dar, dacă vă pasionează subiectul, vizitați pagina oficială a cursului.

Definiție

Segregated Free Lists reprezintă o structură de date formată dintr-un vector, care, pentru fiecare intrare, reține adresa de început a unei liste duble înlănțuite cu blocuri de memorie liberă de aceeași dimensiune. Inițial, dimensiunile blocurilor vor fi puteri ale lui 2, pentru ca în urma alocărilor, să fie fragmentate sau reconstituite, de la caz la caz.

Așa cum îi spune și numele, această structură de date reține doar blocurile nealocate, pentru cele alocate fiind folosită o altă structură de date.

 Img 1

Cerință

Un alocator de memorie are rolul de a rezerva memorie, la nivel de bibliotecă, tradițional prin apeluri precum malloc() sau calloc(). Acestea marchează ca fiind folosite anumite zone de memorie dintr-un pool de bytes prealocat, care, în cazul acestei teme, este organizat sub forma de segregated free lists. De asemenea, alocatorul de memorie se ocupă și cu eliberarea zonelor rezervate, apelul de bibliotecă aferent fiind free().

Alocatorul de memorie pe care îl veți implementa va avea următoarele funcționalități (sau API):

  • un vector de liste dublu înlănțuite, numit segregated free lists; fiecare intrare din acest vector va indica către o listă dublu înlănțuită ce reține zonele de memorie liberă de aceeași dimensiune
  • o altă structură de date la alegerea voastră care va reține zonele de memorie ocupată, împreună cu conținutul acestora.

Comenzi posibile (40p)

Input-ul este oferit de la stdin, iar output-ul la stdout, respectând formatul următor:

  • INIT_HEAP <adresă_start_heap> <număr_liste> <număr_bytes_per_listă> <tip_reconstituire>
    • Creează structura de date Segregated Free Lists pentru un heap care începe de la adresa <adresă_start_heap>, cu un număr de liste dublu înlănțuite de <număr_liste> ce rețin <număr_bytes_per_listă> octeți fiecare. Pașii sunt următorii:
      1. Se creează <număr_liste> liste care vor conține adrese de început contigue și crescătoare pentru fiecare dimensiune.
      2. Listele vor reține în noduri blocuri de dimensiuni reprezentând puteri ale lui 2, începând cu 8,
      3. Adresele din heap vor fi în intervalul [heap_base, heap_base + heap_size).
    • Parametrul <tip_reconsituire> va fi relevant pentru cerința FREE, găsiți mai multe detalii la secțiunea acesteia.

<adresă_start_heap> reprezintă un număr pozitiv, nenul.

Garantăm faptul că numărul de bytes corespunzător oricărei liste este mai mare sau egal decât dimensiunea asignată acesteia. Cu alte cuvinte, inițial, există cel puțin un bloc pentru fiecare dimensiune de listă în parte.

  • MALLOC <nr_bytes>
    • Alocă <nr_bytes> octeți de memorie. Pașii sunt următorii:
      1. Caută un bloc în vectorul de liste cu o dimensiune mai mare sau egală decât <nr_bytes> și îl scoate din listă.
        • Dacă există mai multe blocuri cu o dimensiune egală cu <nr_bytes>, se alege cea cu adresa mai mică.
        • Dacă există doar blocuri cu o dimensiune mai mare decât <nr_bytes>, se alege blocul cu adresa mai mică și se fragmentează, adică se marchează zona de <nr_bytes> octeți ca fiind alocată, iar restul de octeți până la dimensiunea blocului se adaugă în lista corespunzătoare dimensiunii noi.
        • Dacă nu există blocuri libere cu dimensiunea mai mare sau egală cu <nr_bytes>, se afișează mesajul de eroare “Out of memory\n”.
      2. Marchează blocul ca fiind alocat în structura de date aleasă de voi pentru managementul zonelor folosite.

Fragmentarea reprezintă procesul prin care un bloc se împarte în alte două blocuri mai mici, dintre care primul va fi folosit pentru o alocare, în timp ce al doilea va rămâne liber, fiind plasat în lista corespunzătoare dimensinii sale.

  • FREE <adresă>
    • Marchează ca liber blocul care a fost alocat de la adresa <adresă> în funcție de politica de reconstituire setată în funcția INIT_HEAP. Pașii sunt următorii:
      1. Caută și scoate adresa <adresă> din structura de date aleasă de voi pentru managementul zonelor folosite.
        • Dacă adresa nu a fost alocată deja sau dacă nu este început de block, se afișează mesajul de eroare “Invalid free\n”.
      2. Se introduce blocul de memorie proaspăt eliberată în vectorul de liste dublu înlănțuite.
        • Dacă <tip_reconstituire> = 0, în lista corespunzătoare dimensiunii sale.
        • Dacă <tip_reconstituire> = 1, vedeți bonusul.
  • FREE 0x0 este întotdeauna valid (echivalentul free(NULL) din C) și nu are niciun efect.

Reconstituirea reprezintă procesul prin care un bloc care a fost fragmentat la alocare își recapătă dimensiunea inițială, prin alipirea tuturor blocurilor care l-au compus. Acest fel de compunere are loc doar atunci când <tip_reconstituire> este egal cu 1, iar fragmentele blocului sunt măcar în parte din nou libere.

  • READ <adresă> <nr_bytes>
    • Afișează în format ASCII size octeți începând de la adresa <adresă>.
      • Dacă începând cu adresa <adresă> nu a fost alocat niciun bloc, se afișează mesajul de eroare “Segmentation fault (core dumped)\n” și se face un dump al memoriei, după regulile de la funcția DUMP_MEMORY.
      • Dacă adresa <adresă> a fost alocată, dar există un octet din intervalul [adresă, adresă + nr_bytes) care nu a fost alocat prealabil, se afișează mesajul de eroare “Segmentation fault (core dumped)\n” și se face un dump al memoriei, după regulile de la funcția DUMP_MEMORY.
      • Dacă toți octeții ce trebuie afișați au fost alocați deja, se afișează conținutul cerut, indiferent dacă acesta face parte din mai multe alocări consecutive.

Garantăm faptul că fiecare operație de READ are loc pe o zonă de memorie în care s-a efectuat o operație de WRITE prealabil.

  • WRITE <adresă> <date> <nr_bytes>
    • Scrie <nr_bytes> octeți din vectorul de caractere <date> la adresa <adresă>; dacă <nr_bytes> este mai mare decât lungimea vectorului, se scrie doar strlen(date).
      • Dacă începând cu adresa <adresă> nu a fost alocat niciun bloc, se afișează mesajul de eroare “Segmentation fault (core dumped)\n” și se face un dump al memoriei, după regulile de la funcția DUMP_MEMORY.
      • Dacă adresa <adresă> a fost alocată, dar există un octet din intervalul [adresă, adresă + nr_bytes) care nu a fost alocat prealabil, se afișează mesajul de eroare “Segmentation fault (core dumped)\n” și se face un dump al memoriei, după regulile de la funcția DUMP_MEMORY.
      • Dacă întreaga porțiune în care încercăm să scriem a fost alocată total, se copiază conținutul cerut, indiferent dacă acesta face parte din mai multe alocări consecutive.
    • Șirul de caractere care va fi scris la adresa dată este delimitat de ghilimele, care nu vor face parte din conținutul copiat. Caracterele pot fi orice simbol ASCII valid, inclusiv spațiu. Totuși, string-ul nu se întinde pe linii multiple.
  • DUMP_MEMORY
    • Afișează zonele libere și zonele alocate împreună cu conținutul blocurilor care au fost scrise, după cum urmează:
+++++DUMP+++++
Total memory: <dimensiune_heap> bytes
Total allocated memory: <nr_bytes_alocați> bytes
Total free memory: <nr_bytes_liberi> bytes
Number of free blocks: <nr_blocuri_libere>
Number of allocated blocks: <nr_blocuri_alocate>
Number of malloc calls: <nr_apeluri_malloc>
Number of fragmentations: <nr_fragmentări>
Number of free calls: <nr_apeluri_free>
Blocks with <x1> bytes - <nr_blocuri_cu_dimensiune_x1> free block(s) : 0x<adresă_start_bloc_1> <0xadresă_start_bloc_2> ... <adresă_start_bloc_n>
Blocks with <x2> bytes - <nr_blocuri_cu_dimensiune_x2> free block(s) : 0x<adresă_start_bloc_1> <0xadresă_start_bloc_2> ... <adresă_start_bloc_n>
.
.
.
Blocks with <xn> bytes - <nr_blocuri_cu_dimensiune_xn> free block(s) : 0x<adresă_start_bloc_1> <0xadresă_start_bloc_2> ... <adresă_start_bloc_n>
Allocated blocks : (0x<adresă_start_bloc_1> - <dimensiune_bloc_1>) (0x<adresă_start_bloc_2> - <dimensiune_bloc_2>) ... (0x<adresă_start_bloc_n> - <dimensiune_n>)
-----DUMP-----

Blocurile trebuie afișate în ordinea crescătoare a dimensiunilor, iar în cadrul listelor de aceeași dimensiune, în ordinea crescătoare a adreselor.

  • DESTROY_HEAP
    • Eliberează toată memoria alocată și încheie programul.

  • Toate adresele sunt citite și afișate în format hexa, cu litere mici.
  • Dimensiunea unei linii de input este de maxim 600 de caractere.

Exemplu

Exemplu

Fie următorul input:

INIT_HEAP 0x1 4 256 0
MALLOC 10
MALLOC 16
MALLOC 2000
WRITE 0x111 "GIGEL" 5
DUMP_MEMORY
READ 0x113 2
FREE 0x111
FREE 0x111
DESTROY_HEAP

Output-ul corespunzător acestui input este:

Out of memory
+++++DUMP+++++
Total memory: 1024 bytes
Total allocated memory: 26 bytes
Total free memory: 998 bytes
Free blocks: 59
Number of allocated blocks: 2
Number of malloc calls: 2
Number of fragmentations: 1
Number of free calls: 0
Blocks with 6 bytes - 1 free block(s) : 0x10b
Blocks with 8 bytes - 32 free block(s) : 0x1 0x9 0x11 0x19 0x21 0x29 0x31 0x39 0x41 0x49 0x51 0x59 0x61 0x69 0x71 0x79 0x81 0x89 0x91 0x99 0xa1 0xa9 0xb1 0xb9 0xc1 0xc9 0xd1 0xd9 0xe1 0xe9 0xf1 0xf9
Blocks with 16 bytes - 14 free block(s) : 0x121 0x131 0x141 0x151 0x161 0x171 0x181 0x191 0x1a1 0x1b1 0x1c1 0x1d1 0x1e1 0x1f1
Blocks with 32 bytes - 8 free block(s) : 0x201 0x221 0x241 0x261 0x281 0x2a1 0x2c1 0x2e1
Blocks with 64 bytes - 4 free block(s) : 0x301 0x341 0x381 0x3c1
Allocated blocks : (0x101 - 10) (0x111 - 16)
-----DUMP-----
GE
Invalid free

Explicația este:

  • Inițial, heap-ul conține blocuri de 4 mărimi diferite (8, 16, 32, 64 biți), fiecare listă de blocuri având o lungime totală de 256 biți.
  • În total, avem un spațiu de memorie de 1024 de bytes, împărțit astfel:
    • Pool-ul de blocuri de dimensiune 8 conține adrese de la 0x1 la 0x100. (256 / 8 = 32 blocuri)
    • Pool-ul de blocuri de dimensiune 16 conține adrese de la 0x101 la 0x200. (256 / 16 = 16 blocuri)
    • Pool-ul de blocuri de dimensiune 32 conține adrese de la 0x201 la 0x300. (256 / 32 = 8 blocuri)
    • Pool-ul de blocuri de dimensiune 64 conține adrese de la 0x301 la 0x400. (256 / 64 = 4 blocuri).
  • Se alocă un spațiu de 10 bytes, acesta va fi în pool-ul de 16 bytes. Adresa returnată va fi 0x101. Cum dimensiunea cerută pentru alocare este mai mică decât blocul primit la alocare, acesta se va fragmenta. Vom aloca 10 adrese începând de la adresa 0x101 și restul adreselor (de la 0x10B la la 0x110) vor forma un block liber de 6 octeți.
  • Se alocă un spațiu de 16 bytes, acesta va fi în pool-ul de 16 bytes. Adresa returnată va fi prima disponibilă din pool, adică 0x111. În acest caz nu avem nevoie de fragmentare.
  • Se încearcă alocarea unui spațiu de 2000 bytes, dar acest lucru nu este posibil, deoarece 2000 depășește dimensiunea celui mai mare bloc posibil. Se va afișa mesajul de eroare “Out of memory”.
  • Se scrie string-ul GIGEL la adresa 0x111. La adresa 0x111 va fi litera G, la 0x112 I, la 0x113 G șamd.
  • Se afișează DUMP-ul de memorie, conform specificațiilor menționate în enunț.
  • Se citesc 2 bytes de la adresa 0x113, GE.
  • Se eliberează memoria de la adresa 0x111.
  • Se încearcă eliberarea memoriei de la adresa 0x111, care nu este alocată. Se va afișa “Invalid free”.

Tratarea erorilor (40p)

La primirea comenzilor pot apărea erori de input. Pe acestea, voi trebuie să le tratați corespunzător, prin afișarea unui mesaj sugestiv:

  • OUT_OF_MEMORY
    • Dacă nu există blocuri cu o dimensiune mai mare sau egală cu cea care trebuie alocată se afișează mesajul de eroare “Out of memory\n”.

E posibil ca, însumând blocuri de dimensiune mai mică, să existe suficientă memorie pentru apelul de MALLOC, totuși, dacă aceasta nu se găsește în totalitate în același bloc, alocarea eșuează.

  • INVALID_FREE
    • Dacă se dorește eliberarea unei zone de memorie începând cu o adresă care nu a fost alocată deja se afișează mesajul de eroare “Invalid free\n”.
    • Dacă se dorește eliberarea unei zone de memorie începând cu o adresă care nu reprezintă început de bloc (adică nu a rezultat în urma comenzii MALLOC) se afișează mesajul de eroare “Invalid free\n”.
  • SEGMENTATION_FAULT
    • Dacă se dorește citirea de la o adresă care nu a fost alocată, se afișează mesajul de eroare “Segmentation fault (core dumped)\n” și se face un dump al memoriei, după regulile de la funcția DUMP_MEMORY.
    • Dacă se dorește citirea de la o adresă care a fost alocată, dar care pe parcursul citirii conține zone nealocate, se afișează mesajul de eroare “Segmentation fault (core dumped)\n” și se face un dump al memoriei, după regulile de la funcția DUMP_MEMORY.
    • Dacă se dorește scrierea începând cu o adresă care nu a fost alocată, se afișează mesajul de eroare “Segmentation fault (core dumped)\n” și se face un dump al memoriei, după regulile de la funcția DUMP_MEMORY.
    • Dacă se dorește scrierea începând cu o adresă care a fost alocată, dar care pe parcursul scrierii conține zone nealocate, se afișează mesajul de eroare “Segmentation fault (core dumped)\n” și se face un dump al memoriei, după regulile de la funcția DUMP_MEMORY.

Bonus (20p)

În cadrul bonusului, veți trata cazul în care parametrul <tip_reconstituire> din cadrul INIT_HEAP este egal cu 1, cu alte cuvinte reconstituirea la FREE a blocurilor care au provenit din altul mai mare, în urma unei fragmentări. Reconstituirea are loc și pentru fragmentări intermediare: un bloc uriaș a fost fragmentat, blocul fragment a fost fragmentat la rândul său și așa mai departe. În acest caz, reconstituirea ar trebui să fie recursivă.

Un exemplu sumar ar fi

Fie situația în care avem un singur bloc liber, de dimensiune 16 bytes,
care începe de la adresa 0x1.

MALLOC 6
---> Se fragmentează blocul de 16 bytes într-o parte de 6 bytes
     ce va fi alocată și o parte de 10 bytes ce rămâne liberă
     Se întoarce adresa 0x1.
MALLOC 5
---> Se fragmentează blocul de 10 bytes într-o parte de 5 bytes
     ce va fi alocată și o parte de 5 bytes ce rămâne liberă
     Se întoarce adresa 0x7.
FREE 0x7
---> Se alipește fragmentul de 5 bytes cu fragmentul liber de
     5 bytes.
     Rezultă un bloc de dimensiune 10 bytes.
FREE 0x1
---> Se alipește fragmentul de 6 bytes cu fragmentul liber de
     10 bytes.
     Rezultă un bloc de dimensiune 16 bytes.

Un alt exemplu ar fi

Fie situația în care avem un singur bloc liber, de dimensiune 16 bytes,
care începe de la adresa 0x1.

MALLOC 6
---> Se fragmentează blocul de 16 bytes într-o parte de 6 bytes
     ce va fi alocată și o parte de 10 bytes ce rămâne liberă
     Se întoarce adresa 0x1.
MALLOC 5
---> Se fragmentează blocul de 10 bytes într-o parte de 5 bytes
     ce va fi alocată și o parte de 5 bytes ce rămâne liberă
     Se întoarce adresa 0x7.
FREE 0x1
---> Se creează un bloc nou de dimensiune 6 bytes.
     Există 2 liste cu câte un bloc fiecare, unul de 5 bytes și unul de 6 bytes.
FREE 0x7
---> Se alipește fragmentul de 5 bytes cu fragmentele de 5 și 6 bytes.
     Există o singură listă, cu un singur bloc, de dimensiune 16

README (10p)

  • Fișierul de README va avea numele README sau README.md și va fi pus în rădăcina arhivei. În acest fișier veți detalia implementarea voastră, ce vi s-a părut interesant și care a fost cea mai dificilă parte a temei.
  • Găsiți aici un template de README. Recomandăm folosirea formatului Markdown în scrierea acestuia.

Coding style (10p)

Mențiuni

  • Se garantează faptul că operația de READ va citi din zone scrise anterior cu WRITE. Cu alte cuvinte o operație validă de READ va avea mereu loc după o operație validă de WRITE.
  • Indexarea se face de la 0 pentru orice tip de listă.
  • Alocarea memoriei se va realiza dinamic. Alocarea statică a bufferelor a căror dimensiune nu se cunoaște la compile-time, atrage dupa ea depunctări.
  • Se cere ca structura de date folosită să fie generică. Pentru implementare lipsită de genericitate, se vor pierde 20p din totalul temei.

  • O structură de date generică are rolul de a nu duplica (pe cât posibil) cod permițând adăugarea în cadrul ei a oricărui tip de date. (hint: void *)
  • Exemplu: Vedeți în cadrul scheletului de lab unde a trebuit să implementați exact această situație. (hint: data_size)

  • Nu este indicată utilizarea variabilelor globale. Utilizarea acestora aduce pierderea a 20p din totalul temei.
  • Eliberarea memoriei se va verifica folosind utilitarul Valgrind. O temă ce conține memory leaks va atrage după sine punctaj de 0p pe testul respectiv.

  • Este obligatorie construirea unei implementări care folosește conceptul de vector de liste dublu înlănțuite.
  • Este obligatoriu să realizați eliminarea blocurilor din vectorul de liste atunci când acestea sunt alocate.
  • Nerespectarea acestor constrângeri va conduce la notarea temei cu 0 puncte.

Checker

Arhivă checker: sfl_checker.zip

Pentru a utiliza checker-ul:

  • Dezarhivați fișierul .zip în folderul in care se află implementarea voastră.
  • Asigurați-vă că Makefile-ul vostru conține regulile build, run_sfl și clean.
  • Rulați folosind comanda ”./check”.

Temele vor fi trimise pe Moodle.

Arhiva trebuie sa contina:

  • sursele .c si .h
  • fișier Makefile cu trei reguli:
    • regula build: în urma căreia se genereaza un executabil numit sfl
    • regula run_sfl: în urma căreia va rula executabilul sfl
    • regula clean care șterge executabilul și fișierele obiect
  • Compilarea trebuie să se realizeze cu flagurile -Wall -Wextra -std=c99
  • fișier README care să conțină detalii despre implementarea temei.
sd-ca/teme/tema1-2024.txt · Last modified: 2024/03/27 21:24 by maria.sfiraiala
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0