C alocare dinamică a memoriei - C dynamic memory allocation
| Biblioteca standard C |
|---|
| Subiecte generale |
| Anteturi diverse |
Alocarea dinamică a memoriei C se referă la gestionarea manuală a memoriei pentru alocarea dinamică a memoriei în limbajul de programare C printr-un grup de funcții din biblioteca standard C , și anume malloc , realloc , calloc și gratuit .
C ++ Limbajul de programare include aceste funcții; cu toate acestea, operatorii noi și șterse oferă funcționalități similare și sunt recomandați de autorii acelei limbi. Cu toate acestea, există mai multe situații în care utilizarea new/deletenu este aplicabilă, cum ar fi codul de colectare a gunoiului sau codul sensibil la performanță și o combinație de mallocși placement newpoate fi necesară în locul newoperatorului de nivel superior .
Sunt disponibile multe implementări diferite ale mecanismului real de alocare a memoriei, utilizat de malloc . Performanța lor variază atât în timpul execuției, cât și în memoria necesară.
Justificare
Limbajul de programare C gestionează memorie static , în mod automat , sau dinamic . Variabilele cu durată statică sunt alocate în memoria principală, de obicei împreună cu codul executabil al programului și persistă pe toată durata de viață a programului; variabilele cu durată automată sunt alocate pe stivă și intră și pleacă pe măsură ce funcțiile sunt apelate și returnate. Pentru variabilele cu durată statică și cu durată automată, dimensiunea alocării trebuie să fie constantă în timp de compilare (cu excepția matricelor automate cu lungime variabilă). Dacă dimensiunea necesară nu este cunoscută până la rulare (de exemplu, dacă datele de dimensiuni arbitrare sunt citite de la utilizator sau dintr-un fișier disc), atunci utilizarea obiectelor de date de dimensiuni fixe este inadecvată.
Durata de viață a memoriei alocate poate provoca, de asemenea, îngrijorare. Nici memoria statică, nici cea automată nu sunt adecvate pentru toate situațiile. Datele alocate automat nu pot persista pe mai multe apeluri funcționale, în timp ce datele statice persistă pe toată durata programului, indiferent dacă sunt necesare sau nu. În multe situații, programatorul necesită o mai mare flexibilitate în gestionarea duratei de viață a memoriei alocate.
Aceste limitări sunt evitate prin utilizarea alocării dinamice a memoriei , în care memoria este gestionată mai explicit (dar mai flexibil), de obicei prin alocarea acesteia din magazinul gratuit (numit informal „heap”), o zonă de memorie structurată în acest scop. În C, funcția de bibliotecă malloceste utilizată pentru a aloca un bloc de memorie pe heap. Programul accesează acest bloc de memorie printr-un indicator care mallocrevine. Atunci când memoria nu mai este necesară, este trecut indicatorul către freecare alocă memoria, astfel încât să poată fi folosit în alte scopuri.
Descrierea originală a lui C indica faptul că callocși se cfreeaflau în biblioteca standard, dar nu malloc. Codul pentru un model simplu de implementare a unui manager de stocare pentru Unix a fost dat cu funcțiile interfeței de utilizator allocși freeca funcții, și folosind sbrkapelul de sistem pentru a solicita memorie de la sistemul de operare. Documentația Ediția a 6-a Unix oferă allocși freefuncții de alocare a memoriei la nivel scăzut. mallocȘi freerutine în forma lor modernă sunt complet descrise în manualul de al 7 - lea Edition Unix.
Unele platforme oferă apeluri de bibliotecă sau funcții intrinseci, care permit alocarea dinamică în timpul rulării din stiva C, mai degrabă decât din heap (de exemplu alloca()). Această memorie este eliberată automat la sfârșitul funcției de apelare.
Prezentare generală a funcțiilor
Funcțiile de alocare a memoriei dinamice C sunt definite în stdlib.hantet ( cstdlibantet în C ++).
| Funcţie | Descriere |
|---|---|
malloc
|
alocă numărul specificat de octeți |
realloc
|
mărește sau scade dimensiunea blocului de memorie specificat, mutându-l dacă este necesar |
calloc
|
alocă numărul specificat de octeți și le inițializează la zero |
free
|
eliberează blocul de memorie specificat înapoi în sistem |
Diferențele dintre malloc()șicalloc()
-
malloc()ia un singur argument (cantitatea de memorie de alocat în octeți), în timp cecalloc()are nevoie de două argumente (numărul de variabile de alocat în memorie și dimensiunea în octeți a unei singure variabile). -
malloc()nu inițializează memoria alocată, în timp cecalloc()garantează că toți octeții blocului de memorie alocat au fost inițializați la 0. - Pe unele sisteme de operare,
calloc()poate fi implementat prin indicarea inițială a tuturor paginilor adreselor virtuale ale memoriei alocate către o pagină numai în citire din toate 0 și alocarea numai a paginilor fizice de citire-scriere atunci când sunt scrise adresele virtuale, o metodă numită copie- la scriere .
Exemplu de utilizare
Crearea unei matrice de zece numere întregi cu scop automat este simplă în C:
int array[10];
Cu toate acestea, dimensiunea matricei este fixată la momentul compilării. Dacă doriți să alocați dinamic o matrice similară, poate fi utilizat următorul cod:
int *array = (int*)malloc(10 * sizeof(int));
Aceasta calculează numărul de octeți pe care zece numere întregi îl ocupă în memorie, apoi solicită ca mulți octeți de la mallocși atribuie rezultatul unui pointer numit array(datorită sintaxei C, pointerii și matricele pot fi utilizate în mod interschimbabil în unele situații).
Deoarece mallocs-ar putea să nu poată deservi solicitarea, s-ar putea să returneze un pointer nul și este o bună practică de programare să verificați acest lucru:
int *array = malloc(10 * sizeof(int));
if (array == NULL) {
fprintf(stderr, "malloc failed\n");
return -1;
}
Când programul nu mai are nevoie de matricea dinamică , trebuie să apeleze în cele din urmă freepentru a returna memoria pe care o ocupă în magazinul gratuit:
free(array);
Memoria pusă deoparte mallocnu este inițializată și poate conține cruft : resturile de date utilizate anterior și aruncate. După alocarea cu malloc, elementele matricei sunt variabile neinițializate . Comanda callocva returna o alocare care a fost deja ștearsă:
int *array = calloc(10, sizeof(int));
Cu realloc putem redimensiona cantitatea de memorie către care arată un pointer. De exemplu, dacă avem un pointer care acționează ca o matrice de dimensiuni și dorim să-l schimbăm într-o matrice de dimensiuni , putem folosi realloc.
int *arr = malloc(2 * sizeof(int));
arr[0] = 1;
arr[1] = 2;
arr = realloc(arr, 3 * sizeof(int));
arr[2] = 3;
Rețineți că reallocul trebuie presupus că a schimbat adresa de bază a blocului (adică dacă nu a reușit să extindă dimensiunea blocului original și, prin urmare, a alocat un nou bloc mai mare în altă parte și a copiat conținutul vechi în el). Prin urmare, orice indicații către adrese din blocul original nu mai sunt valabile.
Tastați siguranța
mallocreturnează un pointer void ( void *), care indică faptul că este un pointer către o regiune de tip de date necunoscut. Utilizarea turnării este necesară în C ++ datorită sistemului puternic de tip, în timp ce acest lucru nu este cazul în C. Se poate „arunca” (vezi conversia de tip ) acest indicator către un tip specific:
int *ptr, *ptr2;
ptr = malloc(10 * sizeof(*ptr)); /* without a cast */
ptr2 = (int *)malloc(10 * sizeof(*ptr)); /* with a cast */
Exista avantaje si dezavantaje in realizarea unei astfel de distributii.
Avantajele turnării
- Includerea distribuției poate permite unui program sau funcție C să se compileze ca C ++.
- Exprimate permite pre-1989 versiuni ale
malloccare au returnat inițial ochar *. - Distribuirea poate ajuta dezvoltatorul să identifice neconcordanțe în dimensionarea tipului în cazul în care tipul indicatorului de destinație se schimbă, mai ales dacă indicatorul este declarat departe de
malloc()apel (deși compilatoarele moderne și analizorii statici pot avertiza asupra unui astfel de comportament fără a necesita distribuția).
Dezavantaje la turnare
- Conform standardului C, distribuția este redundantă.
- Adăugarea distribuției poate masca eșecul de a include antetul
stdlib.h, în care se găsește prototipul funcției pentrumalloc. În absența unui prototip pentrumalloc, standardul C90 necesită ca compilatorul C să presupună cămallocreturnează unint. Dacă nu există distribuție, C90 necesită un diagnostic atunci când acest număr întreg este atribuit indicatorului; totuși, cu distribuția, acest diagnostic nu ar fi produs, ascunzând un bug. Pe anumite arhitecturi și modele de date (cum ar fi LP64 pe sistemele pe 64 de biți, undelongși pointerii sunt pe 64 de biți șiintsunt pe 32 de biți), această eroare poate avea drept rezultat un comportament nedefinit, întrucât declaratul implicitmallocreturnează o valoare de 32 de biți în timp ce funcția de fapt definită returnează o valoare pe 64 de biți. În funcție de convențiile de apelare și de aspectul memoriei, acest lucru poate duce la distrugerea stivei . Această problemă este mai puțin probabil să treacă neobservată în compilatoarele moderne, deoarece C99 nu permite declarații implicite, așa că compilatorul trebuie să producă un diagnostic chiar dacă presupuneintreturnarea. - Dacă tipul indicatorului este modificat la declarația sa, este posibil să fie nevoie să schimbați toate liniile unde
malloceste apelat și aruncat.
Erori frecvente
Utilizarea necorespunzătoare a alocării dinamice a memoriei poate fi frecvent o sursă de erori. Acestea pot include erori de securitate sau blocări ale programului, cel mai adesea din cauza defecțiunilor de segmentare .
Cele mai frecvente erori sunt următoarele:
- Nu se verifică erorile de alocare
- Alocarea de memorie nu este garantată pentru a reuși și poate întoarce un pointer nul. Utilizarea valorii returnate, fără a verifica dacă alocarea are succes, invocă un comportament nedefinit . Acest lucru duce de obicei la blocare (din cauza defecțiunii de segmentare rezultate pe dereferința indicatorului nul), dar nu există nicio garanție că se va întâmpla o blocare, astfel încât să ne bazăm pe acest lucru poate duce și la probleme.
- Scurgeri de memorie
- Eșecul de a repartiza memoria utilizând
freeduce la acumularea de memorie care nu poate fi reutilizată, care nu mai este utilizată de program. Acest lucru risipește resursele de memorie și poate duce la erori de alocare atunci când aceste resurse sunt epuizate. - Erori logice
- Toate alocările trebuie să urmeze același model: alocarea utilizând
malloc, utilizarea pentru stocarea datelor, alocarea utilizândfree. Nerespectarea acestui tipar, cum ar fi utilizarea memoriei după un apel cătrefree( indicatorul suspendat ) sau înainte de un apel cătremalloc( indicatorul sălbatic ), apelarea defreedouă ori („dublu liber”) etc., cauzează de obicei o eroare de segmentare și are ca rezultat o blocarea programului. Aceste erori pot fi tranzitorii și greu de depanat - de exemplu, memoria eliberată nu este, de obicei, recuperată imediat de sistemul de operare și, prin urmare, indicatorii suspendați pot persista o vreme și par să funcționeze.
În plus, ca o interfață care precede standardizarea ANSI C mallocși funcțiile sale asociate au comportamente care au fost lăsate în mod intenționat implementării pentru a le defini singure. Una dintre ele este alocarea de lungime zero, care este mai mult o problemă, reallocdeoarece este mai frecvent să redimensionați la zero. Deși atât POSIX, cât și Specificația Single Unix necesită gestionarea corectă a alocărilor de dimensiuni 0 fie prin returnare, fie prin NULLaltceva care poate fi eliberat în siguranță, nu toate platformele sunt obligate să respecte aceste reguli. Printre numeroasele erori dublu-libere la care a dus, WhatsApp RCE din 2019 a fost deosebit de proeminent. O modalitate de a înfășura aceste funcții pentru a le face mai sigure este prin simpla verificare a alocărilor de dimensiuni 0 și transformarea lor în cele de dimensiunea 1. (Returnarea NULLare propriile probleme: în caz contrar indică o defecțiune în afara memoriei. În cazul reallocar fi semnalat că memoria originală nu a fost mutată și eliberată, ceea ce din nou nu este cazul mărimii 0, ducând la dublu-liber.)
Implementări
Implementarea gestionării memoriei depinde în mare măsură de sistemul de operare și de arhitectură. Unele sisteme de operare furnizează un alocator pentru malloc, în timp ce altele furnizează funcții pentru a controla anumite regiuni de date. Același alocator de memorie dinamică este adesea folosit pentru a implementa ambele mallocși operatorul newîn C ++ .
Bazat pe grămadă
Implementarea alocatorului se face de obicei folosind heap-ul sau segmentul de date . Alocatorul se va extinde de obicei și va contracta heap-ul pentru a îndeplini cererile de alocare.
Metoda grămezii suferă de câteva defecte inerente, care provin în întregime din fragmentare . Ca orice metodă de alocare a memoriei, heap-ul va deveni fragmentat; adică vor exista secțiuni de memorie folosită și neutilizată în spațiul alocat de pe heap. Un bun alocator va încerca să găsească o zonă neutilizată a memoriei deja alocate de utilizat înainte de a recurge la extinderea heap-ului. Problema majoră a acestei metode este că heap-ul are doar două atribute semnificative: baza sau începutul heap-ului în spațiul de memorie virtuală; și lungimea sau dimensiunea sa. Heap-ul necesită suficientă memorie de sistem pentru a-și umple întreaga lungime, iar baza sa nu se poate schimba niciodată. Astfel, orice zone mari de memorie neutilizată sunt irosite. Heap-ul se poate „bloca” în această poziție dacă există un segment mic folosit la sfârșitul heap-ului, care ar putea pierde orice cantitate de spațiu de adrese. Pe schemele de alocare a memoriei leneșe, precum cele care se găsesc adesea în sistemul de operare Linux, o grămadă mare nu rezervă neapărat memoria de sistem echivalentă; o va face doar la prima scriere (citirile paginilor de memorie ne-mapate returnează zero). Granularitatea acestuia depinde de dimensiunea paginii.
dlmalloc și ptmalloc
Doug Lea a dezvoltat domeniul public dlmalloc („Doug Lea's Malloc”) ca alocator general, începând din 1987. Biblioteca GNU C (glibc) este derivată din ptmalloc („pthreads malloc”) a lui Wolfram Gloger, o furcă a dlmalloc cu îmbunătățiri legate de threading. Începând cu noiembrie 2019, cea mai recentă versiune a dlmalloc este versiunea 2.8.6 din august 2012.
dlmalloc este un alocator de etichete de graniță. Memoria de pe heap este alocată ca „bucăți”, o structură de date aliniată pe 8 octeți care conține un antet și memorie utilizabilă. Memoria alocată conține o cheltuială de 8 sau 16 octeți pentru dimensiunea blocului și a steagurilor de utilizare (similar cu un vector de droguri ). Bucățile nealocate stochează, de asemenea, indicii către alte bucăți libere din spațiul utilizabil, ceea ce face ca dimensiunea minimă a bucății să fie de 16 octeți pe sistemele pe 32 de biți și 24/32 (depinde de aliniere) octeți pe sistemele pe 64 de biți.
Memoria nealocată este grupată în „ coșuri ” de dimensiuni similare, implementate utilizând o listă dublă de bucăți (cu indicii stocate în spațiul nealocat din interiorul bucății). Coșurile sunt sortate după mărime în trei clase:
- Pentru cererile sub 256 de octeți (o cerere „smallbin”), se folosește un simplu alocator de cea mai bună potrivire cu două puteri. Dacă nu există blocuri libere în coșul respectiv, un bloc din cel mai înalt coș următor este împărțit în două.
- Pentru solicitări de 256 de octeți sau mai mult, dar sub pragul mmap , dlmalloc din v2.8.0 utilizează un algoritm de trie bitwise în loc ("arbore"). Dacă nu mai există spațiu liber pentru a satisface cererea, dlmalloc încearcă să mărească dimensiunea heap-ului, de obicei prin apelul de sistem brk . Această caracteristică a fost introdusă după ce a fost creat ptmalloc (de la v2.7.x) și, ca urmare, nu face parte din glibc, care moștenește vechiul alocator de cea mai bună potrivire.
- Pentru solicitări peste pragul mmap (o cerere „mare”), memoria este întotdeauna alocată utilizând apelul de sistem mmap . Pragul este de obicei 256 KB. Metoda mmap rezolvă problemele cu tampoane uriașe care prind o mică alocare la sfârșit după expirarea lor, dar alocă întotdeauna o pagină întreagă de memorie, care pe multe arhitecturi are o dimensiune de 4096 octeți.
Dezvoltatorul de jocuri Adrian Stone susține că dlmalloc, în calitate de alocator de etichete de graniță, este neprietenos pentru sistemele de consolă care au memorie virtuală, dar care nu au cerere de paginare . Acest lucru se datorează faptului că apelurile sale de reducere și de creștere a pool-ului (sysmalloc / systrim) nu pot fi utilizate pentru a aloca și comite pagini individuale de memorie virtuală. În absența paginării cererii, fragmentarea devine o preocupare mai mare.
FreeBSD și jemalloc NetBSD
De la FreeBSD 7.0 și NetBSD 5.0, vechea mallocimplementare (phkmalloc) a fost înlocuită de jemalloc , scris de Jason Evans. Principalul motiv pentru aceasta a fost lipsa de scalabilitate a phkmalloc în ceea ce privește multithreading-ul. Pentru a evita disputa de blocare, jemalloc folosește "arene" separate pentru fiecare procesor . Experimentele de măsurare a numărului de alocări pe secundă în aplicația multithreading au arătat că acest lucru îl face să scară liniar cu numărul de fire, în timp ce atât pentru phkmalloc cât și pentru dlmalloc performanța a fost invers proporțională cu numărul de fire.
OpenBSD's malloc
Implementarea de către OpenBSD a mallocfuncției folosește mmap . Pentru solicitări mai mari decât o pagină, întreaga alocare este recuperată folosind mmap; dimensiuni mai mici sunt alocate din pool-urile de memorie menținute de mallocîntr-un număr de „pagini bucket”, de asemenea alocate cu mmap. La un apel către free, memoria este eliberată și neasciată din spațiul de adresă al procesului folosind munmap. Acest sistem este conceput pentru a îmbunătăți securitatea, profitând de caracteristicile de randomizare a aspectului spațiului de adrese și de funcțiile paginii gap, implementate ca parte a mmap apelului de sistem al OpenBSD și pentru a detecta erorile de utilizare după liber - întrucât o alocare de memorie mare este complet nelimitată după eliberare , utilizarea ulterioară provoacă o eroare de segmentare și încetarea programului.
Hoard malloc
Hoard este un alocator al cărui obiectiv este performanța scalabilă a alocării memoriei. La fel ca alocatorul OpenBSD, Hoard folosește mmapexclusiv, dar gestionează memoria în bucăți de 64 kilobyte numite superblocuri. Heap-ul lui Hoard este împărțit în mod logic într-un singur heap global și un număr de heap pe procesor. În plus, există o memorie cache locală care poate conține un număr limitat de superblocuri. Alocând numai de la superblocuri pe heap-ul local pe per-thread sau per-procesor și mutând superblocurile cele mai goale în heap-ul global, astfel încât să poată fi refolosite de alte procesoare, Hoard menține fragmentarea scăzută în timp ce atinge o scalabilitate aproape liniară cu numărul de fire de execuție .
mimalloc
Un alocator de memorie compact de uz general open-source de la Microsoft Research cu accent pe performanță. Biblioteca are aproximativ 11.000 de linii de cod .
Thread-caching malloc (tcmalloc)
Fiecare fir are un spațiu de stocare local pentru alocări mici. Pentru alocări mari se pot utiliza mmap sau sbrk . TCMalloc , un malloc dezvoltat de Google, are colectarea gunoiului pentru stocarea locală a firelor moarte. TCMalloc este considerat a fi de peste două ori mai rapid decât ptmalloc-ul glibc pentru programele cu mai multe fire.
În nucleu
Nucleele sistemului de operare trebuie să aloce memorie la fel ca programele de aplicație. Cu toate acestea, implementarea mallocîn cadrul unui nucleu diferă semnificativ de implementările utilizate de bibliotecile C. De exemplu, bufferele de memorie ar putea avea nevoie să se conformeze restricțiilor speciale impuse de DMA sau funcția de alocare a memoriei ar putea fi apelată din contextul de întrerupere. Acest lucru necesită o mallocimplementare strâns integrată cu subsistemul de memorie virtuală al nucleului sistemului de operare.
Malloc suprem
Deoarece mallocși rudele sale pot avea un impact puternic asupra performanței unui program, nu este neobișnuit să suprascrieți funcțiile pentru o anumită aplicație prin implementări personalizate care sunt optimizate pentru tiparele de alocare a aplicației. Standardul C nu oferă nicio modalitate de a face acest lucru, dar sistemele de operare au găsit diverse modalități de a face acest lucru prin exploatarea legăturii dinamice. O modalitate este să legați pur și simplu într-o altă bibliotecă pentru a suprascrie simbolurile. Un alt lucru, utilizat de Unix System V.3 , este să creeze mallocși să freefuncționeze indicii pe care o aplicație le poate reseta la funcții personalizate.
Limite de mărime a alocării
Cel mai mare bloc de memorie posibil mallocpoate fi alocat depinde de sistemul gazdă, în special de dimensiunea memoriei fizice și de implementarea sistemului de operare.
Teoretic, cel mai mare număr ar trebui să fie valoarea maximă care poate fi păstrată într-un size_ttip, care este un întreg nesemnat dependent de implementare care reprezintă dimensiunea unei zone de memorie. În standardul C99 și ulterior, este disponibil ca SIZE_MAXconstantă de la <stdint.h>. Deși nu este garantat de ISO C , este de obicei .
2^(CHAR_BIT * sizeof(size_t)) - 1
Pe sistemele glibc, cel mai mare bloc de memorie posibil mallocpoate fi alocat este doar jumătate din această dimensiune, și anume .
2^(CHAR_BIT * sizeof(ptrdiff_t) - 1) - 1
Extensii și alternative
Implementările bibliotecii C livrate cu diferite sisteme de operare și compilatoare pot veni cu alternative și extensii la mallocinterfața standard . Printre acestea se remarcă:
-
alloca, care alocă un număr solicitat de octeți pe stiva de apeluri . Nu există nicio funcție de repartizare corespunzătoare, deoarece de obicei memoria este repartizată de îndată ce funcția apelantă revine.allocaa fost prezent pe sistemele Unix încă din 32 / V (1978), dar utilizarea sa poate fi problematică în unele contexte (de exemplu, încorporate). Deși acceptat de mulți compilatori, acesta nu face parte din standardul ANSI-C și, prin urmare, este posibil să nu fie întotdeauna portabil. De asemenea, poate provoca probleme minore de performanță: duce la cadre de stivă de dimensiuni variabile, astfel încât atât indicatorii de stivă, cât și indicatorii de cadru trebuie gestionați (cu cadre de stivă de dimensiuni fixe, unul dintre acestea este redundant). Alocările mai mari pot crește, de asemenea, riscul unui comportament nedefinit din cauza unei revărsări a stivei . C99 a oferit tablouri cu lungime variabilă ca mecanism alternativ de alocare a stivei - cu toate acestea, această caracteristică a fost retrogradată la opțional în standardul C11 de mai târziu . -
POSIX definește o funcție
posix_memaligncare alocă memoria cu alinierea specificată de apelant. Alocările sale sunt alocatefree, astfel încât implementarea trebuie, de obicei, să facă parte din biblioteca malloc.
Vezi si
- Debord de tampon
- Depanator de memorie
- Protecția memoriei
- Mărimea paginii
- Matrice cu lungime variabilă
Referințe
linkuri externe
- Definiția malloc în standardul IEEE Std 1003.1
- Lea, Doug ; Proiectarea bazei alocatorului glibc
- Gloger, Wolfram; Pagina principală ptmalloc
- Berger, Emery; Pagina de pornire Hoard
- Douglas, Niall; Pagina de pornire nedmalloc
- Evans, Jason; Pagina principală jemalloc
- Algoritmi simpli de alocare a memoriei în comunitatea OSDEV
- Michael, Maged M .; Alocare dinamică a memoriei dinamică fără blocare
- Bartlett, Jonathan; Gestionarea memoriei interne - Alegerile, compromisurile și implementările alocării dinamice
- Pagina wiki de reducere a memoriei (GNOME) cu multe informații despre remedierea malloc
- Proiect standard C99, inclusiv TC1 / TC2 / TC3
- Câteva referințe utile despre C
- ISO / IEC 9899 - Limbaje de programare - C
- Înțelegerea glibc malloc