lauantai 23. elokuuta 2008

C ja muisti osa 3 - Muistin varaaminen





No niin. Ensimmäiset Segmentation Fault:it siis lienee aikaansaatu. Älkää pelätkö, mikäli harrastatte C-ohjelmointia jatkossakin, tulette näkemään tuon virheen vielä monen monta kertaa. Itse asiassa, kaikkein kokeneinkin ohjelmoija joutuu toisinaan nöyrtymään tuon ilmoituksen edessä...

Segmentation Fault kaikessa yksinkertaisuudessaan tarkoittaa, että ohjelmanne erehtyi käpistelemään jotain muistialuetta jonne sillä ei olisi ollut asiaa. Yksinkertainen tapa saada tämä aikaan, on kääntää ja ajaa vaikka seuraava koodipätkä:


int main()
{
char *osoitin;
strcpy
(
osoitin,
"kaadu! Tama kirjoitetaan varaamattomaan muistilohkoon"
);
return 0;
}


Pohditaanpa jälleen hieman muistia ja käyttöjärjestelmää.

ohjelmaa ajettaessa käyttöjärjestelmä varaamaa sopivan köntin muistia ohjelmaan kirjoitetuille tavallisille muuttujille, tiedolle siitä mistä funktiosta mihinkin on hypätty jne. Tätä ennalta varattua muistikönttiä kutsutaan pinoksi, eli stackiksi. Pino on muistialue, joka pysyy varattuna ohjelman suorituksen ajan.

Mietitäänpä sitten hieman jotain todellista sovellusta, Esim. tekstieditoria. Nyt ohjelman käynnistyessä ei mitenkään voida tietää tarvittavan muistin määrää, sillä käyttäjän kirjoittamat merkit täytyy tallettaa muistiin siksi aikaa, kunnes käyttäjä tallettaa kirjoittamansa merkit tiedostoon. Miten tällaista ongelmaa lähestytään? Voisimme tietysti päättää, että editoriin sopii maksimissaan X merkkiä, ja varata tämän muistimäärän etukäteen. Tämä kuitenkin olisi kömpelöä, sillä se muodostaisi ylärajan editoitavan tekstin koolle, ja varaisi aina ohjelman käynnistyessä kyseisen määrän - ja koska yleensä käyttäjät kirjoittavat eri mittaisia tiedostoja, olisi suurimman tiedoston vaatima muisti paljon keskimääräisen tiedoston vaatimaa muistia suurempi. Ja kuitenkin joka kerran joutuisi editori käynnistyessään varaamaan muistia suurinta tiedostoa varten... Yksinkertaisesti sanottua, editori kuluttaisi muistia järjettömän paljon, melkein kuin M$:n sovellukset...

Ratkaisu on nk. dynaaminen muistinvaraus. Eli C-kieli mahdollistaa muistin varaamisen ajon aikana. Tämä dynaaminen varaus tehdään käyttämättömästä muistialueesta "keosta" (heap). Eli kun ohjelma tarvitsee lisää muistia, se pyytää käyttöjärjestelmää varaamaan tarvitsemansa tilan, ja saa paluuarvona varatun tilan alkuosoitteen.

Jatketaampa ajatusleikkiä. Kuvitellaan, että olemme tehneet webbiselaimen dynaamisen muistinvaraamisen avulla siten, että aina kun käyttäjä avaa sivuston, sivuston vaatima tila on dynaamisesti varattu. Kuvitellaan edelleen, että käyttäjä surffaa koko työpäivän netissä, aukoen ja sulkien sivuja... Tästä päästään toiseen erittäin tärkeään dynaamiseen muistinkäyttöön liittyvään asiaan, eli muistin vapauttamiseen.

Normaalit muuttujat jotka on otettu pinosta vapautetaan automaattisesti sen funktion suorituksen loputtua, jossa muuttuja on esitelty. (10 psiteen kysymys, mikä vaara piilee osoittimissa, jotka osoittavat näihin nk. lokaaleihin muuttujiin?) Sen sijaan dynaamisesti varattu muisti pysyy varattuna aina ohjelman suorituksen loppuun asti, ellei sitä erikseen vapauteta! Vapauttamatta jääneitä muistilohkoja kutsutaan muistivuodoiksi, ja ohjelmissa joita on suunniteltu suoritettavan pitkän aikaa, muistivuodot voivat olla todellinen riesa, aiheuttaen ajanoloon koko koneen hidastumista ohjelman rohmutessa lisää ja lisää muistia.

