Sr-04-lab-1.0

From Studia Informatyczne

Spis treści

Wywołanie zwrotne

Wprowadzenie

Omówione dotąd modele wywołań zdalnych nie dają możliwości odbioru wyniku działania procedury zdalnej w przypadku dowolnie długotrwałego przetwarzania. Zwrócenie wyniku może jednak zostać zrealizowane jako wywołanie zwrotne, w którym serwer, po zakończeniu przetwarzania, wywołuje procedurę zdalną klienta. Rysunek pokazuje przykład realizacji takiego wywołania. W wywołaniu zwrotnym klient staje się na pewien czas serwerem, aby móc odebrać wynik zdalnej procedury. Istotną zaletą wywołania zwrotnego jest możliwość nieprzerwanej pracy klienta. Z reguły wymaga to jednak uruchomienia dodatkowego wątku, którego zadaniem będzie oczekiwanie na odbiór wyniku z serwera.

Wywołanie zwrotne
Wywołanie zwrotne

Wywołanie zwrotne można oczywiście łączyć z obsługą żądań w procesach potomnych co zobrazowano na rysunku. Takie podejście zostanie zaprezentowane w następnych przykładach.

Wywołanie zwrotne z procesem potomnym
Wywołanie zwrotne z procesem potomnym

Przykład aplikacji

Jako przykład rozważmy znów serwer rkill z punktu:

program RKILL_PRG {
  version RKILL_VERSION_1 {
    void rkill(int pid, int sig) = 1;
  } = 1;
} = 0x20000000;

Zwróćmy uwagę, że funkcja rkill() nie zwraca żadnej wartości. Definicję serwera należy uzupełnić o definicję wywołania zwrotnego klienta (plik rkillcb.x):

program RKILL_CB_PRG {
  version RKILL_CB_VERSION_1 {
    void sendresult(int res) = 1;
  } = 1;
} = 0x40000000;

Serwer ten definiuje funkcję sendresult(), której zadaniem będzie przesłanie wyniku przetwarzania do klienta. Funkcja ta również nie zwraca żadnego wyniku.

Definicje serwerów należy przetworzyć generatorem kodu rpcgen:

# rpcgen -N -a rkill.x
# rpcgen -N -a rkillcb.x

Oto lista modyfikacji jakie należy wprowadzić do wygenerowanych plików:

  1. Usunięcie pliku Makefile.rkillcb. Całość sterowania kompilacją znajdzie się w pliku Makefile.rkill.
  2. Modyfikacja pliku Makefile.rkill. Należy uzupełnić listę modułów programowych wchodzących w skład kodu klienta i serwera dopisując odpowiednie wartości do zmiennych TARGETS_SVC.c i TARGETS_CLNT.c:
    TARGETS_SVC.c = ... rkillcb_clnt.c rkillcb_client.c
    TARGETS_CLNT.c = ... rkillcb_svc.c rkillcb_server.c
    Powyższa modyfikacja wskazuje na to, że aplikacja klienta będzie się składać z właściwego klienta (m.in. pliki rkill_client.c i rkill_clnt.c) oraz z serwera wywołania zwrotnego (pliki rkillcb_server.c i rkillcb_svc.c). Analogicznie w skład aplikacji serwera wejdzie dodatkowo klient wywołania zwrotnego (pliki rkillcb_client.c i rkillcb_clnt.c).
  3. Przekazywanie argumentów po stronie klienta (plik rkill_client.c). Należy odczytać argumenty z linii poleceń i przekazać je poprzez funkcję rkill_prg_1() aż do pieńka klienta czyli do funkcji rkill_1().
  4. Modyfikacja klienta wywołania zwrotnego (plik rkillcb_client.c): całkowite usunięcie definicji funkcji main(), zmiana nazwy funkcji rkill_cb_prg_1() na rkill_cb_1() (Dla uniknięcia konfliktu z funkcją o tej samej nazwie z pieńka serwera wywołania zwrotnego.), wyprowadzenie argumentu res na zewnątrz i dodanie deklaracji tej funkcji do pliku nagłówkowego rkillcb.h. Funkcja main() dla serwera zdefiniowana jest w pliku rkill_svc.c.
  5. Implementacja właściwego serwera (przedstawiona w przykładzie). Uwaga: istotne jest dołączenie odpowiednich plików nagłówkowych.
  6. Implementacja serwera wywołania zwrotnego. Zadaniem serwera jest odbiór wyniku zdalnej procedury i wyświetlenie go na ekranie.
  7. Zmiana nazwy funkcji main() w pieńku serwera wywołania zwrotnego (plik rkillcb_svc.c) na main2() i dodanie jej deklaracji do pliku nagłówkowego rkillcb.h. Definicja funkcji main() dla aplikacji klienta znajduje się w pliku rkill_client.c.
  8. Rozszerzenie implementacji właściwego klienta. Klient po wywołaniu zdalnej procedury (rkill_prg_1()) może po prostu wykonać kod serwera wywołania zwrotnego (funkcja main2() z pliku rkillcb_svc.c).

