Parallel Virtual Machine
Parallel Virtual Machine, PVM (wirtualna maszyna równoległa) – zestaw narzędzi do tworzenia oprogramowania dla sieci równolegle połączonych komputerów. Został zaprojektowany i stworzony by umożliwić łączenie komputerów o różnych konfiguracjach sprzętowych w jeden równolegle działający komputer.
Historia PVM
PVM był tworzony na University of Tennessee, The Oak Ridge National Laboratory, Emory University i Carnegie Mellon University. Jego pierwsza wersja została napisana w ORNL w 1989 roku. Następnie przepisano ją zupełnie w University of Tennessee i udostępniono jako wersję drugą w marcu 1991. Wersja trzecia ukazała się w 1993 roku.
Dzisiaj PVM jest nadal aktywnym projektem i nadal stosuje się go w programowaniu równoległym.
Opis działania PVM
PVM jest narzędziem służącym głównie do pośrednictwa w wymianie informacji pomiędzy procesami uruchomionymi na oddzielnych maszynach oraz do zarządzania nimi za pośrednictwem konsoli pvm. Z punktu widzenia obecnej definicji maszyny wirtualnej, pvm w sumie nie zasługuje na swoją nazwę, powinien się bardziej nazywać „zarządca procesów zrównoleglonych”, jednakże historia pvm sięga dawnych czasów, kiedy to nazewnictwo było nieco inaczej rozumiane.
W dużym uproszczeniu PVM pozwala na „połączenie” większej ilości komputerów w jeden. Odbywa się to na zasadzie dołączania kolejnych hostów (komputerów), na których został zainstalowany i skonfigurowany pvm. Podłączanie hosta jest w uproszczeniu procedurą połączenia przez rsh, uruchomieniu demona pvm i dostarczenia mu informacji o tym, kto jest jego „rodzicem” oraz o parametrach istniejącej sieci.
W momencie, kiedy cała „maszyna wirtualna” już jest skonfigurowana, rola pvm sprowadza się do stanowienia pomostu wymiany informacji pomiędzy procesami, które się w pvm „zarejestrują”, oraz umożliwienia administrowania stanem zarejestrowanych w pvm procesów. Z takiego punktu widzenia pvm można nazwać raczej „demonem komunikacji międzyprocesowej” i można to podeprzeć najprostszym argumentem, mianowicie pvm nie stanowi interfejsu do systemu operacyjnego lub maszyny, na której działa dany proces.
PVM pozwala zautomatyzować proces uruchamiania aplikacji na innych komputerach. Jeżeli uruchomimy z poziomu normalnej konsoli program, który następnie zarejestruje się w pvm i użyje funkcji pvm_spawn(), aby uruchomić dowolną ilość procesów, to już rolą PVM będzie zatroszczenie się, gdzie zostaną one uruchomione oraz samo ich uruchomienie (możliwe jest podanie parametru uściślającego lokalizacje docelową uruchomienia). W przypadkach obliczeń, gdzie procesy-dzieci, rodzą się i umierają w bliżej nieokreślonych interwałach i zjawiska te przeplatają się, zautomatyzowanie dostarczane przez PVM okazuje się bardzo pomocne, ponieważ odciąża programistę z potrzeby troski o prawidłowe rozłożenie obliczeń na hosty.
Identyfikacja w PVM odbywa się przez oddzielne numery identyfikacyjne przydzielane procesom w momencie rejestrowania się w PVM. Numery te stanowią zupełną abstrakcję od systemu operacyjnego i sprzętu, na którym uruchomiony jest dany proces. Numer identyfikacyjny jest bardzo istotnym elementem PVM, ponieważ stanowi on niejako adres danego procesu i jest on niepowtarzalny w zakresie pojedynczej maszyny PVM. Adres ten jest wykorzystywany przy wszystkich procedurach dotyczących innych procesów – można przesłać informacje pod dany adres, można „zabić” zdalny adres i wykonać wiele innych czynności.
W artykule tym wiele razy zostało (i zostanie) poruszone pojęcie „przyłączenia się do PVM” czy też „zarejestrowania się w PVM”. Pojęcia te znaczą dokładnie to samo i oznaczają wywołanie po raz pierwszy jakiejkolwiek procedury PVM, przez co demon PVM przydziela danemu procesowi numer identyfikacyjny i zapisuje informacje o nim w swojej bazie procesów.
Przesyłanie informacji pomiędzy programami
W pvm wyróżniamy dwa rodzaje wysyłania informacji: blokujące i nieblokujące. Przesyłanie blokujące to takie, które blokuje program wysyłający na procedurze wysłania do momentu aż program (lub programy) odbierający wywoła procedurę otrzymywania informacji i na odwrót, blokuje program odbierający na procedurze odbierania do momentu, aż zostaną mu wysłane jakieś informacje przez program nadający. Przesyłanie nieblokujące – jak wynika z nazwy – jest pozbawione tych negatywnych czynników, jednakże wymaga ono nieraz wsparcia przez „procesy zewnętrzne (proces pvmmsg)”, oraz niejednokrotnie wymaga synchronizacji procesów w celu ich skomunikowania. Dlatego dla prostych programów przeważnie wykorzystuje się przesyłanie blokujące.
Do przesyłania informacji w PVM stosuje się „bufory”. Każdy proces, który zarejestruje się w PVM wywołując dowolną jego procedurę (najczęściej stosuje się wywołanie pvm_mytid()), zostaje wyposażony w jeden bufor nadawczy i jeden bufor odbiorczy. Możliwe jest użycie buforów innych niż domyślne, jednakże wymaga to ich utworzenia.
Procedura przesyłania dzieli się na:
- Wysłanie informacji przez program „nadający”
- inicjalizację bufora wysyłania
- spakowanie danych do bufora
- wysłanie wiadomości
- Odebranie informacji przez programy „odbierające”
- odebranie wiadomości
- rozpakowanie informacji z bufora
Do bufora można spakować więcej niż jedną informację w celu wysłania ich większej ilości naraz, jednakże należy pamiętać o ich rozpakowaniu z bufora odbiorczego w odpowiedniej kolejności.
Przykładowe procedury przesyłania informacji
Komunikacja punkt-punkt
pvm_send
int retval = pvm_send(int tid ,int msgtag); //
Parametry
- tid – identyfikator procesu, do którego ma zostać wysłany komunikat.
- msgtag – liczba całkowita definiowana przez użytkownika jako etykieta komunikatu (powinna być >=0).
- retval – kod statusu zwracany przez funkcję (retval < 0 oznacza błąd podczas wykonania operacji).
pvm_recv
int retval = pvm_recv(int tid ,int msgtag); //
Parametry
- tid – identyfikator procesu, od którego ma zostać odebrany komunikat.
- msgtag – liczba całkowita definiowana przez użytkownika jako etykieta komunikatu (powinna być >=0).
- retval – kod statusu zwracany przez funkcję (retval < 0 oznacza błąd podczas wykonania operacji).
Komunikacja grupowa
pvm_mcast
int retval = pvm_mcast( int *tids, int ntask, int msgtag); //
Parametry
- tids – tablica liczb całkowitych o ntask elementach zawierających identyfikatory zadań procesów, do których ma zostać wysłany komunikat.
- ntask – ilość procesów, do których będzie wysłany komunikat.
- msgtag – liczba całkowita definiowana przez użytkownika jako etykieta komunikatu (powinna być >=0).
- retval – kod statusu zwracany przez funkcję (retval < 0 oznacza błąd podczas wykonania operacji).
Uwagi Funkcja wysyła komunikat asynchronicznie do wszystkich procesów, których identyfikatory zadań są zapamiętane w tablicy tids, z wyjątkiem procesu wysyłającego. Etykieta msgtag jest wprowadzona dla rozróżniania przesyłanych komunikatów.
Proces odbierający komunikat może zrobić to albo funkcją pvm_recv() albo pvm_nrecv().
PRZYKŁAD:
#define ileprocow 6 int nproc = ileprocow; char *nazwaGrupy; nazwaGrupy = "mc-row"; int tIds[nproc]; //przeważnie tablice procesów uzyskujemy z funkcji pvm_spawn taskNo = pvm_spawn("mc-slave", (char**) NULL, PvmTaskDefault, "", (nproc -1), tIds); // //i przeważnie stosujemy ją do rozsyłania danych początkowych //do procesów dzieci. W tym wypadku nazwa grupy i ile procesów //będzie w grupie (istotna informacja dla wywoływania pvm_barrier) pvm_initsend( PvmDataDefault); // pvm_pkint(&nproc, 1, 0); // pvm_pkstr(nazwaGrupy); // pvm_mcast(tIds, taskNo, mcastmsg); //
pvm_bcast
int retval = pvm_bcast( char *group, int msgtag); //
Parametry:
- group – nazwa grupy (łańcuch znakowy).
- msgtag – etykieta komunikatu określana przez użytkownika. Etykieta msgtag musi być >=0 i pozwala programowi rozróżniać rodzaje komunikatów.
- retval – kod statusu zwracany przez funkcję (retval < 0 oznacza błąd podczas wykonania operacji).
Uwagi:
Funkcja wysyła komunikat zapamiętany w aktywnym buforze komunikatów do wszystkich procesów danej grupy. Komunikat nie jest przesyłany do procesu wysyłającego (jeżeli należy do tej grupy). Funkcja może zostać wywołana przez proces, który nie należy do tej grupy. Wysłanie komunikatu jest asynchroniczne (ale realizacja jest sekwencyjna – nie jest to typowy 'broadcast'). Funkcja w pierwszej kolejności określa identyfikatory zadań procesów danej grupy a następnie rozpoczyna wysyłanie do tych procesów komunikatu. Jeżeli w trakcie wykonania tej funkcji grupa się zmieni, to nie będzie to miało wpływu na to, do których procesów zostanie wysłany komunikat.
PRZYKŁAD
pvm_reduce
int retval = pvm_reduce( void (*func)(), void *data, int count, int datatype, int msgtag, char *group, int root);
Parametry
- func – funkcja definiująca operację, jaka ma być wykonana na globalnych danych. Dostępne są już zdefiniowane funkcje: PvmMax, PvmMin, PvmSum, PvmProduct. Użytkownik może zdefiniować swoją własną funkcję.
- data – wskaźnik do pierwszego elementu tablicy lokalnych danych. Po wykonaniu operacji proces root w tablicy tej otrzyma rezultat.
- count – liczba elementów danych w tablicy data.
- datatype – liczba całkowita specyfikująca typ danych w tablicy data.
- msgtag – etykieta komunikatu ustawiana przez użytkownika >= 0.
- group – nazwa grupy.
- root – liczba całkowita; numer procesu grupy (ang. instance number), który otrzyma rezultat operacji.
- retval – kod statusu zwracany przez funkcję (retval < 0 oznacza błąd podczas wykonania operacji). Użycie kodu błędu nie jest wymagane, jednak stanowi pomoc w trakcie debugowania.
Uwagi
Funkcja wykonuje globalną operację jak np. min, max dla wszystkich procesów należących do jednej grupy procesów. Każdy proces grupy wywołuje tę funkcję ze swoimi lokalnymi danymi, natomiast wynik globalnej operacji pojawi się w wyspecyfikowanym przez użytkownika procesie root. Proces ten jest identyfikowany poprzez specjalny numer w tej grupie. System PVM dostarcza następujące globalne operacje, które mogą być podane jako parametr func:
- PvmMin
- PvmMax
- PmvSum
- PvmProduct
Są one zdefiniowane dla wszystkich podstawowych typów danych. W parametrze datatype dla określenia typu danych wejściowych stosuje się przedefiniowane identyfikatory typów jak np.:
- PVM_BYTE
- PVM_SHORT
- PVM_INT
- PVM_FLOAT
- PVM_DOUBLE
- PVM_LONG
Aby zastosować własną funkcję, której nazwa będzie parametrem func, powinna ona być zdefiniowana w następujący sposób:
void userfunc( int *datatype, void *x, void *y, int *num, int *retval);
Parametry x, y są tablicami wyspecyfikowanego typu datatype zawierających num elementów. Parametr retval zwraca informację o błędzie. Tablica y zawiera wyniki operacji zdefiniowanej przez tę funkcję. Funkcja pvm_reduce() nie blokuje procesów grupy. Dlatego jeżeli proces ją wywoła a następnie opuści grupę zanim proces root wywoła pvm_reduce() to może to powodować występowanie błędu.
PRZYKŁAD:
#define reducemsg 12 #define nproc 10 char *nazwaGrupy; nazwaGrupy="moja_grupa" //najpierw trzeba uzyskac rGid, czyli numer członka //grupy u którego „pojawi” się wynik, w większości //wypadków jest to rodzic grupy //dla rodzica grupy uzyskujemy jego własny numer int parentId = pvm_parent(); // int rGid = pvm_getinst(nazwaGrupy, parentId); // // dla procesu-dziecka uzyskujemy numer rodzica: int myId = pvm_mytid(); // int rGid = pvm_getinst(nazwaGrupy, myId); // //jeżeli wywołujemy pvm_reduce zaraz po przystąpieniu do grupy, wypadałoby //zsynchronizować procesy, ponieważ wywołanie pmv_reduce a następnie //przystąpienie do grupy członka, który też wywoła pwm_reduce, generuje dziwne //błędy. info = pvm_barrier(nazwaGrupy, nproc); // pvm_reduce( PvmSum, &ilewkole, 1, PVM_LONG, reducemsg, nazwaGrupy, rGid); // //jeżeli stosujemy pvm_reduce na końcu programu WYSOCE wskazane jest zsynchronizowane //wszystkich członków grupy, ponieważ pvm_reduce jest komunikacją nieblokującą //może się pojawić sytuacja, że któryś proces wywoła tę funkcję i opuści grupę //zanim inni członkowie grupy ją wywołają, co przeważnie prowadzi do błędów info = pvm_barrier(nazwaGrupy, nproc); //
Konfiguracja środowiska PVM
Aby móc korzystać z biblioteki PVM(pvm3) należy ustawić zmienne środowiskowe:
export PVM_ROOT=/usr/share/pvm3 export PVM_ARCH=`$PVM_ROOT/lib/pvmgetarch`
Kompilacja i odpluskwianie (debugging)
Kompilacja kodu źródłowego:
gcc -o master master.c -I$PVM_ROOT/include -L$PVM_ROOT/lib/$PVM_ARCH -lpvm3 -lgpvm3
Niestety PVM można już określić mianem martwego projektu głównie z racji tego, iż wyparły go metody bardziej efektywne i mniej zawodne. Dlatego też wszelkie wsparcie jest znikome i pojawiające się błędy są trudne do okiełznania.
Do przykładowych błędów można zaliczyć problemy z zarządzaniem pamięcią w połączeniu z „rotacją” zmiennych (chodzi o przesunięcie bitów w zmiennej, np. wszystkie w prawo o jeden, a ostatni na pierwsze miejsce). Można się spotkać z tym przy niektórych generatorach liczb losowych o dużej rozdzielczości dostępnych w Internecie. Objawia się to tym, iż program, który wywołuje tę funkcję, natychmiastowo opuszcza PVM, będąc tam jednak nadal zarejestrowanym (jest obecny w konsoli PVM). Wszelkie próby łączności z tym programem za pomocą PVM kończą się komunikatem „instance not found”. Jest to wyjątkowo frustrujący błąd, ponieważ poszukiwania jego rozwiązania zawsze są zwracane w kierunku konfiguracji PVM ... co prowadzi przeważnie donikąd.
Funkcją pomocną przy odpluskwianiu jest
pvm_catchout()
która wywołana w głównym programie uruchamianym z konsoli powoduje przechwycenie wszystkich wyjść procesów dzieci wysyłanych na konsole wyjściową (za pomocą std::cout oraz printf()).
Przykładowe programy korzystające z PVM
„HELLO WORLD”
///// master.c ///// #include <stdio.h> #include "pvm3.h" main() { int cc, tid, msgtag; char buf[100]; printf("Jestem t%x\n", pvm_mytid()); cc = pvm_spawn("slave", (char**)0, 0, "", 1, &tid); // if (cc == 1) { msgtag = 1; pvm_recv(tid, msgtag); // pvm_upkstr(buf); // printf("from t%x: %s\n", tid, buf); } else printf("can't start slave\n"); pvm_exit(); // } /////////////////// ///// slave.c ///// #include "pvm3.h" main() { int ptid, msgtag; char buf[100]; ptid = pvm_parent(); // strcpy(buf, "hello, world from "); gethostname(buf + strlen(buf), 64); msgtag = 1; pvm_initsend(PvmDataDefault); // pvm_pkstr(buf); // pvm_send(ptid, msgtag); // pvm_exit(); // } ///////////////////
Obliczanie liczby PI metodą „Monte Carlo”
///// mc-master.c ///// #include <stdio.h> #include <pvm3.h> #include <time.h> #include <stdlib.h> #define ileprocow 6 #define mcastmsg 10 #define bcastmsg 11 #define reducemsg 12 #define ilelosowan 1400000000 /*W przypadku tego programu należy pamiętać, iż nazwa programu „niewolnika” musi być mc-slave (bądź dowolna inna, w przypadku gdy zmienimy wartość w pvm_spawn) */ int main(int argc, char *argv[]) { int nproc = ileprocow; long i, ile = ilelosowan, seed = time(NULL), ilewkole = 0; char *nazwaGrupy; nazwaGrupy = "mc-row"; double interwal, x, y; long double pi; time_t poczatek, koniec; int myId = pvm_mytid(), taskNo, tIds[nproc], inum, info; poczatek = time(0); taskNo = pvm_spawn("mc-slave", (char**) NULL, PvmTaskDefault, "", ( nproc -1 ), tIds); // pvm_initsend( PvmDataDefault ); // pvm_pkint(&nproc, 1, 0); // pvm_pkstr(nazwaGrupy); // pvm_mcast(tIds, taskNo, mcastmsg); // inum = pvm_joingroup( nazwaGrupy ); // info = pvm_barrier( nazwaGrupy , nproc ); // ile = ile / nproc; pvm_initsend(PvmDataDefault); // pvm_pklong(&ile , 1 , 0); // pvm_bcast( nazwaGrupy, bcastmsg ); // srand( seed * myId - myId ); for(ile; ile > 0 ; ile--) { x = ((double)rand() / RAND_MAX)*2 - 1; y = ((double)rand() / RAND_MAX)*2 - 1; if(!(((x*x) + (y*y))>1)) ilewkole++ ; }; int myGid = pvm_getinst(nazwaGrupy, myId); // info = pvm_barrier( nazwaGrupy , nproc ); // pvm_reduce( PvmSum, &ilewkole, 1, PVM_LONG, reducemsg, nazwaGrupy, myGid); // info = pvm_barrier( nazwaGrupy , nproc ); // pvm_exit(); // pi = ilewkole ; pi = pi * 4; pi = pi / ilelosowan; printf("punktow w kole = %d sposrod = %d\n", ilewkole, ilelosowan); printf("liczba pi wynosi: %.64Le\n", pi); koniec = time(0); interwal = koniec - poczatek; printf("\nIlosc procesow obliczeniowych: %d \nCzas trwania obliczen: %.0lf sekund\n", nproc, interwal); return 0; } ///////////////////
///// mc-slave.c ///// #include <stdio.h> #include <pvm3.h> #include <time.h> #include <stdlib.h> #define mcastmsg 10 #define bcastmsg 11 #define reducemsg 12 int main(int argc, char *argv[]){ char *nazwaGrupy; int nproc; int parentId = pvm_parent(), myId = pvm_mytid(), info, inum; long ile, ilewkole = 0, seed = time(NULL); double x, y; pvm_recv(parentId, mcastmsg); // pvm_upkint(&nproc, 1, 0); // pvm_upkstr(nazwaGrupy); // inum = pvm_joingroup(nazwaGrupy); // info = pvm_barrier(nazwaGrupy, nproc); // pvm_recv(parentId, bcastmsg); // pvm_upklong(&ile, 1, 0); // srand(seed * myId - myId); for (ile; ile > 0; ile--) { x = ((double)rand() / RAND_MAX)*2 - 1; y = ((double)rand() / RAND_MAX)*2 - 1; if(!(((x*x) + (y*y))>1)) ilewkole++; }; int pGid = pvm_getinst(nazwaGrupy, parentId); // info = pvm_barrier(nazwaGrupy, nproc); // pvm_reduce( PvmSum, &ilewkole, 1, PVM_LONG, reducemsg, nazwaGrupy, pGid); // info = pvm_barrier(nazwaGrupy, nproc); // pvm_exit(); // return 0; } ///////////////////