Kihagyás

PVM

A gyakorlat anyaga

PVM (Parallel Virtual Machine) nem egy önálló nyelv, hanem egy alapnyelvre (pl. C) épített eljáráskönyvtár. Tulajdonképpen a PVM egy szoftverrendszer, amellyel hálózatba kapcsolt számítógépeket egyetlen nagy párhuzamos számítógépként lehet látni, és kezelni. A rendszerbe kapcsolhatunk több - akár különböző típusú - számítógépet. A rendszer a rajta futó programok szempontjából ezek után egy nagy, elosztott memóriájú virtuális számítógépnek látszik: különböző processzorokon, különböző programokat indíthatunk el, melyek közötti szinkronizációt, és kommunikációt a PVM könyvtári függvényekkel oldhatjuk meg.

A folyamatok a PVM terminológiában a task elnevezést kapták. Az elindított feladatok a szülő folyamattól és egymástól függetlenül, aszinkron módon futnak. Minden feladat egy egyedi feladatazonosítót kap. Az üzenetküldés közvetlen, nincsenek csatornák, a küldő és a fogadó utasítások a feladatazonosítót használják címzésre. A kommunikáció aszinkron módon zajlik, tehát a küldő nem várja be a fogadót, hanem az üzenet feladása után folytatja a futását. Az üzenetekkel kapcsolatban viszont biztosított, hogy sorrendjük megőrződik.

Végezetül fontos kitérni arra is, hogy esetenként a PVM program debug-olása nehézeskes lehet, hiszen az elosztott megvalósítás miatt az adott számítógépen futó program nem kap információt mi az oka a késlekedésnek (azaz várakozik egy adat fogadására, ami sosem fog megérkezni). A szintaktikai ellenőrzést a fordító elvégzi, a szemantikai hibák felderítése viszont minden esetben a programozó feladata.

PVM daemon

Egy számítógép azáltal kerül be a PVM rendszerbe, hogy rajta elindítjuk a PVM daemon szolgáltatását. Az adott gépen futó feladatok a lokális daemon-nal tartják a kapcsolatot, a daemon-ok pedig egymással kommunikálva hozzák létre a virtuális gépet. A rendszer kipróbálásához egy gép is elég, sőt teszteléshez leginkább ez ajánlott, így mi is hasonlóképp fogunk eljárni. A következő utasításokat fogjuk használni:

  • pmv: elindítja a PVM deamon-t

  • quit: kilép a pvm deamon-ból (de a háttérben továbbra is futtatja)

  • halt: kilép a pvm deamon-ból (és a háttérben is bezárja)

Forrásfájlok fordításának menete: gcc -o program program.c -lpvm3, majd futtatása: ./program

Figyeljünk arra, hogy a "server" és a "client" fájlt külön-külön le kell fordtanunk, illetve a spawn függvényben (lásd később) a megfelelő abszolút elérési útvonalat adjuk meg.

Kommunikációért és feladatkezelésért felelős függvények

A PVM függvények mind pvm_xxx alakúak:

  • int pvm_mytid(void): saját feladat azonosító
  • int pvm_parent(void): Szülő feladat azonosító
  • int pvm_exit(void): kilépés a PVM rendszerből

  • int pvm_spawn(char* task, char** argv, int flag, char* where, int ntask, int* tids): feladat(ok) indítása. A hívó feladat az újonnan indított(ak) szülője lesz

    • task: Az inditandó feladat(ok) végrehajtható programjának elérési útja a fájlrendszerben
    • argv: parancssori argumentumok
    • flag: meghatározza a következő paraméter értelmezését. PvmTaskDefault esetén a rendszer maga választja ki az indítandó feladatok helyét
    • where: itt adhatjuk meg azt a hosztgépet (amennyiben az előző paraméter PvmTaskHost-tal lett inicializálva), amelyen a feladatok indulnak
    • ntask:feladatpéldányok száma
    • tids: ntask méretű tömb első elemére mutató pointer. Ide kerülnek az elindított feladatok azonosítói, hogy késöbb tudjunk rájuk hivatkozni

