Kihagyás

Linux IPC - II.

Ebben az anyagrészben először megnézzük, hogyan tudunk olyan közös memóriaszegmenst létrehozni, amelyhez minden létrehozott folyamat hozzáfé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ítja 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 határozza meg a memóriaszegmens méretét, ez azonban a virtuális memória legkisebb egységének (getconf PAGE_SIZE) többszöröséhez lesz felkerekítve

  • Az shmflg paraméter a szegmens létrehozásának módját, hozzáférését szabályozza, pl. IPC_CREAT esetén, ha még nem létezik a megadott kulcshoz tartozó memória szegmens, akkor új szegmenst hoz létre. Ezenkívül még a jogosultság kerül itt beállításra, ami 0666 esetén írási és olvasási jogosultságot jelent minden felhasználói csoportnak (https://chmodcommand.com/).

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 függvény visszatérési értéke).

  • shmaddr paraméter egy memóriacímet vár, ahová a szegmenst szeretnénk csatlakoztatni. NULL-lal inicializálva a rendszer választ egy elérhető pozíciót a hívó folyamat címtartományában.

  • Tipikusan az shmflgjelző a 0 értékre lesz inicializálva, ezáltal a szegmens olvasható és írható. De megkaphatja a SHM_RDONLY értéket is, ekkor ez a szegmens 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.

Jogosultsági szempontok

Az shmget a szegmens létrehozásakor az általános hozzáférési jogokat állítja be a felhasználói csoportok számára, míg az shmat a folyamatok számára biztosítja az egyedi hozzáférést.

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

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

 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űnek mondható, lényegében egy 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, ebben az anyagrészben mi ilyen szemaforral fogunk foglalkozni. A szemaforhoz társított két művelet a wait(), illetve a signal(), ezeket tekintsük atominak (azaz a művelet megszakítás nélkül végrehajtódik és egy időben csak egy folyamat futtathatja valamelyiket). Ez azért fontos feltétel, mert 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, ezért szükségünk van egy hardveres megoldásra, ami megszakíthatatlanul kiolvassa egy memóriaterület tartalmát, majd beír oda egy másik értéket. Ezt test-and-set CPU 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 nevezzü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, ahol a vertikális tengely a folyamatok időbeli futását jelenti:

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, ahol a vertikális tengely a folyamatok időbeli futását jelenti:

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 paraméterek nagyon hasonlóan viselkednek, mint a korábban látott shmget(..) függvény hasonló elnevezésű paraméterei (egyedi kulcs, hozzáférési jogosultságok)

  • A halmazban lévő szemaforok számát a nsems paraméter állítja be (mi 1-re állítjuk, mivel egy szemaforral 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

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

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), amely a szemaforon végrehajtandó műveletet végzi.

  • semid: a szemaforhalmaz azonosítója.

  • 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ő, sops paraméterben (ha mondjuk a szemaforhalmaz több szemafort tartalmaz, akkor mindegyik szemaforhoz külön sops struktúra tartozhat)

semaphore.h

A gyakorlat során a szemafor használatát leegyszerűsítjük annak érdekében, hogy az OS szintű utasítások helyett a párhuzamos végrehajtásra illetve a folyamatinterakciókra koncentráljunk. Így a szemafor műveleteket az általunk definiált header fájlból kapjuk meg.

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
#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
#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. Az erőforrások (memóriaszegmens, szemafor) nem kerülnek azonnal törlésre, ha más folyamatok még használják őket.

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ó feladatok

(1) Készíts egy programot Linux IPC segítségével, amely egy 100 elemű, véletlenszerű számokkal feltöltött tömb összegzését végzi el négy gyerekfolyamat segítségével.

  • A tömböt egy közös memóriaszegmensen keresztül érjék el a folyamatok.

  • Minden gyerekfolyamat a tömb egyenlő részét kapja meg, és kiszámítja a kapott intervallumban található számok összegét.

  • Az összegzést egy másik közös memóriaszegmens segítségével történjen, itt kölcsönös kizárás segítségével biztosítsd az atomi hozzáférést.

  • A szülő folyamat feladata, hogy elindítsa a gyerekfolyamatokat, majd megvárja azok befejeződését és végül kiírja az összegzett eredményt.

Ügyelj a program struktúrálásra: a szülő és gyerekfolyamatok feladatait szervezd ki függvényekbe, a main metódus fő feladata a folyamatok létrehozása legyen!

(2) Hasonlítsuk össze az előző feladatban a szekvenciális és a párhuzamos végrehajtás futási idejét. Ehhez a CLOCK_MONOTONIC időmérőt használjuk, amely az általános időméréshez a legalkalmasabb, mivel a tényleges eltelt időt méri (figyelmen kívül hagyva a CPU használatot), és így pontosan tükrözi a program futásának valós időbeli lefolyását.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <stdio.h>
#include <time.h>

int main() {
    struct timespec start_time, end_time;

    clock_gettime(CLOCK_MONOTONIC, &start_time);
      // utasítások    
    clock_gettime(CLOCK_MONOTONIC, &end_time);

    long elapsed_time = (end_time.tv_sec - start_time.tv_sec) * 1e9 + 
                        (end_time.tv_nsec - start_time.tv_nsec);

    printf("Execution time = %ld ns\n", elapsed_time);
}

Kapcsolódó linkek

Linux manual

test-and-set


Utolsó frissítés: 2025-02-23 11:46:35