Mutta katsotaanpa nyt se C:n tyypillisin tapa varata muistia:


int main()
{
int *luku;
char *teksti;

luku=(int *)malloc(sizeof(int));
if(luku==NULL)
{
printf("Muistin varaaminen epaonnistui!");
return -1;
}
*luku=5;
//varataan tilaa sanalle viisi:
//tarvitaan 6 charia, 5 kirjaimille ja yksi lopun '\0':lle
teksti=(char *)malloc((5+1)*sizeof(char));
if(teksti==NULL)
{
printf("Muistin varaaminen epaonnistui!");
return -1;
}
strcpy(teksti,"viisi");
printf("%d eli %s",*luku,teksti);
//vapautetaan muisti
free(teksti);
free(luku);
return 0;
}


Eli avainsanat muistin varaamiseen ja vapautukseen ovat malloc() ja free(). malloc() varaa muistia niin monta tavua kuin argumenttina annetaan. Eli argumenttina on annettava halutun tietotyypin koko. onnistuessaan muistinvarauksessa malloc palauttaa varatun muistialueen alkuosoitteen, joka talletetaan osoittimeen. Epäonnistuessaan malloc palauttaa NULL:in (eli 0 osoitteen). mallocin paluuarvo on aina syytä tarkistaa, sillä NULL osoitteen käpistely myöhemmin johtaa tuhoon ja hävitykseen...

free() funktio vapauttaa varatun muistin, mutta jättää osoittimen sojottamaan vapautettuun muistilohkoon. Luonnollisestikin vapautetun muistilohkon käyttö johtaa softan kaatumiseen, joten vapautuksen jälkeen osoitin on käyttökelvoton kunnes se jälleen laitetaan osoittamaan jotain järkevää. Ja viimeksi, myös vapautetun muistilohkon uudelleen vapautus johtaa ohjelman kippaamiseen, samoin kuin pinosta varatun muistipalan vapautus.

Ihan viho viimeksi mainitsen vielä [] operaattorin. Eli esittelemällä muuttujan muodossa:

tyyppi nimi[koko];

saamme muistialueen johon on varattu tilaa koko muuttujalle. Myöhemmin koodissa voimme käyttää [N] operaattoria päästäksemme käsiksi N:nteen taulukkoon talletettuun arvoon.


int taulukko[10];
int *osoitin;

osoitin=(int *)malloc(10*sizeof(int));


Mikä edellä olevassa on osoittimen ja taulukon ero? Ero on se, että taulukko on nyt varattu stackista, ja sitä EI voi vapauttaa free() kutsulla, vaan se säilyy varattuna koko ohjelman suorituksen ajan. Ja tähän sopii vielä pieni huomautus sizeof() funktiosta, joka siis kertoo montako tavua tilaa jokin tyyppi/muuttuja vie muistista. sizeof() kuitenkin muutetaan luvuksi jo ohjelmaa käännettäessä, joten:


int taulukko[10];
int *osoitin;

osoitin=(int *)malloc(10*sizeof(int));
printf
(
"taulukko=%d osoitin=%d",
sizeof(taulukko),
sizeof(osoitin)
);


tulostaa eri koot taulukolle ja osoittimelle. (taulukolle 10*4 eli 10 int:n viemän tilan ja osoittimelle arkkitehtuurista riippuen 4 tai 8, eli osoittimen vaatiman tilan). Ja viimeinen esimerkki siitä, miten [] operaattorilla accessoidaan tiettyyn muistipaikkaan:


char taulukko[10];
char *osoitin;

osoitin=(int *)malloc(10*sizeof(int));
strcpy(taulukko,"kymmenen!");
strcpy(osoitin,"kymmenen!");
osoitin[8]='?';
taulukko[0]='x';
printf("taulukko=%s osoitin=%s\n", taulukko, osoitin);

Tämä tulostaisi:
taulukko=xymmenen! osoitin=kymmenen?

Huomaa siis, että [] operaattoria käytettäessä, taulukon ensimmäinen arvo on 0, ja viimeinen on koko-1. Eli:

char taulu[10];
taulu[10]='\0';