PVM-ben az üzenetküldés három lépésből áll, ezek: inicializálás, becsomagolás, tényleges küldés.

  • int pvm_initsend(int encoding): inicializálja az üzenetküldési buffer-t (azaz egy új üzenetet kezd el építeni). Az encoding-nak tipikusan akkor van szerepe, amikor több, különböző architektúrájú gép kommunikál, tehát az adatot olyan formába alakítani, amit az egyik gép elküldhet, a másik pedig megérthet. A PvmDataDefault egy szabványos, platformfüggetlen formátum, így bármilyen gép tudja fogadni az adatokat. PvmDataRaw csak azonos architektúra esetén ajánlott, mivel nincs adatkonverzió.

  • int pvm_pk*( ...) függvénycsalád szolgál a különböző alaptípusok becsomagolására. Ez minden esetben egy adott típusú tömböt csomagol be, ezért az nitem mondja meg, hogy mennyi elemet szeretnék becsomagolni a tömbből, a stride mondja meg, hogy mekkora lépésközzel vegye az elemeket a memóriából, míg a value a tömb első elemére mutató pointer:

    • pvm_pkbyte(char* value, int nitem, int stride)
    • pvm_pkdouble(double* value, int nitem, int stride)
    • pvm_pkfloat(float* value, int nitem, int stride)
    • pvm_pkint(int* value, int nitem, int stride)
    • pvm_pklong(long* value, int nitem, int stride)
    • pvm_pkstr(char * )
  • int pvm_send( int tid, int msgtag): az aktív küldési pufferben található előkészített üzenetet a megadott címkével ellátva elküldi. A fogadóra nem vár, azonnal visszatér, amint a lokális deamon átvette az üzenetet. A tid azonosító feladatnak elküldi msgtag címkével.

  • int pvm_mcast(int* tids, int ntask, int msgtag): az ntask méretű tids tömbben megadott azonosítójú összes taszknak elküldi az üzenetet. Ezt a módszert üzenetszórásnak (multicast) nevezzük.

Az üzenetfogadás első lépésben tulajdonképpen azt jelenti, hogy a levelesládából egy már beérkezett és ott eltárolt üzenetet kiveszünk feldolgozásra.

  • int pvm_recv( int tid, int msgtag): ennek a függvénynek a meghívása azt eredményezi, hogy a feladat addig várakozik, amíg egy a feltételeknek megfelelő üzenet nem érkezik.A feltételeket a két paraméter jelöli ki, a tid a küldő feladatok, a msgtag pedig a címkék közötti szűrésre alkalmas. (-1,-1) paraméterekkel meghívva azt jelenti, hogy bármit elfogadunk bárhonnan.

  • int pvm_upk*( ...): a kicsomagoló függvények, a megfelelő pvm_pk* függvények párjai

Összességében elmondható, hogy a PVM függvényei támogatják a heterogén rendszerek egységbe szervezését, azonban az egyszerűség kedvéért mi lokálisan fogjuk futtatni a saját gépünkön, ez sokat könnyít a PVM függvényeinek paraméterezésein is. A gyakorlaton vett minden feladat kettő, párban álló fájlban lesz megtalálható. Az egyik fájl mindig egy szerver-szerű működést fog leírni, míg a másik egy kliens-szerű működést, de persze tetszőleges hálózati topológia leírható, ahogyan az a következő ábrán látható. Azaz mivel tudjuk, hogy PVM-ben üzenetküldés a taszkazonósító segítségével történik, így, amennyiben egy kliens ismeri egy másik kliens azonosítóját, a kliens-kliens üzenetváltás sincs kizárva, illetve az is megenegedett, hogy egy "kliens" hozzon létre újabb taskot a spawn sehítségével.

topology

Gyakorlati példák

A következő kódrészlet egy szokásos Hello World megvalósítását mutatja be a PVM programok tipikus szerver-kliens struktúrájával. Gyakorlásnak indítsunk több kliens taszkot (több taszk spawn-olásával vagy akár külön fájlokban definiálva az egyes működéseket).

 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
#include "pvm3.h"
#include "stdlib.h"
#include "stdio.h"
#include "string.h"

