Kihagyás

Linux IPC - II.

A gyakorlat anyaga

Ebben az anyagrészben először megnézzük, hogyan tudunk közös memóriaszegmenst létrehozni, amelyet minden létrehozott folyamat elér, ezt követően létrehozunk egy bináris szemafort, amellyel megvalósítjuk a kölcsönös kizárást és a szinkronizációt.

Közös memóriaszegmens létrehozása

Először az int shmget(key_t key, size_t size, int shmflg) függvényt használjuk egy új memóriaszegmens létrehozására, amely a szegmens azonosítójával fog visszatérni (de lehetne használni egy már létező szegmenshez való hozzáféréshez is).

  • A key paraméter egy egyedi kulcs, amely azonosítója a szegmenst: IPC_PRIVATE-tel inicializálva egy olyan új szegmens kerül létrehozásra, amely csak a hívó folyamatok - a szülő és az általa létrehozott gyerek folyamatok - között lesz érvényes, más külső folyamat nem fog tudni hozzáférni.

  • A size paraméter az aktuális memórialap méretének többszöröséhez lesz felkerekítve (1 esetén getconf PAGE_SIZE méretre).

  • Az shmflg a szegmens további konfigurációját állítja be, pl. IPC_CREAT esetén egy új szegmens kerül létrehozásra, ha az adott kulccsal nem lett szegmens létrehozva. IPC_EXCL esetén az előbbi esetben hiba dobódik. Ezenkívül még a jogosultság kerül beállításra, ami 0666 esetén írási és olvasási jogosultságot jelent (lásd chmod parancs).

Ezt követően az void *shmat(int shmid, const void *shmaddr, int shmflg) függvényt használjuk, amellyel egy osztott memóriaszegmenst tudunk a hívó folyamat címteréhez rendelni.

  • shmid lesz a korábban létrehozott memóriaszegmens azonosítója (azaz a shmget visszatérési értéke).

  • shmaddr paraméter egy memóriacímet vár, ahova a szegmenst szeretnénk becsatolni. NULL-lal inicializálva egy szabad memórialaphoz csatolja a szegmenst (a rendszer választ).

  • Tipikusan az shmflgjelző a 0 értékre lesz inicializálva, ezáltal a szegmens olvasható és írható. De kaphatja az SHM_RDONLY értéket is, ekkor ez a megosztott memória csak olvasható memóriaként lesz csatolva (tipikusan akkor, ha a hívó folyamatnak csak olvasási jogosultsága van) vagy lehet SHM_EXEC, ekkor a szegmens tartalma végrehajtható jogosultságot kap.

A szükséges header fájlok: sys/types.h, sys/shm.h és sys/ipc.h.

Ezen utasítások követően tehát az adott típusra cast-olt pointer a megosztott memóriaszegmensre fog mutatni a szülő és a gyerekfolyamatok esetében is.

A következő programban létrehozunk egy közös memóriaszegmenst szöveg tárolására (text változó), amelyet aztán a gyerekfolyamat módosít (string.h-ban elérhető függvény segítségével). A szülőfolyamat először bevárja a gyerekfolyamatot, majd kiírja a módosított szöveget bizonyítva, hogy a korábban létrehozott memóriaszegmens meg van osztva a szülő és a gyerekfolyamat(ok) között.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string.h>
#include <sys/wait.h>

int main() {
  int shmid = shmget(IPC_PRIVATE, 1, 0666 | IPC_CREAT);
  char * text = (char * ) shmat(shmid, NULL, 0);

  strcpy(text, "Initial");
  printf("%s\n", text);
  pid_t PID = fork();

  if (PID == 0) {
    strcpy(text, "ModifiedByChild");
  } else if (PID > 0) {
    wait(NULL);
    printf("%s\n", text);
  } else {
    exit(EXIT_FAILURE);
  }
}

Szemafor létrehozása

A szemafor definíciója egyszerű, lényegében olyan absztrakt adattípus, amellyel megvalósítható a kölcsönös kizárás és a szinkronizáció. Két része van, az egyik tárolja a szemafor értékét, a másik pedig egy várakozási sor. Amennyiben a szemafor értéke a 0-t és az 1-et veszi fel, úgy bináris szemafornak nevezzük, mi ilyen szemaforral fogunk foglalkozni. A szemaforhoz társított két művelet a wait(), illetve a signal(), ezeket tekintsük atominak. Ennek az az oka, hogy a wait és a signal művelet egy-egy kritikus szakasz a szemafor belső változóira nézve. Ezt a problémát egy tényleges szemafor implementációnál szoftveresen nem tudnánk megoldani, szükségünk van egy hardveres megoldásra, megszakíthatatlanul kiolvassa egy memóriarekesz tartalmát, majd beír oda egy másik értéket. Ezt test-and-set utasításnak nevezik:

