Sr-03-lab-1.0

From Studia Informatyczne

Spis treści

Asynchroniczne RPC

Przebieg zdalnego wywołania

Biblioteka RPC po wywołaniu zdalnej procedury oczekuje standardowo około 25 sekund na odpowiedź. Po upływie tego czasu pieniek klienta zgłasza wystąpienie błędu przekroczenia czasu oczekiwania (timeout) pomimo, że odpowiedź nadchodzi. Można to zweryfikować wstawiając do implementacji zdalnej metody wywołanie funkcji:

sleep(30);

powodującej wydłużenie czasu realizacji zdalnego wywołania. Wykonanie programu klienta kończy się w takiej sytuacji błędem:

# rkill_client localhost 2314
call failed: RPC: Timed out

Błąd wynika oczywiście z przekroczenia czasu oczekiwania na odpowiedź. Ciekawa jest jednak reakcja serwera:

# rkill_server
Zdalna procedura
Zdalna procedura
Zdalna procedura
Zdalna procedura
Zdalna procedura

Okazuje się, że zdalna procedura została wywołana aż 5 razy. Jest to wynik strategii stosowanej podczas korzystania z bezpołączeniowego protokołu transportowego UDP. Wysłanie komunikatu UDP do serwera i brak odpowiedzi po upływie czasu określonego parametrem RETRY_TIMEOUT powoduje wysłanie kolejnego komunikatu. Domyślna wartość parametry RETRY_TIMEOUT wynosi 5 sekund, co powoduje, że w przeciągu 25 sekund klient zdąży wysłać 5 komunikatów z żądaniem. Po upływie czasu określanego parametrem TIMEOUT następuje zasygnalizowanie błędu warstwie wyższej, czyli aplikacji. Serwer obsługuje poszczególne żądania sekwencyjnie, ale warstwa komunikacyjna systemu operacyjnego odbiera przesyłane komunikaty i buforuje je, co powoduje ich późniejsze wykonanie.

Schemat komunikacji z serwerem po protokole UDP dla długotrwałych procedur
Schemat komunikacji z serwerem po protokole UDP dla długotrwałych procedur

Inne zachowanie biblioteki RPC można zaobserwować gdy protokołem transportowym będzie protokół TCP. W takiej sytuacji klient nie retransmituje żądania, ponieważ ma gwarancję, że komunikat dotarł do serwera. Błąd przekroczenia czasu spowoduje przerwanie zdalnego wywołania, ale serwer odbierze jedynie pojedyncze żądanie.

Proste wywołanie asynchroniczne

Jeżeli nie można określić maksymalnego czasu wykonania zdalnej procedury, pozostaje wykonanie wywołania asynchronicznego, czyli takiego, w którym serwer ma dowolnie dużo czasu na realizację żądania. Asynchronizm powoduje również, że klient nie jest blokowany realizacją zdalnego wywołania i może kontynuować swoje przetwarzanie. Rysunek przedstawia schemat komunikacji w przypadku wywołania asynchronicznego.

Wywołanie asynchroniczne
Wywołanie asynchroniczne

Oczywistą konsekwencją wywołania asynchronicznego jest niemożliwość przekazania wyniku zdalnej procedury do klienta. Oznacza to, że definicja usługi z punktu musi zostać zmieniona do postaci:

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

Najprostszą realizacją asynchronicznych wywołań jest modyfikacja domyślnego czasu oczekiwania na odpowiedź. Czas oczekiwania klienta jest zdefiniowany w nagłówku pieńka klienta poprzez statyczną strukturę TIMEOUT:

static struct timeval TIMEOUT = { 25, 0 };

W strukturze timeval pole tv_sec określa liczbę sekund, a pole tv_usec liczbę mikrosekund (zobacz np. stronę pomocy systemowej utimes(3)). Czas ten można również zmodyfikować w pliku implementacyjnym klienta korzystając z funkcji clnt_control() (Funkcja clnt_control() umożliwia również zmianę parametru RETRY_TIMEOUT.). W przypadku przykładu należy w tym celu zmienić plik rkill_client.c w miejscu, gdzie jest tworzony uchwyt komunikacyjny:

struct timeval tm = { 60, 0 };
...
clnt = clnt_create (host, RKILL_PRG, RKILL_VERSION_1, "udp");
...
clnt_control(clnt, CLSET_TIMEOUT, &tm);

Szczególną zmianą czasu oczekiwania jest ustawienie go na wartość 0, co w praktyce oznacza ignorowanie odpowiedzi i permanentne zgłaszanie błędu timeout. Błąd przekroczenia czasu oczekiwania powinien oczywiście być ignorowany:

