Sr-03-lab-1.0: Różnice pomiędzy wersjami
Lab 03 - Wywolania asynchroniczne |
(Brak różnic)
|
Aktualna wersja na dzień 19:54, 1 wrz 2006
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.

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.

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
.

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.

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:

Zadania
- Przetestuj obsługę zdalnego wywołania metody
rkill()
po dodaniu do jej implementacji opóźnienia 30-sekundowego. - Sprawdź obsługę zdalnego wywołania w przypadku użycia protokołu transportowego TCP.
- 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ć.
- 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.
- Zmodyfikuj implementację z poprzedniego punktu przenosząc obsługę żądań klientów do procesów potomnych. Ponownie zweryfikuj obsługę współbieżnych żądań.