A wait és a signal pszeudokódja a következő:

1
2
3
4
5
6
void wait() {
  if (value == 0)  // foglalt az erőforrás?
    queue.add();     // ha igen, akkor várakozzunk
  else
    --value;         // ha nem, akkor foglaljuk le
};

1
2
3
4
5
6
void signal() {
  if (NOT queue.isEmpty())  // várakozik más az erőforrásra?
    queue.release();        // ha igen, akkor adjuk oda neki
  else
    ++value;                // ha nem, akkor szabadítsuk fel
};
Kölcsönös kizárás megvalósításához a következő szükséges: a szemafor kezdőértékét 1-re állítjuk, majd a folyamatok kritikus szakaszát a wait()-tel kezdjük és a signal()-lal zárjuk. A programnak egy adott programrészét kritikus szakasznak tekintjük, ha abban programrészben bármely időpillanatban csak és kizárólag egy folyamat tartózkodhat. Szinkronizáció esetén a kezdőérték 0 lesz, az egyik folyamat adott pontján elhelyezzük az értesítésért felelő signal() műveletet, a másik folyamat várakozási pontjában pedig a wait() műveletet.

A folyamatinterakciókhoz tarozik még egy fogalom (amivel ebben az anyagrészben nem foglalkozunk), a holtpont. Ez egy nemkívánatos állapot, a szálak közötti körkörös függés esetén a program blokkolódik.

Tekintsük át és értelmezzük a korábbi pszeudokódok segítségével, hogy milyen esetei vannak a kölcsönös kizárásnak két folyamatra nézve:

kolcsonos-kizaras

Tekintsük át és értelmezzük a korábbi pszeudokódok segítségével, hogy milyen esetei vannak a szinkronizációnak két folyamatra nézve:

szinkronizacio

A szemafor létrehozására szolgáló int semget(key_t key, int nsems, int semflg) függvény létrehoz egy szemaforhalmazt.

  • A key és a semflg megegyzik a korábban látott shmget(..) függvény paramétereivel

  • A halmazban lévő szemaforok számát a nsems paraméter állítja be (1-re állítjuk, mivel 1 szeamforral szeretnénk dolgozni).

A int semctl(int semid, int semnum, int cmd, ...) a szemafor inicializálását és állapotának lekérését végzi.

  • semid: a szemaforhalmaz azonosítója.

  • semnum: a szemafor indexe a halmazban.

  • cmd: az elvégzendő művelet, pl. SETVAL esetén a szemafor kezdőértékét adjuk meg, GETVAL esetén az aktuális értékét kérjük le, de lehetőség lenne további szemaforjellemzőket is lekérdezni az IPC_STAT segítségével.

  • ...: egy opcionális union típusú paraméter, amely az előző (cmd) érték szerint lehet pl. egy érték (SETVAL-hoz) vagy egy eredmény-struktúra (IPC_STAT).

Az utolsó függvény, amit a szemafor működéséhez szükséges, a int semop(int semid, struct sembuf *sops, size_t nsops), aely a szemaforon végrehajtandó műveletet végzi.

  • sops: ez egy struktúra vagy tömb, amely tartalmazza a művelet. A struktúrának része asem_num, amely a szemafor indexét jelzi a halmazon belül, sem_flg, amely blokkoló futást eredményez és csak akkor folytatódik a futás, ha a szemaforművelet befejeződött, és végül a sem_op, amely negatív paraméter esetén csökkenti, pozitív paraméter esetén növeli a szemafor értékét.

  • nsops: a struktúrák száma az előző paraméterben.

A szükséges header fájl: sys/sem.h.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <sys/sem.h> 

int CSinit(int value) {

  int semid = semget(IPC_PRIVATE, 1, 0666 | IPC_CREAT);
  semctl(semid, 0, SETVAL, value); 

  return semid;
}

static void CSoper(int semid, int op) {
  struct sembuf sb;

  sb.sem_num = 0; 
  sb.sem_flg = 0; 
  sb.sem_op = op; 

  semop(semid, & sb, 1);
}