Poniżej przedstawiono implementację serwera z wywołaniem zwrotnym:

  1: #include <unistd.h>
  2: #include <signal.h>
  3: #include <arpa/inet.h>
  4: #include "rkill.h"
  5: #include "rkillcb.h"
  6: 
  7: void *
  8: rkill_1_svc(int pid, int sig, struct svc_req *rqstp)
  9: {
 10:   static char * result = NULL;
 11: 
 12:   printf("rkill(%d,%d)\n", pid, sig);
 13:   if (fork()==0)
 14:   {
 15:     int res;
 16:     sleep(3);
 17:     res = kill(pid, sig);
 18:     rkill_cb_1(inet_ntoa(rqstp->rq_xprt->xp_raddr.sin_addr), res);
 19:     printf("rkill(%d,%d)  ==>   %d\n", pid, sig, res);
 20:     exit(0);
 21:   }
 22: 
 23:   return (void *) &result;
 24: }

Przepływ sterowania w ramach poszczególnych plików składowych aplikacji został przedstawiony na rysunku:

                   Klient                                          Serwer
+-----------------------------------------+     +-----------------------------------------+
|                                         |     |                                         |
| +----------------+   +----------------+ |     | +----------------+   +----------------+ |
| | rkill_client.c |==>|  rkill_clnt.c  |========>|  rkill_svc.c   |==>| rkill_server.c | |
| +----------------+   +----------------+ |     | +----------------+   +----------------+ |
|         /\                              |     |                              ||         |
|         ||                              |     |                              \/         |
| +----------------+   +----------------+ |     | +----------------+   +----------------+ |
| |rkillcb_server.c|<==| rkillcb_svc.c  |<========| rkillcb_clnt.c |<==|rkillcb_client.c| |
| +----------------+   +----------------+ |     | +----------------+   +----------------+ |
|                                         |     |                                         |
+-----------------------------------------+     +-----------------------------------------+

Procedura svc_run()

Po wprowadzeniu powyższych modyfikacji klient spowoduje wywołanie zdalnej procedury, serwer wykona ją, wywoła procedurę zwrotną po stronie klienta, ale przetwarzanie po stronie klienta nie zakończy się. Wynika to z charakteru pracy standardowego serwera, którego zadaniem jest praca w pętli nieskończonej. Implementacja klienta korzysta ze standardowego serwera (funkcja main2() w pliku rkillcb_svc.c). Serwer RPC wywołuje funkcję svc_run(), której zadaniem jest nieskończone oczekiwanie i obsługa żądań RPC. Klient korzystający z wywołania zwrotnego powinien oczekiwać tylko na jedno wywołanie RPC od serwera, co wymaga zmodyfikowania funkcji svc_run() przedstawionej w przykładzie:

  1: #include <rpc/rpc.h>
  2: #include <errno.h>
  3: #include "rkillcb.h"   /* tu znajduje sie deklaracja zmiennej done */
  4: 
  5: void svc_run2()
  6: {
  7:   fd_set readfds;
  8: 
  9:   for (;;)
 10:   {
 11:     readfds = svc_fdset;
 12:     switch (select(_rpc_dtablesize(), &readfds, (fd_set*)NULL, (fd_set*)NULL,
 13:              (struct timeval*)NULL))
 14:     {
 15:       case -1:
 16:         if (errno == EINTR) continue;
 17:         perror("svc_run: - select failed");
 18:         return;
 19:       case 0:
 20:         continue;
 21:       default:
 22:         svc_getreqset(&readfds);
 23:         if (done) return;
 24:     }
 25:   }
 26: }

Procedura svc_run2() monitoruje funkcją select() deskryptory wskazane zmienną svc_fdset i po stwierdzeniu odbioru nowego żądania wywołuje funkcję svc_getreqset() przetwarzającą wywołanie. Zewnętrzna zmienna done informuje procedurę czy nastąpiło już poprawne wywołanie procedury, na którą oczekuje klient. W linii 23 następuje wyjście z procedury svc_run2() po poprawnym obsłużeniu wywołania RPC. Pieniek serwera wywołania zwrotnego (plik rkillcb_svc.c) powinien więc wywoływać funkcję svc_run2() zamiast standardowej svc_run(). Zmienna done powinna być inicjowana wewnątrz aplikacji klienta (plik rkill_client.c) i modyfikowana w implementacji procedury zwrotnej (plik rkillcb_server.c). Wymaga to dodania deklaracji tej zmiennej do pliku nagłówkowego rkillcb.h i definicji do kodu klienta. Poprawna kompilacja kodu klienta wymaga również dołączenia definicji funkcji svc_run2() do kodu klienta, co oznacza konieczność wskazania w pliku Makefile nazwy pliku z implementacją:

TARGETS_CLNT.c = ... svc_run2.c

Deklaracja funkcji svc_run2() powinna również trafić do pliku nagłówkowego rkillcb.h.

Funkcja svc_run() standardowo nie kończy się nigdy, dlatego pieniek serwera wyświetla błąd po jej zakończeniu. Komunikat ten, jak również i występujące po nim zakończenie procesu funkcją exit(), należy oczywiście usunąć.

Rejestracja tymczasowej usługi

