Prosessin muistiavaruus

Linuxissa muistinhallinta on toteutettu käyttäen ns. sivuttavaa virtuaalimuistia. Periaatteena on se, että jokainen prosessi luulee toimivansa kokonaan omassa, yhtenäisessä muistialueessa, eli omassa muistiavaruudessa.

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:

Jaetut muistialueet

Jaettuja muistialueita voidaan käyttää kahden eri API:n kautta. Käytämme seuraavassa Posix-standardissa määritelytä tapaa. Samaan tapaan mutta hieman eri syntaksin mukaisesti toimii System V:n määrittelyn mukainen jaetun muistin API. Posix-API on syntaksiltaan ehkä aavistuksen verran selkeämpi, joten valitsemme sen.

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 muistialueeseen
Tä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 muistialueelle
mmap-funktiolla on 6 parametria:
  1. Osoite mihin muistialue halutaan mapata, yleensä 0, jolloin KJ valitsee sopivan osoitteen
  2. Muistialueen koko, esimerkissä definellä määritelty vakio
  3. Mitä oikeuksia prosessi saa muistialueelle esimerkissä PROT_READ | PROT_WRITE eli sekä luku että kirjoitusoikeus
  4. Jaetaanko muistialue usean prosessin kesken (MAP_SHARED tai MAP_PRIVATE)
  5. Tiedostokuvaaja mapattavaan muistialueeseen
  6. Mistä kohtaa jaettua muistialuetta mappaus aloitetaan (0 tarkoittaa muistialueen alusta)
Jos tarvetta, katso tarkemmin man-sivulta.

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;

Kilpailutilanne ja kriittinen alue

Ohjelmassa 9-3.c havaitsemme mielenkiintoisen ilmiön. Vanhempi ja lapsi käyttävät jaettua muistialuetta. Muistialueen ensimmäinen muistipaikka nollataan ensin:
  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 muistipaikkaan
Moniajojä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            0
Ongelman 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 muistipaikkaan
suoritus 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.

Semafoori

Semafoori on käyttöjärjestelmän tarjoama palvelu kriittisten alueiden suojaamiseksi. Linuxissa semaforeja voidaan käyttää kahden eri API:n kautta. Keskitymme seuraavassa Posix-semaforeihin. System V API määrittelee myös käyttötavan semaforeille, mutta Posix-semaforit ovat huomattavasti helpompikäyttöisiä ja selkeämpiä.

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:
  • sem_wait vähentää semaforin arvoa yhdellä jos semaforin arvo on positiivinen.
    Jos semaforin arvo on nolla, kutsuja blokkaa ja jää odottamaan niin kauaksi aikaa kunnes semaforin arvo on taas positiivinen.
  • sem_post kasvataa semaforin arvoa yhdellä.

    Jos semaforin arvo oli nolla, niin sem_post:in seurauksena mahdolliset semaforia odottavat prosessit pääsevät yrittämään sem_wait-operaation suorituksen loppuun saattamista. Odottavista prosesseista vain yksi saa sem_wait:in suoritetuksi.

  • 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.

    Prosessien synkronointi semaforin avulla

    Semaforia voi käyttää myös prosessien väliseen synkronointiin. Ohjelmassa 9-5.c otetaan käyttöön semafori, jonka alkuarvo on 0.
    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.