Mälu haldamine

Lihtsaim viis andmete salvestamiseks on defineerida nende jaoks sobivad muutujad. Kui te aga ei tea täpselt, kui palju andmeid teie programm vajab, siis ei saa te ka defineerida õiget hulka muutujaid. Kui te loote näiteks programmi, mis haldab teie tuttavate nimesid, aadresse ja telefoninumbreid ning salvestab vajalikud andmed mingisse faili, siis võite defineerida vastava andmestruktuuri ja oma andmesegmendis näiteks 5000 sellisest struktuurist koosneva massiivi. Tõenäoliselt ei ole teil rohkem kui 5000 tuttavat. Ikkagi on selline programm piiratud võimetega. Ta ei suuda mingil juhul lugeda rohkem kui 5000 sissekandest koosnevat andmefaili. Peale selle salvestatakse andmesegmendi sisu ka moodustatud *.EXE failis, nii on tulemuseks üsna suur *.EXE fail, mis oma suurust kuidagi ei õigusta. Kui te sellise programmi stardite, siis reserveerib ta suure hulga mälu, mida ta tegelikult ei vaja. Kuna enamus selle programmi andmefaile on tõenäoliselt tunduvalt väikesemad kui 5000 sissekannet, siis on suur osa mälust pidevalt asjatult reserveeritud. Operatsioonisüsteemis DOS ei ole see veel nii suur probleem, kuna te nagunii saate korraga kasutada ainult üht programmi. Kui te aga loote programmi mingi operatsioonisüsteemi jaoks, mis võimaldab mitmiktegumreshiimi nagu näiteks OS/2 või UNIX, siis peaks iga programm üritama süsteemi ressurssidega kokkuhoidlikult ümber käia, et korraga suudaksid töötada võimalikult palju programme.

Selleks võiks niisuguste andmestruktuuride jaoks reserveerida mälu dünaamiliselt. Operatsioonisüsteem DOS suudab otseselt kasutada vaid 1 MB mälu. See mälu jaotatakse järgmiselt:



Joonis 5: Mälu haldamine operatsioonisüsteemis DOS

Nagu näete, võib teie programm operatsioonisüsteemis DOS kasutada kogu mälu alates ohjurprogrammide lõpust kuni 640 KB piirini. Uuemates DOSi versioonides kasutatakse vabu mälublokke 640 KB ja 1 MB vahel ohjurprogrammide ja ühe osa DOSi tuumiku jaoks nii, et paremal juhul jääb teie programmile kasutada ligi 600 KB. Kuna DOS kasutab protsessorit reaalrezhiimis, siis ei suudeta adresseerida rohkem mälu kui 1MB. Selles rezhiimis kasutab protsessor 16-bitiseid aadresse. Selliselt vastab protsessori töö täpselt kunagi laialtlevinud i8086 protsessorile. Sellest ajast on pärit ka operatsioonisüsteem DOS koos oma piirangutega.

Programm koosneb ühest või enamast koodi- ja ühest või enamast andmesegmendist. Koodisegmendid sisaldavad masinkoodis programmi teksti: avaldisi, lauseid ja funktsioone. Andmesegmentidesse salvestatakse muutujate väärtused. Seejuures eristatakse initsialiseeritud (muutujad, millele omistatakse kohe mingi väärtus) ja initsialiseerimata muutujate vahel.


Joonis 6: Programmi segmentide jaotus

Joonisel 6 näete programmi segmentide jaotust operatsioonisüsteemis DOS. Joonise vasakul äärel näete nimesid, mida translaator omistab vastavatele segmentidele. Kui te ei soovi just otse assembleris programmeerida, siis ei oma need nimed suuremat tähtsust. Kohe peale ohjurprogrammide lõppu laaditakse programmi koodisegmendid. Seejärel tulevad andmesegmendid. Andmesegmendi alguses salvestatakse initsialiseeritud muutujad ja konstandid. Peale neid reserveeritakse ruumi initsialiseerimata muutujatele. Programmi *.EXE faili andmesegmendis reserveeritakse ruumi vaid muutujatele. Kui te aga laadite programmi mällu, siis suurendab operatsioonisüsteem andmesegmenti. Andmesegmendi lõppu luuakse piirkond ajutiste muutujate jaoks - pinu (stack). Kui te näiteks kutsute programmis välja mingi funktsiooni, siis salvestatakse tema kohalikud muutujad pinusse. Pinu "kasvab" andmesegmendi lõpust alguse poole. Kui väljakutsutud funktsioon kutsub omakorda välja mingeid funktsioone, siis salvestatakse nende kohalikud muutujad pinusse peale esimese funktsiooni muutujaid. Niipea, kui hetkel aktiivne funktsioon oma töö lõpetab, vabastatakse ka tema kohalike muutujate jaoks kasutatud mälu ja seega väheneb pinu suurus. Tema alumine piir nihkub ülespoole.