kirjoittaa '\0':n taulukon ulkopuolelle!

EDIT:
Jäi vaivaamaan... Nimittäin unohdin mainita realloc() ja calloc() kutsut, jotka malloc():n lisäksi ovat erittäin käyttökelpoisia. Mutta koska kömmin jo sängystä tekemään tämän lisäyksen, en jaksa niistä enempää jaaritella. Kannattaa kuitenkin katsoa ne vaikka man sivuilta, vaikka tuota tällä sivulla olevaa man hakua käyttäen. (toivottavasti se toimii, en voi itse testata, koska se on samalla sellainen mainoshässäkkä, ja olen sitoutunut olemaan käyttämättä sitä :rolleyes:)
Ja nyt alkaa taas uni painaa, joten palataan jälleen joku toinen päivä :)

4 kommenttia:

Maz kirjoitti...

Korjattu 10.09.2008, Kiitos Metabolixille huomautuksista.

Korjaukset:
1. kedosta => keosta (heapista)
2. poistettu väite että kääntäjä estimoi ohjelman vaatiman pinon koon.
3. muutettu muutama stack sana suomenkieliseen vastineeseen.

Anonyymi kirjoitti...

Kommenttia/kysymystä muistivaraus esimerkkiin.

int main()
{
char *testi;
testi = (char *)malloc(sizeof(testi));
strcpy(testi,"testiapukkaa");
printf("testi %s\n");
free(testi);
return 0;
}

Yllä oleva koodi toimii. Kuitenkin esimerkissäsi oli malloc((5+1)*sizeof(char)), mites tämän selität, koska tuo toimii ilman tuota (5+1)* ?

Maz kirjoitti...

Joo, eli hyvällä onnella ohjelma saattaa toimia, kirjotettaessa varaamattomaan muistipaikkaan. Tässä piileekin yksi muistin ylikirjoituksen suurimmista vaaroista. Ohjelma saattaa toimia 100 kertaa täysin moitteettomasti, ja 101:llä kertaa kaatua kuin se kuuluisa tuhatjalkainen. Eli esimerkkisi toimii tuurilla. Syy on se, että testi osoittimen osoittaman malloc:lla varatun muistialueen jälkeinen muisti ei sisältänyt mitään ohjelman kannalta kriittistä tietoa, ja ettei käyttöjärjestelmä suojannut sitä tälläkertaa. Seuraavalla ajolla tilanne ei kuitenkaan välttämättä ole sama. Puretaanpa vielä esimerkkisi.

Voit koettaa printata sizeof(testi) tuossa malloc:n jälkeen. 32 bittisellä arkkitehtuurilla saat ulos luvun 4, 64 bittisessä arkkitehtuurissa luvun 8.(4 tavua joka on 32 bittisessä arkkitehtuurissa muistiosoitteen vaatima tila, eli osoittimen - joka siis sisältää muistipaikan alkuosoitteen - vaatima tila. 64 bittisessä arkkitehtuurissa osoitin on yleensä 64 bittiä, eli 8 tavua.)

Sitten laskettaessa tila jota "testiapukkaa" vaatii saadaan:
12 kirjainta+\0, eli 13*sizeof(char). Ja kun char tietotyyppi vaatii tilaa yhden tavun, saadaan siis tarvittavaksi tilaksi 13 tavua. Eli esimerkkikoodissasi 32 bittisellä arkkitehtuurilla kirjoitetaan 9 tavua satunnaiseen muistipaikkaan, 64 bittisessä arkkitehtuurissa 5 tavua. Mikäli ajat ohjelmasi linux ympäristössä, voit koettaa ajaa sen esim. valgrindin kanssa, joka on ilmainen ja erittäin hyvä työkalu mm. muisitin ylikirjoitusten paikannukseen. Todennäköisesti saat varoituksia invalid writestä.

Voit myös koettaa kasvattaa testitekstisi pituutta, jolloin ylikirjoitettavan muistin koko kasvaa, ja windowsin kaltainen, ei niin tarkasti muistinkäyttöä valvova käyttöjärjestelmäkin havainnee virheen.

Anonyymi kirjoitti...

Hej,

Sulla on muistivuoto toisessa koodiesimerkissä tällä sivulla.
Yksi 'free(luku);' puuttuu.