int main() {

    char buf[100];
    int tid;

    int success = pvm_spawn("/workdir/2021/hello_client", NULL, PvmTaskDefault, "", 1, &tid);
    if(success < 0) {
        exit(EXIT_FAILURE);
    }

    pvm_recv(-1,-1); 

    pvm_upkstr(buf);

    printf("%s\n", buf);

    pvm_exit();

    exit(EXIT_SUCCESS);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "pvm3.h"
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include "time.h"
#include <unistd.h> 

int main() {

    char buf[100];

    strcpy(buf, "Hello world!");

    pvm_initsend(PvmDataDefault);

    pvm_pkstr(buf);

    pvm_send(pvm_parent(),1);

    pvm_exit();

    exit(EXIT_SUCCESS);
}

A leggyakoribb hiba PVM programok esetén, hogy az adatküldések és fogadások száma nem egyezik meg, ezért ezeket érdemes külön függvényekbe kiszervezni, ahogyan az alábbi master-slave megvalósítás esetén is tesszük:

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

void sendint(int tid, int data) {
    pvm_initsend(PvmDataDefault);
    pvm_pkint(&data, 1, 1);
    pvm_send(tid, 1);
}

int receiveint() {
    pvm_recv(-1,-1);
    int num;
    pvm_upkint(&num, 1, 1);
    return num;
}

int main() {
    int tid[2];
    int a,b;
    pvm_spawn("/workdir/2021/sum_worker", NULL, PvmTaskDefault, "", 2, tid);

    sendint(tid[0], 10);
    sendint(tid[1], 15);

    a=receiveint();
    b=receiveint();

    printf("%d\n",a+b);

    pvm_exit();
    exit(EXIT_SUCCESS);
}
 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
39
#include "stdio.h"
#include "stdlib.h"
#include "pvm3.h"

void log_msg(char* message, int value) {
    FILE *log_file;
    log_file = fopen("path to pvm_log.txt", "a");
    if (log_file != NULL) {
        fprintf(log_file, "%s - %d\n", message, value);
        fclose(log_file);
    }
}

void sendint(int tid, int data) {
    pvm_initsend(PvmDataDefault);
    pvm_pkint(&data, 1, 1);
    pvm_send(tid, 1);
}

int receiveint() {
    pvm_recv(-1,-1);
    int num;
    pvm_upkint(&num, 1, 1);
    return num;
}

int main() {

    int szam = receiveint();
    int i;
    int temp = 0;
    for(i=1;i<szam;i++){
        temp+=i;
    }

    sendint(pvm_parent(), temp);
    pvm_exit();
    exit(EXIT_SUCCESS);
}

Csővezetékes rendezés

A PVM lezárásaként megnézzük, hogyan tudjuk az Occam-ban is láttott ún. csővezetékes rendezést implementálni a PVM függvénykönyvtár segítségével. Fontos kiemelni mégegyszer azt, hogy a PVM asszinkron módon kommunikál, így a csővezeték egyes fázisai a saját ütemében, egymástól függetlenül fog haladni. A program helyes működése azon múlik, hogy a PVM garantálja, hogy az elküldött üzenetek sorrendje nem változik meg.

Első lépésként a csővezeték egyes komponeneseinek ismerniük kell azt a szomszédját, amelynek elküldi majd az adatot. Először tehát elküldjük minden feladatnak azt az azonosítót, hogy ő kinek fogja továbbítani. Értelemszerűen ha N db számot szeretnék rendezni, akkor N db feladatra lesz szükség. Az utolsó processz majd a szervernek küldi vissza mindig az adott számot.

Ezt követően az összes számot elküldjük az első feladatnak. Végezetül a szerver már a rendezett listát fogja megkapni, amit ki is iratunk. Az egyszerűség kevéért ismételten haszálni fogjuk a küldéshez és a fogadáshoz a sendint-receiveint függvényeket.

 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
39
40
41
42
43
44
45
46
47
#include "stdio.h"
#include "stdlib.h"
#include "pvm3.h"

#define N 10

void sendint(int tid, int data) {
    pvm_initsend(PvmDataDefault);
    pvm_pkint(&data, 1, 1);
    pvm_send(tid, 1);
}

int receiveint() {
    pvm_recv(-1,-1);
    int num;
    pvm_upkint(&num, 1, 1);
    return num;
}

int main() {

    int tid[N];

    int number[N] = {2,3,5,1,2,3,5,1,2,9};

    pvm_spawn("/workdir/2021/sort_task", NULL, PvmTaskDefault, "", N, tid);

    int i;

    for(i=0; i<N-1; i++) {
        sendint(tid[i], tid[i+1]);
    }

    sendint(tid[N-1], pvm_mytid());

    for(i=0; i<N; i++) {
        sendint(tid[0], number[i]);
    }

    for(i=0; i<N; i++) {
        printf("%d", receiveint());
    }

    printf("\n");
    pvm_exit();
    exit(EXIT_SUCCESS);
}
 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
39
40
#include "stdio.h"
#include "stdlib.h"
#include "pvm3.h"

#define N 10

void sendint(int tid, int data) {
    pvm_initsend(PvmDataDefault);
    pvm_pkint(&data, 1, 1);
    pvm_send(tid, 1);
}

int receiveint() {
    pvm_recv(-1,-1);
    int num;
    pvm_upkint(&num, 1, 1);
    return num;
}

int main() {

    int to_tid = receiveint();

    int max = receiveint();

    int i;
    for(i=0; i<N-1; i++) {
        int num = receiveint();
        if(num > max) {
            int tmp = max;
            max = num;
            num = tmp;
        }
        sendint(to_tid, num);
    }

    sendint(to_tid, max);
    pvm_exit();
    exit(EXIT_SUCCESS);
}

Gyakorló feladat

Készíts egy olyan PVM programot, amely egy double értékeket tároló tömb elemein végez el hatványozást a következő módon:

  • a szerver bontsa fel a tömböt egyenlő részekre, majd ezeket az intervallumokat küldje el (egyesével vagy egyben) külön kliens taszkoknak

  • ezt követően üzenetszórás segítségével minden kliensnek küldjön el egy értéket, a kliensek pedig végezzék el a hatványozást a résztömb elemein

  • végül a kliensek küldjék vissza a módosított résztömböt, amit aztán a szerver ki tud írni a képernyőre

Szükség esetén küldhetsz extra információt a klienseknek (pl. a résztömb méretét).

Kapcsolódó linkek

PVM hivatalos oldala

PVM könyv


Utolsó frissítés: 2025-04-12 05:33:34