Dünaamilist mälu reserveeritakse kahes kohas: programmi andmesegmendis peale initsialiseerimata muutujate piirkonna lõppu (HEAP) ja peale programmi kõikide segmentide lõppu üldisest vabast mälust (FAR HEAP). Dünaamilise mälu reserveerimine programmi andmesegmendist on natuke kiirem. Selleks kasutatakse päisefailis ALLOC.H (või STDLIB.H) defineeritud funktsioone malloc(), calloc(), realloc() ja free().

void* malloc(size_t size);
void* calloc(size_t nitems, size_t size);
void* realloc(void* pBuf, size_t size);
void free(void* pBuf);

Funktsioon calloc() reserveerib kohalikust andmesegmendist küllaldaselt mälu nitems elemendist koosnevale andmestruktuurile, mille iga element on size baidi suurune. Uue mälubloki suurus on seega vähemalt nitems * size baiti. Tegelikult võib mälublokk olla veel natuke suuremgi. Selle põhjuseks on see, et mälu reserveeritakse 16-baidiste osade kaupa. Iga bloki suurus on seega 16-ga jaguv arv ja kui ta seda ei ole, siis seda suurendatakse natuke. Neid arvutusi ei pea te muidugi ise tegema. Funktsioonides kasutatud uus tüüp size_t on defineeritud kui täisarv ja seda kasutatakse mälublokkide suuruse määramiseks. Kui reserveerimine õnnestus, siis loovutab funktsioon oma väljakutsujale viida antud mälublokile. Selle viida abil saab nüüd mälubloki sisu muuta ja lugeda. Kui reserveerimine ebaõnnestus, siis on funktsiooni väärtuseks NULL. Seda väärtust peab ilmtingimata kontrollima, kuna nimetatud funktsiooni ebaõnnestmine on mälu vähesuse puhul küllaltki tõenäoline. Peale reserveerimist täidab funktsioon calloc() uue mälubloki sisu nullidega. Nii kindlustatakse, et programm ei vahetaks selles mälupiirkonnas juhuslikult olnud väärtusi enda poolt sisestatuga. Näide:

/* avab faili ja loeb sealt k sissekannet tüübist isik */
/* funktsiooni väärtuseks on viit mälublokile kui 
   kõikõnnestus ja NULL vea puhul */
char* ReadFile(char *pFileName)
{
  int	 k;
  struct isik *pIsikud;
  ...
	/* ava fail ja salvesta sissekannete arv muutujasse k */
  ...
	/* ürita reserveerida küllaldaselt mälu sissekannete jaoks */
	/* kui tulemuseks on NULL, siis lõpeta funktsiooni töö */
	/* saadud viit on tüübist void* ja tuleb enne omistamist */
	/* tüüpi struct isik * konverteerida */
	/* isikute arv on teada -> k, iga elemendi suuruse määrame */
	/* operaatoriga sizeof() */

  if(NULL == (pIsikud = (struct isik *)calloc(k, sizeof(isik))))
    return NULL;
	/* kõik on korras! Nüüd on aeg asuda antud blokki täitma */
	...
  return pIsikud;
}
...
/* Nüüd võib kasutada mälublokki nagu päris tavalist massiivi */
int	i, k;		/* k väärtus tuleb enne välja arvutada */
char	pBuf[20];
...
for(i = 0; i < k; i++) {
  strcpy(pBuf, pIsikud[i].name);
  ...
}

Funktsioon malloc() reserveerib kohalikust mälusegmendist size baidi suuruse mälubloki, kuid ei täida tema sisu nullidega. Siin ei arvuta funktsioon bloki suurust ja seega tuleb argumendina sisestada sobiv avaldis. Vahel ei ole mälubloki nullidega täitmine ka vajalik. Näiteks siis, kui teil on vaja ajutiselt salvestada sümbolite jada, kus ei ole tegemist teatud hulga kindla suurusega andmestruktuuridega ja seega ei ole vaja ka funktsiooni abi mälubloki suuruse arvutamiseks. Vajalik suurus on sümbolite arv + 1 (lõpumärgi jaoks). Kui te kopeerite sümbolid reserveeritud mälublokki funktsiooniga strcpy(), siis lisab nimetatud funktsioon ise jada lõppu nulli ja seega ei ole bloki initsialiseerimine lihtsalt vajalik.