void CSwait(int semid) {
  CSoper(semid, -1);
}

void CSsignal(int semid) {
  CSoper(semid, 1);
}

Az elkövetkező két példában először a szülő- és gyerekfolyamat közötti szinkronizációra, majd ezt követően a szülő- és gyerekfolyamat közötti kölcsönös kizárásra látunk szemléltető példát.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <unistd.h>
#include "semaphore.h"

static int cnt = 0;

void work(const char * msg) {
  printf("%s %d\n", msg, cnt++);
  sleep(1);
}

int main() {
  int shmid = CSinit(0);

  if (fork()) {
    work("Parent");
    work("Parent");
    CSwait(shmid);
    work("Parent");
    work("Parent");
    work("Parent");
    work("Parent");
  } else {
    work("Child");
    work("Child");
    work("Child");
    work("Child");
    CSsignal(shmid);
    work("Child");
    work("Child");
  }

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <unistd.h>
#include "semaphore.h"

void work(const char * msg) {
  int i;

  for (i = 0; i < 10; ++i) {
    printf("%s %2d\n", msg, i);
    sleep(1);
  }
}

int main() {
  int shmid = CSinit(1);

  if (fork()) {
    CSwait(shmid);
    work("Parent");
    CSsignal(shmid);
  } else {
    CSwait(shmid);
    work("Child");
    CSsignal(shmid);
  }

}

Bináris és nem-bináris szemafor

A nem-bináris szemafor értéke 0 és N között lehet, ahol N a kritikus szakaszba szabadon belépő és onnan kilépő folyamatok száma. Ezáltal ez nem garantálja a kölcsönös kizárást, mivel egyszerre több folyamat is beléphet a kritikus szakaszba. Viszont van, hogy erre van szükség, pl. az étkező-filozófusok probléma esetén. Linux IPC esetén közvetlenül nem tudunk nem bináris szemafort létrehozni.

Memóriaszegmens és szemafor felszabadítása

Az ipcs utasítás szolgál arra, hogy kilistázzuk a már létrehozott megosztott memóriaszegmenseket és szemaforokat (a listában az shmget és az shmget függvények visszatérési értékeit kapjuk meg).

Ezeket szükség esetén törülhetjük:

1
2
shmctl(shmid,  IPC_RMID, 0); // memóriaszegmens esetén
semctl(shmid, 1, IPC_RMID); // szemafor esetén

Érdekesség

Fork Bomb néven híresült el az a szolgáltatásmegtakadással járó támadás (Denial of Service vagy DoS), amely során egy folyamat folyamatosan replikálja önmagát elérve ezzel, hogy a Linux alapú operációs rendszer kifogyjon a memóriából, illetve a CPU-t is folyamatosan terhelés alatt tartja. Amennyiben aktiválódott, úgy a rendszer teljes újraindítása szükséges, hiszen az összes létrehozott folyamatot egyidejűleg kellene megszüntetni.

Egy lehetséges kivédése a támadásnak, ha maximáljuk az adott felhasználó által létrehozható folyamatok számát.

1
2
3
4
5
6
7
#include <stdio.h>
#include <unistd.h>

int main() {
  while (1)
    fork();
}

Gyakorló feladat

Írj egy C programot ami Linux IPC-t illetve egy header fájlba kiszervezett szemafort használ. Hozz létre egy közös memóriaszegmenst egész számok tárolására és inicializáld egy pozitív számmal.

A szülő folyamat hozzon létre 10 gyerekfolyamatot (a gyerekfolyamatok szülője ugyanaz a folyamat legyen).

  • Ha a gyerekfolyamat azonosítója páros, akkor altasd a folyamatot 3 mp-ig, majd szorozd be szegmens értékét 3-mal.

  • Egyébként altasd a folyamatot 1 mp-ig és adj a szegmens értékéhez 7-et.

A szülő folyamat várja meg a gyerekfolyamatok terminálását, majd írja ki az eredményt. K ölcsönös kizárás segítségével biztosítsd, hogy a gyerekfolyamatok ne írják felül egymás műveleteit. Csak is kizárólag a szükséges kódrészlet legyen része a kritikus szekciónak.

Kapcsolódó linkek

Linux manual

test-and-set


Utolsó frissítés: 2024-03-14 11:30:05