Kihagyás

Linux IPC - I.

A gyakorlat anyaga

A legegyszerűbben úgy kerülhetünk párhuzamos programozási környezetbe, ha veszünk egy hagyományos szekvenciális programozási nyelvet és kiegészítjük párhuzamos konstrukciókkal, pl.: a C nyelv + Linux rendszerhívások.

A párhuzamos folyamatok közötti kommunikáció (IPC - Inter Process Communication) kifejezetten azokra a mechanizmusokra utal, amelyeket az operációs rendszer biztosít a folyamatok számára a közös adatok kezeléséhez. Ezzel a módszerrel akár közös memóriás, akár osztott memóriás modellben is programozhatunk.

A fork() hívás

A fork() hívás szolgál arra, hogy egy új folyamatot hozzunk létre a hívó folyamat duplikálásával. Ez nem a teljes hivó folyamatra vonatkozik a kezdetektől, hanem ami a fork() hívás helyétől kezdve fogja ugyanazt a programot futtatni. Erről a legegyszerűbben megbizonyosodhatunk, ha az alább található fork() hívásunk elé szintén elhelyezünk egy tetszőleges kiíratást (ez csak egyszer fog kiírásra kerülni).

Egy folyamatot bizonyos ideig szüneteltetni tudunk a sleep(..) függvény segítségével, amely paraméterben a szüneteltés (altatás) hosszát várja másodpercben. Egészítsük ki a példánkat elegendően hosszú altatással a fork() hívás előtt és után is, majd futtasuk a programot. Nyissunk egy terminált, és a ps -ao pid,ppid,psr,comm utasítás segítségével bizonyosodjunk meg róla, hogy a folyamat valóban megduplázódott (akár az utasítás többszöri kiadásával a terminálban).

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

int main() {
  fork();
  printf("Hello world!\n");
}

Egészítsük ki a programunkat több fork() hívással. Az egymást követő n db fork() hívás 2^n db folyamatot eredményez. A fork és a sleep függvényeket használatához szükség lesz még az <unistd.h> header fájlra, ezen kívül a standard I/O műveletekre van még szükségünk (<stdio.h>).

Folyamatok azonosítói

Előző példában megnéztük hogy tudunk új processzt létrehozni, azonban ezeket a processzeket szeretnék azonosítani is valamilyen módon, illetve a folyamatok között alá- és fölérendeltségi viszonyt is meg szeretnék állapítani. Egy fork() hívás esetén tehát megkülönbözhetünk szülő és gyerek folyamatot: a függvényhívás visszatérési értéke 0 lesz gyerek folyamat esetén, nagyobb, mint 0 lesz szülő processz esetén, egyéb esetben pedig hiba történt a folyamat létrehozásakor. A pid_t típus egy signed int típust jelent, ami megegyezik a GNU C library esetén az int típussal.

Mintaprogramunk tartalmazza még a gyerek folyamatok esetén alkalmazandó _exit(EXIT_SUCCESS) és a szülő folyamat esetén alkalmazandó exit(EXIT_SUCCESS) függvényhívásokat (<stdlib.h>), amellyel megelőzhetjük, a standard I/O csatornák többszöri lezáródását, mielőtt a programunk terminálna.

Fontos megjegyezni továbbá, hogy megkülönböztetjük a processzek azonosítóit a C programon belül és a folyamatok Linux rendszerbeli azonosítóit. Hogy lekérjük az adott folyamat azonosítóját, használjuk a getpid() függvényt, a folyamat szülőjéhez tartozó azonosító lekéréséhez pedig a getppid() függvényt. További érdekesség, hogy a C programon belül a szülő szál a gyerek folyamat Linux rendszerbeli azonosítóját kapja meg.

 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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void child(int pid) {
  printf("Child\n");
}

void parent(int pid) {
  printf("Parent\n");
}

int main() {
  pid_t PID = fork();

  if (PID == 0) {
    child(PID);
  } else if (PID > 0) {
    parent(PID);
  } else {
    exit(EXIT_FAILURE);
  }

  if (PID == 0) {
    _exit(EXIT_SUCCESS);
  } else {
    exit(EXIT_SUCCESS);
  }
}

Egészítsük ki a programot még egy fork() hívással és hasonlítsuk össze a létrejött 4 folyamat C programbeli, illetve a Linux rendszerbeli azonosítóit (getpid(),getppid()).

Virtuális memória

Ahhoz, hogy közös memóriát tudjunk létrehozni, további függvényhívásokra lesz majd szükség. Most nézzük meg, hogyan reagál az alábbi program, ha egy közösnek tűnő változót mindkét folyamat módosítja:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>
#include <unistd.h>

int main() {
  int x = 1;

  if (fork() == 0)
    printf("Child has x = %d\n", --x);
  else
    printf("Parent has x = %d\n", ++x);
}

Ahogy látjuk, a két folyamat egymástól függetlenül módosítja a változó értékétg. Amikor a fork() hívás megtörténik, akkor a szülőhöz tartozó memórialapról másolat készül, amit a gyerek folyamat örököl. Az is előfordulhat, hogy a függvényhíváskor nem történik másolat, hanem a szülő és a gyerek folyamat megosztja egymás között a memórialapot és csupán akkor készül másolat, ha valamelyik folyamat módosítaná a memórialapot (lásd Copy-on-write).

Másfelelől ha kiegészítjük a programunkat a változó memóriacímének kiíratásával, láthatjuk, hogy a két memóriacím megegyezik. Ez azonban nem jelenti azt, hogy a szülő és a gyerek folyamathoz tartozó változó ugyanazon a fizikai pozícióban van eltárolva, hanem a modern számítógépek virtuális memóriacímzése végett egyezik meg a memóriacím (azaz minden folyamat memóriája ugyanazon a virtuális címen kezdődik). Ezt szintén leellenőrizhetjük, ha az x változó címét kiiratjuk a programban.