struct rpc_err err;
...
result_1 = rkill_1(rkill_1_pid, clnt);
if (result_1 == NULL) {
  clnt_geterr(clnt, &err);
  if (err.re_status != RPC_TIMEDOUT) {
    clnt_perror (clnt, "call failed");
  }
}

Funkcja clnt_geterr() umożliwia pobranie szczegółowych informacji o zaistniałym błędzie (Jest to analogia do zmiennej globalnej errno przechowującej kod błędu ostatnio wywołanej funkcji systemowej. Zobacz również stronę pomocy systemowej errno(3).). Struktura rpc_err posiada pole re_status, które przechowuje kod zaistniałego błędu. W powyższym przykładzie wyświetlana będzie więc informacja o wszystkich błędach oprócz RPC_TIMEDOUT.

Przykład zawiera pełen kod implementacji klienta wykorzystującego asynchroniczne wywołanie:

  1: #include "rkill.h"
  2: 
  3: void
  4: rkill_prg_1(char *host, int pid)
  5: {
  6: 	CLIENT *clnt;
  7: 	void  *result_1;
  8:   struct timeval tm = { 0, 0 };
  9:   struct rpc_err err;
 10: 
 11: #ifndef	DEBUG
 12: 	clnt = clnt_create (host, RKILL_PRG, RKILL_VERSION_1, "udp");
 13: 	if (clnt == NULL) {
 14: 		clnt_pcreateerror (host);
 15: 		exit (1);
 16: 	}
 17:   clnt_control(clnt, CLSET_TIMEOUT, (char*)&tm);
 18: #endif	/* DEBUG */
 19: 
 20: 	result_1 = rkill_1(pid, clnt);
 21: 	if (result_1 == (void *) NULL) {
 22:     clnt_geterr(clnt, &err); 
 23:     if (err.re_status != RPC_TIMEDOUT) {
 24: 		  clnt_perror (clnt, "call failed");
 25:     }
 26: 	}
 27: #ifndef	DEBUG
 28: 	clnt_destroy (clnt);
 29: #endif	 /* DEBUG */
 30: }
 31: 
 32: 
 33: int
 34: main (int argc, char *argv[])
 35: {
 36: 	char *host;
 37: 
 38: 	if (argc < 3) {
 39: 		printf ("usage: %s server_host pid\n", argv[0]);
 40: 		exit (1);
 41: 	}
 42: 	host = argv[1];
 43: 	rkill_prg_1 (host, atoi(argv[2]));
 44:   exit (0);
 45: }

Modyfikacja pieńka serwera

Ciekawą, choć niezalecaną oficjalnie implementacją asynchronicznego wywołania procedury jest modyfikacja pieńka serwera tak, aby wysyłał odpowiedź na żądanie klienta zanim wykona zdalną procedurę. Zabezpiecza to oczywiście klienta przed błędem typu timeout i dodatkowo daje mu potwierdzenie poprawnego wywołania procedury po stronie serwera. Podobnie jednak jak w przykładzie z zerowym czasem oczekiwania na odpowiedź nie będzie możliwe przesłanie wyniku procedury. Przykład pokazuje zmodyfikowaną procedurę pieńka serwera. Wewnątrz instrukcji switch (od linii 26) następuje zainicjowanie zmiennych reprezentujących filtry kodujące argument i wynik zdalnej procedury oraz zmiennej wskazującej na procedurę, która ma być wywołana. Linia 50 zawiera wywołanie zdalnej procedury. Wcześniej jednak (linia 46) następuje odesłanie odpowiedzi. Jako wynik przetwarzania przesyłany jest pusty wskaźnik NULL. W przypadku usług, które udostępniają większą liczbę funkcji obsługę asynchronicznych wywołań należałoby przenieść do wnętrza instrukcji switch.

