Käyttöjärjestelmä sijoittelee ohjelmien muistiavaruuden osat eli muistisivut eri puolille koneen fyysistä muistia. Ne osat muistialuetta eli muistisivut, jotka ei ole aktiivisessa käytössä saatetaan sijoittaa tilapäisesti kiintolevyn swap-alueelle. Kun swap-alueella oleville muistialueille viitataan, syntyy sivunpuutoskeskeytys ja KJ noutaa tarvittavat muistisivut swap-alueelta keskusmuistiin. Kaikki tämä tapahtuu koneen käyttäjän, prosessin ja sovellusohjelmoijan kannalta huomaamattomasti. Muistisivujen swappaamisen huomaa siitä, että kone hidastuu.
Jos usea prosessi suorittaa samaa koodia (kokonaista sovellusta tai kirjastofunktiota), jakavat prosessit saman keskusmuistissa olevan koodin. Tämä säästää huomattavasti fyysistä muistia sillä lähes jokainen käynnissä oleva prosessi käyttää esim. C-standardikirjastoa, jonka täytyy olla siis vaan kertaalleen ladattuna muistiin.
Jokaisen prosessin datan tallettamiseen käyttämät muistialueet ovat normaalisti toisistaan täysin erilliset. Myös äiti- ja lapsiprosessin kohdalla tilanne on sama. Vaikka lapsi on klooni äidistä, ja lapsella on kaikki samat muuttujat mitä äidillä on määritelty (ennen forkkia), on molemmilla muuttujista oma kopionsa.
On kuitenkin mahdollista ottaa käyttöön datan talletukseen tarkoitettuja muistialueita, joita voidaan liittää useamman prosessin muistiavaruuteen. Tällöin sama alue fyysistä muistia on liitetty eli mapattu useamman kuin yhden prosessin muistiavaruuteen.
Seuraava kuva havainnollistaa Linuxin muistinhallinnan periaatetta:
Jaettu muistialue luodaan komennolla shm_open. man-sivulta:
NAME shm_open - Create/open POSIX shared memory objects SYNOPSIS #include < sys/types.h> #include < sys/mman.h> int shm_open(const char *name, int oflag, mode_t mode); DESCRIPTION shm_open creates and opens a new, or opens an existing, POSIX shared memory object. A POSIX shared memory object is in effect a handle which can be used by unrelated processes to mmap(2) the same region of shared memory. The operation of shm_open is analogous to that of open(2). name speci- fies the shared memory object to be created or opened. For portable use, name should have an initial slash (/) and contain no embedded slashes. oflag is a bit mask created by ORing together exactly one of O_RDONLY or O_RWDR and any of the other flags listed here: O_RDONLY Open the object for read access. O_RDWR Open the object for read-write access. O_CREAT Create the shared memory object if it does not exist. A new shared memory object initially has zero length - the size of the object can be set using ftruncate(2). (The newly-allocated bytes of a shared memory object are automat- O_EXCL If O_CREAT was also specified, and a share memory object with the given name already exists, return an error. O_TRUNC If the shared memory object already exists, truncate it to zero bytes. On successful completion shm_open returns a new file descriptor refer- ring to the shared memory object. The file descriptor is normally used in subsequent calls to ftrun- cate(2) (for a newly-created object) and mmap(2). After a call to mmap(2) the file descriptor may be closed without affecting the memory mapping. RETURN VALUE On success, shm_open returns a non-negative file descriptor. On fail- ure, shm_open returns -1.shm_open siis toimii hyvin samaan tapaan kuin tiedoston avaamiseen ja luomiseen käytettävä open. Erityisen mielenkiintoinen seikka on se, että muistialueilla on samankaltainen polkunimi kuin normaalieilla tiedostoilla. Nimen täytyy aina alkaa /-merkillä. Tarkastellaan ohjelmaa 9-1.c:
#define MSIZE 4096 int main(int argc, char *argv[]){ int id, fd; char *m_nimi = "/omamuisti"; // jaetun muistialueen nimi // luodaan jaettu muistialue fd = shm_open( m_nimi, O_CREAT | O_EXCL | O_RDWR, 0600 ); if ( fd == -1 ){ perror("ongelma jaetun muistialueen luomisessa"); return -1; } // fd on nyt tiedostokuvaaja, joka viittaa jaettuun muistialueeseenTässä luodaan /omamuisti-niminen jaettu muistialue, jota voidaan sekä kirjoittaa että lukea (O_RDWR). Oikeudet 0600, eli luku ja kirjoitusoikeus omistajalla, muilla ei mitään. O_EXCL aiheuttaa sen, että jos muistialue on jo olemassa, operaatio epäonnistuu. Muistialueeseen viitataan tästä lähtien int-tyyppisen muuttujan fd avulla. Teknisessä mielessä fd on tiedostokuvaaja, eli muistialueeseen viitataan teknisessä mielessä samoin kuin normaaliin tiedostoon.
Kun muistialue on luotu, näkyy se hakemistossa /dev/shm:
[luuma@telinux1 luuma]$ ls -l /dev/shm/ total 0 -rw------- 1 luuma luuma 0 loka 28 13:40 omamuisti [luuma@telinux1 luuma]$Kuten listauksesta näkyy, muistialueen koko on 0. Komennolla ftruncate muistialueelle määritellään haluttu koko:
// muistialueen koko on aluksi 0, asetetaan muistialueelle haluttu koko // ftruncate-funktiolla, huom: MSIZE definellä määritelty vakio ftruncate( fd, MSIZE );Jos tämän jälkeen katsotaan jaettujen muistialueiden listausta, näyttää tilane seuraavalta:
[luuma@telinux1 luuma]$ ls -l /dev/shm/ total 0 -rw------- 1 luuma luuma 4096 loka 28 13:42 omamuisti [luuma@telinux1 luuma]$Muistialueen kooksi kannattaa aina valita joku sivukoon 4096 moninkerta. Vaikka tarvetta olisi ainoastaan yhdelle tavulle, on pienin jaetun muistialueen varausyksikkö kuitenkin 4096. Huom: tämä on arkkitehtuuririippuvaista, jollain muulla prosessorityypillä (kuin pc-prosessoreilla) sivun koko saattaa olla jotain muuta. Sivukoon voi selvittää käyttämällä sysinfo-komentoa.
Muisti täytyy vielä liittää eli mapata prosessin muistiavaruuteen komennolla mmap:
// mapataan jaettu muistialue prosessin muistiavaruuteen void *p; p = mmap( 0, MSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ); if ( p == MAP_FAILED ){ perror("ongelma jaetun muistialueen luomisessa"); return -1; } //p osoittaa jaetulle muistialueellemmap-funktiolla on 6 parametria:
Operaatio palauttaa void-tyyppisen osoitteen mapatun muistialueen alkuun. Eli esimerkissä muistialueeseen pääsee käsiksi osoitinmuuttujan p kautta.
Tämän jälkeen esimerkissämme prosessi forkkaa lapsen. Huomionarvoista on se, että nyt sekä äiti että lapsi näkevät saman muistialueen johon molemmat voivat viitata osoittimella p.
Vanhempi kirjoittaa muistialueelle merkkijonon jonka lapsi tulostaa. Lopussa molemmat poistavat jaetun muistialueen käytöstään käyttäen komentoja munmap ja shm_unlink.
id = fork(); if ( id==0 ){ // HUOM: vaikka jaettu muistialue mapattiin ennen fork:ia, se säilyy lapselle // odotetaan, että vanhempi kirjoittaa dataa jaetulle muistialueelle sleep(1); printf("jaetussa muistissa: %s\n", (char *)p); //poistetaan jaettu muistialue lapsen käytöstä munmap( p, MSIZE ); shm_unlink( m_nimi ); exit(0); } strcpy( (char *)p, mj ); // odotetaan lasta wait( NULL ); // poistetaan jaettu muistialue vanhemman käytöstä munmap( p, MSIZE ); shm_unlink( m_nimi ); // kukaan ei käytä jaettua muistialuetta, joten se poistuu return 0; }Jos muistialuetta ei poisteta, jää muistialue edelleen koneeseen ja siihen on mahdollisuus viitata jatkossakin käyttäen muistialueen nimeä. On siis mahdollista, että jaettua muistialuetta käyttävät prosessit ovat käynnissä eri aikaan. Tällöin muistialue toimii hieman kuten tiedosto.
HUOM: käännettäessä jaettuja muistialueita sisältävää koodia, on kääntäjälle annettava optio -lrt tällöin kääntäjä osaa linkittää mukaan tarvittavat kirjastot.
Esimerkissä 9-2.c äiti ja lapsi jakavat muistialueen. Äiti tallettaa muistialueelle joukon lukuja. Lapsi tulostaa luvut ja laskee niiden summan. Tulos välitetään jaetun muistialueen kautta vanhemmalle joka printtaa sen ruudulle.
Koska muistia käytetään int-lukujen tallettamiseen, castataan mmap:in palauttama void-osoitin heti int-osoittimeksi:
// mapataan jaettu muistialue prosessin muistiavaruuteen // alueelle talletetaan int:tejä, castataan osoitin heti oikeaan tyyppiin int *alue; alue = (int *) mmap( 0, MSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ); if ( alue == MAP_FAILED ){ perror("ongelma jaetun muistialueen luomisessa"); return -1; }Jaettua muistialuetta käytetään siten, että vanhempi tallettaa alkuun välitettavien lukujen lukumäärän ja heti tämän perään ne luvut jotka lapsen on tarkoitus tulostaa ja summata. Lapsi tallettaa summan kaikkien lukujen perään. Muisti näyttää seuraavalta:
--------- alue --> | lkm | // muuttujan lkm arvo 4 --------- taulukko --> | 3 | taulukko[0] --------- | 4 | taulukko[1] --------- | 2 | taulukko[2] --------- | 7 | taulukko[3] --------- | | <-- alue+lkm+1 // lapsi tallettaa summan tähän ---------Selvyyden vuoksi vanhempi ja lapsi käyttävät osoitinmuuttujaa taulukko viittaamaan siihen kohtaan, mistä alkaa välitettävät luvut sisältävä muistialue.
Vanhemman koodi:
// sovitaan että ensimmäisenä talletettavien lukujen määrä *alue = lkm; // tämän jälkeen muistialueelle talletetaan lukutaulukko, // otetaan uusi osoitin kuvaamaan taulukkoa int *taulukko = alue+1; for ( i=0; i < lkm; i++){ taulukko[i] = rand()%10; // sama kuin *(alue+1+i) = ... } // odotetaan lasta wait( NULL ); // muistialueella taulukon jälkeen on lukujen summa printf("lukujen summa %d\n", *(alue+1+lkm) );Lapsen koodi:
if ( id==0 ){ sleep(1); // odotetaan että vanhempi laittaa luvut muistiin // HUOM: sleepin käyttö toisen prosessin odottamiseen on erittäin // huono tapa, jota ei tule käyttää kunnollisessa koodissa // parempia tapoja esim. signaalit ja pian opittavat semaforit int lkm = *alue; // ensimmäisenä lukujen lukumäärä int *taulukko = alue+1; int summa = 0; for ( i=0; i < lkm; i++ ){ summa = summa + taulukko[i]; printf(" %d\n", taulukko[i]); } // talletetaan lukujen summa jaetulle muistialueelle lukujen jälkeen *(alue+lkm+1) = summa;
luku = (int *) mmap( 0, MSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ); if ( luku == MAP_FAILED ){ perror("ongelma jaetun muistialueen luomisessa"); return -1; } // nollataan jaetun muistialueen alussa oleva muistipaikka *luku = 0; printf("jaetun muuttujan arvo alussa %d\n", *luku );Tämän jälkeen lapsi kasvattaa yhdellä muistipaikan arvoa LIMIT (vakio jonka arvo 1000000) kertaa ja vanhempi vähentää muistipaikan arvoa yhdellä LIMIT kertaa. Lopuksi tulostetaan jaetun muistipaikan arvo. Vanhemman koodi:
// vähennetään jaetun muistialueen alussa olevaa muistipaikkaa int i; for ( i=0; i < LIMIT; i++ ) *luku = *luku-1; // odotetaan lasta wait( NULL ); printf("jaetun muuttujan arvo lopussa %d\n", *luku );Kaiken järjen mukaan lopputulosen pitäisi olla nolla. Miksi näin ei kuitenkaan ole?
Syy on seuraava. Komento *luku = *luku-1; näyttää C-kielen tasolla yhdeltä, yhdessä askeleessa eli jakamattomasti suoritettavalta komennolta. Konekielen tasolla näin ei kuitenkaan ole. Komennnon toteuttaa useampi konekielinen käsky. Oletetaan, että jaettu muisti alkaa osoitteesta 0x10000. Konekielen tasolla muuttujan vähennys menee suunnilleen seuraavasti:
MOV EAX 0x10000 // siirrä muistipaikan arvo rekisteriin EAX DEC EAX // vähennä rekisterin arvoa yhdellä MOV 0x10000 EAX // siirrä rekisterin EAX arvo muistipaikkaanMoniajojärjestelmässä suoritettavan prosessin vaihto voi tapahtua millä hetkellä tahansa. Mitä tapahtuu, jos prosessi vaihtuu kesken vähennys- tai lisäyskomennon suorittamisen?
Seuraavassa kaaviossa käsky käskyltä suoritus, jossa aluksi jaetussa muistissa arvo 0 ja sekä vanhempi että lapsi suorittavat operaationsa kertaalleen. Lopputuloksen pitäisi siis olla 0. Vanhemman suoritus alkaa ensin.
koodi koodi rekisteri EAX muisti vanhempi: lapsi: vanhempi lapsi 0x10000 ==================================================================================================== - - 0 MOV EAX 0x10000 0 - 0 < KJ vaihtaa suoritukseen lapsiprosessin > MOV EAX 0x10000 0 0 0 INC EAX 0 1 0 MOV 0x10000 EAX 0 1 1 < KJ vaihtaa suoritukseen vanhemman > DEC EAX -1 1 1 MOV 0x10000 0x10000 -1 1 -1
Vanhempi on alussa suorituksessa, mutta tehtyään ensimmäisen kolmesta muistin arvoa vähentävästä komennostaan, siirtyy lapsi suoritukseen. Vanhempi siis ehtii lukea muistipaikan arvon rekisteriin EAX. Lapsi suorittaa kasvatusoperaation kokonaisuudessaan ja sen jälkeen vanhempi palaa suoritusvuoroon. Vanhemmalla on nyt rekisterissä EAX muistipaikan vanha arvo ja kun vanhempi suorittaa vähennysoperaation, tulee muistin arvoksi -1.
Jos KJ ei olisi keskeyttänyt vanhemman suoritusta juuri ikävässä kohdassa, kaikki olisi mennyt hyvin:
koodi koodi rekisteri EAX muisti vanhempi: lapsi: vanhempi lapsi 0x10000 ==================================================================================================== - - 0 MOV EAX 0x10000 0 - 0 DEC EAX -1 - 0 MOV 0x10000 0x10000 -1 - -1 < KJ vaihtaa suoritukseen lapsiprosessin > MOV EAX 0x10000 -1 -1 -1 INC EAX -1 0 -1 MOV 0x10000 EAX -1 0 0Ongelman voi siis ajatella johtuvan huonosta tuurista ajoituksen suhteen. Tilanteesta käytetään nimitystä kilpailutilanne (engl. race condition), joka siis tarkoittaa sitä, että ohjelman oikea toimivuus riippuu ajoituksesta.
Moniajokäyttöjärjestelmässä ei tule koskaan tehdä mitään oletuksia sen suhteen, miten skeduleri antaa prosesseille suoritusaikaa. Oikeastaan paras on varautua aina pahimpaan.
Edellä ongelma siis on siinä, että käskyjonon:
MOV EAX 0x10000 // siirrä muistipaikan arvo rekisteriin EAX DEC EAX // vähennä rekisterin arvoa yhdellä MOV 0x10000 EAX // siirrä rekisterin EAX arvo muistipaikkaansuoritus keskeytyy. Jos nämä kolme käskyä suoritettaisiin aina "yhtenä askeleena", siten että kukaan muu ei pääse väliin sotkemaan, ei ongelmaa esiintyisi. Ongelmia tulee kun molemmat prosessit tekevät limittäin muistipaikkaa 0x10000 käsitteleviä komentoja. Huomattavaa on, että moniprosessori- tai monytdinkoneella ongelmia voi tulla myös siinä tapauksessa että lapsi ja vanhempi ovat aidosti yhtä aikaa suorittamassa ja näin sotkeutuvat keskenään.
Sellaista osaa koodista, jonka suoritukseen kukaan muu ei saisi päästä väliin, sanotaan kriittiseksi alueeksi (engl. critical section).
On olemassa useita erilaisia menetelmiä kriittisen alueen suojaamiseksi. Esim. Javassa säikeistetyssä ohjelmassa voidaan määritellä että jokin metodi on synchronized. Tällöin metodin koodia päästään suorittamaan vain yhdestä säikeestä kerrallaan.
Tarkastellaan seuraavassa ohjelmaa 9-4.c, joka on 9-3.c:n oikein toimiva versio, jossa jaetun muuttujan käsittely on suojattu semaforin avulla.
Semafori on muuttuja, jonka tyyppi on sem_t. Käytännössä ohjelmassa tarvitaan osoitin semaforiin. Samaan tapaan kuin jaetuilla muistialueilla, myös semaforeilla on nimi, joka on /-merkillä alkava merkkijono.
Seuraavassa luodaan "/oma_semafori" niminen semafori, jolla on alkuarvo 1.
int main(int argc, char *argv[]){ char *s_nimi = "/oma_semafoori"; // semaforin nimi // luodaan nimetty semafori, jolla alkuarvo 1 sem_t *mysem; mysem = sem_open( s_nimi, O_CREAT | O_EXCL, 0600, 1 ); if ( mysem==SEM_FAILED ){ perror("semaforin kanssa ongelmia"); exit(-1); }Semaforin luominen siis tapahtuu sem_open-funktiolla. Parametreina nimi, avaustapa, oikeudet ja semaforin alkuarvo. Jos avataan jo olemassa oleva semafori, on toisen parametrin arvona 0 ja kahta viimeistä parametria ei tarvita.
Huom: samaan tapaan kuin jaetut muistialueet, myös semaforit näkyvät hakemistossa /dev/shm/.
Komennon man-sivua ei jostain syystä ole cs.stafia.fi-koneella.man-sivu löytyy esim. täältä. Ote man-sivulta:
SEM_OPEN(3) Linux Programmer's Manual SEM_OPEN(3) NAME sem_open - initialise and open a named semaphore SYNOPSIS #include < semaphore.h> sem_t *sem_open(const char *name, int oflag); sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); DESCRIPTION sem_open() creates a new POSIX semaphore or opens an existing semaphore. The semaphore is identified by name. For details of the construction of name, see sem_overview(7). The oflag argument specifies flags that control the operation of the call. If O_CREAT is specified in oflag, then the semaphore is created if it does not already exist. If both O_CREAT and O_EXCL are specified in oflag, then an error is returned if a semaphore with the given name already exists. If O_CREAT is specified in oflag, then two additional arguments must be supplied. The mode argument specifies the permissions to be placed on the new semaphore, as for open(2). The value argument specifies the initial value for the new semaphore. RETURN VALUE On success, sem_open() returns the address of the new semaphore; this address is used when calling other semaphore-related functions. On error, sem_open() returns SEM_FAILED, with errno set to indicate the error.Semaforin toimintaa selitetään man sivulla sem_overview. Tätäkään ei cs.stadia.fi:tä löydy, linkki man-sivulle tässä. Ote man-sivulta:
SEM_OVERVIEW(7) Linux Programmerâs Manual SEM_OVERVIEW(7) NAME sem_overview - Overview of POSIX semaphores DESCRIPTION POSIX semaphores allow processes and threads to synchronise their actions. A semaphore is an integer whose value is never allowed to fall below zero. Two operations can be performed on semaphores: increment the semaphore value by one (sem_post(3)); and decrement the semaphore value by one (sem_wait(3)). If the value of a semaphore is currently zero, then a sem_wait(3) operation will block until the value becomes greater than zero.Semafori siis on muuttuja, jolla on int-arvo. Semaforin tärkeimmät operaatiot ovat: Kriittisen alueen suojaus toteutetaan siten, että käytetään semaforia, jolla alkuarvo 1. Ennen kriittiselle alueelle menoa kutsutaan sem_wait (semaforin arvoksi tulee 0) ja poistuttaessa sem_post (semaforin arvoksi taas 1 tai odottaja herätetään). Jos toinen prosessi yrittää päästä kriittiselle alueelle samalla kun kriittisellä alueella on jo yksi prosessi, on semaforin arvona 0 ja kriittiselle alueelle yrittävä jää odottamaan niin kauaksi aikaa kun kriittiseltä alueelta poistuva herättää odottajan suorittamalla sem_post-operaation.
Kriittisen alueen suojaus tapahtuu siis seuraavasti:
sem_wait(mysem); // kriittisen alueen ohjelmakoodi sem_post(mysem);
Lapsen ja vanhemman koodi esimerkistämme 9-4.c:
if ( id==0 ){ // kasvatetaan jaetun muistialueen alussa olevaa muistipaikkaa int i; for ( i=0; i < LIMIT; i++ ) { sem_wait(mysem); *luku = *luku+1; sem_post(mysem); } // poistetaan muistialue lapselta munmap( luku, MSIZE ); shm_unlink( m_nimi ); // suljetaan semafoori lapselta sem_close(mysem); exit(0); } // vähennetään jaetun muistialueen alussa olevaa muistipaikkaa int i; for ( i=0; i < LIMIT; i++ ) { sem_wait(mysem); *luku = *luku-1; sem_post(mysem); } // odotetaan lasta wait( NULL ); printf("jaetun muuttujan arvo lopussa %d\n", *luku ); // poistetaan jaettu muistialue vanhemman käytöstä munmap( luku, MSIZE ); shm_unlink( m_nimi ); // suljetaan ja tuhotaan semafori sem_close(mysem); sem_unlink(s_nimi); return 0; }Suorituksen lopussa semafori suljetaan sem_close:lla ja vanhempi poistaa semaforin sem_unlink-operaatiolla.
int main(int argc, char *argv[]){ sem_t *mysem; int id; char *sem_mj = "/omasemafori"; mysem = sem_open(sem_mj, O_CREAT | O_EXCL, 0600, 0 );Tämän jälkeen forkataan lapsi, joka suorittaa heti semaforille sem_wait-operaation. Koska semaforin arvo on 0, blokkaa lapsi operaation suoritettuaan.
id = fork(); if ( id==0 ){ printf("lapsi odottaa \n"); sem_wait(mysem); printf("lapsi jatkaa \n"); // suljetaan semafoori sem_close(mysem); exit(0); }Vanhempi odottaa 2 sekuntia ja tekee operaation sem_post. Tämä aiheuttaa sen, että lapsi herää ja pääsee jatkamaan suoritustaan.
Semaforit ovat ehkä paras tapa toteuttaa tämän tyylinen prosessien välisen etenemisen synkronointi. Saman asian voi toki hoitaa myös signaaleilla. Missään tapauksessa synkronointia ei saa tehdä sleep:ien avulla niin kuin muutamassa aiemmassa esimerkissä tehtiin.