Folyamatok ábrázolása

A következő példában gondoljuk végig hányszor kerül kiíratásra a "Hello world!" üzenet. Továbbra is igaz, hogy n fork() hívás esetén 2^n folyamat jön létre, azonban gondoljuk végig a függvényhívások és a kiíratások sorrendjét (összehasonlítva az első kikommentezett blokkal).

 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<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main() {
  int i;

  for (i = 0; i < 4; i++) {
    fork();
    printf("Hello world! \n");
  }

  /*
  fork();
  fork();
  fork();
  fork();
  printf("Hello world! \n");
  */

  /*
  if(fork() || fork()){
    fork();
  }
  printf("1 ");
  */
}

Az egymást követő fork() hívások könnyen ábrázolhatóak egy gráfként, ahol a csomópontokban az egyes folyamatok szerepelnek, például a fenti programban található második kikommentezett blokk ábrázolását és kimenetét mutatja be a következő ábra (draw.io forrás).

processes

Folyamatok bevárása és számuk korlátozása

Bizonyos esetekben szükség van arra, hogy az egyes folyamatok bevárják egymást, például ha egy adott problémát több részfeladatra pontottunk és a részfeladatokat feldolgozó (al)folyamatok különböző ideig számolnak, majd a részeredményeket összegezni szeretnék a fő folyamatban. Erre szolgál nekünk a wait(NULL) függvényünk (sys/wait.h), amellyel képes a programunk egy folyamatot bevárni (nem tudjuk megmondani, melyiket). Lehetőség van egy konkrét folyamatot is bevárni a waitpid(..) függvénnyel (lásd Kapcsolódó linkek) További megkötés, hogy csak a szülő folyamat képes bevárni a gyerek folyamatokat a wait(..) segítségével. A szülő folyamat addig lesz blokkolva, amíg a gyermekfolyamat nem küld vissza egy kilépési állapotot az operációs rendszernek, a vezérlés aztán visszakerül a szülő folyamathoz.

 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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

void child() {
  sleep(3);
  printf("Child\n");
}

void parent() {
  wait(NULL);
  printf("Parent\n");
}

int main() {
  pid_t PID = fork();

  if (PID == 0) {
    child();
  } else if (PID > 0) {
    parent();
  } else {
    exit(EXIT_FAILURE);
  }

  if (PID == 0) {
    _exit(EXIT_SUCCESS);
  } else {
    exit(EXIT_SUCCESS);
  }
}

Bizonyos esetben szükség van arra, hogy egy szülő folyamat bizonyos számú gyerek folyamatot hozzon létre, azaz szeretnék megakadályozni, hogy a gyerek folyamatok további folyamatok őseként viselkedjen. Ezt a legkönyebben például úgy érthetjük el, hogy a gyerek folyamat esetén a ciklusból a break utasítás segítségével kilépünk.

 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
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

void child() {
  printf("Child\n");
}

void parent() {
  printf("Parent\n");
}

int main() {
  pid_t PID;
  int i;

  for (i = 0; i < 4; i++) {
    PID = fork();

    if (PID == 0) {
      break;
    } else if (PID < 0) {
      exit(EXIT_FAILURE);
    }
  }

  if (PID == 0) {
    child();
    _exit(EXIT_SUCCESS);
  } else {
    parent();
    exit(EXIT_SUCCESS);
  }
}

Végezetül az előző példát egészítsük ki azzal, hogy a gyerekfolyamatok szüneteltetést a szülő folyamat bevárja. Az n db létrehozott gyerek folyamat bevárásához n db wait(NULL) utasítás szükséges. Ebben a példában a gyerek folyamatok 3 másodpercig vannak szüneteltetve, azonban érdemes végiggondolni, hogy hogyan tudnánk különböző ideig szüneteltetni az egyes folyamatokat. Világosan látszik, hogy önmagában a "hagyományos módon történő" randomszám generálás nem jó megoldás, hiszen az egyes folyamatok ugyanabból a seed-ból indíthatják a pszeudorandom szám generálást (emlékeztetőül: szülő memórialapjának lemásolása, megegyező változókészletek). Azonban kombinálhatjuk a random szám generálását egy egyedi azonosítóval, például minden folyamat rendelkezik egy Linux rendszerbeli egyedi azonosítóval (getpid()).

 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
38
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>

void child() {
  sleep(3);
  printf("Child: %d\n", getpid());
}

void parent() {
  int i;
  for (i = 0; i < 4; i++) {
    printf("Waited: %d\n", wait(NULL));
  }
}

int main() {
  int i;
  pid_t PID;

  for (i = 0; i < 4; i++) {
    PID = fork();

    if (PID == 0) {
      break;
    } else if (PID < 0) {
      exit(EXIT_FAILURE);
    }
  }

  if (PID == 0) {
    child();
  } else if (PID > 0) {
    parent();
  }
}

Gyakorló feladat

Mi az alábbi program kimenete és miért? Az indoklás egy lehetséges módja a folyamatok ábrázolása a korábbi példához hasonlóan (pl. papíron olvasható rajzzal, vagy draw.io-n). Lehetőség szerint egységes jelölést használjunk (pl. a bal részfa jelölje mindig >0 esetet, a jobb részfa pedig a =0 esetet).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include<stdio.h>
#include<unistd.h>

int main() { 
    if (fork() && (!fork())) { 
        if (fork() || fork()) { 
            fork(); 
        } 
    } 
    printf("2 "); 
} 

Kapcsolódó linkek

Linux manual


Utolsó frissítés: 2024-03-08 09:13:59