Wywołanie asynchroniczne z wczesnym odesłaniem odpowiedzi
Wywołanie asynchroniczne z wczesnym odesłaniem odpowiedzi
  1: #include "rkill.h"
  2: #include <stdio.h>
  3: #include <stdlib.h>
  4: #include <rpc/pmap_clnt.h>
  5: #include <string.h>
  6: #include <memory.h>
  7: #include <sys/socket.h>
  8: #include <netinet/in.h>
  9: 
 10: static void *
 11: _rkill_1 (int  *argp, struct svc_req *rqstp)
 12: {
 13:   return (rkill_1_svc(*argp, rqstp));
 14: }
 15: 
 16: static void
 17: rkill_prg_1(struct svc_req *rqstp, register SVCXPRT *transp)
 18: {
 19:   union {
 20:     int rkill_1_arg;
 21:   } argument;
 22:   char *result;
 23:   xdrproc_t _xdr_argument, _xdr_result;
 24:   char *(*local)(char *, struct svc_req *);
 25: 
 26:   switch (rqstp->rq_proc) {
 27:   case NULLPROC:
 28:     (void) svc_sendreply (transp, (xdrproc_t) xdr_void, (char *)NULL);
 29:     return;
 30: 
 31:   case rkill:
 32:     _xdr_argument = (xdrproc_t) xdr_int;
 33:     _xdr_result = (xdrproc_t) xdr_void;
 34:     local = (char *(*)(char *, struct svc_req *)) _rkill_1;
 35:     break;
 36: 
 37:   default:
 38:     svcerr_noproc (transp);
 39:     return;
 40:   }
 41:   memset ((char *)&argument, 0, sizeof (argument));
 42:   if (!svc_getargs (transp, (xdrproc_t) _xdr_argument, (caddr_t) &argument)) {
 43:     svcerr_decode (transp);
 44:     return;
 45:   }
 46:   if (!svc_sendreply(transp, (xdrproc_t) _xdr_result, NULL)) {
 47:     svcerr_systemerr (transp);
 48:   }
 49:   else {
 50:     result = (*local)((char *)&argument, rqstp);
 51:   }
 52:   if (!svc_freeargs (transp, (xdrproc_t) _xdr_argument, (caddr_t) &argument)) {
 53:     fprintf (stderr, "%s", "unable to free arguments");
 54:     exit (1);
 55:   }
 56:   return;
 57: }

Procesy potomne

Ostatnia propozycja realizacji wywołania asynchronicznego polega na wykorzystaniu procesów potomnych do realizacji samej procedury. Ponieważ proces główny serwera będzie zajmował się jedynie odbiorem żądań i tworzeniem nowych procesów, jego odpowiedź będzie trafiała natychmiast do klientów. Całe przetwarzanie (potencjalnie długie) odbędzie się w nowym procesie potomnym. Umożliwia to współbieżne wykonywanie wielu zdalnych procedur przez pojedynczy serwer. Przykład prezentuje implementację zdalnej procedury takiego serwera. Pełen kod serwera musi uwzględnić całościowo problem zarządzania procesami potomnymi, a więc m.in. problem zarządzania procesami zombie.

Wywołanie asynchroniczne z procesem potomnym
Wywołanie asynchroniczne z procesem potomnym
  1: #include "rkill.h"
  2: 
  3: void *
  4: rkill_1_svc(int pid,  struct svc_req *rqstp)
  5: {
  6:   if (fork()==0)
  7:   {
  8:     printf("Zatrzymywanie procesu %d.\n", pid);
  9:     kill(pid, 9);
 10:     sleep(30);
 11:     exit(0);
 12:   }
 13: 
 14: 	return 1;
 15: }

Procesy potomne lub przetwarzanie wielowątkowe można wykorzystać również w przypadku synchronicznego wywołania procedur w celu zrównoleglenia przetwarzania po stronie serwera i jednocześnie minimalizacji czasu odpowiedzi dla krótkich żądań. Wymaga to modyfikacji pieńka serwera w celu utworzenia nowego procesu (wątku) dla każdego wywołania zdalnej procedury. Schemat przetwarzania zdalnego wywołania procedury w takim podejściu został zaprezentowany na rysunku:

Wywołanie synchroniczne z procesem potomnym
Wywołanie synchroniczne z procesem potomnym

Zadania

  1. Przetestuj obsługę zdalnego wywołania metody rkill() po dodaniu do jej implementacji opóźnienia 30-sekundowego.
  2. Sprawdź obsługę zdalnego wywołania w przypadku użycia protokołu transportowego TCP.
  3. Zaimplementuj wywołanie asynchroniczne poprzez ustawienie zerowego czasu odczekiwania na odpowiedź. Aplikacja klienta nie powinna sygnalizować żadnego błędu, jeżeli przesłanie żądanie udaje się przesłać.
  4. Zmodyfikuj pieniek serwera standardowej implementacji zdalnego wywołania metody (z domyślnym czasem oczekiwania na odpowiedź), tak aby odsyłał potwierdzenie wykonania metody zaraz po odebraniu żądania. Sprawdź jak realizowana będzie teraz współbieżna obsługa długich, 30-sekundowych żądań pochodzących od dwóch różnych klientów.
  5. Zmodyfikuj implementację z poprzedniego punktu przenosząc obsługę żądań klientów do procesów potomnych. Ponownie zweryfikuj obsługę współbieżnych żądań.