Funktsioon realloc() muudab juba allokeeritud mälubloki suurust. Funktsiooni esimene parameeter on viit mälublokile, mille suurust tuleb muuta ja teine parameeter määrab tolle bloki uue suuruse. Funktsiooni väärtuseks on viit uuele blokile. Enamasti on see võrdne vana viidaga, kuid juhul kui blokki ei saanud suurendada, sest talle järgnev mälu oli juba mingi muu bloki poolt reserveeritud, siis reserveeritakse sobiva suurusega blokk uues kohas ja kopeeritakse vana bloki sisu uude blokki. Seega võib uus viit vanast erineda. Kui funktsioon ei suutnud mälu vähesuse tõttu blokki suurendada, siis on funktsiooni väärtuseks NULL. Sel juhul on vana viit veel kehtiv ja osutab vanale blokile, mis omab endist suurust.

Kord reserveeritud mälublokid tuleb kindlasti enne programmi lõppu mälust eemaldada. Selleks kasutatakse funktsiooni free(), millele antakse üle viit mälust eemaldatavale blokile. Peale bloki mälust eemaldamist ei tohi antud viita enam kasutada, enne kui talle on omistatud mingi teine väärtus, sest ta näitab veel piirkonnale, mis enam programmile ei kuulu. Kui te ei eemalda kõiki reserveeritud blokke mälust enne programmi lõppu, siis ei tee seda ka operatsioonisüsteem, sest ta ei tea, kellele need kuuluvad ja kas neid veel vajatakse. Nii tekivad mälus blokid, mida keegi ei kasuta ja mis takistavad järgmistel programmidel selle mälu kasutamist.

Kõik nimetatud funktsioonid reserveerivad mälu programmi kohalikust andmesegmendist. See on suhteliselt kiire, kuid sellised blokid ei tohi ületada suuruselt 64 KB. Seejuures tuleb meeles pidada, et seda mälu vajab ka programmi pinu. Kui pinu enam suureneda ei saa, kuna te olete reserveerinud kogu mälu andmesegmendist, siis lõpeb programmi töö veateatega. Kui te vajate väikest hulka mälu vaid ajutiselt, siis reserveerige see kohalikust andmesegmendist, sest see on kiirem, ja vabastage ta kohe, kui te teda enam ei vaja. Kui te aga peate salvestama suure hulga andmeid, siis tuleks reserveerida mälu üldisest vabast mälust (FAR HEAP). Selleks kasutatakse funktsioone:

void far* farcalloc(unsigned long nitems, unsigned long size);
void far* farmalloc(unsigned long nsize);
void far* farrealloc(void far* pBuf, unsigned long newsize);
void farfree(void far pBuf);

Need funktsioonid on sarnased eelpool toodud funktsioonidele, ainult et nad reserveerivad mälu üldisest vabast mälust ja reserveeritud blokid võivad olla suuremad kui 64 KB. Loovutatud viidad on nüüd pikad viidad (sisaldavad peale blokisisese aadressi ka segmendi aadressi) ja nende salvestamiseks tuleb kasutada ka sobivaid muutujaid. Kui te ei kasuta selliste funktsioonide juures pikki viitasid, vaid omistate saadud viida lihtsale lühikesele muutujale, siis salvestatakse vaid saadud viida viimane osa, s.o. segmendisisene aadress, segmendi aadress aga "unustatakse". Kui te nüüd hiljem seda viita kasutate, siis oletab programm, et lühike viit näitab programmi andmesegmendis asuvatele andmetele ja see tekitab vea.

Teadmaks, kui palju te võite mälu reserveerida, kasutage funktsioone coreleft() või farcoreleft().

	/* tiny, small ja medium mälumudeliga programmides */
unsigned coreleft( void );
	/* compact, large ja huge mälumudeliga programmides */
unsigned long coreleft( void );
	/* kasutatav kõikides programmides väljaarvatud tiny mälumudeliga programmides */
unsigned long farcoreleft( void );

Funktsioon coreleft() teatab, kui palju mälu on programmi andmesegmendis veel vaba. Funktsioon farcoreleft() hangib samad andmed üldise vaba mälu kohta. Nagu näha, on funktsiooni coreleft() tulemus sõltuv programmi mälumudelist.