Klient uruchamiając lokalny serwer musi go zarejestrować w usłudze portmap. Rejestracja powinna odbywać się z użyciem tymczasowego numeru usługi RPC, gdyż w przeciwnym wypadku może wystąpić konflikt pomiędzy użytkownikami uruchamiającymi swoje aplikacje na jednym komputerze. Rejestracji usługi dokonuje pieniek serwera zawarty w pliku rkillcb_svc.c (funkcja main2() z punktu). Przykład prezentuje funkcję register_tmp() rejestrującą tymczasowy serwer RPC. Jest to zmodyfikowana wersja funkcji main() standardowego pieńka serwera. Funkcja pmap_set() jest odpowiedzialna za rejestrację usługi RPC w usłudze portmap. Pętla while w linii 5 dokonuje próbnej rejestracji pod kolejnymi numerami począwszy od 0x40000000.

  1: int register_tmp(SVCXPRT *transp)
  2: {
  3:   long prognum = 0x40000000;
  4: 
  5:   while(pmap_set(prognum, RKILL_CB_VERSION_1, IPPROTO_UDP, transp->xp_port)==0)
  6:   {
  7:     prognum++;
  8:   }
  9:   if (!svc_register(transp, prognum, RKILL_CB_VERSION_1,
 10:                     rkill_cb_prg_1, IPPROTO_UDP))
 11:   {
 12:     fprintf (stderr, "%s", "unable to register transient procedure.");
 13:     exit(1);
 14:   }
 15:   return prognum;
 16: }

Aplikacja klienta przed wywołaniem zdalnej metody powinna zarejestrować procedurę RPC do wywołania zwrotnego, następnie oczekiwać w funkcji svc_run2() i na końcu wyrejestrować tymczasowo zarejestrowaną usługę.

Numer tymczasowo zarejestrowanej usługi musi trafić do serwera, aby mógł on przekazać zwrotnie wynik przetwarzania do właściwego klienta. Najprościej można to zrobić podczas samego wywołania zdalnej procedury. Definicja usługi rkill musi więc ulec modyfikacji:

program RKILL_PRG {
  version RKILL_VERSION_1 {
    void rkill(int prognum, int pid, int sig) = 1;
  } = 1;
} = 0x20000000;

Wymagana będzie więc zmiana implementacji klienta wywołania zwrotnego (plik rkillcb_client.c) w celu przekazania mu właściwego numeru usługi RPC. Ostateczna implementacja funkcji main() w aplikacji klienta wygląda więc następująco:

int
main (int argc, char *argv[])
{
  char *host;
  int prognum;
  int pid;
  int sig;

  if (argc < 4) {
    printf ("usage: %s server_host pid sig\n", argv[0]);
    exit (1);
  }
  host = argv[1];
  pid = atoi(argv[2]);
  sig = atoi(argv[3]);
  done = 0;

  SVCXPRT *transp = svcudp_create(RPC_ANYSOCK);
  if (transp == NULL)
  {
    fprintf (stderr, "%s", "cannot create udp service.");
    exit(1);
  }
  prognum = register_tmp(transp);
  rkill_prg_1(host, prognum, pid, sig);
  svc_run2();
  svc_unregister(prognum, RKILL_CB_VERSION_1);
  svc_destroy(transp);
  exit (0);
}

Funkcja svc_unregister() wyrejestrowuje tymczasową usługę RPC, a funkcja svc_destroy() zwalnia zasoby uchwytu komunikacyjnego.

Dla poprawnej kompilacji projektu deklaracja funkcji register_tmp() powinna zostać dołączona do pliku nagłówkowego rkillcb.h.

Kontrola praw dostępu

Zdalne wywołania mogą być uzupełnione o informację identyfikującą użytkownika. Poniższy fragment kodu powinien zostać umieszczony w implementacji klienta:

clnt = clnt_create(...);
...
clnt->cl_auth = authunix_create_default();

Funkcja authunix_create_default() powoduje dołączenie do wywołania procedury informacji o nazwie komputera klienta, identyfikatorze użytkownika (UID) i identyfikatorze jego grupy (GID). Po stronie serwera informacje te dostępne są poprzez strukturę svc_req przekazywaną do zdalnej struktury:

struct authunix_parms *aup;
aup = rqstp->rq_clntcred;
printf("Komputer: %s\n", aup->aup_machname);
printf("UID     : %d\n", aup->aup_uid);
printf("GID     : %d\n", aup->aup_gid)

Zadania

  1. Zweryfikuj poprawność wykonania aplikacji klienta w przypadku współbieżnego uruchamiania wielu kopii na jednym komputerze.
  2. Zrealizuj wielowątkową implementację klienta z wywołaniem zwrotnym. Zadaniem dodatkowego wątku ma być oczekiwanie na odpowiedź z serwera.
  3. Zrealizuj aplikację rozpraszającą obliczenia na N komputerów z wykorzystaniem RPC. Komputery obliczeniowe powinny udostępniać zdalne wywołanie wykonujące fragment obliczeń i zwracające wyniki poprzez wywołanie zwrotne. Aplikacja klienta prezentuje ostateczny wynik po odebraniu wyników cząstkowych od wszystkich N serwerów.